mirror of
https://github.com/genxium/DelayNoMore
synced 2024-12-26 19:58:56 +00:00
471 lines
16 KiB
Go
471 lines
16 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/ByteArena/box2d"
|
|
"go.uber.org/zap"
|
|
"io/ioutil"
|
|
"math"
|
|
. "server/common"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
LOW_SCORE_TREASURE_TYPE = 1
|
|
HIGH_SCORE_TREASURE_TYPE = 2
|
|
SPEED_SHOES_TYPE = 3
|
|
|
|
LOW_SCORE_TREASURE_SCORE = 100
|
|
HIGH_SCORE_TREASURE_SCORE = 200
|
|
|
|
FLIPPED_HORIZONTALLY_FLAG uint32 = 0x80000000
|
|
FLIPPED_VERTICALLY_FLAG uint32 = 0x40000000
|
|
FLIPPED_DIAGONALLY_FLAG uint32 = 0x20000000
|
|
)
|
|
|
|
// For either a "*.tmx" or "*.tsx" file. [begins]
|
|
type TmxOrTsxProperty struct {
|
|
Name string `xml:"name,attr"`
|
|
Value string `xml:"value,attr"`
|
|
}
|
|
|
|
type TmxOrTsxProperties struct {
|
|
Property []*TmxOrTsxProperty `xml:"property"`
|
|
}
|
|
|
|
type TmxOrTsxPolyline struct {
|
|
Points string `xml:"points,attr"`
|
|
}
|
|
|
|
type TmxOrTsxObject struct {
|
|
Id int `xml:"id,attr"`
|
|
Gid *int `xml:"gid,attr"`
|
|
X float64 `xml:"x,attr"`
|
|
Y float64 `xml:"y,attr"`
|
|
Properties *TmxOrTsxProperties `xml:"properties"`
|
|
Polyline *TmxOrTsxPolyline `xml:"polyline"`
|
|
Width *float64 `xml:"width,attr"`
|
|
Height *float64 `xml:"height,attr"`
|
|
}
|
|
|
|
type TmxOrTsxObjectGroup struct {
|
|
Draworder string `xml:"draworder,attr"`
|
|
Name string `xml:"name,attr"`
|
|
Objects []*TmxOrTsxObject `xml:"object"`
|
|
}
|
|
|
|
type TmxOrTsxImage struct {
|
|
Source string `xml:"source,attr"`
|
|
Width int `xml:"width,attr"`
|
|
Height int `xml:"height,attr"`
|
|
}
|
|
|
|
// For either a "*.tmx" or "*.tsx" file. [ends]
|
|
|
|
// Within a "*.tsx" file. [begins]
|
|
type Tsx struct {
|
|
Name string `xml:"name,attr"`
|
|
TileWidth int `xml:"tilewidth,attr"`
|
|
TileHeight int `xml:"tileheight,attr"`
|
|
TileCount int `xml:"tilecount,attr"`
|
|
Columns int `xml:"columns,attr"`
|
|
Image []*TmxOrTsxImage `xml:"image"`
|
|
Tiles []*TsxTile `xml:"tile"`
|
|
}
|
|
|
|
type TsxTile struct {
|
|
Id int `xml:"id,attr"`
|
|
ObjectGroup *TmxOrTsxObjectGroup `xml:"objectgroup"`
|
|
Properties *TmxOrTsxProperties `xml:"properties"`
|
|
}
|
|
|
|
// Within a "*.tsx" file. [ends]
|
|
|
|
// Within a "*.tmx" file. [begins]
|
|
type TmxLayerDecodedTileData struct {
|
|
Id uint32
|
|
Tileset *TmxTileset
|
|
FlipHorizontal bool
|
|
FlipVertical bool
|
|
FlipDiagonal bool
|
|
}
|
|
|
|
type TmxLayerEncodedData struct {
|
|
Encoding string `xml:"encoding,attr"`
|
|
Compression string `xml:"compression,attr"`
|
|
Value string `xml:",chardata"`
|
|
}
|
|
|
|
type TmxLayer struct {
|
|
Name string `xml:"name,attr"`
|
|
Width int `xml:"width,attr"`
|
|
Height int `xml:"height,attr"`
|
|
Data *TmxLayerEncodedData `xml:"data"`
|
|
Tile []*TmxLayerDecodedTileData
|
|
}
|
|
|
|
type TmxTileset struct {
|
|
FirstGid uint32 `xml:"firstgid,attr"`
|
|
Name string `xml:"name,attr"`
|
|
TileWidth int `xml:"tilewidth,attr"`
|
|
TileHeight int `xml:"tileheight,attr"`
|
|
Images []*TmxOrTsxImage `xml:"image"`
|
|
Source string `xml:"source,attr"`
|
|
}
|
|
|
|
type TmxMap struct {
|
|
Version string `xml:"version,attr"`
|
|
Orientation string `xml:"orientation,attr"`
|
|
Width int `xml:"width,attr"`
|
|
Height int `xml:"height,attr"`
|
|
TileWidth int `xml:"tilewidth,attr"`
|
|
TileHeight int `xml:"tileheight,attr"`
|
|
Properties []*TmxOrTsxProperties `xml:"properties"`
|
|
Tilesets []*TmxTileset `xml:"tileset"`
|
|
Layers []*TmxLayer `xml:"layer"`
|
|
ObjectGroups []*TmxOrTsxObjectGroup `xml:"objectgroup"`
|
|
}
|
|
|
|
// Within a "*.tmx" file. [ends]
|
|
|
|
func (d *TmxLayerEncodedData) decodeBase64() ([]byte, error) {
|
|
r := bytes.NewReader([]byte(strings.TrimSpace(d.Value)))
|
|
decr := base64.NewDecoder(base64.StdEncoding, r)
|
|
if d.Compression == "zlib" {
|
|
rclose, err := zlib.NewReader(decr)
|
|
if err != nil {
|
|
Logger.Error("tmx data decode zlib error: ", zap.Any("encoding", d.Encoding), zap.Any("compression", d.Compression), zap.Any("value", d.Value))
|
|
return nil, err
|
|
}
|
|
return ioutil.ReadAll(rclose)
|
|
}
|
|
Logger.Error("tmx data decode invalid compression: ", zap.Any("encoding", d.Encoding), zap.Any("compression", d.Compression), zap.Any("value", d.Value))
|
|
return nil, errors.New("Invalid compression.")
|
|
}
|
|
|
|
func (l *TmxLayer) decodeBase64() ([]uint32, error) {
|
|
databytes, err := l.Data.decodeBase64()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if l.Width == 0 || l.Height == 0 {
|
|
return nil, errors.New("Zero width or height.")
|
|
}
|
|
if len(databytes) != l.Height*l.Width*4 {
|
|
Logger.Error("TmxLayer decodeBase64 invalid data bytes:", zap.Any("width", l.Width), zap.Any("height", l.Height), zap.Any("data lenght", len(databytes)))
|
|
return nil, errors.New("Data length error.")
|
|
}
|
|
dindex := 0
|
|
gids := make([]uint32, l.Height*l.Width)
|
|
for h := 0; h < l.Height; h++ {
|
|
for w := 0; w < l.Width; w++ {
|
|
gid := uint32(databytes[dindex]) |
|
|
uint32(databytes[dindex+1])<<8 |
|
|
uint32(databytes[dindex+2])<<16 |
|
|
uint32(databytes[dindex+3])<<24
|
|
dindex += 4
|
|
gids[h*l.Width+w] = gid
|
|
}
|
|
}
|
|
return gids, nil
|
|
}
|
|
|
|
type Vec2DList []*Vec2D
|
|
type Polygon2DList []*Polygon2D
|
|
type StrToVec2DListMap map[string]*Vec2DList // Note that it's deliberately NOT using "map[string]Vec2DList", for the easy of passing return value to "models/room.go".
|
|
type StrToPolygon2DListMap map[string]*Polygon2DList // Note that it's deliberately NOT using "map[string]Polygon2DList", for the easy of passing return value to "models/room.go".
|
|
|
|
func TmxPolylineToPolygon2DInB2World(pTmxMapIns *TmxMap, singleObjInTmxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline) (*Polygon2D, error) {
|
|
if nil == targetPolyline {
|
|
return nil, nil
|
|
}
|
|
|
|
singleValueArray := strings.Split(targetPolyline.Points, " ")
|
|
pointsCount := len(singleValueArray)
|
|
|
|
if pointsCount >= box2d.B2_maxPolygonVertices {
|
|
return nil, errors.New(fmt.Sprintf("During `TmxPolylineToPolygon2DInB2World`, you have a polygon with pointsCount == %v, exceeding or equal to box2d.B2_maxPolygonVertices == %v, of polyines [%v]", pointsCount, box2d.B2_maxPolygonVertices, singleValueArray))
|
|
}
|
|
|
|
theUntransformedAnchor := &Vec2D{
|
|
X: singleObjInTmxFile.X,
|
|
Y: singleObjInTmxFile.Y,
|
|
}
|
|
theTransformedAnchor := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedAnchor)
|
|
thePolygon2DFromPolyline := &Polygon2D{
|
|
Anchor: &theTransformedAnchor,
|
|
Points: make([]*Vec2D, len(singleValueArray)),
|
|
}
|
|
|
|
for k, value := range singleValueArray {
|
|
thePolygon2DFromPolyline.Points[k] = &Vec2D{}
|
|
for kk, v := range strings.Split(value, ",") {
|
|
coordinateValue, err := strconv.ParseFloat(v, 64)
|
|
if nil != err {
|
|
panic(err)
|
|
}
|
|
if 0 == (kk % 2) {
|
|
thePolygon2DFromPolyline.Points[k].X = (coordinateValue)
|
|
} else {
|
|
thePolygon2DFromPolyline.Points[k].Y = (coordinateValue)
|
|
}
|
|
}
|
|
|
|
// Transform to B2World space coordinate.
|
|
tmp := &Vec2D{
|
|
X: thePolygon2DFromPolyline.Points[k].X,
|
|
Y: thePolygon2DFromPolyline.Points[k].Y,
|
|
}
|
|
transformedTmp := pTmxMapIns.continuousObjLayerVecToContinuousMapNodeVec(tmp)
|
|
thePolygon2DFromPolyline.Points[k].X = transformedTmp.X
|
|
thePolygon2DFromPolyline.Points[k].Y = transformedTmp.Y
|
|
}
|
|
|
|
return thePolygon2DFromPolyline, nil
|
|
}
|
|
|
|
func TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns *TmxMap, singleObjInTsxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline, pTsxIns *Tsx) (*Polygon2D, error) {
|
|
if nil == targetPolyline {
|
|
return nil, nil
|
|
}
|
|
var factorHalf float64 = 0.5
|
|
offsetFromTopLeftInTileLocalCoordX := singleObjInTsxFile.X
|
|
offsetFromTopLeftInTileLocalCoordY := singleObjInTsxFile.Y
|
|
|
|
singleValueArray := strings.Split(targetPolyline.Points, " ")
|
|
pointsCount := len(singleValueArray)
|
|
|
|
if pointsCount >= box2d.B2_maxPolygonVertices {
|
|
return nil, errors.New(fmt.Sprintf("During `TsxPolylineToOffsetsWrtTileCenterInB2World`, you have a polygon with pointsCount == %v, exceeding or equal to box2d.B2_maxPolygonVertices == %v", pointsCount, box2d.B2_maxPolygonVertices))
|
|
}
|
|
|
|
thePolygon2DFromPolyline := &Polygon2D{
|
|
Anchor: nil,
|
|
Points: make([]*Vec2D, pointsCount),
|
|
TileWidth: pTsxIns.TileWidth,
|
|
TileHeight: pTsxIns.TileHeight,
|
|
}
|
|
|
|
/*
|
|
[WARNING] In this case, the "Treasure"s and "GuardTower"s are put into Tmx file as "ImageObject"s, of each the "ProportionalAnchor" is (0.5, 0). Therefore we calculate that "thePolygon2DFromPolyline.Points" are "offsets(in B2World) w.r.t. the BottomCenter". See https://shimo.im/docs/SmLJJhXm2C8XMzZT for details.
|
|
*/
|
|
|
|
for k, value := range singleValueArray {
|
|
thePolygon2DFromPolyline.Points[k] = &Vec2D{}
|
|
for kk, v := range strings.Split(value, ",") {
|
|
coordinateValue, err := strconv.ParseFloat(v, 64)
|
|
if nil != err {
|
|
panic(err)
|
|
}
|
|
if 0 == (kk % 2) {
|
|
// W.r.t. center.
|
|
thePolygon2DFromPolyline.Points[k].X = (coordinateValue + offsetFromTopLeftInTileLocalCoordX) - factorHalf*float64(pTsxIns.TileWidth)
|
|
} else {
|
|
// W.r.t. bottom.
|
|
thePolygon2DFromPolyline.Points[k].Y = float64(pTsxIns.TileHeight) - (coordinateValue + offsetFromTopLeftInTileLocalCoordY)
|
|
}
|
|
}
|
|
|
|
// No need to transform for B2World space coordinate because the marks in a Tsx file is already rectilinear.
|
|
}
|
|
|
|
return thePolygon2DFromPolyline, nil
|
|
}
|
|
|
|
func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, firstGid int, gidBoundariesMapInB2World map[int]StrToPolygon2DListMap) error {
|
|
pTsxIns := &Tsx{}
|
|
err := xml.Unmarshal(byteArrOfTsxFile, pTsxIns)
|
|
if nil != err {
|
|
panic(err)
|
|
}
|
|
/*
|
|
// For debug-printing only. -- YFLu, 2019-09-04.
|
|
|
|
reserializedTmxMap, err := pTmxMapIns.ToXML()
|
|
if nil != err {
|
|
panic(err)
|
|
}
|
|
*/
|
|
|
|
for _, tile := range pTsxIns.Tiles {
|
|
globalGid := (firstGid + int(tile.Id))
|
|
/**
|
|
A tile xml string could be
|
|
|
|
```
|
|
<tile id="13">
|
|
<objectgroup draworder="index">
|
|
<object id="1" x="-154" y="-159">
|
|
<properties>
|
|
<property name="boundary_type" value="guardTower"/>
|
|
</properties>
|
|
<polyline points="0,0 -95,179 18,407 361,434 458,168 333,-7"/>
|
|
</object>
|
|
</objectgroup>
|
|
</tile>
|
|
```
|
|
, we currently REQUIRE that "`an object of a tile` with ONE OR MORE polylines must come with a single corresponding '<property name=`type` value=`...` />', and viceversa".
|
|
|
|
Refer to https://shimo.im/docs/SmLJJhXm2C8XMzZT for how we theoretically fit a "Polyline in Tsx" into a "Polygon2D" and then into the corresponding "B2BodyDef & B2Body in the `world of colliding bodies`".
|
|
*/
|
|
|
|
theObjGroup := tile.ObjectGroup
|
|
if nil == theObjGroup {
|
|
continue
|
|
}
|
|
for _, singleObj := range theObjGroup.Objects {
|
|
if nil == singleObj.Polyline {
|
|
// Temporarily omit those non-polyline-containing objects.
|
|
continue
|
|
}
|
|
if nil == singleObj.Properties.Property || "boundary_type" != singleObj.Properties.Property[0].Name {
|
|
continue
|
|
}
|
|
|
|
key := singleObj.Properties.Property[0].Value
|
|
|
|
var theStrToPolygon2DListMap StrToPolygon2DListMap
|
|
if existingStrToPolygon2DListMap, ok := gidBoundariesMapInB2World[globalGid]; ok {
|
|
theStrToPolygon2DListMap = existingStrToPolygon2DListMap
|
|
} else {
|
|
gidBoundariesMapInB2World[globalGid] = make(StrToPolygon2DListMap, 0)
|
|
theStrToPolygon2DListMap = gidBoundariesMapInB2World[globalGid]
|
|
}
|
|
|
|
var pThePolygon2DList *Polygon2DList
|
|
if _, ok := theStrToPolygon2DListMap[key]; ok {
|
|
pThePolygon2DList = theStrToPolygon2DListMap[key]
|
|
} else {
|
|
thePolygon2DList := make(Polygon2DList, 0)
|
|
theStrToPolygon2DListMap[key] = &thePolygon2DList
|
|
pThePolygon2DList = theStrToPolygon2DListMap[key]
|
|
}
|
|
|
|
thePolygon2DFromPolyline, err := TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns)
|
|
if nil != err {
|
|
panic(err)
|
|
}
|
|
*pThePolygon2DList = append(*pThePolygon2DList, thePolygon2DFromPolyline)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMapInB2World map[int]StrToPolygon2DListMap) (int32, int32, int32, int32, StrToVec2DListMap, StrToPolygon2DListMap, error) {
|
|
toRetStrToVec2DListMap := make(StrToVec2DListMap, 0)
|
|
toRetStrToPolygon2DListMap := make(StrToPolygon2DListMap, 0)
|
|
/*
|
|
Note that both
|
|
- "Vec2D"s of "toRetStrToVec2DListMap", and
|
|
- "Polygon2D"s of "toRetStrToPolygon2DListMap"
|
|
|
|
are already transformed into the "coordinate of B2World".
|
|
*/
|
|
|
|
for _, objGroup := range pTmxMapIns.ObjectGroups {
|
|
switch objGroup.Name {
|
|
case "PlayerStartingPos":
|
|
var pTheVec2DListToCache *Vec2DList
|
|
_, ok := toRetStrToVec2DListMap[objGroup.Name]
|
|
if false == ok {
|
|
theVec2DListToCache := make(Vec2DList, 0)
|
|
toRetStrToVec2DListMap[objGroup.Name] = &theVec2DListToCache
|
|
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
|
|
} else {
|
|
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
|
|
}
|
|
for _, singleObjInTmxFile := range objGroup.Objects {
|
|
theUntransformedPos := &Vec2D{
|
|
X: singleObjInTmxFile.X,
|
|
Y: singleObjInTmxFile.Y,
|
|
}
|
|
thePosInWorld := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedPos)
|
|
*pTheVec2DListToCache = append(*pTheVec2DListToCache, &thePosInWorld)
|
|
}
|
|
case "Barrier":
|
|
// Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is exactly overlapping with "Polygon2D.Points[0]" w.r.t. B2World.
|
|
var pThePolygon2DListToCache *Polygon2DList
|
|
_, ok := toRetStrToPolygon2DListMap[objGroup.Name]
|
|
if false == ok {
|
|
thePolygon2DListToCache := make(Polygon2DList, 0)
|
|
toRetStrToPolygon2DListMap[objGroup.Name] = &thePolygon2DListToCache
|
|
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
|
|
} else {
|
|
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
|
|
}
|
|
|
|
for _, singleObjInTmxFile := range objGroup.Objects {
|
|
if nil == singleObjInTmxFile.Polyline {
|
|
continue
|
|
}
|
|
if nil == singleObjInTmxFile.Properties.Property || "boundary_type" != singleObjInTmxFile.Properties.Property[0].Name || "barrier" != singleObjInTmxFile.Properties.Property[0].Value {
|
|
continue
|
|
}
|
|
|
|
thePolygon2DInWorld, err := TmxPolylineToPolygon2DInB2World(pTmxMapIns, singleObjInTmxFile, singleObjInTmxFile.Polyline)
|
|
if nil != err {
|
|
panic(err)
|
|
}
|
|
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld)
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
return int32(pTmxMapIns.Width), int32(pTmxMapIns.Height), int32(pTmxMapIns.TileWidth), int32(pTmxMapIns.TileHeight), toRetStrToVec2DListMap, toRetStrToPolygon2DListMap, nil
|
|
}
|
|
|
|
func (pTmxMap *TmxMap) ToXML() (string, error) {
|
|
ret, err := xml.Marshal(pTmxMap)
|
|
return string(ret[:]), err
|
|
}
|
|
|
|
type TileRectilinearSize struct {
|
|
Width float64
|
|
Height float64
|
|
}
|
|
|
|
func (pTmxMapIns *TmxMap) continuousObjLayerVecToContinuousMapNodeVec(continuousObjLayerVec *Vec2D) Vec2D {
|
|
var tileRectilinearSize TileRectilinearSize
|
|
tileRectilinearSize.Width = float64(pTmxMapIns.TileWidth)
|
|
tileRectilinearSize.Height = float64(pTmxMapIns.TileHeight)
|
|
tileSizeUnifiedLength := math.Sqrt(tileRectilinearSize.Width*tileRectilinearSize.Width*0.25 + tileRectilinearSize.Height*tileRectilinearSize.Height*0.25)
|
|
isometricObjectLayerPointOffsetScaleFactor := (tileSizeUnifiedLength / tileRectilinearSize.Height)
|
|
cosineThetaRadian := (tileRectilinearSize.Width * 0.5) / tileSizeUnifiedLength
|
|
sineThetaRadian := (tileRectilinearSize.Height * 0.5) / tileSizeUnifiedLength
|
|
|
|
transMat := [...][2]float64{
|
|
{isometricObjectLayerPointOffsetScaleFactor * cosineThetaRadian, -isometricObjectLayerPointOffsetScaleFactor * cosineThetaRadian},
|
|
{-isometricObjectLayerPointOffsetScaleFactor * sineThetaRadian, -isometricObjectLayerPointOffsetScaleFactor * sineThetaRadian},
|
|
}
|
|
convertedVecX := transMat[0][0]*continuousObjLayerVec.X + transMat[0][1]*continuousObjLayerVec.Y
|
|
convertedVecY := transMat[1][0]*continuousObjLayerVec.X + transMat[1][1]*continuousObjLayerVec.Y
|
|
converted := Vec2D{
|
|
X: convertedVecX,
|
|
Y: convertedVecY,
|
|
}
|
|
return converted
|
|
}
|
|
|
|
func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continuousObjLayerOffset *Vec2D) Vec2D {
|
|
layerOffset := Vec2D{
|
|
X: 0,
|
|
Y: float64(pTmxMapIns.Height*pTmxMapIns.TileHeight) * 0.5,
|
|
}
|
|
|
|
calibratedVec := continuousObjLayerOffset
|
|
convertedVec := pTmxMapIns.continuousObjLayerVecToContinuousMapNodeVec(calibratedVec)
|
|
|
|
toRet := Vec2D{
|
|
X: layerOffset.X + convertedVec.X,
|
|
Y: layerOffset.Y + convertedVec.Y,
|
|
}
|
|
|
|
return toRet
|
|
}
|