2022-09-20 23:50:01 +08:00
package models
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/xml"
"errors"
"go.uber.org/zap"
"io/ioutil"
"math"
. "server/common"
"strconv"
"strings"
)
const (
LOW_SCORE_TREASURE_TYPE = 1
HIGH_SCORE_TREASURE_TYPE = 2
2022-09-29 12:21:04 +08:00
SPEED_SHOES_TYPE = 3
2022-09-20 23:50:01 +08:00
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".
2022-10-01 17:26:37 +08:00
func tmxPolylineToPolygon2D ( pTmxMapIns * TmxMap , singleObjInTmxFile * TmxOrTsxObject , targetPolyline * TmxOrTsxPolyline ) ( * Polygon2D , error ) {
2022-09-20 23:50:01 +08:00
if nil == targetPolyline {
return nil , nil
}
singleValueArray := strings . Split ( targetPolyline . Points , " " )
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 )
}
}
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
}
2022-10-01 17:26:37 +08:00
func tsxPolylineToOffsetsWrtTileCenter ( pTmxMapIns * TmxMap , singleObjInTsxFile * TmxOrTsxObject , targetPolyline * TmxOrTsxPolyline , pTsxIns * Tsx ) ( * Polygon2D , error ) {
2022-09-20 23:50:01 +08:00
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 )
thePolygon2DFromPolyline := & Polygon2D {
Anchor : nil ,
Points : make ([] * Vec2D , pointsCount ),
TileWidth : pTsxIns . TileWidth ,
TileHeight : pTsxIns . TileHeight ,
}
/*
2022-10-01 17:26:37 +08:00
[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 the "thePolygon2DFromPolyline.Points" are "offsets w.r.t. the BottomCenter". See https://shimo.im/docs/SmLJJhXm2C8XMzZT for details.
2022-09-20 23:50:01 +08:00
*/
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 )
}
}
}
return thePolygon2DFromPolyline , nil
}
2022-10-01 17:26:37 +08:00
func DeserializeTsxToColliderDict ( pTmxMapIns * TmxMap , byteArrOfTsxFile [] byte , firstGid int , gidBoundariesMap map [ int ] StrToPolygon2DListMap ) error {
2022-09-20 23:50:01 +08:00
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 ))
/**
2022-09-26 23:09:18 +08:00
A tile xml string could be
2022-09-20 23:50:01 +08:00
```
<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".
2022-10-01 17:26:37 +08:00
Refer to https://shimo.im/docs/SmLJJhXm2C8XMzZT for how we theoretically fit a "Polyline in Tsx" into a "Polygon2D".
2022-09-20 23:50:01 +08:00
*/
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
2022-10-01 17:26:37 +08:00
if existingStrToPolygon2DListMap , ok := gidBoundariesMap [ globalGid ]; ok {
2022-09-20 23:50:01 +08:00
theStrToPolygon2DListMap = existingStrToPolygon2DListMap
} else {
2022-10-01 17:26:37 +08:00
gidBoundariesMap [ globalGid ] = make ( StrToPolygon2DListMap , 0 )
theStrToPolygon2DListMap = gidBoundariesMap [ globalGid ]
2022-09-20 23:50:01 +08:00
}
var pThePolygon2DList * Polygon2DList
if _ , ok := theStrToPolygon2DListMap [ key ]; ok {
pThePolygon2DList = theStrToPolygon2DListMap [ key ]
} else {
thePolygon2DList := make ( Polygon2DList , 0 )
theStrToPolygon2DListMap [ key ] = & thePolygon2DList
pThePolygon2DList = theStrToPolygon2DListMap [ key ]
}
2022-10-01 17:26:37 +08:00
thePolygon2DFromPolyline , err := tsxPolylineToOffsetsWrtTileCenter ( pTmxMapIns , singleObj , singleObj . Polyline , pTsxIns )
2022-09-20 23:50:01 +08:00
if nil != err {
panic ( err )
}
* pThePolygon2DList = append ( * pThePolygon2DList , thePolygon2DFromPolyline )
}
}
return nil
}
2022-10-01 17:26:37 +08:00
func ParseTmxLayersAndGroups ( pTmxMapIns * TmxMap , gidBoundariesMap map [ int ] StrToPolygon2DListMap ) ( int32 , int32 , int32 , int32 , StrToVec2DListMap , StrToPolygon2DListMap , error ) {
2022-09-20 23:50:01 +08:00
toRetStrToVec2DListMap := make ( StrToVec2DListMap , 0 )
toRetStrToPolygon2DListMap := make ( StrToPolygon2DListMap , 0 )
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
2022-10-01 20:45:38 +08:00
}
pTheVec2DListToCache = toRetStrToVec2DListMap [ objGroup . Name ]
2022-09-20 23:50:01 +08:00
for _ , singleObjInTmxFile := range objGroup . Objects {
theUntransformedPos := & Vec2D {
X : singleObjInTmxFile . X ,
Y : singleObjInTmxFile . Y ,
}
thePosInWorld := pTmxMapIns . continuousObjLayerOffsetToContinuousMapNodePos ( theUntransformedPos )
* pTheVec2DListToCache = append ( * pTheVec2DListToCache , & thePosInWorld )
}
case "Barrier" :
2022-10-01 17:26:37 +08:00
// Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is exactly overlapping with "Polygon2D.Points[0]".
2022-09-20 23:50:01 +08:00
var pThePolygon2DListToCache * Polygon2DList
_ , ok := toRetStrToPolygon2DListMap [ objGroup . Name ]
if false == ok {
thePolygon2DListToCache := make ( Polygon2DList , 0 )
toRetStrToPolygon2DListMap [ objGroup . Name ] = & thePolygon2DListToCache
}
2022-10-01 20:45:38 +08:00
pThePolygon2DListToCache = toRetStrToPolygon2DListMap [ objGroup . Name ]
2022-09-20 23:50:01 +08:00
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
}
2022-10-01 17:26:37 +08:00
thePolygon2DInWorld , err := tmxPolylineToPolygon2D ( pTmxMapIns , singleObjInTmxFile , singleObjInTmxFile . Polyline )
2022-09-20 23:50:01 +08:00
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
}