diff --git a/TODO.md b/TODO.md index 72898aa..88df884 100644 --- a/TODO.md +++ b/TODO.md @@ -22,6 +22,14 @@ slog.Debug("get observer", "minmoves", observer.LevelScore, "file", source.File, "line", source.Line) +- LDTK: + - to each level add these properties: + - level (num) + - name + - description + - background (image name w/o .png) + - min-moves (num, moves required to win) + ## Collider Rework [abandoned: see branch collider-system, fails] - do not use the map anymore for collision detection diff --git a/assets/levels/openquell.ldtk b/assets/levels/openquell.ldtk new file mode 100644 index 0000000..0bfa9a4 --- /dev/null +++ b/assets/levels/openquell.ldtk @@ -0,0 +1,686 @@ +{ + "__header__": { + "fileType": "LDtk Project JSON", + "app": "LDtk", + "doc": "https://ldtk.io/json", + "schema": "https://ldtk.io/files/JSON_SCHEMA.json", + "appAuthor": "Sebastien 'deepnight' Benard", + "appVersion": "1.5.3", + "url": "https://ldtk.io" + }, + "iid": "267e9380-d7b0-11ee-a97e-35bec9c19d52", + "jsonVersion": "1.5.3", + "appBuildId": 473703, + "nextUid": 11, + "identifierStyle": "Capitalize", + "toc": [], + "worldLayout": "Free", + "worldGridWidth": 256, + "worldGridHeight": 256, + "defaultLevelWidth": 256, + "defaultLevelHeight": 256, + "defaultPivotX": 0, + "defaultPivotY": 0, + "defaultGridSize": 32, + "defaultEntityWidth": 32, + "defaultEntityHeight": 32, + "bgColor": "#40465B", + "defaultLevelBgColor": "#696A79", + "minifyJson": false, + "externalLevels": false, + "exportTiled": false, + "simplifiedExport": false, + "imageExportMode": "None", + "exportLevelBg": true, + "pngFilePattern": null, + "backupOnSave": false, + "backupLimit": 10, + "backupRelPath": null, + "levelNamePattern": "Level_%idx", + "tutorialDesc": null, + "customCommands": [], + "flags": [], + "defs": { "layers": [ + { + "__type": "Entities", + "identifier": "Entities", + "type": "Entities", + "uid": 5, + "doc": null, + "uiColor": null, + "gridSize": 32, + "guideGridWid": 0, + "guideGridHei": 0, + "displayOpacity": 1, + "inactiveOpacity": 0.6, + "hideInList": false, + "hideFieldsWhenInactive": true, + "canSelectWhenInactive": true, + "renderInWorldView": true, + "pxOffsetX": 0, + "pxOffsetY": 0, + "parallaxFactorX": 0, + "parallaxFactorY": 0, + "parallaxScaling": true, + "requiredTags": [], + "excludedTags": [], + "autoTilesKilledByOtherLayerUid": null, + "uiFilterTags": [], + "useAsyncRender": false, + "intGridValues": [], + "intGridValuesGroups": [], + "autoRuleGroups": [], + "autoSourceLayerDefUid": null, + "tilesetDefUid": null, + "tilePivotX": 0, + "tilePivotY": 0, + "biomeFieldUid": null + }, + { + "__type": "Tiles", + "identifier": "Tiles", + "type": "Tiles", + "uid": 2, + "doc": null, + "uiColor": null, + "gridSize": 32, + "guideGridWid": 0, + "guideGridHei": 0, + "displayOpacity": 1, + "inactiveOpacity": 1, + "hideInList": false, + "hideFieldsWhenInactive": false, + "canSelectWhenInactive": true, + "renderInWorldView": true, + "pxOffsetX": 0, + "pxOffsetY": 0, + "parallaxFactorX": 0, + "parallaxFactorY": 0, + "parallaxScaling": true, + "requiredTags": [], + "excludedTags": [], + "autoTilesKilledByOtherLayerUid": null, + "uiFilterTags": [], + "useAsyncRender": false, + "intGridValues": [], + "intGridValuesGroups": [], + "autoRuleGroups": [], + "autoSourceLayerDefUid": null, + "tilesetDefUid": 1, + "tilePivotX": 0, + "tilePivotY": 0, + "biomeFieldUid": null + } + ], "entities": [ + { + "identifier": "Player_Primary", + "uid": 3, + "tags": [], + "exportToToc": false, + "allowOutOfBounds": false, + "doc": null, + "width": 32, + "height": 32, + "resizableX": false, + "resizableY": false, + "minWidth": null, + "maxWidth": null, + "minHeight": null, + "maxHeight": null, + "keepAspectRatio": false, + "tileOpacity": 1, + "fillOpacity": 0.08, + "lineOpacity": 0, + "hollow": false, + "color": "#2F3BBE", + "renderMode": "Tile", + "showName": true, + "tilesetId": 1, + "tileRenderMode": "FitInside", + "tileRect": { "tilesetUid": 1, "x": 32, "y": 96, "w": 32, "h": 32 }, + "uiTileRect": null, + "nineSliceBorders": [], + "maxCount": 0, + "limitScope": "PerLevel", + "limitBehavior": "MoveLastOne", + "pivotX": 0, + "pivotY": 0, + "fieldDefs": [] + }, + { + "identifier": "Collectible", + "uid": 4, + "tags": [], + "exportToToc": false, + "allowOutOfBounds": false, + "doc": null, + "width": 32, + "height": 32, + "resizableX": false, + "resizableY": false, + "minWidth": null, + "maxWidth": null, + "minHeight": null, + "maxHeight": null, + "keepAspectRatio": false, + "tileOpacity": 1, + "fillOpacity": 0.08, + "lineOpacity": 0, + "hollow": false, + "color": "#FEAE34", + "renderMode": "Tile", + "showName": true, + "tilesetId": 1, + "tileRenderMode": "FitInside", + "tileRect": { "tilesetUid": 1, "x": 32, "y": 32, "w": 32, "h": 32 }, + "uiTileRect": null, + "nineSliceBorders": [], + "maxCount": 0, + "limitScope": "PerLevel", + "limitBehavior": "MoveLastOne", + "pivotX": 0, + "pivotY": 0, + "fieldDefs": [] + }, + { + "identifier": "Obstacle_West", + "uid": 7, + "tags": [], + "exportToToc": false, + "allowOutOfBounds": false, + "doc": null, + "width": 32, + "height": 32, + "resizableX": false, + "resizableY": false, + "minWidth": null, + "maxWidth": null, + "minHeight": null, + "maxHeight": null, + "keepAspectRatio": false, + "tileOpacity": 1, + "fillOpacity": 0.08, + "lineOpacity": 0, + "hollow": false, + "color": "#BE4A2F", + "renderMode": "Tile", + "showName": true, + "tilesetId": 1, + "tileRenderMode": "FitInside", + "tileRect": { "tilesetUid": 1, "x": 0, "y": 96, "w": 32, "h": 32 }, + "uiTileRect": null, + "nineSliceBorders": [], + "maxCount": 0, + "limitScope": "PerLevel", + "limitBehavior": "MoveLastOne", + "pivotX": 0, + "pivotY": 0, + "fieldDefs": [] + }, + { + "identifier": "Obstacle_East", + "uid": 8, + "tags": [], + "exportToToc": false, + "allowOutOfBounds": false, + "doc": null, + "width": 32, + "height": 32, + "resizableX": false, + "resizableY": false, + "minWidth": null, + "maxWidth": null, + "minHeight": null, + "maxHeight": null, + "keepAspectRatio": false, + "tileOpacity": 1, + "fillOpacity": 0.08, + "lineOpacity": 0, + "hollow": false, + "color": "#D77643", + "renderMode": "Tile", + "showName": true, + "tilesetId": 1, + "tileRenderMode": "FitInside", + "tileRect": { "tilesetUid": 1, "x": 64, "y": 32, "w": 32, "h": 32 }, + "uiTileRect": null, + "nineSliceBorders": [], + "maxCount": 0, + "limitScope": "PerLevel", + "limitBehavior": "MoveLastOne", + "pivotX": 0, + "pivotY": 0, + "fieldDefs": [] + }, + { + "identifier": "Obstacle_North", + "uid": 9, + "tags": [], + "exportToToc": false, + "allowOutOfBounds": false, + "doc": null, + "width": 32, + "height": 32, + "resizableX": false, + "resizableY": false, + "minWidth": null, + "maxWidth": null, + "minHeight": null, + "maxHeight": null, + "keepAspectRatio": false, + "tileOpacity": 1, + "fillOpacity": 0.08, + "lineOpacity": 0, + "hollow": false, + "color": "#F77622", + "renderMode": "Tile", + "showName": true, + "tilesetId": 1, + "tileRenderMode": "FitInside", + "tileRect": { "tilesetUid": 1, "x": 0, "y": 64, "w": 32, "h": 32 }, + "uiTileRect": null, + "nineSliceBorders": [], + "maxCount": 0, + "limitScope": "PerLevel", + "limitBehavior": "MoveLastOne", + "pivotX": 0, + "pivotY": 0, + "fieldDefs": [] + }, + { + "identifier": "Obstacle_South", + "uid": 10, + "tags": [], + "exportToToc": false, + "allowOutOfBounds": false, + "doc": null, + "width": 32, + "height": 32, + "resizableX": false, + "resizableY": false, + "minWidth": null, + "maxWidth": null, + "minHeight": null, + "maxHeight": null, + "keepAspectRatio": false, + "tileOpacity": 1, + "fillOpacity": 0.08, + "lineOpacity": 0, + "hollow": false, + "color": "#E43B44", + "renderMode": "Tile", + "showName": true, + "tilesetId": 1, + "tileRenderMode": "FitInside", + "tileRect": { "tilesetUid": 1, "x": 32, "y": 64, "w": 32, "h": 32 }, + "uiTileRect": null, + "nineSliceBorders": [], + "maxCount": 0, + "limitScope": "PerLevel", + "limitBehavior": "MoveLastOne", + "pivotX": 0, + "pivotY": 0, + "fieldDefs": [] + } + ], "tilesets": [ + { + "__cWid": 3, + "__cHei": 5, + "identifier": "Map2", + "uid": 1, + "relPath": "../sprites/map.png", + "embedAtlas": null, + "pxWid": 96, + "pxHei": 160, + "tileGridSize": 32, + "spacing": 0, + "padding": 0, + "tags": [], + "tagsSourceEnumUid": null, + "enumTags": [], + "customData": [], + "savedSelections": [], + "cachedPixelData": { "opaqueTiles": "110000000000000", "averageColors": "f777f766f766f9533c965c355c355c354b355c35987d687d1b4700000000" } + } + ], "enums": [], "externalEnums": [], "levelFields": [] }, + "levels": [ + { + "identifier": "Level_0", + "iid": "267fa4f0-d7b0-11ee-a97e-d90512e08363", + "uid": 0, + "worldX": -224, + "worldY": 0, + "worldDepth": 0, + "pxWid": 640, + "pxHei": 480, + "__bgColor": "#696A79", + "bgColor": null, + "useAutoIdentifier": true, + "bgRelPath": null, + "bgPos": null, + "bgPivotX": 0.5, + "bgPivotY": 0.5, + "__smartColor": "#ADADB5", + "__bgPos": null, + "externalRelPath": null, + "fieldInstances": [], + "layerInstances": [ + { + "__identifier": "Entities", + "__type": "Entities", + "__cWid": 20, + "__cHei": 15, + "__gridSize": 32, + "__opacity": 1, + "__pxTotalOffsetX": 0, + "__pxTotalOffsetY": 0, + "__tilesetDefUid": null, + "__tilesetRelPath": null, + "iid": "99cd5060-d7b0-11ee-a97e-3f143b461cd1", + "levelId": 0, + "layerDefUid": 5, + "pxOffsetX": 0, + "pxOffsetY": 0, + "visible": true, + "optionalRules": [], + "intGridCsv": [], + "autoLayerTiles": [], + "seed": 1148260, + "overrideTilesetUid": null, + "gridTiles": [], + "entityInstances": [ + { + "__identifier": "Player_Primary", + "__grid": [7,5], + "__pivot": [0,0], + "__tags": [], + "__tile": { "tilesetUid": 1, "x": 32, "y": 96, "w": 32, "h": 32 }, + "__smartColor": "#2F3BBE", + "iid": "c33a2c20-d7b0-11ee-a97e-03409aba0392", + "width": 32, + "height": 32, + "defUid": 3, + "px": [224,160], + "fieldInstances": [], + "__worldX": 0, + "__worldY": 160 + }, + { + "__identifier": "Collectible", + "__grid": [11,7], + "__pivot": [0,0], + "__tags": [], + "__tile": { "tilesetUid": 1, "x": 32, "y": 32, "w": 32, "h": 32 }, + "__smartColor": "#FEAE34", + "iid": "f2b2ff50-d7b0-11ee-98fb-b34c447bc14d", + "width": 32, + "height": 32, + "defUid": 4, + "px": [352,224], + "fieldInstances": [], + "__worldX": 128, + "__worldY": 224 + } + ] + }, + { + "__identifier": "Tiles", + "__type": "Tiles", + "__cWid": 20, + "__cHei": 15, + "__gridSize": 32, + "__opacity": 1, + "__pxTotalOffsetX": 0, + "__pxTotalOffsetY": 0, + "__tilesetDefUid": 1, + "__tilesetRelPath": "../sprites/map.png", + "iid": "3f368c70-d7b0-11ee-a97e-65a98b860ebf", + "levelId": 0, + "layerDefUid": 2, + "pxOffsetX": 0, + "pxOffsetY": 0, + "visible": true, + "optionalRules": [], + "intGridCsv": [], + "autoLayerTiles": [], + "seed": 4985586, + "overrideTilesetUid": null, + "gridTiles": [ + { "px": [192,128], "src": [64,0], "f": 0, "t": 2, "d": [86], "a": 1 }, + { "px": [224,128], "src": [64,0], "f": 0, "t": 2, "d": [87], "a": 1 }, + { "px": [256,128], "src": [64,0], "f": 0, "t": 2, "d": [88], "a": 1 }, + { "px": [288,128], "src": [64,0], "f": 0, "t": 2, "d": [89], "a": 1 }, + { "px": [320,128], "src": [64,0], "f": 0, "t": 2, "d": [90], "a": 1 }, + { "px": [352,128], "src": [64,0], "f": 0, "t": 2, "d": [91], "a": 1 }, + { "px": [384,128], "src": [64,0], "f": 0, "t": 2, "d": [92], "a": 1 }, + { "px": [416,128], "src": [0,0], "f": 0, "t": 0, "d": [93], "a": 1 }, + { "px": [416,128], "src": [64,0], "f": 0, "t": 2, "d": [93], "a": 1 }, + { "px": [192,160], "src": [64,0], "f": 0, "t": 2, "d": [106], "a": 1 }, + { "px": [256,160], "src": [64,0], "f": 0, "t": 2, "d": [108], "a": 1 }, + { "px": [416,160], "src": [64,0], "f": 0, "t": 2, "d": [113], "a": 1 }, + { "px": [192,192], "src": [64,0], "f": 0, "t": 2, "d": [126], "a": 1 }, + { "px": [256,192], "src": [64,0], "f": 0, "t": 2, "d": [128], "a": 1 }, + { "px": [320,192], "src": [64,0], "f": 0, "t": 2, "d": [130], "a": 1 }, + { "px": [352,192], "src": [64,0], "f": 0, "t": 2, "d": [131], "a": 1 }, + { "px": [416,192], "src": [64,0], "f": 0, "t": 2, "d": [133], "a": 1 }, + { "px": [192,224], "src": [64,0], "f": 0, "t": 2, "d": [146], "a": 1 }, + { "px": [256,224], "src": [64,0], "f": 0, "t": 2, "d": [148], "a": 1 }, + { "px": [320,224], "src": [64,0], "f": 0, "t": 2, "d": [150], "a": 1 }, + { "px": [416,224], "src": [64,0], "f": 0, "t": 2, "d": [153], "a": 1 }, + { "px": [192,256], "src": [64,0], "f": 0, "t": 2, "d": [166], "a": 1 }, + { "px": [256,256], "src": [64,0], "f": 0, "t": 2, "d": [168], "a": 1 }, + { "px": [320,256], "src": [64,0], "f": 0, "t": 2, "d": [170], "a": 1 }, + { "px": [416,256], "src": [64,0], "f": 0, "t": 2, "d": [173], "a": 1 }, + { "px": [192,288], "src": [64,0], "f": 0, "t": 2, "d": [186], "a": 1 }, + { "px": [320,288], "src": [64,0], "f": 0, "t": 2, "d": [190], "a": 1 }, + { "px": [416,288], "src": [64,0], "f": 0, "t": 2, "d": [193], "a": 1 }, + { "px": [192,320], "src": [64,0], "f": 0, "t": 2, "d": [206], "a": 1 }, + { "px": [224,320], "src": [64,0], "f": 0, "t": 2, "d": [207], "a": 1 }, + { "px": [256,320], "src": [64,0], "f": 0, "t": 2, "d": [208], "a": 1 }, + { "px": [288,320], "src": [64,0], "f": 0, "t": 2, "d": [209], "a": 1 }, + { "px": [320,320], "src": [64,0], "f": 0, "t": 2, "d": [210], "a": 1 }, + { "px": [352,320], "src": [64,0], "f": 0, "t": 2, "d": [211], "a": 1 }, + { "px": [384,320], "src": [64,0], "f": 0, "t": 2, "d": [212], "a": 1 }, + { "px": [416,320], "src": [64,0], "f": 0, "t": 2, "d": [213], "a": 1 } + ], + "entityInstances": [] + } + ], + "__neighbours": [] + }, + { + "identifier": "_First_Try", + "iid": "024390f0-d7b0-11ee-98fb-1bc6ad7e4172", + "uid": 6, + "worldX": 480, + "worldY": 0, + "worldDepth": 0, + "pxWid": 640, + "pxHei": 480, + "__bgColor": "#696A79", + "bgColor": null, + "useAutoIdentifier": false, + "bgRelPath": "../sprites/background-lila.png", + "bgPos": "Cover", + "bgPivotX": 0.5, + "bgPivotY": 0.5, + "__smartColor": "#ADADB5", + "__bgPos": { "topLeftPx": [0,0], "scale": [1,1], "cropRect": [0,0,640,480] }, + "externalRelPath": null, + "fieldInstances": [], + "layerInstances": [ + { + "__identifier": "Entities", + "__type": "Entities", + "__cWid": 20, + "__cHei": 15, + "__gridSize": 32, + "__opacity": 1, + "__pxTotalOffsetX": 0, + "__pxTotalOffsetY": 0, + "__tilesetDefUid": null, + "__tilesetRelPath": null, + "iid": "0243b800-d7b0-11ee-98fb-f1914bde56c8", + "levelId": 6, + "layerDefUid": 5, + "pxOffsetX": 0, + "pxOffsetY": 0, + "visible": true, + "optionalRules": [], + "intGridCsv": [], + "autoLayerTiles": [], + "seed": 2072933, + "overrideTilesetUid": null, + "gridTiles": [], + "entityInstances": [ + { + "__identifier": "Obstacle_West", + "__grid": [6,8], + "__pivot": [0,0], + "__tags": [], + "__tile": { "tilesetUid": 1, "x": 0, "y": 96, "w": 32, "h": 32 }, + "__smartColor": "#BE4A2F", + "iid": "8aab4e10-d7b0-11ee-98fb-2171f9170515", + "width": 32, + "height": 32, + "defUid": 7, + "px": [192,256], + "fieldInstances": [], + "__worldX": 672, + "__worldY": 256 + }, + { + "__identifier": "Player_Primary", + "__grid": [4,5], + "__pivot": [0,0], + "__tags": [], + "__tile": { "tilesetUid": 1, "x": 32, "y": 96, "w": 32, "h": 32 }, + "__smartColor": "#2F3BBE", + "iid": "8d2c2c90-d7b0-11ee-98fb-d523a27935ba", + "width": 32, + "height": 32, + "defUid": 3, + "px": [128,160], + "fieldInstances": [], + "__worldX": 608, + "__worldY": 160 + }, + { + "__identifier": "Collectible", + "__grid": [4,9], + "__pivot": [0,0], + "__tags": [], + "__tile": { "tilesetUid": 1, "x": 32, "y": 32, "w": 32, "h": 32 }, + "__smartColor": "#FEAE34", + "iid": "a1dfb440-d7b0-11ee-98fb-933f35aeb72b", + "width": 32, + "height": 32, + "defUid": 4, + "px": [128,288], + "fieldInstances": [], + "__worldX": 608, + "__worldY": 288 + }, + { + "__identifier": "Collectible", + "__grid": [14,6], + "__pivot": [0,0], + "__tags": [], + "__tile": { "tilesetUid": 1, "x": 32, "y": 32, "w": 32, "h": 32 }, + "__smartColor": "#FEAE34", + "iid": "f7ec8340-d7b0-11ee-98fb-1fa75c7ce959", + "width": 32, + "height": 32, + "defUid": 4, + "px": [448,192], + "fieldInstances": [], + "__worldX": 928, + "__worldY": 192 + }, + { + "__identifier": "Obstacle_South", + "__grid": [15,6], + "__pivot": [0,0], + "__tags": [], + "__tile": { "tilesetUid": 1, "x": 32, "y": 64, "w": 32, "h": 32 }, + "__smartColor": "#E43B44", + "iid": "45c2d010-d7b0-11ee-98fb-13d8f32ffff0", + "width": 32, + "height": 32, + "defUid": 10, + "px": [480,192], + "fieldInstances": [], + "__worldX": 960, + "__worldY": 192 + } + ] + }, + { + "__identifier": "Tiles", + "__type": "Tiles", + "__cWid": 20, + "__cHei": 15, + "__gridSize": 32, + "__opacity": 1, + "__pxTotalOffsetX": 0, + "__pxTotalOffsetY": 0, + "__tilesetDefUid": 1, + "__tilesetRelPath": "../sprites/map.png", + "iid": "0243b801-d7b0-11ee-98fb-79b1102fb094", + "levelId": 6, + "layerDefUid": 2, + "pxOffsetX": 0, + "pxOffsetY": 0, + "visible": true, + "optionalRules": [], + "intGridCsv": [], + "autoLayerTiles": [], + "seed": 2952425, + "overrideTilesetUid": null, + "gridTiles": [ + { "px": [96,128], "src": [64,0], "f": 0, "t": 2, "d": [83], "a": 1 }, + { "px": [128,128], "src": [64,0], "f": 0, "t": 2, "d": [84], "a": 1 }, + { "px": [160,128], "src": [64,0], "f": 0, "t": 2, "d": [85], "a": 1 }, + { "px": [192,128], "src": [64,0], "f": 0, "t": 2, "d": [86], "a": 1 }, + { "px": [224,128], "src": [64,0], "f": 0, "t": 2, "d": [87], "a": 1 }, + { "px": [384,128], "src": [64,0], "f": 0, "t": 2, "d": [92], "a": 1 }, + { "px": [416,128], "src": [64,0], "f": 0, "t": 2, "d": [93], "a": 1 }, + { "px": [448,128], "src": [64,0], "f": 0, "t": 2, "d": [94], "a": 1 }, + { "px": [480,128], "src": [64,0], "f": 0, "t": 2, "d": [95], "a": 1 }, + { "px": [512,128], "src": [64,0], "f": 0, "t": 2, "d": [96], "a": 1 }, + { "px": [96,160], "src": [64,0], "f": 0, "t": 2, "d": [103], "a": 1 }, + { "px": [224,160], "src": [64,0], "f": 0, "t": 2, "d": [107], "a": 1 }, + { "px": [384,160], "src": [64,0], "f": 0, "t": 2, "d": [112], "a": 1 }, + { "px": [512,160], "src": [64,0], "f": 0, "t": 2, "d": [116], "a": 1 }, + { "px": [96,192], "src": [64,0], "f": 0, "t": 2, "d": [123], "a": 1 }, + { "px": [224,192], "src": [64,0], "f": 0, "t": 2, "d": [127], "a": 1 }, + { "px": [384,192], "src": [64,0], "f": 0, "t": 2, "d": [132], "a": 1 }, + { "px": [512,192], "src": [64,0], "f": 0, "t": 2, "d": [136], "a": 1 }, + { "px": [224,224], "src": [64,0], "f": 0, "t": 2, "d": [147], "a": 1 }, + { "px": [384,224], "src": [64,0], "f": 0, "t": 2, "d": [152], "a": 1 }, + { "px": [224,256], "src": [64,0], "f": 0, "t": 2, "d": [167], "a": 1 }, + { "px": [384,256], "src": [64,0], "f": 0, "t": 2, "d": [172], "a": 1 }, + { "px": [448,256], "src": [64,0], "f": 0, "t": 2, "d": [174], "a": 1 }, + { "px": [224,288], "src": [64,0], "f": 0, "t": 2, "d": [187], "a": 1 }, + { "px": [384,288], "src": [64,0], "f": 0, "t": 2, "d": [192], "a": 1 }, + { "px": [448,288], "src": [64,0], "f": 0, "t": 2, "d": [194], "a": 1 }, + { "px": [96,320], "src": [64,0], "f": 0, "t": 2, "d": [203], "a": 1 }, + { "px": [128,320], "src": [64,0], "f": 0, "t": 2, "d": [204], "a": 1 }, + { "px": [160,320], "src": [64,0], "f": 0, "t": 2, "d": [205], "a": 1 }, + { "px": [192,320], "src": [64,0], "f": 0, "t": 2, "d": [206], "a": 1 }, + { "px": [224,320], "src": [64,0], "f": 0, "t": 2, "d": [207], "a": 1 }, + { "px": [384,320], "src": [64,0], "f": 0, "t": 2, "d": [212], "a": 1 }, + { "px": [416,320], "src": [64,0], "f": 0, "t": 2, "d": [213], "a": 1 }, + { "px": [448,320], "src": [64,0], "f": 0, "t": 2, "d": [214], "a": 1 }, + { "px": [480,320], "src": [64,0], "f": 0, "t": 2, "d": [215], "a": 1 }, + { "px": [512,320], "src": [64,0], "f": 0, "t": 2, "d": [216], "a": 1 } + ], + "entityInstances": [] + } + ], + "__neighbours": [] + } + ], + "worlds": [], + "dummyWorldIid": "267ee1a0-d7b0-11ee-a97e-53f0a359eae1" +} \ No newline at end of file diff --git a/assets/loader-levels.go b/assets/loader-levels.go index 32aabca..3478994 100644 --- a/assets/loader-levels.go +++ b/assets/loader-levels.go @@ -1,23 +1,17 @@ package assets import ( - "bufio" _ "image/png" - "io/fs" "log" - "log/slog" "openquell/config" "openquell/util" - "os" - "path/filepath" - "sort" - "strconv" "strings" "github.com/hajimehoshi/ebiten/v2" + "github.com/solarlune/ldtkgo" ) -var Levels = LoadLevels("levels") +var Project = LoadLDTK("levels") var Tiles = InitTiles() // Tile: contains image, identifier (as used in level data) and @@ -186,44 +180,22 @@ func NewTileHiddenDoor(class []string) *Tile { } // used to map level data bytes to actual tiles -type TileRegistry map[byte]*Tile - -// holds a raw level spec: -// -// Name: the name of the level file w/o the .lvl extension -// Background: an image name used as game background for this level -// Description: text to display on top -// Data: a level spec consisting of chars of the above mapping and spaces, e.g.: -// #### -// # # -// #### -// -// Each level data must be 20 chars wide (= 640 px width) and 15 chars -// high (=480 px height). -type RawLevel struct { - Number int - Name string - Description string - Background *ebiten.Image - Data []byte - MinMoves int -} +type TileRegistry map[string]*Tile func InitTiles() TileRegistry { return TileRegistry{ - ' ': {Id: ' ', Class: "floor", Renderable: false}, - //'#': NewTileBlock("block-grey32"), - '#': NewTileBlock("block-greycolored"), - 'B': NewTileBlock("block-orange-32"), - 'S': NewTilePlayer(Primary), - 's': NewTilePlayer(Secondary), - 'o': NewTileCollectible("collectible-orange"), - '+': NewTileObstacle("obstacle-star", config.All), - '^': NewTileObstacle("obstacle-north", config.North), - 'v': NewTileObstacle("obstacle-south", config.South), - '<': NewTileObstacle("obstacle-west", config.West), - '>': NewTileObstacle("obstacle-east", config.East), - '*': NewTileParticle([]string{ + "floor": {Id: ' ', Class: "floor", Renderable: false}, + "default": NewTileBlock("block-greycolored"), + "solidorange": NewTileBlock("block-orange-32"), + "player-primary": NewTilePlayer(Primary), + "player-secondary": NewTilePlayer(Secondary), + "collectible": NewTileCollectible("collectible-orange"), + "obstacle-star": NewTileObstacle("obstacle-star", config.All), + "obstacle-north": NewTileObstacle("obstacle-north", config.North), + "obstacle-south": NewTileObstacle("obstacle-south", config.South), + "obstacle-west": NewTileObstacle("obstacle-west", config.West), + "obstacle-east": NewTileObstacle("obstacle-east", config.East), + "particle": NewTileParticle([]string{ //"particle-ring-1", "particle-ring-2", "particle-ring-3", @@ -231,91 +203,56 @@ func InitTiles() TileRegistry { "particle-ring-5", "particle-ring-6", }), - 't': NewTileTranswall([]string{"transwall", "block-orange-32"}), - 'W': NewTileHiddenDoor([]string{"block-greycolored", "block-greycolored-damaged"}), + "transient": NewTileTranswall([]string{"transwall", "block-orange-32"}), + "hiddendoor": NewTileHiddenDoor([]string{"block-greycolored", "block-greycolored-damaged"}), } } -// load levels at compile time into ram, creates a slice of raw levels -func LoadLevels(dir string) []RawLevel { - levels := []RawLevel{} - - // we use embed.FS to iterate over all files in ./levels/ - entries, err := assetfs.ReadDir(dir) +// load LDTK project at compile time into ram +func LoadLDTK(dir string) *ldtkgo.Project { + fd, err := assetfs.Open("levels/openquell.ldtk") if err != nil { - log.Fatalf("failed to read level dir %s: %s", dir, err) - } - - sort.Slice(entries, func(i, j int) bool { - return entries[i].Name() < entries[j].Name() - }) - - for idx, levelfile := range entries { - if levelfile.Type().IsRegular() && strings.Contains(levelfile.Name(), ".lvl") { - path := filepath.Join("assets", dir) - level := ParseRawLevel(path, levelfile) - level.Number = idx - - levels = append(levels, level) - - slog.Debug("loaded level", "path", path, "file", levelfile) - } - } - - return levels -} - -func ParseRawLevel(dir string, levelfile fs.DirEntry) RawLevel { - fd, err := os.Open(filepath.Join(dir, levelfile.Name())) - if err != nil { - log.Fatalf("failed to read level file %s: %s", levelfile.Name(), err) + log.Fatalf("failed to open LDTK file levels/openquell.ldtk: %s", err) } defer fd.Close() - name := strings.TrimSuffix(levelfile.Name(), ".lvl") - name = name[3:] - des := "" - background := &ebiten.Image{} - data := []byte{} - minmoves := 0 + fileinfo, err := fd.Stat() + if err != nil { + log.Fatalf("failed to stat() LDTK file levels/openquell.ldtk: %s", err) + } - scanner := bufio.NewScanner(fd) - for scanner.Scan() { - // ignore any whitespace - line := scanner.Text() + filesize := fileinfo.Size() + buffer := make([]byte, filesize) - // ignore empty lines - if len(line) == 0 { - continue + _, err = fd.Read(buffer) + if err != nil { + log.Fatalf("failed to read bytes from LDTK file levels/openquell.ldtk: %s", err) + } + + project, err := ldtkgo.Read(buffer) + if err != nil { + panic(err) + } + + // do some sanity checks + properties := []string{"min-moves", "background", "level", "name", "descrption"} + need := len(properties) + + for idx, level := range project.Levels { + have := 0 + + for _, property := range level.Properties { + if util.Contains(properties, property.Identifier) { + have++ + } } - switch { - case strings.Contains(line, "Background:"): - haveit := strings.Split(line, ": ") - if util.Exists(Assets, haveit[1]) { - background = Assets[haveit[1]] - } - case strings.Contains(line, "Description:"): - haveit := strings.Split(line, ": ") - des = haveit[1] - case strings.Contains(line, "MinMoves:"): - haveit := strings.Split(line, ": ") - minmoves, err = strconv.Atoi(haveit[1]) - if err != nil { - log.Fatal("Failed to convert MinMoves to int: %w", err) - } - default: - // all other non-empty and non-equalsign lines are - // level definition matrix data, merge thes into data - data = append(data, line+"\n"...) + if have != need { + log.Fatalf("level definition for level %d (%s) invalid: %d missing properties\n required: %s", + idx, level.Identifier, need-have, strings.Join(properties, ", "), + ) } } - return RawLevel{ - Name: name, - Data: data, - Background: background, - Description: des, - MinMoves: minmoves, - } + return project } diff --git a/assets/loader-sprites.go b/assets/loader-sprites.go index 7a6fa4c..1bf0fa8 100644 --- a/assets/loader-sprites.go +++ b/assets/loader-sprites.go @@ -16,7 +16,7 @@ import ( // Maps image name to image data type AssetRegistry map[string]*ebiten.Image -//go:embed levels/*.lvl sprites/*.png fonts/*.ttf +//go:embed sprites/*.png fonts/*.ttf levels/*.ldtk var assetfs embed.FS var Assets = LoadImages("sprites") diff --git a/assets/sprites/block-grey.png b/assets/sprites/block-grey.png deleted file mode 100644 index 87614a6..0000000 Binary files a/assets/sprites/block-grey.png and /dev/null differ diff --git a/assets/sprites/makemap.sh b/assets/sprites/makemap.sh new file mode 100644 index 0000000..7d1a386 --- /dev/null +++ b/assets/sprites/makemap.sh @@ -0,0 +1,2 @@ +#!/bin/sh +montage -tile 4x0 -geometry +0+0 block* collectible-orange.png obstacle-* sphere-blue* transwall.png map.png && okular map.png diff --git a/assets/sprites/map.png b/assets/sprites/map.png new file mode 100644 index 0000000..5efa9cb Binary files /dev/null and b/assets/sprites/map.png differ diff --git a/game/level_scene.go b/game/level_scene.go index 106eed0..dc5a52e 100644 --- a/game/level_scene.go +++ b/game/level_scene.go @@ -28,10 +28,11 @@ func NewLevelScene(game *Game, startlevel int) Scene { func (scene *LevelScene) GenerateLevels(game *Game) { min := []int{} - for _, level := range assets.Levels { + for _, level := range assets.Project.Levels { level := level - scene.Levels = append(scene.Levels, NewLevel(game, 32, &level)) - min = append(min, level.MinMoves) + scene.Levels = append(scene.Levels, NewLevel(game, 32, level)) + level.PropertyByIdentifier("min-moves") + min = append(min, level.PropertyByIdentifier("min-moves").AsInt()) } scene.Game.Observer.SetupLevelScore(min) diff --git a/game/levels.go b/game/levels.go index ba4e3c0..eb2bf62 100644 --- a/game/levels.go +++ b/game/levels.go @@ -2,18 +2,17 @@ package game import ( "image" - "log" "log/slog" "openquell/assets" "openquell/components" "openquell/grid" "openquell/observers" "openquell/systems" - "openquell/util" "strings" "github.com/hajimehoshi/ebiten/v2" "github.com/mlange-42/arche/ecs" + "github.com/solarlune/ldtkgo" ) type Map map[image.Point]*assets.Tile @@ -31,13 +30,14 @@ type Level struct { Grid *grid.Grid } -func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level { +func NewLevel(game *Game, cellsize int, plan *ldtkgo.Level) *Level { systemlist := []systems.System{} gridcontainer := &grid.GridContainer{} systemlist = append(systemlist, - systems.NewGridSystem(game.World, game.ScreenWidth, game.ScreenHeight, cellsize, plan.Background)) + systems.NewGridSystem(game.World, game.ScreenWidth, game.ScreenHeight, cellsize, + assets.Assets[plan.PropertyByIdentifier("background").AsString()])) systemlist = append(systemlist, systems.NewCollectibleSystem(game.World)) @@ -63,8 +63,8 @@ func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level { World: game.World, Width: game.ScreenWidth, Height: game.ScreenHeight, - Description: plan.Description, - Name: plan.Name, + Description: plan.PropertyByIdentifier("description").AsString(), + Name: plan.Identifier, GridContainer: gridcontainer, Systems: systemlist, } @@ -113,25 +113,51 @@ func (level *Level) SetupGrid(game *Game) { } // parses a RawLevel and generates a mapslice from it, which is being used as grid -func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) (Map, Map) { +func LevelToSlice(game *Game, level *ldtkgo.Level, tilesize int) (Map, Map) { size := game.ScreenWidth * game.ScreenHeight mapslice := make(Map, size) backupmap := make(Map, size) - for y, line := range strings.Split(string(level.Data), "\n") { - if len(line) != game.ScreenWidth/tilesize && y < game.ScreenHeight/tilesize { - log.Fatalf("line %d doesn't contain %d tiles, but %d", - y, game.ScreenWidth/tilesize, len(line)) - } + for _, layer := range level.Layers { + switch layer.Type { + case ldtkgo.LayerTypeTile: + // load tile from LDTK tile layer, use sprites from associated map. - for x, char := range line { - if !util.Exists(assets.Tiles, byte(char)) { - log.Fatalf("unregistered tile type %c encountered", char) + if tiles := layer.AllTiles(); len(tiles) > 0 { + for _, tileData := range tiles { + // Subimage the Tile from the already loaded map, + // but referenced from LDTK file, that way we + // could use multiple tileset images + tile := assets.Tiles["default"] + tile.Sprite = assets.Assets[strings.TrimSuffix(layer.Tileset.Path, ".png")].SubImage( + image.Rect(tileData.Src[0], + tileData.Src[1], + tileData.Src[0]+layer.GridSize, + tileData.Src[1]+layer.GridSize)).(*ebiten.Image) + + mapslice[image.Point{tileData.Position[0], tileData.Position[1]}] = tile + backupmap[image.Point{tileData.Position[0], tileData.Position[1]}] = tile.Clone() + } } - tile := assets.Tiles[byte(char)] - mapslice[image.Point{x, y}] = tile - backupmap[image.Point{x, y}] = tile.Clone() + case ldtkgo.LayerTypeEntity: + // load mobile tiles (they call them entities) using static map map.png. + tileset := assets.Assets["map"] + + for _, entity := range layer.Entities { + if entity.TileRect != nil { + tile := assets.Tiles[entity.Identifier] + + tileRect := entity.TileRect + tile.Sprite = tileset.SubImage( + image.Rect(tileRect.X, tileRect.Y, + tileRect.X+tileRect.W, + tileRect.Y+tileRect.H)).(*ebiten.Image) + + mapslice[image.Point{entity.Position[0], entity.Position[1]}] = tile + backupmap[image.Point{entity.Position[0], entity.Position[1]}] = tile.Clone() + } + } } } diff --git a/go.mod b/go.mod index ae5ab37..954b741 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,11 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/solarlune/ldtkgo v0.9.4-0.20240310011150-66aa15c2ab56 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/tidwall/gjson v1.9.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tinne26/etxt v0.0.8 // indirect github.com/tlinden/yadu v0.1.3 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect diff --git a/go.sum b/go.sum index 72da15c..c0dff75 100644 --- a/go.sum +++ b/go.sum @@ -253,6 +253,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/solarlune/ldtkgo v0.9.4-0.20240310011150-66aa15c2ab56 h1:QW8w9YQbIlIW053jM2SfBKAFGyd4maoq0AawZsb9rO4= +github.com/solarlune/ldtkgo v0.9.4-0.20240310011150-66aa15c2ab56/go.mod h1:PP4XtlnCSwWo7iexI/NJ3aS3INmiT42o066o6Dx52rs= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -262,6 +264,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/gjson v1.9.3 h1:hqzS9wAHMO+KVBBkLxYdkEeeFHuqr95GfClRLKlgK0E= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc= github.com/tinne26/etxt v0.0.8/go.mod h1:QM/hlNkstsKC39elTFNKAR34xsMb9QoVosf+g9wlYxM= github.com/tlinden/yadu v0.1.2 h1:TYYVnUJwziRJ9YPbIbRf9ikmDw0Q8Ifixm+J/kBQFh8= diff --git a/grid/grid.go b/grid/grid.go index 3dec702..658e451 100644 --- a/grid/grid.go +++ b/grid/grid.go @@ -158,7 +158,7 @@ func (grid *Grid) RemoveTile(point image.Point) { } func (grid *Grid) SetFloorTile(point image.Point) { - grid.Map[point] = assets.Tiles[' '] + grid.Map[point] = assets.Tiles["floor"] } func (grid *Grid) SetSolidTile(tile *assets.Tile, point image.Point) { diff --git a/systems/collectible_system.go b/systems/collectible_system.go index 43b570d..370a133 100644 --- a/systems/collectible_system.go +++ b/systems/collectible_system.go @@ -114,8 +114,8 @@ func (system *CollectibleSystem) AddParticle(position *components.Position) { pos, particle, timer := ptmapper.Get(entity) observer.AddEntity(entity, particleID) - particle.Index = assets.Tiles['*'].Particle - particle.Tiles = assets.Tiles['*'].Tiles + particle.Index = assets.Tiles["particle"].Particle + particle.Tiles = assets.Tiles["particle"].Tiles pos.Update( position.X-(16), // FIXME: use global tilesize! diff --git a/systems/hud_system.go b/systems/hud_system.go index 64c9727..698af9d 100644 --- a/systems/hud_system.go +++ b/systems/hud_system.go @@ -8,16 +8,17 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/mlange-42/arche/ecs" + "github.com/solarlune/ldtkgo" ) type HudSystem struct { World *ecs.World Cellsize int Observer *observers.GameObserver - Plan *assets.RawLevel + Plan *ldtkgo.Level } -func NewHudSystem(world *ecs.World, plan *assets.RawLevel) System { +func NewHudSystem(world *ecs.World, plan *ldtkgo.Level) System { system := &HudSystem{ Observer: observers.GetGameObserver(world), World: world, @@ -45,13 +46,14 @@ func (system *HudSystem) Draw(screen *ebiten.Image) { */ score := fmt.Sprintf("Score: %d", system.Observer.GetScore()) - level := fmt.Sprintf("Level %d %s", system.Plan.Number, system.Plan.Name) + level := fmt.Sprintf("Level %d %s", system.Plan.PropertyByIdentifier("level").AsInt(), + system.Plan.PropertyByIdentifier("name").AsString()) assets.FontRenderer.Renderer.SetSizePx(20) assets.FontRenderer.Renderer.SetTarget(screen) system.Print(score, 515, 22) - system.Print(system.Plan.Description, 10, 470) + system.Print(system.Plan.PropertyByIdentifier("description").AsString(), 10, 470) system.Print(level, 10, 22) } diff --git a/systems/obstacle_system.go b/systems/obstacle_system.go index 7c607a5..d08d2fb 100644 --- a/systems/obstacle_system.go +++ b/systems/obstacle_system.go @@ -2,7 +2,6 @@ package systems import ( "log/slog" - "openquell/assets" "openquell/components" . "openquell/components" . "openquell/config" @@ -146,33 +145,6 @@ func (system *ObstacleSystem) Draw(screen *ebiten.Image) { } } -func (system *ObstacleSystem) AddParticle(position *components.Position) { - observer := observers.GetGameObserver(system.World) - - ptmapper := generic.NewMap3[ - components.Position, - components.Particle, - components.Timer, - ](system.World) - - particleID := ecs.ComponentID[components.Particle](system.World) - - entity := ptmapper.New() - pos, particle, timer := ptmapper.Get(entity) - observer.AddEntity(entity, particleID) - - particle.Index = assets.Tiles['*'].Particle - particle.Tiles = assets.Tiles['*'].Tiles - - pos.Update( - position.X-(16), // FIXME: use global tilesize! - position.Y-(16), - 64, - ) - - timer.Start(PARTICLE_LOOPWAIT) -} - // return true if obstacle weapon points into the direction the player is moving func CheckObstacleSide(playervelocity *Velocity, obsdirection int) bool { movingdirection := playervelocity.InvertDirection()