├── .gitignore ├── LICENSE.md ├── examples ├── image-templates │ └── rock.png ├── snapshots │ ├── 23m02-dev_landscape.zip │ ├── 23m03-dev_landscape.sgjs │ ├── 23w18a-dev_fallthrough.sgjs │ ├── 23w18a-dev_landscape.sgjs │ ├── 23w18a-dev_tree-template.sgjs │ ├── 23w20a-dev_template-100x100.sgjs │ ├── 23w20a_landscape.sgjs │ ├── 23w41a-dev_landscape-two-buffers.sgjs │ ├── 23w49a-dev.sgjs │ └── 23w52a-dev.sgjs └── tools │ ├── template-external.json │ ├── template-rock.json │ └── template-rock.zip ├── index.html ├── package-lock.json ├── package.json ├── readme.md ├── rollup.config.js ├── src ├── Analytics.js ├── Assets.js ├── core │ ├── CircleIterator.js │ ├── Counter.js │ ├── DeterministicRandom.js │ ├── Element.js │ ├── ElementArea.js │ ├── ElementHead.js │ ├── ElementTail.js │ ├── FloodFillPainter.js │ ├── Marker.js │ ├── Objective.js │ ├── SandGame.js │ ├── SandGameGraphics.js │ ├── SandGameOverlay.js │ ├── SandGameScenario.js │ ├── Snapshot.js │ ├── SnapshotMetadata.js │ ├── Splash.js │ ├── TemplateBlockPainter.js │ ├── TemplateLayeredPainter.js │ ├── brush │ │ ├── AbstractEffectBrush.js │ │ ├── Brush.js │ │ ├── Brushes.js │ │ ├── ColorBrush.js │ │ ├── ColorMovingPaletteBrush.js │ │ ├── ColorNoiseBrush.js │ │ ├── ColorPaletteRandomBrush.js │ │ ├── ColorRandomize.js │ │ ├── ColorTextureBrush.js │ │ ├── CountingBrush.js │ │ ├── CustomBrush.js │ │ ├── MeltingBrush.js │ │ ├── RandomBrush.js │ │ ├── RandomElementBrush.js │ │ └── SolidBodyBrush.js │ ├── processing │ │ ├── Processor.js │ │ ├── ProcessorContext.js │ │ ├── ProcessorDefaults.js │ │ ├── ProcessorExtensionSpawnFish.js │ │ ├── ProcessorExtensionSpawnGrass.js │ │ ├── ProcessorExtensionSpawnTree.js │ │ ├── ProcessorModuleFire.js │ │ ├── ProcessorModuleFish.js │ │ ├── ProcessorModuleGrass.js │ │ ├── ProcessorModuleMeteor.js │ │ ├── ProcessorModuleSolidBody.js │ │ ├── ProcessorModuleTree.js │ │ ├── ProcessorModuleWater.js │ │ └── VisualEffects.js │ ├── rendering │ │ ├── Renderer.js │ │ ├── Renderer2D.js │ │ ├── RendererInitializer.js │ │ ├── RendererNull.js │ │ ├── RendererWebGL.js │ │ ├── RenderingMode.js │ │ ├── RenderingModeElementType.js │ │ ├── RenderingModeHeatmap.js │ │ └── assets │ │ │ ├── heatmap.palette.png │ │ │ ├── temperature.palette.csv │ │ │ └── temperature.txt │ ├── scene │ │ ├── Scene.js │ │ ├── SceneImplHardcoded.js │ │ ├── SceneImplModFlip.js │ │ ├── SceneImplResize.js │ │ ├── SceneImplSnapshot.js │ │ ├── SceneImplTemplate.js │ │ └── Scenes.js │ └── tool │ │ ├── ActionTool.js │ │ ├── CursorDefinition.js │ │ ├── CursorDefinitionElementArea.js │ │ ├── GlobalActionTool.js │ │ ├── InsertElementAreaTool.js │ │ ├── InsertRandomSceneTool.js │ │ ├── MeteorTool.js │ │ ├── MoveTool.js │ │ ├── Point2BrushTool.js │ │ ├── PointBrushTool.js │ │ ├── RoundBrushTool.js │ │ ├── RoundBrushToolForSolidBody.js │ │ ├── TemplateSelectionFakeTool.js │ │ ├── Tool.js │ │ ├── ToolInfo.js │ │ └── Tools.js ├── def │ ├── BrushDefs.js │ ├── Defaults.js │ ├── PredicateDefs.js │ ├── SceneDefs.js │ ├── StructureDefs.js │ ├── TemplateDefs.js │ ├── ToolDefs.js │ └── assets │ │ ├── brushes │ │ ├── ash.palette.csv │ │ ├── coal.palette.csv │ │ ├── gravel.palette.csv │ │ ├── sand.palette.csv │ │ ├── soil.palette.csv │ │ ├── steam.palette.csv │ │ ├── thermite-1.palette.csv │ │ ├── thermite-2.palette.csv │ │ ├── tree-leaf-dark.palette.csv │ │ ├── tree-leaf-light.palette.csv │ │ ├── tree-root.palette.csv │ │ ├── tree-wood-dark.palette.csv │ │ ├── tree-wood-light.palette.csv │ │ ├── wall.palette.csv │ │ └── water.palette.csv │ │ ├── structures │ │ ├── tree-leaf-cluster-templates.json │ │ └── tree-trunk-templates.json │ │ └── templates │ │ ├── rock-1.png │ │ ├── rock-2.png │ │ ├── rock-3.png │ │ ├── rock-4.png │ │ ├── rock-5.png │ │ ├── rock-6.png │ │ ├── rock-icon.png │ │ ├── rock-lg-1.png │ │ ├── rock-lg-2.png │ │ ├── rock-lg-icon.png │ │ ├── rock-sm-1.png │ │ ├── rock-sm-2.png │ │ ├── rock-sm-3.png │ │ ├── rock-sm-icon.png │ │ ├── sand-castle.png │ │ ├── wooden-house-icon.png │ │ └── wooden-house.png ├── gui │ ├── Controller.js │ ├── DomBuilder.js │ ├── ServiceIO.js │ ├── ServiceToolManager.js │ ├── SizeUtils.js │ ├── action │ │ ├── Action.js │ │ ├── ActionBenchmark.js │ │ ├── ActionDialogChangeCanvasSize.js │ │ ├── ActionDialogChangeElementSize.js │ │ ├── ActionDialogTemplateSelection.js │ │ ├── ActionFill.js │ │ ├── ActionIOExport.js │ │ ├── ActionIOImport.js │ │ ├── ActionRecord.js │ │ ├── ActionReportProblem.js │ │ ├── ActionRestart.js │ │ ├── ActionScreenshot.js │ │ └── ActionsTest.js │ └── component │ │ ├── Component.js │ │ ├── ComponentButton.js │ │ ├── ComponentButtonAdjustScale.js │ │ ├── ComponentButtonReport.js │ │ ├── ComponentButtonRestart.js │ │ ├── ComponentButtonStartStop.js │ │ ├── ComponentContainer.js │ │ ├── ComponentFormTemplate.js │ │ ├── ComponentSimple.js │ │ ├── ComponentStatusIndicator.js │ │ ├── ComponentViewCanvas.js │ │ ├── ComponentViewCanvasInner.js │ │ ├── ComponentViewCanvasOverlayCursor.js │ │ ├── ComponentViewCanvasOverlayDebug.js │ │ ├── ComponentViewCanvasOverlayMarker.js │ │ ├── ComponentViewCanvasOverlayScenario.js │ │ ├── ComponentViewElementSizeSelection.js │ │ ├── ComponentViewSceneSelection.js │ │ ├── ComponentViewTemplateSelection.js │ │ ├── ComponentViewTestTools.js │ │ ├── ComponentViewTools.js │ │ └── assets │ │ ├── element-size-1.png │ │ ├── element-size-2.png │ │ ├── element-size-3.png │ │ ├── element-size-4.png │ │ ├── icon-adjust-scale.svg │ │ ├── icon-pause.svg │ │ ├── icon-play.svg │ │ ├── icon-reset.svg │ │ ├── icon-square-check.svg │ │ ├── icon-square-dotted.svg │ │ └── icon-square.svg ├── io │ ├── ResourceSnapshot.js │ ├── ResourceTool.js │ ├── ResourceUtils.js │ └── Resources.js ├── main.js └── style.css ├── test └── test.js └── tools ├── palette-designer ├── index.html ├── tool-palette-designer.css └── tool-palette-designer.js ├── perlin-texture-designer ├── examples │ ├── a.js │ ├── b.js │ ├── c.js │ ├── metal.js │ └── rock.js ├── index.html └── script.js └── tree-template-builder ├── data.js ├── index.html ├── index.js ├── leaf-cluster-template-builder ├── index.html ├── index.js └── templates.png └── trunk-template-builder ├── index.html ├── index.js ├── template-00.png ├── template-01.png ├── template-02.png ├── template-03.png ├── template-04.png ├── template-05.png ├── template-06.png └── template-07.png /.gitignore: -------------------------------------------------------------------------------- 1 | /*.iml 2 | /.idea 3 | 4 | .DS_Store 5 | node_modules 6 | dist 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 3 | 4 | --- 5 | 6 | Sand Game JS is open-source software in the technical sense of the term. 7 | Open sources facilitate scripting, scenario writing, and modding. 8 | You are free to download the source codes, compile them on your own, and experiment. 9 | Forks on GitHub are acceptable, but a backlink should be maintained. 10 | 11 | However, Sand Game JS is NOT open-source software in the legal sense of the term. 12 | It is not automatically permitted to publish distributions, derivative works, or parts thereof. 13 | Please contact me, and if it does not represent competition, you will receive permission, or we can agree on the terms. 14 | 15 | Naturally, I am not interested in being swept away by competition using the code that I wrote myself. 16 | I hope you understand my position, the goal is to protect sandsaga.com. 17 | -------------------------------------------------------------------------------- /examples/image-templates/rock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/image-templates/rock.png -------------------------------------------------------------------------------- /examples/snapshots/23m02-dev_landscape.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23m02-dev_landscape.zip -------------------------------------------------------------------------------- /examples/snapshots/23m03-dev_landscape.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23m03-dev_landscape.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w18a-dev_fallthrough.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w18a-dev_fallthrough.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w18a-dev_landscape.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w18a-dev_landscape.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w18a-dev_tree-template.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w18a-dev_tree-template.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w20a-dev_template-100x100.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w20a-dev_template-100x100.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w20a_landscape.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w20a_landscape.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w41a-dev_landscape-two-buffers.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w41a-dev_landscape-two-buffers.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w49a-dev.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w49a-dev.sgjs -------------------------------------------------------------------------------- /examples/snapshots/23w52a-dev.sgjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/snapshots/23w52a-dev.sgjs -------------------------------------------------------------------------------- /examples/tools/template-external.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "tool", 3 | "info": { 4 | "displayName": "Logo", 5 | "category": "template", 6 | "icon": { 7 | "imageData": null 8 | } 9 | }, 10 | "action": { 11 | "type": "image-template", 12 | "imageData": "/favicon-32x32.png", 13 | "brush": "sand", 14 | "threshold": 50, 15 | "randomFlipHorizontally": true 16 | } 17 | } -------------------------------------------------------------------------------- /examples/tools/template-rock.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "tool", 3 | "info": { 4 | "displayName": "Rock", 5 | "category": "template", 6 | "icon": { 7 | "imageData": null 8 | } 9 | }, 10 | "action": { 11 | "type": "image-template", 12 | "imageData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAtCAYAAADYxvnjAAAACXBIWXMAAAsTAAALEwEAmpwYAAAJPUlEQVRYR9WYyY8cVx3HP++92nsdxzNjz+AF5ZwLIjgkjoJCHMtELAELOQthkYIQ4oKU/4JIPnJDcEQowCWxYsdADkZxWCJkLHkShzHjZdqe9sx0dXdVdVfVexyqp6enPTNeYkP4XrpV9V7Xp3/7K4v/E1njFz6t+q+AHvv2UbO42KBSKVMqlfj1b94Q42tupwcO+tSTB83ly5dJ4oRer0ej0eDg44+Z2dnZuwJ+oKBPPXnQ5HmG53ksL6/g5BnGGAA+/vhjDjz6ebNj4iFOnHz7tsAPFHRqaor5+X8RRzFKSmzbxrJsoqiL7wfkecaNGzc48OgXzNm/vL8t7AMDfe7wsybr9/FdjzzLibtdLCHJ0xTHsjF5jms7xFGMXwr43COPmL+fO7cl7AMBPfr1rxnP80izjCzNMEbjui7amMKyjkOWZrRaLTzPI0liarUaX3n2kHnr5KlNYe876JFDzxiALM9xXZd+v4+QgqBUot/vkxtDHIbYts3kzkl6vR7KUvR6PWx7a5yt79yDjh193uSZxhhDpVJhdWUFrXMkah1aCAI/wBhNGIZ4vofruliBRZIk4z851H0DffX7r5h2uw1CIwR0OiFXr11BCpBoumELCQghwBikkDiuhZQSI0ALMHJTrwP3CfTY0efNzZs3KZfLNJshu6anyfK8cKuU48uHEga01vT7fRwHHMcZXzLUJwb9yY9eNfPz88zMzNBoLKK1xnYcolYLKSXoom6OqqilGgCdZWRaY9s2jmNvXDiiTwT64x/+wFy7do0g8DHG0Ov18P2ALEvpdjuUSiWidme43hiDyQ1CCAzFHxBKonVOluXDdZvpE4HevNlkaucO8lyzunKTuNvhs3v3k6Yp1681cByniMkRGWMwpoAFEMBD9R0kSUK9Ut2wdlT3BPrz48f3n3731PzEjglMllOtVpmbu8DU1BTaGPzAp9VqsWfPHtI0He4zxqAHLZSRT2MMUkmUtTXO1ne20ek/nZ63XYfJyUmSbsTS0hKVSoV6vU69VuPC3AUmJyfp9/vjWzECjCniE0D3+2QmRyiJdb+TKU4Sdk7N0u10YZC1rutSrVZxXZd2uz3oPoU1xVrZ0QWkHkmwdDCoSCn5xS9/tWV9umvQrx45ZGZmZnFdB9/3Wbh0iSiKqddreJ7HlStXkFKRJAmWUkWHslw0mhwzhDQDpHK5RBTF9Hq9kafcqrsC/d4rLxudpZTLZbK8z+rqKsYYJibqlEplAOYvzZOmGQBSSUpBgJCCKEnodDp4ngcwyHmIuhGe723bPuEuQbXWzMzsJo4T0rRPmmaUy+Wh65eXl+l0uigpUVZhVcfeGHdrloRBNxKDirql0wvdMegrL79ohBAoZdHptAkCH9tx6MfJoORo5ubmAFCWIs9ywjCkVC7jex6WZVEul0nzwtp6ABaUAqSQmybeqO4YNAxDHn74YRzHodfr8dDOHVy7epVyUKJarWKMIYoibNsa1smJHRMIIdHGIKTA8zzcsX6epTlCFu10O90R6Hdf/o6pT1TZvXuGD/76PlIpwjAcxmKtVuPs2bO4rkuaFh1Gqq17PIAUAm0MOtcIIe5PjK6dc7qdDpVqldnZWS5cOE+lUmEtHFqt1mC4KJLlTqWNwRISy1I8d+SwefPE5uen24K+9MKLxvd9JicnWV66idaaRqOB53kYo5menuH8+X+SZzm2vfVQMS4hBGiBUnJoTa3XG8G4bgvaXl2mUvIJV1vkOsVWiuXmDZRSTO+aAmBubg7LstFZThCMZLmRrIQtbMvB83ws26Lb6eIHPiABjVIOWVZ4bGUlXN87pm1BX3rhmPEdF9d1WVi4xO7du0iSCCkljmMhheD69esopfAH9XHjECLw/RIAWZ6RDTIe1teNDy3PPnPInHzn1nPTlqDHj//sSx/87QNarVWq1SqlUoCUiiiKsCwL13URQnDx4kWyNCNzMqRUpKMdxkgQ2ycVFLBreRDH0djdQluCvvfn9/4ohGDf/v00m03yPCVJEuI4oVqtDke4hX8voCxFp9PFtq2xeigRysa2rA2u30rGGOIoHr8MbAPaWl5FWYoP2x0qlQq9XoyUEqUk1WoVKS2azWVsxxlOQ/1+H2MEUiospZBKYigsmiQxJBCUilAwuUYNKkae66JU6aJUfev5b5o3fvfbDe7fFPTg448ZqSSVSoUwDFlaWqI+USMMQ/bMzhLHCZOTk5w5cwYAMXCvEBIhJErKYr6UCj0AHdfo8LymtcaQJLda9RbQg48/ZgD6/YQoEoAGsV42ut0u5XIZL/DpRF0soYb3ABhYUioLZSmUWf8TAGYwPQ1HPzRCGtAaKQxIi/bI8WVNG0APfflpk6ZFLM7s38fCwgLVaoWJep0ojoexWavVmL80T5blWLaiKDWFlCqsalkKKewthw2jzXDbWiIBKCm3t+hzRw6btBcTeA6B59BsNgmCgE7YYe9n9hJHHXZPTxJ2ujRuNPnoo4/w/dIgrgqrCjEYh5D0ezlCaFzXHzxhrJgLjTGQpoW7obC6NoZebxvQtBfjeR4rK6sjt8G1bDrtNkoVr11c18W2DYuLiwA4jjeMNTkWc5tp3XqDEGA9HADykVo7qiGoZdmsrKySZWsLCwu4tmJ5pUmtViOOY/xSmTiOiZIY3/dBCgyFNTWgBv4cT5Q7kRCCXhLxj3Pnb9k8BH3r5Glx5NDTJopG61xRLtrtdnGQi/tIq8f169cJgmDbNxv3qtnZWf5x7vz45Y3JdOLUH8QTXzxggqBE88Yi5XIFjSTT4PoBQkiktFgNO7hu8dIhTTMsS+E4DkpZgBxaU8n17wy6z7qpirjW2gz3N5tNulfvsDPV63Uaiw2mp3exvLyMlMUDLl68yL59+4mSDh9+OMfOnTsRQuA4Nq7r4nk+jmNTq+0oGsOgli4tNdF5Tpr10bqYVS2r2NNoFHG+5urjr7/+jZ++9trvhzAjugX0zRNvi8OHnjHtcBWtNd1uMYSEYUitVuNGc4W9e/chhECIYkwDMEbT76c0m00A8iyj3WkB6yC301aQsAkowNun3hFHDj1tkiSh1WphjEEpyeXLlzHC4sqVK8zM7B7GqBKGd8+8d0cw96pNQaGIV4Cnnjiw4TTz7pmzAuDqYmP08gPXlqBrWgP7X+u2oJ8W/QeNmD3lCnMBgQAAAABJRU5ErkJggg==", 13 | "brush": "rock", 14 | "threshold": 50, 15 | "randomFlipHorizontally": true 16 | } 17 | } -------------------------------------------------------------------------------- /examples/tools/template-rock.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/examples/tools/template-rock.zip -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sand Game JS 7 | 8 | 10 | 12 | 20 | 21 | 22 | 23 | 55 | 56 | 57 |
58 |

Sand Game JS

59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 | Loading... 68 |
69 |
70 |
71 |
72 |
73 |
74 | 79 | 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sand-game-js", 3 | "version": "1.0.0-SNAPSHOT", 4 | "copyright": "/* Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved */", 5 | "dependencies": { 6 | "cubic-spline": "^3.0.3", 7 | "fflate": "^0.7.4", 8 | "file-saver": "^2.0.5", 9 | "simplex-noise": "^4.0.1" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-commonjs": "^24.0.1", 13 | "@rollup/plugin-image": "^3.0.2", 14 | "@rollup/plugin-json": "^6.1.0", 15 | "@rollup/plugin-node-resolve": "^15.0.1", 16 | "@rollup/plugin-terser": "^0.4.3", 17 | "cssnano": "^6.0.2", 18 | "postcss-header": "^3.0.3", 19 | "rollup": "^2.70.1", 20 | "rollup-plugin-postcss": "^4.0.2", 21 | "rollup-plugin-string": "^3.0.0", 22 | "serve": "^14.2.1" 23 | }, 24 | "scripts": { 25 | "build": "rollup -c", 26 | "dev": "rollup -c -w", 27 | "test": "node test/test.js", 28 | "pretest": "npm run build", 29 | "serve": "serve" 30 | }, 31 | "files": [ 32 | "dist" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | :bulb: **Note: Sand Game JS is now being developed as part of the Sand Saga codebase since the release of [SandSaga.com](https://sandsaga.com) on April 1, 2024.** 2 | 3 | # Sand Game JS 4 | 5 | Sand Game JS is a fast and powerful falling-sand game engine for desktop & mobile browsers. 6 | It allows players to experiment with various elements, such as sand, soil, water and fire. 7 | It is primarily tested on Google Chrome and Google Chrome for Android. 8 | WebGL 2 is utilized for fast rendering. 9 | 10 | **You can play it here: https://sandsaga.com** 11 | 12 | The engine itself contains 5 scenes and some tools (see image below). 13 | But it allows for defining custom elements, tools, templates, scenes, objectives, and customization of various settings. 14 | 15 | Engine web page: https://harag.cz/app/sand-game-js 16 | 17 | Dev build: https://harag.cz/app/sand-game-js?stage=dev (with test tools enabled, sometimes with experimental changes) 18 | 19 | Note: Sand Game JS is a browser-based successor to [Sand Game 2](https://github.com/Hartrik/Sand-Game-2), which was originally developed in Java (JavaFX) from 2014 to 2016~17. 20 | 21 | 22 | ## Preview 23 | 24 | ![Sand Game JS preview](https://files.harag.cz/www/app/sand-game-js/preview-with-gui.png) 25 | 26 | With grass and trees growing on soil, and other natural processes, it offers a unique experience. 27 | 28 | 29 | ## Development 30 | 31 | **Read the license before forking!** 32 | 33 | ### Build 34 | 35 | Install [Node](https://nodejs.org/en) which contains npm. 36 | 37 | `npm install` downloads dependencies.. 38 | 39 | `npm run build` builds the library to `dist`. 40 | 41 | `npm run dev` builds the library, then keeps rebuilding it whenever the source files change using rollup-watch. 42 | 43 | `npm test` builds the library, then tests it. 44 | 45 | ### Run 46 | 47 | A web server is needed to open index.html correctly. 48 | - IDEs like IntelliJ IDEA start web server automatically. 49 | - `npm run serve` starts web server from command line, http://localhost:3000 50 | 51 | ### Debugging tips 52 | 53 | - Use `alt` + `ctrl` + `shift` + `middle mouse button` to debug an element. 54 | - Stop processing using `ctrl` + `enter` and then press (or hold) `ctrl` + `space` for running one simulation iteration. 55 | - Alternatively `ctrl` + `shift` + `space` will run the specified number of iterations – at once, without rendering and delays. 56 | - Global variables, accessible from browser console: `sandGame`, `brushes` 57 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import image from '@rollup/plugin-image'; 4 | import json from '@rollup/plugin-json'; 5 | import { string } from "rollup-plugin-string"; 6 | import terser from '@rollup/plugin-terser'; 7 | import postcss from 'rollup-plugin-postcss'; 8 | import header from "postcss-header"; 9 | import cssnano from "cssnano"; 10 | import pkg from './package.json'; 11 | 12 | const PLUGINS_COMMON = [ 13 | resolve(), // so Rollup can find libraries 14 | commonjs(), // so Rollup can convert libraries to an ES modules 15 | 16 | image({ 17 | include: [ 18 | "**/assets/**.png", 19 | "**/assets/brushes/**.png", 20 | "**/assets/templates/**.png" 21 | ], 22 | exclude: [] 23 | }), 24 | string({ 25 | include: [ 26 | "**/assets/*.svg", 27 | "**/assets/*.csv", 28 | "**/assets/brushes/*.csv", 29 | ], 30 | exclude: [] 31 | }), 32 | json({ 33 | compact: true, 34 | include: [ 35 | "**/assets/structures/*.json", 36 | ], 37 | exclude: [] 38 | }) 39 | ]; 40 | 41 | 42 | let OUTPUTS = [ 43 | { 44 | // browser-friendly UMD build 45 | name: 'SandGameJS', 46 | file: 'dist/sand-game-js.umd.js', 47 | banner: pkg.copyright, 48 | format: 'umd', 49 | sourcemap: true, 50 | } 51 | ] 52 | 53 | const devBuild = process.env.npm_lifecycle_script.endsWith('-w'); 54 | if (devBuild) { 55 | console.log('DEV build'); 56 | } else { 57 | console.log('PROD build'); 58 | 59 | const PLUGINS_MIN = [ 60 | terser({ 61 | sourceMap: true, 62 | format: { 63 | preamble: pkg.copyright, 64 | comments: false 65 | } 66 | }) 67 | ]; 68 | 69 | OUTPUTS.push({ 70 | // browser-friendly UMD build, MINIMIZED 71 | name: 'SandGameJS', 72 | file: 'dist/sand-game-js.umd.min.js', 73 | format: 'umd', 74 | sourcemap: true, 75 | plugins: PLUGINS_MIN 76 | }); 77 | } 78 | export default [ 79 | { 80 | input: 'src/main.js', 81 | plugins: PLUGINS_COMMON, 82 | output: OUTPUTS 83 | }, 84 | { 85 | input: 'src/style.css', 86 | plugins: [ 87 | postcss({ 88 | extract: true, 89 | modules: false, 90 | sourceMap: true, 91 | plugins: [ 92 | cssnano({ 93 | preset: 'default', 94 | }), 95 | header({ 96 | header: pkg.copyright, 97 | }) 98 | ], 99 | }), 100 | ], 101 | output: { 102 | file: 'dist/sand-game-js.css', 103 | }, 104 | onwarn(warning, warn) { 105 | if (warning.code === 'FILE_NAME_CONFLICT') return; 106 | warn(warning); 107 | } 108 | }, 109 | ]; 110 | -------------------------------------------------------------------------------- /src/Analytics.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ToolDefs from "./def/ToolDefs.js"; 4 | 5 | /** 6 | * 7 | * @version 2024-01-21 8 | * @author Patrik Harag 9 | */ 10 | export default class Analytics { 11 | 12 | static EVENT_NAME = 'app_sand_game_js'; 13 | static FEATURE_APP_INITIALIZED = 'initialized'; 14 | static FEATURE_SCENARIO_COMPLETED = 's_completed'; 15 | static FEATURE_RENDERER_FALLBACK = 'renderer_fallback'; 16 | 17 | // options bar 18 | static FEATURE_PAUSE = 'pause'; 19 | static FEATURE_DRAW_PRIMARY = 'draw_primary'; 20 | static FEATURE_DRAW_SECONDARY = 'draw_secondary'; 21 | static FEATURE_DRAW_TERTIARY = 'draw_tertiary'; 22 | static FEATURE_DRAW_LINE = 'draw_line'; 23 | static FEATURE_DRAW_RECT = 'draw_rect'; 24 | static FEATURE_DRAW_FLOOD = 'draw_flood'; 25 | static FEATURE_STATUS_DISPLAYED = 'status_displayed'; 26 | static FEATURE_OPTIONS_DISPLAYED = 'options_displayed'; 27 | static FEATURE_RENDERER_PIXELATED = 'renderer_pixelated'; 28 | static FEATURE_RENDERER_SHOW_CHUNKS = 'renderer_show_chunks'; 29 | static FEATURE_RENDERER_SHOW_HEATMAP = 'renderer_show_heatmap'; 30 | static FEATURE_CANVAS_SIZE_CHANGE = 'canvas_size_change'; 31 | static FEATURE_SWITCH_SCENE = 'switch_scene'; 32 | static FEATURE_RESTART_SCENE = 'restart_scene'; 33 | static FEATURE_SWITCH_SCALE = 'switch_scale'; 34 | static FEATURE_IO_EXPORT = 'io_export'; 35 | static FEATURE_IO_IMPORT = 'io_import'; 36 | static FEATURE_IO_IMAGE_TEMPLATE = 'io_image_template'; 37 | 38 | static #USED_FEATURES = new Set(); 39 | 40 | 41 | static triggerToolUsed(tool) { 42 | const category = tool.getInfo().getCategory(); 43 | if (category === ToolDefs.CATEGORY_BRUSH) { 44 | const feature = 'brush_' + tool.getInfo().getCodeName(); 45 | Analytics.triggerFeatureUsed(feature); 46 | } else if (category === ToolDefs.CATEGORY_TEMPLATE) { 47 | Analytics.triggerFeatureUsed('brush_template'); 48 | } 49 | } 50 | 51 | static triggerFeatureUsed(feature) { 52 | if (!Analytics.#USED_FEATURES.has(feature)) { 53 | // report only the first usage 54 | Analytics.#USED_FEATURES.add(feature); 55 | Analytics.#report({ 56 | 'app_sand_game_js_feature': feature 57 | }); 58 | } 59 | } 60 | 61 | static #report(properties) { 62 | if (typeof gtag === 'function') { 63 | try { 64 | gtag('event', Analytics.EVENT_NAME, properties); 65 | } catch (e) { 66 | console.warn(e); 67 | } 68 | } 69 | // console.log('event: ' + Analytics.EVENT_NAME + ' = ' + JSON.stringify(properties)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Assets.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2023-12-09 7 | */ 8 | export default class Assets { 9 | 10 | /** 11 | * 12 | * @param base64 13 | * @param maxWidth {number|undefined} 14 | * @param maxHeight {number|undefined} 15 | * @returns {Promise} 16 | */ 17 | static asImageData(base64, maxWidth=undefined, maxHeight=undefined) { 18 | function countSize(imageWidth, imageHeight) { 19 | let w = imageWidth; 20 | let h = imageHeight; 21 | 22 | if (maxWidth !== undefined && w > maxWidth) { 23 | const wScale = w / maxWidth; 24 | w = maxWidth; 25 | h = h / wScale; 26 | } 27 | if (maxHeight !== undefined && h > maxHeight) { 28 | const hScale = h / maxHeight; 29 | h = maxHeight; 30 | w = w / hScale; 31 | } 32 | 33 | return [Math.trunc(w), Math.trunc(h)]; 34 | } 35 | 36 | return new Promise((resolve, reject) => { 37 | try { 38 | // http://stackoverflow.com/questions/3528299/get-pixel-color-of-base64-png-using-javascript 39 | let image = new Image(); 40 | image.onload = () => { 41 | let canvas = document.createElement('canvas'); 42 | let [w, h] = countSize(image.width, image.height); 43 | canvas.width = w; 44 | canvas.height = h; 45 | 46 | let context = canvas.getContext('2d'); 47 | context.drawImage(image, 0, 0, canvas.width, canvas.height); 48 | 49 | let imageData = context.getImageData(0, 0, canvas.width, canvas.height); 50 | resolve(imageData); 51 | }; 52 | image.onerror = () => { 53 | reject('Cannot load image'); 54 | }; 55 | image.src = base64; 56 | } catch (e) { 57 | reject(e); 58 | } 59 | }); 60 | } 61 | } -------------------------------------------------------------------------------- /src/core/CircleIterator.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2023-07-23 7 | */ 8 | export default class CircleIterator { 9 | 10 | // This may look ugly but it's all I need 11 | 12 | // BLUEPRINT_3 and BLUEPRINT_4 are not needed, but they are used frequently 13 | 14 | static BLUEPRINT_3 = [ 15 | ' 333', 16 | ' 32223', 17 | '3211123', 18 | '3210123', 19 | '3211123', 20 | ' 32223', 21 | ' 333', 22 | ]; 23 | 24 | static BLUEPRINT_4 = [ 25 | ' 444 ', 26 | ' 43334', 27 | ' 4322234', 28 | '432111234', 29 | '432101234', 30 | '432111234', 31 | ' 4322234', 32 | ' 43334', 33 | ' 444' 34 | ]; 35 | 36 | static BLUEPRINT_9 = [ 37 | ' 99999 ', 38 | ' 998888899', 39 | ' 9988777778899', 40 | ' 998776666677899', 41 | ' 987665555566789', 42 | ' 98766554445566789', 43 | ' 98765543334556789', 44 | '9876554322234556789', 45 | '9876543211123456789', 46 | '9876543210123456789', 47 | '9876543211123456789', 48 | '9876554322234556789', 49 | ' 98765543334556789', 50 | ' 98766554445566789', 51 | ' 987665555566789', 52 | ' 998776666677899', 53 | ' 9988777778899', 54 | ' 998888899', 55 | ' 99999' 56 | ]; 57 | 58 | /** 59 | * 60 | * @param blueprint {string[]} 61 | * @param handler {function(dx: number, dy: number, level: number)} 62 | */ 63 | static iterate(blueprint, handler) { 64 | const w = blueprint[0].length; 65 | const h = blueprint.length; 66 | const offsetX = Math.trunc(w / 2); 67 | const offsetY = Math.trunc(h / 2); 68 | 69 | for (let i = 0; i < blueprint.length; i++) { 70 | const row = blueprint[i]; 71 | for (let j = 0; j < row.length; j++) { 72 | const char = row.charAt(j); 73 | if (char !== ' ') { 74 | handler(j - offsetX, i - offsetY, +char); 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/core/Counter.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2022-09-25 7 | */ 8 | export default class Counter { 9 | 10 | #currentValue = 0; 11 | #lastValue = 0; 12 | #start = 0; 13 | 14 | tick(currentTimeMillis) { 15 | this.#currentValue++; 16 | if (currentTimeMillis - this.#start >= 1000) { 17 | this.#lastValue = this.#currentValue; 18 | this.#currentValue = 0; 19 | this.#start = currentTimeMillis; 20 | } 21 | } 22 | 23 | getValue() { 24 | return this.#lastValue; 25 | } 26 | 27 | clear() { 28 | this.#lastValue = 0; 29 | } 30 | } -------------------------------------------------------------------------------- /src/core/DeterministicRandom.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * Custom random implementation: "Mulberry32" 5 | * https://stackoverflow.com/questions/521295/seeding-the-random-number-generator-in-javascript/47593316#47593316 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-20 9 | */ 10 | export default class DeterministicRandom { 11 | 12 | static DEFAULT = new DeterministicRandom(106244033); 13 | 14 | static next(seed) { 15 | let t = seed + 0x6D2B79F5; 16 | t = Math.imul(t ^ t >>> 15, t | 1); 17 | t ^= t + Math.imul(t ^ t >>> 7, t | 61); 18 | return ((t ^ t >>> 14) >>> 0) / 4294967296; 19 | } 20 | 21 | /** @type number */ 22 | #last; 23 | 24 | constructor(seed) { 25 | this.#last = seed; 26 | } 27 | 28 | /** 29 | * 30 | * @return {number} (0..1) 31 | */ 32 | next() { 33 | let t = this.#last += 0x6D2B79F5; 34 | t = Math.imul(t ^ t >>> 15, t | 1); 35 | t ^= t + Math.imul(t ^ t >>> 7, t | 61); 36 | return ((t ^ t >>> 14) >>> 0) / 4294967296; 37 | } 38 | 39 | /** 40 | * 41 | * @param max 42 | * @return {number} <0..max) 43 | */ 44 | nextInt(max) { 45 | return Math.trunc(this.next() * max); 46 | } 47 | 48 | /** 49 | * Generator state. 50 | * 51 | * @return {number} integer 52 | */ 53 | getState() { 54 | return this.#last; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/core/Element.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2022-09-09 7 | */ 8 | export default class Element { 9 | elementHead; 10 | elementTail; 11 | 12 | constructor(elementHead = 0, elementTail = 0) { 13 | this.elementHead = elementHead; 14 | this.elementTail = elementTail; 15 | } 16 | } -------------------------------------------------------------------------------- /src/core/ElementTail.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * Tools for working with the element tail. 5 | * 6 | * The element head structure: 0x[flags][red][green][blue] (32b) 7 | *
 8 |  *     |            2b  |            2b  | burnt lvl  2b  | blur type  2b  |
 9 |  *     | color red                                                     8b  |
10 |  *     | color green                                                   8b  |
11 |  *     | color blue                                                    8b  |
12 |  * 
13 | * 14 | * @author Patrik Harag 15 | * @version 2023-12-04 16 | */ 17 | export default class ElementTail { 18 | 19 | static BLUR_TYPE_NONE = 0x0; 20 | /** This element acts as a background = blur can be applied over this element */ 21 | static BLUR_TYPE_BACKGROUND = 0x1; 22 | static BLUR_TYPE_1 = 0x2; 23 | 24 | static HEAT_EFFECT_NONE = 0x0; 25 | static HEAT_EFFECT_1 = 0x1; 26 | static HEAT_EFFECT_2 = 0x2; 27 | static HEAT_EFFECT_3 = 0x3; 28 | 29 | static of(r, g, b, blurType=ElementTail.BLUR_TYPE_NONE, heatEffect=0, burntLevel=0) { 30 | let value = 0; 31 | value = (value | (heatEffect & 0x03)) << 2; 32 | value = (value | (burntLevel & 0x03)) << 2; 33 | value = (value | (blurType & 0x03)) << 8; 34 | value = (value | (r & 0xFF)) << 8; 35 | value = (value | (g & 0xFF)) << 8; 36 | value = value | (b & 0xFF); 37 | return value; 38 | } 39 | 40 | static getColorRed(elementTail) { 41 | return (elementTail >> 16) & 0x000000FF; 42 | } 43 | 44 | static getColorGreen(elementTail) { 45 | return (elementTail >> 8) & 0x000000FF; 46 | } 47 | 48 | static getColorBlue(elementTail) { 49 | return elementTail & 0x000000FF; 50 | } 51 | 52 | static getBlurType(elementTail) { 53 | return (elementTail >> 24) & 0x00000003; 54 | } 55 | 56 | static getBurntLevel(elementTail) { 57 | return (elementTail >> 26) & 0x00000003; 58 | } 59 | 60 | static getHeatEffect(elementTail) { 61 | return (elementTail >> 28) & 0x00000003; 62 | } 63 | 64 | static setColor(elementTail, r, g, b) { 65 | elementTail = (elementTail & ~(0x00FF0000)) | (r << 16); 66 | elementTail = (elementTail & ~(0x0000FF00)) | (g << 8); 67 | elementTail = (elementTail & ~(0x000000FF)) | (b); 68 | return elementTail; 69 | } 70 | 71 | static setBlurType(elementTail, blurType) { 72 | return (elementTail & 0xFCFFFFFF) | (blurType << 24); 73 | } 74 | 75 | static setBurntLevel(elementTail, burntLevel) { 76 | return (elementTail & 0xF3FFFFFF) | (burntLevel << 26); 77 | } 78 | } -------------------------------------------------------------------------------- /src/core/FloodFillPainter.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ElementHead from "./ElementHead"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-08-20 9 | */ 10 | export default class FloodFillPainter { 11 | 12 | static NEIGHBOURHOOD_VON_NEUMANN = 0; 13 | static NEIGHBOURHOOD_MOORE = 1; 14 | 15 | 16 | /** @type ElementArea */ 17 | #elementArea; 18 | 19 | /** @type SandGameGraphics */ 20 | #graphics; 21 | 22 | #neighbourhood; 23 | 24 | /** 25 | * 26 | * @param elementArea {ElementArea} 27 | * @param neighbourhood 28 | * @param graphics {SandGameGraphics} 29 | */ 30 | constructor(elementArea, neighbourhood = FloodFillPainter.NEIGHBOURHOOD_VON_NEUMANN, graphics) { 31 | this.#elementArea = elementArea; 32 | this.#neighbourhood = neighbourhood; 33 | this.#graphics = graphics; 34 | } 35 | 36 | /** 37 | * 38 | * @param x {number} 39 | * @param y {number} 40 | * @param brush {Brush} 41 | */ 42 | paint(x, y, brush) { 43 | const pattern = 0b1111_11100111; // TODO: different for fluid, powder-like... 44 | const matcher = this.#normalize(this.#elementArea.getElementHead(x, y)) & pattern; 45 | 46 | const w = this.#elementArea.getWidth(); 47 | 48 | const pointSet = new Set(); 49 | const queue = []; 50 | 51 | let point = x + y * w; 52 | do { 53 | let x = point % w; 54 | let y = Math.trunc(point / w); 55 | 56 | if (pointSet.has(point)) { 57 | continue; // already completed 58 | } 59 | 60 | this.#graphics.draw(x, y, brush); 61 | pointSet.add(point); 62 | 63 | // add neighbours 64 | this.#tryAdd(x, y - 1, pattern, matcher, pointSet, queue); 65 | this.#tryAdd(x + 1, y, pattern, matcher, pointSet, queue); 66 | this.#tryAdd(x, y + 1, pattern, matcher, pointSet, queue); 67 | this.#tryAdd(x - 1, y, pattern, matcher, pointSet, queue); 68 | 69 | if (this.#neighbourhood === FloodFillPainter.NEIGHBOURHOOD_MOORE) { 70 | this.#tryAdd(x + 1, y + 1, pattern, matcher, pointSet, queue); 71 | this.#tryAdd(x + 1, y - 1, pattern, matcher, pointSet, queue); 72 | this.#tryAdd(x - 1, y + 1, pattern, matcher, pointSet, queue); 73 | this.#tryAdd(x - 1, y - 1, pattern, matcher, pointSet, queue); 74 | } 75 | 76 | } while ((point = queue.pop()) != null); 77 | } 78 | 79 | #tryAdd(x, y, pattern, matcher, pointSet, queue) { 80 | const w = this.#elementArea.getWidth(); 81 | const h = this.#elementArea.getHeight(); 82 | 83 | if (x < 0 || y < 0) { 84 | return; 85 | } 86 | if (x >= w || y >= h) { 87 | return; 88 | } 89 | 90 | if (!this.#equals(x, y, pattern, matcher)) { 91 | return; 92 | } 93 | 94 | const point = x + y * w; 95 | if (pointSet.has(point)) { 96 | return; 97 | } 98 | 99 | queue.push(point); 100 | } 101 | 102 | #equals(x, y, pattern, matcher) { 103 | let elementHead = this.#elementArea.getElementHead(x, y); 104 | elementHead = this.#normalize(elementHead); 105 | return (elementHead & pattern) === matcher; 106 | } 107 | 108 | #normalize(elementHead) { 109 | // wetness is ignored 110 | if (ElementHead.getTypeClass(elementHead) === ElementHead.TYPE_POWDER_WET) { 111 | elementHead = ElementHead.setTypeClass(elementHead, ElementHead.TYPE_POWDER); 112 | } 113 | return elementHead; 114 | } 115 | } -------------------------------------------------------------------------------- /src/core/Marker.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @typedef {object} MarkerConfig 5 | * @property {CSSStyleDeclaration} style 6 | * @property {string|HTMLElement} label 7 | * @property {boolean} visible 8 | */ 9 | 10 | /** 11 | * 12 | * @author Patrik Harag 13 | * @version 2024-01-17 14 | */ 15 | export default class Marker { 16 | 17 | /** @type number */ 18 | #x1; 19 | /** @type number */ 20 | #y1; 21 | /** @type number */ 22 | #x2; 23 | /** @type number */ 24 | #y2; 25 | 26 | /** @type MarkerConfig */ 27 | #config; 28 | 29 | /** @type boolean */ 30 | #visible = true; 31 | /** @type function(boolean)[] */ 32 | #onVisibleChanged = []; 33 | 34 | constructor(x1, y1, x2, y2, config) { 35 | this.#x1 = x1; 36 | this.#y1 = y1; 37 | this.#x2 = x2; 38 | this.#y2 = y2; 39 | this.#config = config; 40 | this.#visible = config.visible === true; 41 | } 42 | 43 | getPosition() { 44 | return [ this.#x1, this.#y1, this.#x2, this.#y2 ]; 45 | } 46 | 47 | /** 48 | * 49 | * @returns {MarkerConfig} 50 | */ 51 | getConfig() { 52 | return this.#config; 53 | } 54 | 55 | // visibility 56 | 57 | isVisible() { 58 | return this.#visible; 59 | } 60 | 61 | setVisible(visible) { 62 | if (this.#visible !== visible) { 63 | // handlers are triggered only on change 64 | this.#visible = visible; 65 | for (let handler of this.#onVisibleChanged) { 66 | handler(visible); 67 | } 68 | } 69 | } 70 | 71 | addOnVisibleChanged(handler) { 72 | this.#onVisibleChanged.push(handler); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/core/Objective.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @typedef {object} ObjectiveConfig 5 | * @property {string} name 6 | * @property {string} description 7 | * @property {boolean} visible 8 | * @property {boolean} active 9 | * @property {function(iteration:number)} checkHandler 10 | */ 11 | 12 | /** 13 | * 14 | * @author Patrik Harag 15 | * @version 2024-01-13 16 | */ 17 | export default class Objective { 18 | 19 | /** @type ObjectiveConfig */ 20 | #config; 21 | 22 | /** @type boolean */ 23 | #visible; 24 | /** @type function(boolean)[] */ 25 | #onVisibleChanged = []; 26 | 27 | /** @type boolean */ 28 | #active; 29 | /** @type function(boolean)[] */ 30 | #onActiveChanged = []; 31 | 32 | /** @type boolean */ 33 | #completed = false; 34 | /** @type function(boolean)[] */ 35 | #onCompletedChanged = []; 36 | 37 | /** 38 | * 39 | * @param config {ObjectiveConfig} 40 | */ 41 | constructor(config) { 42 | this.#config = config; 43 | this.#visible = config.visible === true; 44 | this.#active = config.active === true; 45 | } 46 | 47 | getConfig() { 48 | return this.#config; // TODO: immutable 49 | } 50 | 51 | // visible 52 | 53 | isVisible() { 54 | return this.#visible; 55 | } 56 | 57 | setVisible(visible) { 58 | if (this.#visible !== visible) { 59 | // handlers are triggered only on change 60 | this.#visible = visible; 61 | for (let handler of this.#onVisibleChanged) { 62 | handler(visible); 63 | } 64 | } 65 | } 66 | 67 | addOnVisibleChanged(handler) { 68 | this.#onVisibleChanged.push(handler); 69 | } 70 | 71 | // active 72 | 73 | isActive() { 74 | return this.#active; 75 | } 76 | 77 | setActive(active) { 78 | if (this.#active !== active) { 79 | // handlers are triggered only on change 80 | this.#active = active; 81 | for (let handler of this.#onActiveChanged) { 82 | handler(active); 83 | } 84 | } 85 | } 86 | 87 | addOnActiveChanged(handler) { 88 | this.#onActiveChanged.push(handler); 89 | } 90 | 91 | // status 92 | 93 | isCompleted() { 94 | return this.#completed; 95 | } 96 | 97 | setCompleted(completed) { 98 | if (this.#completed !== completed) { 99 | // handlers are triggered only on change 100 | this.#completed = completed; 101 | for (let handler of this.#onCompletedChanged) { 102 | handler(completed); 103 | } 104 | } 105 | } 106 | 107 | addOnCompleted(handler) { 108 | this.#onCompletedChanged.push(handler); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/core/SandGameOverlay.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Marker from "./Marker"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2024-01-08 9 | */ 10 | export default class SandGameOverlay { 11 | 12 | /** @type number */ 13 | #width; 14 | /** @type number */ 15 | #height; 16 | 17 | /** @type Marker[] */ 18 | #markers = []; 19 | 20 | /** @type function(Marker)[] */ 21 | #onMarkerAdded = []; 22 | 23 | constructor(width, height) { 24 | this.#width = width; 25 | this.#height = height; 26 | } 27 | 28 | /** 29 | * 30 | * @param x1 31 | * @param y1 32 | * @param x2 33 | * @param y2 34 | * @param config {MarkerConfig} 35 | * @param supportNegativeCoordinates {boolean} 36 | * @returns {Marker} 37 | */ 38 | createRectangle(x1, y1, x2, y2, config, supportNegativeCoordinates = false) { 39 | x1 = Math.trunc(x1); 40 | y1 = Math.trunc(y1); 41 | x2 = Math.trunc(x2); 42 | y2 = Math.trunc(y2); 43 | 44 | if (supportNegativeCoordinates) { 45 | x1 = (x1 >= 0) ? x1 : this.#width + x1 + 1; 46 | x2 = (x2 >= 0) ? x2 : this.#width + x2 + 1; 47 | y1 = (y1 >= 0) ? y1 : this.#height + y1 + 1; 48 | y2 = (y2 >= 0) ? y2 : this.#height + y2 + 1; 49 | } 50 | 51 | x1 = Math.max(Math.min(x1, this.#width - 1), 0); 52 | x2 = Math.max(Math.min(x2, this.#width - 1), 0); 53 | y1 = Math.max(Math.min(y1, this.#height - 1), 0); 54 | y2 = Math.max(Math.min(y2, this.#height - 1), 0); 55 | 56 | const marker = new Marker(x1, y1, x2, y2, config); 57 | this.#markers.push(marker); 58 | for (let handler of this.#onMarkerAdded) { 59 | handler(marker); 60 | } 61 | 62 | return marker; 63 | } 64 | 65 | createRectangleWH(x, y, w, h, cssStyles) { 66 | return this.createRectangle(x, y, x + w, y + h, cssStyles); 67 | } 68 | 69 | /** 70 | * 71 | * @returns {Marker[]} 72 | */ 73 | getMarkers() { 74 | return [...this.#markers]; 75 | } 76 | 77 | addOnMarkerAdded(handler) { 78 | this.#onMarkerAdded.push(handler); 79 | } 80 | 81 | getWidth() { 82 | return this.#width; 83 | } 84 | 85 | getHeight() { 86 | return this.#height 87 | } 88 | } -------------------------------------------------------------------------------- /src/core/SandGameScenario.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Splash from "./Splash"; 4 | import Objective from "./Objective"; 5 | import Analytics from "../Analytics"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-01-11 11 | */ 12 | export default class SandGameScenario { 13 | 14 | /** @type Splash[] */ 15 | #splashes = []; 16 | 17 | /** @type function(Splash)[] */ 18 | #onSplashAdded = []; 19 | 20 | /** @type Objective[] */ 21 | #objectives = []; 22 | 23 | /** @type function(Objective)[] */ 24 | #onObjectiveAdded = []; 25 | 26 | #statusCompleted = false; 27 | 28 | /** @type function()[] */ 29 | #onStatusCompleted = []; 30 | 31 | constructor() { 32 | // empty 33 | } 34 | 35 | // splash 36 | 37 | /** 38 | * 39 | * @param config {SplashConfig} 40 | * @returns {Splash} 41 | */ 42 | createSplash(config) { 43 | const splash = new Splash(config); 44 | this.#splashes.push(splash); 45 | for (let handler of this.#onSplashAdded) { 46 | handler(splash); 47 | } 48 | return splash; 49 | } 50 | 51 | getSplashes() { 52 | return [...this.#splashes]; 53 | } 54 | 55 | /** 56 | * 57 | * @param handler {function(Splash)} 58 | */ 59 | addOnSplashAdded(handler) { 60 | this.#onSplashAdded.push(handler); 61 | } 62 | 63 | // objectives 64 | 65 | /** 66 | * 67 | * @param config {ObjectiveConfig} 68 | * @returns {Objective} 69 | */ 70 | createObjective(config) { 71 | const objective = new Objective(config); 72 | this.#objectives.push(objective); 73 | for (let handler of this.#onObjectiveAdded) { 74 | handler(objective); 75 | } 76 | return objective; 77 | } 78 | 79 | getObjectives() { 80 | return [...this.#objectives]; 81 | } 82 | 83 | /** 84 | * 85 | * @param handler {function(Objective)} 86 | */ 87 | addOnObjectiveAdded(handler) { 88 | this.#onObjectiveAdded.push(handler); 89 | } 90 | 91 | // status 92 | 93 | setCompleted() { 94 | if (!this.#statusCompleted) { 95 | this.#statusCompleted = true; 96 | Analytics.triggerFeatureUsed(Analytics.FEATURE_SCENARIO_COMPLETED); 97 | for (let handler of this.#onStatusCompleted) { 98 | handler(); 99 | } 100 | } 101 | } 102 | 103 | addOnStatusCompleted(handler) { 104 | this.#onStatusCompleted.push(handler); 105 | } 106 | } -------------------------------------------------------------------------------- /src/core/Snapshot.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2023-10-11 7 | */ 8 | export default class Snapshot { 9 | 10 | /** @type SnapshotMetadata */ 11 | metadata; 12 | 13 | /** @type ArrayBuffer */ 14 | dataHeads; 15 | 16 | /** @type ArrayBuffer */ 17 | dataTails; 18 | } 19 | -------------------------------------------------------------------------------- /src/core/SnapshotMetadata.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2024-02-01 7 | */ 8 | export default class SnapshotMetadata { 9 | 10 | static CURRENT_FORMAT_VERSION = 6; 11 | 12 | 13 | /** @type number */ 14 | formatVersion; 15 | 16 | /** @type string */ 17 | appVersion; 18 | 19 | /** @type number|undefined */ 20 | created; 21 | 22 | /** @type number|undefined */ 23 | width; 24 | 25 | /** @type number|undefined */ 26 | height; 27 | 28 | /** @type number|undefined */ 29 | scale; 30 | 31 | /** @type number|undefined */ 32 | random; 33 | 34 | /** @type number|undefined */ 35 | iteration; 36 | 37 | /** @type boolean|undefined */ 38 | fallThroughEnabled; 39 | 40 | /** @type boolean|undefined */ 41 | erasingEnabled; 42 | } 43 | -------------------------------------------------------------------------------- /src/core/Splash.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @typedef {object} SplashConfig 5 | * @property {HTMLElement|string} content 6 | * @property {CSSStyleDeclaration} style 7 | * @property {SplashButton[]} buttons 8 | * @property {boolean} visible 9 | */ 10 | /** 11 | * @typedef {object} SplashButton 12 | * @property {string} title 13 | * @property {string} class 14 | * @property {boolean} focus 15 | * @property {function(Splash):void} action 16 | */ 17 | 18 | /** 19 | * 20 | * @author Patrik Harag 21 | * @version 2024-01-13 22 | */ 23 | export default class Splash { 24 | 25 | /** @type SplashConfig */ 26 | #config; 27 | 28 | /** @type boolean */ 29 | #visible; 30 | /** @type function(boolean)[] */ 31 | #onVisibleChanged = []; 32 | 33 | /** 34 | * 35 | * @param config {SplashConfig} 36 | */ 37 | constructor(config) { 38 | this.#config = config; 39 | this.#visible = config.visible === true; 40 | } 41 | 42 | getConfig() { 43 | return this.#config; // TODO: immutable 44 | } 45 | 46 | // visibility 47 | 48 | isVisible() { 49 | return this.#visible; 50 | } 51 | 52 | setVisible(visible) { 53 | if (this.#visible !== visible) { 54 | // handlers are triggered only on change 55 | this.#visible = visible; 56 | for (let handler of this.#onVisibleChanged) { 57 | handler(visible); 58 | } 59 | } 60 | } 61 | 62 | addOnVisibleChanged(handler) { 63 | this.#onVisibleChanged.push(handler); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/core/TemplateBlockPainter.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2022-09-21 7 | */ 8 | export default class TemplateBlockPainter { 9 | 10 | /** @type SandGameGraphics */ 11 | #graphics; 12 | 13 | /** @type string|string[]|null */ 14 | #blueprint = null; 15 | /** @type object|null */ 16 | #brushes = null; 17 | 18 | /** @type number */ 19 | #maxHeight = Number.MAX_SAFE_INTEGER; 20 | 21 | /** @type string */ 22 | #verticalAlign = 'bottom'; 23 | 24 | /** 25 | * 26 | * @param graphics {SandGameGraphics} 27 | */ 28 | constructor(graphics) { 29 | this.#graphics = graphics; 30 | } 31 | 32 | /** 33 | * 34 | * @param blueprint {string|string[]} 35 | * @returns {TemplateBlockPainter} 36 | */ 37 | withBlueprint(blueprint) { 38 | this.#blueprint = blueprint; 39 | return this; 40 | } 41 | 42 | /** 43 | * 44 | * @param brushes 45 | * @returns {TemplateBlockPainter} 46 | */ 47 | withBrushes(brushes) { 48 | this.#brushes = brushes; 49 | return this; 50 | } 51 | 52 | /** 53 | * 54 | * @param maxHeight max template height 55 | * @param align {string} bottom|top 56 | * @returns {TemplateBlockPainter} 57 | */ 58 | withMaxHeight(maxHeight, align = 'bottom') { 59 | this.#maxHeight = maxHeight; 60 | this.#verticalAlign = align; 61 | return this; 62 | } 63 | 64 | paint() { 65 | if (this.#blueprint === null || this.#blueprint.length === 0) { 66 | throw 'Blueprint not set'; 67 | } 68 | if (this.#brushes === null) { 69 | throw 'Brushes not set'; 70 | } 71 | 72 | const blueprint = (typeof this.#blueprint === 'string') 73 | ? this.#blueprint.split('\n') 74 | : this.#blueprint; 75 | 76 | const w = blueprint[0].length; 77 | const h = blueprint.length; 78 | 79 | const ww = Math.ceil(this.#graphics.getWidth() / w); 80 | const hh = Math.ceil(Math.min(this.#graphics.getHeight(), this.#maxHeight) / h); 81 | // note: rounding up is intentional - we don't want gaps, drawRectangle can handle drawing out of canvas 82 | 83 | const verticalOffset = (this.#verticalAlign === 'bottom' ? this.#graphics.getHeight() - (hh * h) : 0); 84 | 85 | for (let y = 0; y < h; y++) { 86 | const line = blueprint[y]; 87 | for (let x = 0; x < Math.min(w, line.length); x++) { 88 | const char = line.charAt(x); 89 | let brush = this.#brushes[char]; 90 | if (brush === undefined) { 91 | if (char === ' ') { 92 | // let this cell empty 93 | continue; 94 | } 95 | throw 'Brush not found: ' + char; 96 | } 97 | this.#graphics.drawRectangle( 98 | x * ww, verticalOffset + (y * hh), 99 | x * ww + ww + 1, verticalOffset + (y * hh) + hh + 1, brush); 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/core/brush/AbstractEffectBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Brush from "./Brush"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2024-02-05 9 | */ 10 | export default class AbstractEffectBrush extends Brush { 11 | 12 | /** @type Brush|undefined */ 13 | #innerBrush; 14 | constructor(innerBrush) { 15 | super(); 16 | this.#innerBrush = innerBrush; 17 | } 18 | 19 | _retrieveElement(x, y, random, oldElement) { 20 | if (this.#innerBrush !== undefined) { 21 | // use inner brush 22 | return this.#innerBrush.apply(x, y, random); 23 | } else { 24 | // use old element 25 | return oldElement; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/core/brush/Brush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Element from "../Element.js"; 4 | import DeterministicRandom from "../DeterministicRandom"; 5 | 6 | /** 7 | * @interface 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-20 11 | */ 12 | export default class Brush { 13 | 14 | /** 15 | * 16 | * @param x 17 | * @param y 18 | * @param random {DeterministicRandom} 19 | * @param oldElement {Element} 20 | * @return {Element} 21 | */ 22 | apply(x, y, random = undefined, oldElement = undefined) { 23 | throw 'Not implemented' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/core/brush/ColorBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import AbstractEffectBrush from "./AbstractEffectBrush"; 4 | import Element from "../Element"; 5 | import ElementTail from "../ElementTail"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-02-05 11 | */ 12 | export default class ColorBrush extends AbstractEffectBrush { 13 | 14 | #r; #g; #b; 15 | 16 | constructor(r, g, b, innerBrush) { 17 | super(innerBrush); 18 | this.#r = r; 19 | this.#g = g; 20 | this.#b = b; 21 | } 22 | 23 | apply(x, y, random, oldElement) { 24 | const element = this._retrieveElement(x, y, random, oldElement); 25 | 26 | const newElementTail = ElementTail.setColor(element.elementTail, this.#r, this.#g, this.#b); 27 | return new Element(element.elementHead, newElementTail); 28 | } 29 | } -------------------------------------------------------------------------------- /src/core/brush/ColorMovingPaletteBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import AbstractEffectBrush from "./AbstractEffectBrush"; 4 | import ElementTail from "../ElementTail"; 5 | import Element from "../Element"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-03-12 11 | */ 12 | export default class ColorMovingPaletteBrush extends AbstractEffectBrush { 13 | 14 | /** @type number[][] */ 15 | #palette; 16 | 17 | /** @type number */ 18 | #stepSize; 19 | 20 | #i = 0; 21 | #direction = 1; 22 | #current = 0; 23 | 24 | constructor(innerBrush, palette, stepSize) { 25 | super(innerBrush); 26 | this.#palette = palette; 27 | this.#stepSize = stepSize; 28 | } 29 | 30 | apply(x, y, random, oldElement) { 31 | const element = this._retrieveElement(x, y, random, oldElement); 32 | 33 | if (this.#palette.length === 0) { 34 | return element; 35 | } 36 | 37 | // retrieve current color 38 | const [r, g, b] = this.#palette[this.#i]; 39 | 40 | if (this.#palette.length > 1) { 41 | // count next index 42 | this.#current += 1; 43 | if (this.#current >= this.#stepSize) { 44 | this.#current = 0; 45 | this.#i += this.#direction; 46 | if (this.#i < 0) { 47 | // switch direction 48 | this.#direction = 1; 49 | this.#i = 1; 50 | } else if (this.#i >= this.#palette.length) { 51 | // switch direction 52 | this.#direction = -1; 53 | this.#i = this.#palette.length - 2; 54 | } 55 | } 56 | } 57 | 58 | const newElementTail = ElementTail.setColor(element.elementTail, r, g, b); 59 | return new Element(element.elementHead, newElementTail); 60 | } 61 | } -------------------------------------------------------------------------------- /src/core/brush/ColorNoiseBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import AbstractEffectBrush from "./AbstractEffectBrush"; 4 | import Element from "../Element"; 5 | import VisualEffects from "../processing/VisualEffects"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-02-06 11 | */ 12 | export default class ColorNoiseBrush extends AbstractEffectBrush { 13 | 14 | #r; #g; #b; 15 | 16 | /** 17 | * @type {object|object[]} 18 | */ 19 | #coefficients; 20 | 21 | #noise; 22 | 23 | constructor(innerBrush, coefficients, r, g, b) { 24 | super(innerBrush); 25 | this.#coefficients = coefficients; 26 | this.#r = r; 27 | this.#g = g; 28 | this.#b = b; 29 | if (Array.isArray(coefficients)) { 30 | this.#noise = VisualEffects.visualNoiseProvider(coefficients.map(c => c.seed)); 31 | } else { 32 | this.#noise = VisualEffects.visualNoiseProvider(coefficients.seed); 33 | } 34 | } 35 | 36 | apply(x, y, random, oldElement) { 37 | const element = this._retrieveElement(x, y, random, oldElement); 38 | 39 | const coefficients = this.#coefficients; 40 | const r = this.#r; // it could be null... 41 | const g = this.#g; 42 | const b = this.#b; 43 | 44 | let newElementTail; 45 | if (Array.isArray(coefficients)) { 46 | newElementTail = this.#noise.visualNoiseCombined(element.elementTail, x, y, coefficients, r, g, b); 47 | } else { 48 | const factor = coefficients.factor; // it could be null... 49 | const threshold = coefficients.threshold; 50 | const force = coefficients.force; 51 | newElementTail = this.#noise.visualNoise(element.elementTail, x, y, factor, threshold, force, r, g, b); 52 | } 53 | return new Element(element.elementHead, newElementTail); 54 | } 55 | } -------------------------------------------------------------------------------- /src/core/brush/ColorPaletteRandomBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import AbstractEffectBrush from "./AbstractEffectBrush"; 4 | import DeterministicRandom from "../DeterministicRandom"; 5 | import ElementTail from "../ElementTail"; 6 | import Element from "../Element"; 7 | 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2024-02-05 12 | */ 13 | export default class ColorPaletteRandomBrush extends AbstractEffectBrush { 14 | 15 | /** @type number[][] */ 16 | #palette; 17 | 18 | constructor(innerBrush, palette) { 19 | super(innerBrush); 20 | if (typeof palette === 'string') { 21 | // parse 22 | this.#palette = palette.split('\n').map(line => line.split(',').map(Number)); 23 | } else { 24 | this.#palette = palette; 25 | } 26 | } 27 | 28 | apply(x, y, random, oldElement) { 29 | const element = this._retrieveElement(x, y, random, oldElement); 30 | 31 | const i = ((random) ? random : DeterministicRandom.DEFAULT).nextInt(this.#palette.length); 32 | const [r, g, b] = this.#palette[i]; 33 | 34 | const newElementTail = ElementTail.setColor(element.elementTail, r, g, b); 35 | return new Element(element.elementHead, newElementTail); 36 | } 37 | } -------------------------------------------------------------------------------- /src/core/brush/ColorRandomize.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import AbstractEffectBrush from "./AbstractEffectBrush"; 4 | import Element from "../Element"; 5 | import ElementTail from "../ElementTail"; 6 | 7 | /** 8 | * This brush provides a bit of randomness to element colors. 9 | * 10 | * @author Patrik Harag 11 | * @version 2024-03-12 12 | */ 13 | export default class ColorRandomize extends AbstractEffectBrush { 14 | 15 | #maxDiff; 16 | 17 | constructor(innerBrush, maxDiff) { 18 | super(innerBrush); 19 | this.#maxDiff = maxDiff; 20 | } 21 | 22 | apply(x, y, random, oldElement) { 23 | const element = this._retrieveElement(x, y, random, oldElement); 24 | 25 | let r = ElementTail.getColorRed(element.elementTail); 26 | let g = ElementTail.getColorGreen(element.elementTail); 27 | let b = ElementTail.getColorBlue(element.elementTail); 28 | 29 | r += random.nextInt(this.#maxDiff) * (random.nextInt(2) === 0 ? 1 : -1); 30 | g += random.nextInt(this.#maxDiff) * (random.nextInt(2) === 0 ? 1 : -1); 31 | b += random.nextInt(this.#maxDiff) * (random.nextInt(2) === 0 ? 1 : -1); 32 | 33 | r = Math.max(0, Math.min(255, r)); 34 | g = Math.max(0, Math.min(255, g)); 35 | b = Math.max(0, Math.min(255, b)); 36 | 37 | const newElementTail = ElementTail.setColor(element.elementTail, r, g, b); 38 | return new Element(element.elementHead, newElementTail); 39 | } 40 | } -------------------------------------------------------------------------------- /src/core/brush/ColorTextureBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import AbstractEffectBrush from "./AbstractEffectBrush"; 4 | import Assets from "../../Assets"; 5 | import ElementTail from "../ElementTail"; 6 | import Element from "../Element"; 7 | 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2024-02-05 12 | */ 13 | export default class ColorTextureBrush extends AbstractEffectBrush { 14 | 15 | /** @type ImageData|null */ 16 | #imageData = null; 17 | 18 | constructor(innerBrush, base64) { 19 | super(innerBrush); 20 | 21 | Assets.asImageData(base64).then(imageData => this.#imageData = imageData); 22 | } 23 | 24 | apply(x, y, random, oldElement) { 25 | const element = this._retrieveElement(x, y, random, oldElement); 26 | 27 | if (this.#imageData != null) { 28 | const cx = x % this.#imageData.width; 29 | const cy = y % this.#imageData.height; 30 | const index = (cy * this.#imageData.width + cx) * 4; 31 | 32 | const red = this.#imageData.data[index]; 33 | const green = this.#imageData.data[index + 1]; 34 | const blue = this.#imageData.data[index + 2]; 35 | // const alpha = this.#imageData.data[index + 3]; 36 | 37 | const newElementTail = ElementTail.setColor(element.elementTail, red, green, blue); 38 | return new Element(element.elementHead, newElementTail); 39 | 40 | } else { 41 | return element; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/core/brush/CountingBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Brush from "./Brush"; 4 | 5 | /** 6 | * Special brush for counting elements. 7 | * 8 | * @author Patrik Harag 9 | * @version 2024-01-10 10 | */ 11 | export default class CountingBrush extends Brush { 12 | 13 | static #NULL_REDICATE = function (elementHead, elementTail) { 14 | return false; 15 | }; 16 | 17 | #predicate; 18 | 19 | #counterPositives = 0; 20 | #counterTotal = 0; 21 | 22 | /** 23 | * 24 | * @param predicate {function(number:elementHead, number:elementTail):boolean} 25 | */ 26 | constructor(predicate = CountingBrush.#NULL_REDICATE) { 27 | super(); 28 | this.#predicate = predicate; 29 | } 30 | 31 | apply(x, y, random, oldElement) { 32 | this.#counterTotal++; 33 | if (oldElement !== null) { 34 | if (this.#predicate(oldElement.elementHead, oldElement.elementTail)) { 35 | this.#counterPositives++; 36 | } 37 | } 38 | return null; 39 | } 40 | 41 | getPositives() { 42 | return this.#counterPositives; 43 | } 44 | 45 | getTotal() { 46 | return this.#counterTotal; 47 | } 48 | 49 | reset() { 50 | this.#counterTotal = 0; 51 | this.#counterPositives = 0; 52 | } 53 | } -------------------------------------------------------------------------------- /src/core/brush/CustomBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Brush from "./Brush"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-02-20 9 | */ 10 | export default class CustomBrush extends Brush { 11 | 12 | /** @type function(x: number, y: number, random: DeterministicRandom, oldElement: Element) */ 13 | #func; 14 | 15 | constructor(func) { 16 | super(); 17 | this.#func = func; 18 | } 19 | 20 | apply(x, y, random, oldElement) { 21 | return this.#func(x, y, random, oldElement); 22 | } 23 | } -------------------------------------------------------------------------------- /src/core/brush/MeltingBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ElementHead from "../ElementHead"; 4 | import ElementTail from "../ElementTail"; 5 | import Element from "../Element"; 6 | import Brush from "./Brush"; 7 | 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2024-02-06 12 | */ 13 | export default class MeltingBrush extends Brush { 14 | 15 | apply(x, y, random, oldElement) { 16 | if (oldElement === null) { 17 | return null; 18 | } 19 | 20 | const heatModIndex = ElementHead.getHeatModIndex(oldElement.elementHead); 21 | if (ElementHead.hmiToMeltingTemperature(heatModIndex) < (1 << ElementHead.FIELD_TEMPERATURE_SIZE)) { 22 | let newElementHead = oldElement.elementHead; 23 | newElementHead = ElementHead.setHeatModIndex(newElementHead, ElementHead.hmiToMeltingHMI(heatModIndex)); 24 | newElementHead = ElementHead.setType(newElementHead, ElementHead.TYPE_FLUID); 25 | 26 | let newElementTail = ElementTail.setBlurType(oldElement.elementTail, ElementTail.BLUR_TYPE_1); 27 | 28 | return new Element(newElementHead, newElementTail); 29 | } 30 | return oldElement; 31 | } 32 | } -------------------------------------------------------------------------------- /src/core/brush/RandomBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DeterministicRandom from "../DeterministicRandom"; 4 | import Brush from "./Brush"; 5 | 6 | /** 7 | * 8 | * @author Patrik Harag 9 | * @version 2024-01-29 10 | */ 11 | export default class RandomBrush extends Brush { 12 | 13 | /** @type Brush[] */ 14 | #list; 15 | 16 | constructor(list) { 17 | super(); 18 | this.#list = list; 19 | } 20 | 21 | apply(x, y, random, oldElement) { 22 | if (this.#list.length > 1) { 23 | const i = ((random) ? random : DeterministicRandom.DEFAULT).nextInt(this.#list.length); 24 | const item = this.#list[i]; 25 | return item.apply(x, y, random, oldElement); 26 | } else if (this.#list.length === 1) { 27 | const item = this.#list[0]; 28 | return item.apply(x, y, random, oldElement); 29 | } else { 30 | return null; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/core/brush/RandomElementBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DeterministicRandom from "../DeterministicRandom"; 4 | import Brush from "./Brush"; 5 | 6 | /** 7 | * 8 | * @author Patrik Harag 9 | * @version 2024-01-29 10 | */ 11 | export default class RandomElementBrush extends Brush { 12 | 13 | /** @type Element[] */ 14 | #elements; 15 | 16 | constructor(elements) { 17 | super(); 18 | this.#elements = elements; 19 | } 20 | 21 | apply(x, y, random, oldElement) { 22 | if (this.#elements.length > 1) { 23 | const i = ((random) ? random : DeterministicRandom.DEFAULT).nextInt(this.#elements.length); 24 | return this.#elements[i]; 25 | } else if (this.#elements.length === 1) { 26 | return this.#elements[0]; 27 | } else { 28 | return null; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/core/brush/SolidBodyBrush.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import AbstractEffectBrush from "./AbstractEffectBrush"; 4 | import Element from "../Element"; 5 | import ElementHead from "../ElementHead"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-03-23 11 | */ 12 | export default class SolidBodyBrush extends AbstractEffectBrush { 13 | 14 | #solidBodyId; 15 | #extendedNeighbourhood; 16 | 17 | constructor(solidBodyId, extendedNeighbourhood, innerBrush) { 18 | super(innerBrush); 19 | this.#solidBodyId = solidBodyId; 20 | this.#extendedNeighbourhood = extendedNeighbourhood; 21 | } 22 | 23 | apply(x, y, random, oldElement) { 24 | const element = this._retrieveElement(x, y, random, oldElement); 25 | 26 | let elementHead = element.elementHead; 27 | const newType = ElementHead.type8Solid(ElementHead.TYPE_STATIC, this.#solidBodyId, this.#extendedNeighbourhood); 28 | elementHead = ElementHead.setType(elementHead, newType); 29 | return new Element(elementHead, element.elementTail); 30 | } 31 | } -------------------------------------------------------------------------------- /src/core/processing/ProcessorContext.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2024-03-23 7 | */ 8 | export default class ProcessorContext { 9 | 10 | static OPT_CYCLES_PER_SECOND = 120; 11 | static OPT_FRAMES_PER_SECOND = 60; 12 | 13 | 14 | /** 15 | * @returns number 16 | */ 17 | getIteration() { 18 | throw 'Not implemented'; 19 | } 20 | 21 | /** 22 | * @returns ProcessorDefaults 23 | */ 24 | getDefaults() { 25 | throw 'Not implemented'; 26 | } 27 | 28 | /** 29 | * @returns {boolean} 30 | */ 31 | isFallThroughEnabled() { 32 | throw 'Not implemented'; 33 | } 34 | 35 | /** 36 | * @returns {boolean} 37 | */ 38 | isErasingEnabled() { 39 | throw 'Not implemented'; 40 | } 41 | 42 | trigger(x, y) { 43 | throw 'Not implemented'; 44 | } 45 | 46 | triggerSolidCreated(elementHead, x, y) { 47 | throw 'Not implemented'; 48 | } 49 | } -------------------------------------------------------------------------------- /src/core/processing/ProcessorDefaults.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Element from "../Element.js"; 4 | import Brush from "../brush/Brush.js"; 5 | 6 | /** 7 | * @interface 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-17 11 | */ 12 | export default class ProcessorDefaults { 13 | 14 | /** 15 | * @return Element 16 | */ 17 | getDefaultElement() { 18 | throw 'Not implemented'; 19 | } 20 | 21 | // brushes 22 | 23 | /** 24 | * @return Brush 25 | */ 26 | getBrushWater() { 27 | throw 'Not implemented'; 28 | } 29 | 30 | /** 31 | * @return Brush 32 | */ 33 | getBrushSteam() { 34 | throw 'Not implemented'; 35 | } 36 | 37 | /** 38 | * @return Brush 39 | */ 40 | getBrushGrass() { 41 | throw 'Not implemented'; 42 | } 43 | 44 | /** 45 | * @return Brush 46 | */ 47 | getBrushTree() { 48 | throw 'Not implemented'; 49 | } 50 | 51 | /** 52 | * @return Brush 53 | */ 54 | getBrushFishHead() { 55 | throw 'Not implemented'; 56 | } 57 | 58 | /** 59 | * @return Brush 60 | */ 61 | getBrushFishBody() { 62 | throw 'Not implemented'; 63 | } 64 | 65 | /** 66 | * @return Brush 67 | */ 68 | getBrushFishCorpse() { 69 | throw 'Not implemented'; 70 | } 71 | 72 | /** 73 | * @return Brush 74 | */ 75 | getBrushFire() { 76 | throw 'Not implemented'; 77 | } 78 | 79 | /** 80 | * @return Brush 81 | */ 82 | getBrushAsh() { 83 | throw 'Not implemented'; 84 | } 85 | 86 | // structures 87 | 88 | /** 89 | * @return [] 90 | */ 91 | getTreeTrunkTemplates() { 92 | throw 'Not implemented'; 93 | } 94 | 95 | /** 96 | * @return [] 97 | */ 98 | getTreeLeafClusterTemplates() { 99 | throw 'Not implemented'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/core/processing/ProcessorExtensionSpawnFish.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ElementHead from "../ElementHead.js"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-10 9 | */ 10 | export default class ProcessorExtensionSpawnFish { 11 | 12 | /** @type ElementArea */ 13 | #elementArea; 14 | /** @type DeterministicRandom */ 15 | #random; 16 | /** @type ProcessorContext */ 17 | #processorContext; 18 | 19 | #counterStartValue = 2; 20 | #counter = 2; 21 | 22 | constructor(elementArea, random, processorContext) { 23 | this.#elementArea = elementArea; 24 | this.#random = random; 25 | this.#processorContext = processorContext; 26 | } 27 | 28 | run() { 29 | if (this.#counter-- === 0) { 30 | this.#counter = this.#counterStartValue; 31 | 32 | const x = this.#random.nextInt(this.#elementArea.getWidth() - 2) + 1; 33 | const y = this.#random.nextInt(this.#elementArea.getHeight() - 2) + 1; 34 | 35 | if (this.#couldSpawnHere(this.#elementArea, x, y)) { 36 | const defaults = this.#processorContext.getDefaults(); 37 | this.#elementArea.setElement(x, y, defaults.getBrushFishHead().apply(x, y, this.#random)); 38 | this.#processorContext.trigger(x, y); 39 | this.#elementArea.setElement(x + 1, y, defaults.getBrushFishBody().apply(x + 1, y, this.#random)); 40 | this.#processorContext.trigger(x + 1, y); 41 | 42 | // increase difficulty of spawning fish again 43 | this.#counterStartValue = this.#counterStartValue << 2; 44 | } 45 | } 46 | } 47 | 48 | #couldSpawnHere(elementArea, x, y) { 49 | // space around 50 | if (x < 1 || y < 1) { 51 | return false; 52 | } 53 | if (x + 1 >= elementArea.getWidth() || y + 1 >= elementArea.getHeight()) { 54 | return false; 55 | } 56 | 57 | // check temperature 58 | const elementHead = elementArea.getElementHead(x, y); 59 | if (ElementHead.getTemperature(elementHead) > 0) { 60 | return false; 61 | } 62 | 63 | // water around 64 | if (!this.#isWater(elementArea, x, y) || !this.#isWater(elementArea, x - 1, y) 65 | || !this.#isWater(elementArea, x + 1, y) || !this.#isWater(elementArea, x + 2, y) 66 | || !this.#isWater(elementArea, x + 1, y + 1) || !this.#isWater(elementArea, x + 2, y + 1) 67 | || !this.#isWater(elementArea, x + 1, y - 1) || !this.#isWater(elementArea, x + 2, y - 1)) { 68 | return false; 69 | } 70 | 71 | // sand around 72 | return this.#isSand(elementArea, x, y + 2) 73 | || this.#isSand(elementArea, x + 1, y + 2); 74 | } 75 | 76 | #isWater(elementArea, x, y) { 77 | if (!elementArea.isValidPosition(x, y)) { 78 | return false; 79 | } 80 | const targetElementHead = elementArea.getElementHead(x, y); 81 | const type = ElementHead.getTypeClass(targetElementHead); 82 | return type === ElementHead.TYPE_FLUID; 83 | } 84 | 85 | #isSand(elementArea, x, y) { 86 | if (!elementArea.isValidPosition(x, y)) { 87 | return false; 88 | } 89 | const targetElementHead = elementArea.getElementHead(x, y); 90 | const type = ElementHead.getTypeClass(targetElementHead); 91 | return type === ElementHead.TYPE_POWDER || type === ElementHead.TYPE_POWDER_WET; 92 | } 93 | } -------------------------------------------------------------------------------- /src/core/processing/ProcessorExtensionSpawnGrass.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ProcessorModuleGrass from "./ProcessorModuleGrass.js"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-08 9 | */ 10 | export default class ProcessorExtensionSpawnGrass { 11 | static MAX_COUNTER_VALUE = 2; 12 | 13 | /** @type ElementArea */ 14 | #elementArea; 15 | /** @type DeterministicRandom */ 16 | #random; 17 | /** @type ProcessorContext */ 18 | #processorContext; 19 | 20 | #counter = ProcessorExtensionSpawnGrass.MAX_COUNTER_VALUE; 21 | 22 | constructor(elementArea, random, processorContext) { 23 | this.#elementArea = elementArea; 24 | this.#random = random; 25 | this.#processorContext = processorContext; 26 | } 27 | 28 | run() { 29 | if (this.#counter-- === 0) { 30 | this.#counter = ProcessorExtensionSpawnGrass.MAX_COUNTER_VALUE; 31 | 32 | const x = this.#random.nextInt(this.#elementArea.getWidth()); 33 | const y = this.#random.nextInt(this.#elementArea.getHeight() - 3) + 2; 34 | 35 | if (ProcessorModuleGrass.canGrowUpHere(this.#elementArea, x, y)) { 36 | const brush = this.#processorContext.getDefaults().getBrushGrass(); 37 | this.#elementArea.setElement(x, y, brush.apply(x, y, this.#random)); 38 | this.#processorContext.trigger(x, y); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/core/processing/ProcessorExtensionSpawnTree.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ElementHead from "../ElementHead.js"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-17 9 | */ 10 | export default class ProcessorExtensionSpawnTree { 11 | static STARTING_COUNTER_VALUE = 1000; 12 | static MAX_COUNTER_VALUE = 4; 13 | 14 | /** @type ElementArea */ 15 | #elementArea; 16 | /** @type DeterministicRandom */ 17 | #random; 18 | /** @type ProcessorContext */ 19 | #processorContext; 20 | 21 | #counter = ProcessorExtensionSpawnTree.STARTING_COUNTER_VALUE; 22 | 23 | constructor(elementArea, random, processorContext) { 24 | this.#elementArea = elementArea; 25 | this.#random = random; 26 | this.#processorContext = processorContext; 27 | } 28 | 29 | run() { 30 | if (this.#counter-- === 0) { 31 | this.#counter = ProcessorExtensionSpawnTree.MAX_COUNTER_VALUE; 32 | 33 | const x = this.#random.nextInt(this.#elementArea.getWidth() - 12) + 6; 34 | const y = this.#random.nextInt(this.#elementArea.getHeight() - 16) + 15; 35 | 36 | if (ProcessorExtensionSpawnTree.couldGrowUpHere(this.#elementArea, x, y)) { 37 | const brush = this.#processorContext.getDefaults().getBrushTree(); 38 | this.#elementArea.setElement(x, y, brush.apply(x, y, this.#random)); 39 | this.#processorContext.trigger(x, y); 40 | } 41 | } 42 | } 43 | 44 | static couldGrowUpHere(elementArea, x, y) { 45 | if (x < 0 || y < 12) { 46 | return false; 47 | } 48 | if (x > elementArea.getWidth() - 5 || y > elementArea.getHeight() - 2) { 49 | return false; 50 | } 51 | let e1 = elementArea.getElementHead(x, y); 52 | if (ElementHead.getBehaviour(e1) !== ElementHead.BEHAVIOUR_GRASS) { 53 | return false; 54 | } 55 | if (ElementHead.getTemperature(e1) > 0) { 56 | return false; 57 | } 58 | let e2 = elementArea.getElementHead(x, y + 1); 59 | if (ElementHead.getBehaviour(e2) !== ElementHead.BEHAVIOUR_SOIL) { 60 | return false; 61 | } 62 | if (ElementHead.getTemperature(e2) > 0) { 63 | return false; 64 | } 65 | 66 | // check space directly above 67 | for (let dy = 1; dy < 18; dy++) { 68 | if (!ProcessorExtensionSpawnTree.#isSpaceHere(elementArea, x, y - dy)) { 69 | return false; 70 | } 71 | } 72 | 73 | // check trees around 74 | for (let dx = -15; dx < 15; dx++) { 75 | if (ProcessorExtensionSpawnTree.#isOtherThreeThere(elementArea, x + dx, y - 4)) { 76 | return false; 77 | } 78 | } 79 | 80 | // check space above - left & right 81 | for (let dy = 10; dy < 15; dy++) { 82 | if (!ProcessorExtensionSpawnTree.#isSpaceHere(elementArea, x - 8, y - dy)) { 83 | return false; 84 | } 85 | if (!ProcessorExtensionSpawnTree.#isSpaceHere(elementArea, x + 8, y - dy)) { 86 | return false; 87 | } 88 | } 89 | 90 | return true; 91 | } 92 | 93 | static #isSpaceHere(elementArea, tx, ty) { 94 | let targetElementHead = elementArea.getElementHead(tx, ty); 95 | if (ElementHead.getTypeClass(targetElementHead) === ElementHead.TYPE_AIR) { 96 | return true; 97 | } 98 | if (ElementHead.getBehaviour(targetElementHead) === ElementHead.BEHAVIOUR_GRASS) { 99 | return true; 100 | } 101 | return false; 102 | } 103 | 104 | static #isOtherThreeThere(elementArea, tx, ty) { 105 | let targetElementHead = elementArea.getElementHead(tx, ty); 106 | let behaviour = ElementHead.getBehaviour(targetElementHead); 107 | if (behaviour === ElementHead.BEHAVIOUR_TREE_TRUNK || behaviour === ElementHead.BEHAVIOUR_TREE) { 108 | return true; 109 | } 110 | return false; 111 | } 112 | } -------------------------------------------------------------------------------- /src/core/processing/ProcessorModuleWater.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ElementHead from "../ElementHead.js"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-20 9 | */ 10 | export default class ProcessorModuleWater { 11 | 12 | /** @type ElementArea */ 13 | #elementArea; 14 | 15 | /** @type DeterministicRandom */ 16 | #random; 17 | 18 | /** @type ProcessorContext */ 19 | #processorContext; 20 | 21 | constructor(elementArea, random, processorContext) { 22 | this.#elementArea = elementArea; 23 | this.#random = random; 24 | this.#processorContext = processorContext; 25 | } 26 | 27 | behaviourWater(elementHead, x, y) { 28 | const typeClass = ElementHead.getTypeClass(elementHead); 29 | const temperature = ElementHead.getTemperature(elementHead); 30 | 31 | if (typeClass === ElementHead.TYPE_FLUID) { 32 | if (temperature > 20) { 33 | const brush = this.#processorContext.getDefaults().getBrushSteam(); 34 | const element = brush.apply(x, y, this.#random); 35 | const newElementHead = ElementHead.setTemperature(element.elementHead, temperature); 36 | this.#elementArea.setElementHeadAndTail(x, y, newElementHead, element.elementTail); 37 | return true; 38 | } 39 | 40 | } else if (typeClass === ElementHead.TYPE_GAS) { 41 | if (temperature < 10) { 42 | const brush = this.#processorContext.getDefaults().getBrushWater(); 43 | const element = brush.apply(x, y, this.#random); 44 | const newElementHead = ElementHead.setTemperature(element.elementHead, temperature); 45 | this.#elementArea.setElementHeadAndTail(x, y, newElementHead, element.elementTail); 46 | return true; 47 | } 48 | } 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/core/rendering/Renderer.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @interface 5 | * 6 | * @author Patrik Harag 7 | * @version 2023-08-27 8 | */ 9 | export default class Renderer { 10 | 11 | trigger(x, y) { 12 | throw 'Not implemented'; 13 | } 14 | 15 | /** 16 | * 17 | * @param changedChunks {boolean[]} 18 | * @return {void} 19 | */ 20 | render(changedChunks) { 21 | throw 'Not implemented'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/rendering/RendererInitializer.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Renderer from "./Renderer"; 4 | import Renderer2D from "./Renderer2D"; 5 | import RenderingModeHeatmap from "./RenderingModeHeatmap"; 6 | import RenderingModeElementType from "./RenderingModeElementType"; 7 | import RendererWebGL from "./RendererWebGL"; 8 | import RendererNull from "./RendererNull"; 9 | 10 | /** 11 | * @interface 12 | * 13 | * @author Patrik Harag 14 | * @version 2023-10-11 15 | */ 16 | export default class RendererInitializer { 17 | 18 | getContextType() { 19 | throw 'Not implemented' 20 | } 21 | 22 | /** 23 | * 24 | * @param elementArea 25 | * @param chunkSize 26 | * @param context 27 | * @return {Renderer} 28 | */ 29 | initialize(elementArea, chunkSize, context) { 30 | throw 'Not implemented' 31 | } 32 | 33 | // static factory methods 34 | 35 | static canvas2d() { 36 | return new RendererInitializer2D(null); 37 | } 38 | 39 | static canvas2dHeatmap() { 40 | return new RendererInitializer2D(new RenderingModeHeatmap()); 41 | } 42 | 43 | static canvas2dElementType() { 44 | return new RendererInitializer2D(new RenderingModeElementType()) 45 | } 46 | 47 | static canvasWebGL() { 48 | return new RendererInitializerWebGL(); 49 | } 50 | 51 | static nullRenderer() { 52 | return new RendererInitializerNull(); 53 | } 54 | } 55 | 56 | class RendererInitializer2D extends RendererInitializer { 57 | 58 | #mode; 59 | 60 | constructor(mode) { 61 | super(); 62 | this.#mode = mode; 63 | } 64 | 65 | getContextType() { 66 | return '2d'; 67 | } 68 | 69 | initialize(elementArea, chunkSize, context) { 70 | let renderer = new Renderer2D(elementArea, chunkSize, context); 71 | if (this.#mode !== null) { 72 | renderer.setMode(this.#mode); 73 | } 74 | return renderer; 75 | } 76 | } 77 | 78 | class RendererInitializerWebGL extends RendererInitializer { 79 | 80 | getContextType() { 81 | return 'webgl2'; 82 | } 83 | 84 | initialize(elementArea, chunkSize, context) { 85 | return new RendererWebGL(elementArea, chunkSize, context); 86 | } 87 | } 88 | 89 | class RendererInitializerNull extends RendererInitializer { 90 | 91 | constructor() { 92 | super(); 93 | } 94 | 95 | getContextType() { 96 | return '2d'; 97 | } 98 | 99 | initialize(elementArea, chunkSize, context) { 100 | return new RendererNull(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/core/rendering/RendererNull.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Renderer from "./Renderer"; 4 | 5 | /** 6 | * Null renderer. For testing purposes - to measure effects of rendering... 7 | * 8 | * @author Patrik Harag 9 | * @version 2023-10-11 10 | */ 11 | export default class RendererNull extends Renderer { 12 | 13 | constructor() { 14 | super(); 15 | } 16 | 17 | trigger(x, y) { 18 | // ignore 19 | } 20 | 21 | render(changedChunks) { 22 | // ignore 23 | } 24 | } -------------------------------------------------------------------------------- /src/core/rendering/RenderingMode.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @author Patrik Harag 5 | * @version 2023-02-18 6 | */ 7 | export default class RenderingMode { 8 | 9 | /** 10 | * Element rendering function. 11 | * 12 | * Default implementation: 13 | *
14 |      *     data[dataIndex] = ElementTail.getColorRed(elementTail);
15 |      *     data[dataIndex + 1] = ElementTail.getColorGreen(elementTail);
16 |      *     data[dataIndex + 2] = ElementTail.getColorBlue(elementTail);
17 |      * 
18 | * 19 | * @param data 20 | * @param dataIndex 21 | * @param elementHead 22 | * @param elementTail 23 | */ 24 | apply(data, dataIndex, elementHead, elementTail) { 25 | throw 'Not implemented' 26 | } 27 | } -------------------------------------------------------------------------------- /src/core/rendering/RenderingModeElementType.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import RenderingMode from "./RenderingMode.js"; 4 | import ElementHead from "../ElementHead.js"; 5 | 6 | /** 7 | * @author Patrik Harag 8 | * @version 2023-08-18 9 | */ 10 | export default class RenderingModeElementType extends RenderingMode { 11 | 12 | constructor() { 13 | super(); 14 | } 15 | 16 | #asColor(elementHead) { 17 | switch (ElementHead.getTypeClass(elementHead)) { 18 | case ElementHead.TYPE_AIR: return [255, 255, 255]; 19 | case ElementHead.TYPE_STATIC: return [0, 0, 0]; 20 | case ElementHead.TYPE_FLUID: return [0, 0, 255]; 21 | case ElementHead.TYPE_POWDER: 22 | case ElementHead.TYPE_POWDER_WET: 23 | case ElementHead.TYPE_POWDER_FLOATING: 24 | if (ElementHead.getTypeModifierPowderSliding(elementHead) === 1) { 25 | if (ElementHead.getTypeModifierPowderDirection(elementHead) === 1) { 26 | return [232, 137, 70]; 27 | } else { 28 | return [255, 0, 0]; 29 | } 30 | } 31 | switch (ElementHead.getTypeClass(elementHead)) { 32 | case ElementHead.TYPE_POWDER: return [36, 163, 57]; 33 | case ElementHead.TYPE_POWDER_WET: return [44, 122, 57]; 34 | case ElementHead.TYPE_POWDER_FLOATING: return [16, 194, 45]; 35 | } 36 | // fallthrough 37 | default: return [255, 0, 125]; 38 | } 39 | } 40 | 41 | apply(data, dataIndex, elementHead, elementTail) { 42 | const [r, g, b] = this.#asColor(elementHead); 43 | 44 | data[dataIndex] = r; 45 | data[dataIndex + 1] = g; 46 | data[dataIndex + 2] = b; 47 | 48 | return false; 49 | } 50 | } -------------------------------------------------------------------------------- /src/core/rendering/RenderingModeHeatmap.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import RenderingMode from "./RenderingMode.js"; 4 | import ElementHead from "../ElementHead.js"; 5 | import Assets from "../../Assets.js"; 6 | 7 | import _ASSET_GRADIENT_RAINBOW from './assets/heatmap.palette.png' 8 | 9 | /** 10 | * @author Patrik Harag 11 | * @version 2023-08-14 12 | */ 13 | export default class RenderingModeHeatmap extends RenderingMode { 14 | 15 | /** @type ImageData */ 16 | #gradientImageData = null; 17 | 18 | constructor() { 19 | super(); 20 | Assets.asImageData(_ASSET_GRADIENT_RAINBOW).then(d => this.#gradientImageData = d); 21 | } 22 | 23 | apply(data, dataIndex, elementHead, elementTail) { 24 | if (this.#gradientImageData === null) { 25 | // not loaded yet 26 | return; 27 | } 28 | 29 | if (elementHead === 0x00) { 30 | // background 31 | data[dataIndex] = 0x00; 32 | data[dataIndex + 1] = 0x00; 33 | data[dataIndex + 2] = 0x00; 34 | } else { 35 | const temperature = ElementHead.getTemperature(elementHead); 36 | const x = Math.trunc(temperature / (1 << ElementHead.FIELD_TEMPERATURE_SIZE) * this.#gradientImageData.width); 37 | const gradIndex = x * 4; 38 | data[dataIndex] = this.#gradientImageData.data[gradIndex]; 39 | data[dataIndex + 1] = this.#gradientImageData.data[gradIndex + 1]; 40 | data[dataIndex + 2] = this.#gradientImageData.data[gradIndex + 2]; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/core/rendering/assets/heatmap.palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/core/rendering/assets/heatmap.palette.png -------------------------------------------------------------------------------- /src/core/rendering/assets/temperature.palette.csv: -------------------------------------------------------------------------------- 1 | 255,51,0 2 | 255,69,0 3 | 255,82,0 4 | 255,93,0 5 | 255,102,0 6 | 255,111,0 7 | 255,118,0 8 | 255,124,0 9 | 255,130,0 10 | 255,135,0 11 | 255,141,11 12 | 255,146,29 13 | 255,152,41 14 | 255,157,51 15 | 255,162,60 16 | 255,166,69 17 | 255,170,77 18 | 255,174,84 19 | 255,178,91 20 | 255,182,98 21 | 255,185,105 22 | 255,189,111 23 | 255,192,118 24 | 255,195,124 25 | 255,198,130 26 | 255,201,135 27 | 255,203,141 28 | 255,206,146 29 | 255,208,151 30 | 255,211,156 31 | 255,213,161 32 | 255,215,166 33 | 255,217,171 34 | 255,219,175 35 | 255,221,180 36 | 255,223,184 37 | 255,225,188 38 | 255,226,192 39 | 255,228,196 40 | 255,229,200 41 | 255,231,204 42 | 255,232,208 43 | 255,234,211 44 | 255,235,215 45 | 255,237,218 46 | 255,238,222 47 | 255,239,225 48 | 255,240,228 49 | 255,241,231 50 | 255,243,234 51 | 255,244,237 52 | 255,245,240 53 | 255,246,243 54 | 255,247,245 55 | 255,248,248 56 | 255,249,251 57 | 255,249,253 58 | 254,250,255 59 | 252,248,255 60 | 250,247,255 61 | 247,245,255 62 | 245,244,255 63 | 243,243,255 64 | 241,241,255 65 | 239,240,255 66 | 238,239,255 67 | 236,238,255 68 | 234,237,255 69 | 233,236,255 70 | 231,234,255 71 | 229,233,255 72 | 228,233,255 73 | 227,232,255 74 | 225,231,255 75 | 224,230,255 76 | 223,229,255 77 | 221,228,255 78 | 220,227,255 79 | 219,226,255 80 | 218,226,255 81 | 217,225,255 82 | 216,224,255 83 | 215,223,255 84 | 214,223,255 85 | 213,222,255 86 | 212,221,255 87 | 211,221,255 88 | 210,220,255 89 | 209,220,255 90 | 208,219,255 91 | 207,218,255 -------------------------------------------------------------------------------- /src/core/rendering/assets/temperature.txt: -------------------------------------------------------------------------------- 1 | temperature.palette.csv contains color temperatures 2 | from 1000 K (the first entry 3 | to 10000 K (the last entry) 4 | by 100 K 5 | -------------------------------------------------------------------------------- /src/core/scene/Scene.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @interface 5 | * 6 | * @author Patrik Harag 7 | * @version 2023-12-20 8 | */ 9 | export default class Scene { 10 | 11 | /** 12 | * @returns [width: number, height: number] 13 | */ 14 | countSize(prefWidth, prefHeight) { 15 | throw 'Not implemented'; 16 | } 17 | 18 | /** 19 | * @param prefWidth {number} 20 | * @param prefHeight {number} 21 | * @param processorDefaults {ProcessorDefaults} 22 | * @param context {CanvasRenderingContext2D|WebGLRenderingContext} 23 | * @param rendererInitializer {RendererInitializer} 24 | * @returns Promise 25 | */ 26 | createSandGame(prefWidth, prefHeight, processorDefaults, context, rendererInitializer) { 27 | throw 'Not implemented'; 28 | } 29 | 30 | /** 31 | * @param prefWidth 32 | * @param prefHeight 33 | * @param defaultElement 34 | * @returns ElementArea 35 | */ 36 | createElementArea(prefWidth, prefHeight, defaultElement) { 37 | throw 'Not implemented'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/core/scene/SceneImplHardcoded.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Scene from "./Scene.js"; 4 | import SandGame from "../SandGame.js"; 5 | import ElementArea from "../ElementArea.js"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-20 11 | */ 12 | export default class SceneImplHardcoded extends Scene { 13 | 14 | name; 15 | description; 16 | 17 | /** @type function(SandGame):Promise|any */ 18 | #apply; 19 | 20 | constructor({name, description, apply}) { 21 | super(); 22 | this.#apply = apply; 23 | this.name = name; 24 | this.description = description; 25 | } 26 | 27 | countSize(prefWidth, prefHeight) { 28 | return [prefWidth, prefHeight]; 29 | } 30 | 31 | async createSandGame(prefWidth, prefHeight, defaults, context, rendererInitializer) { 32 | let elementArea = this.createElementArea(prefWidth, prefHeight, defaults.getDefaultElement()); 33 | let sandGame = new SandGame(elementArea, null, defaults, context, rendererInitializer); 34 | await this.#apply(sandGame); 35 | return sandGame; 36 | } 37 | 38 | createElementArea(prefWidth, prefHeight, defaultElement) { 39 | return ElementArea.create(prefWidth, prefHeight, defaultElement); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/scene/SceneImplModFlip.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Scene from "./Scene.js"; 4 | import SandGame from "../SandGame.js"; 5 | 6 | /** 7 | * Create flipped scene using object composition. 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-20 11 | */ 12 | export default class SceneImplModFlip extends Scene { 13 | 14 | /** 15 | * @type Scene 16 | */ 17 | #original; 18 | 19 | #flipHorizontally; 20 | #flipVertically; 21 | 22 | constructor(scene, flipHorizontally, flipVertically) { 23 | super(); 24 | this.#original = scene; 25 | this.#flipHorizontally = flipHorizontally; 26 | this.#flipVertically = flipVertically; 27 | } 28 | 29 | countSize(prefWidth, prefHeight) { 30 | this.#original.countSize(prefWidth, prefHeight); 31 | } 32 | 33 | async createSandGame(prefWidth, prefHeight, defaults, context, rendererInitializer) { 34 | let elementArea = this.createElementArea(prefWidth, prefHeight, defaults.getDefaultElement()); 35 | return new SandGame(elementArea, null, defaults, context, rendererInitializer); 36 | // TODO: sceneMetadata not set 37 | } 38 | 39 | createElementArea(prefWidth, prefHeight, defaultElement) { 40 | const elementArea = this.#original.createElementArea(prefWidth, prefHeight, defaultElement); 41 | 42 | const width = elementArea.getWidth(); 43 | const height = elementArea.getHeight(); 44 | 45 | if (this.#flipHorizontally) { 46 | for (let y = 0; y < height; y++) { 47 | for (let x = 0; x < Math.trunc(width / 2); x++) { 48 | elementArea.swap(x, y, width - 1 - x, y); 49 | } 50 | } 51 | } 52 | 53 | if (this.#flipVertically) { 54 | for (let y = 0; y < Math.trunc(height / 2); y++) { 55 | for (let x = 0; x < width; x++) { 56 | elementArea.swap(x, y, x, height - 1 - y); 57 | } 58 | } 59 | } 60 | 61 | return elementArea; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/core/scene/SceneImplResize.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Scene from "./Scene.js"; 4 | import SandGame from "../SandGame.js"; 5 | import ElementArea from "../ElementArea.js"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-20 11 | */ 12 | export default class SceneImplTmpResize extends Scene { 13 | 14 | /** 15 | * @type SandGame 16 | */ 17 | #sandGame; 18 | 19 | constructor(sandGame) { 20 | super(); 21 | this.#sandGame = sandGame; 22 | } 23 | 24 | countSize(prefWidth, prefHeight) { 25 | return [prefWidth, prefHeight]; 26 | } 27 | 28 | async createSandGame(prefWidth, prefHeight, defaults, context, rendererInitializer) { 29 | let elementArea = this.createElementArea(prefWidth, prefHeight, defaults.getDefaultElement()); 30 | let sandGame = new SandGame(elementArea, null, defaults, context, rendererInitializer); 31 | this.#sandGame.copyStateTo(sandGame); 32 | return sandGame; 33 | } 34 | 35 | createElementArea(prefWidth, prefHeight, defaultElement) { 36 | return ElementArea.create(prefWidth, prefHeight, defaultElement); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/core/scene/SceneImplSnapshot.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Scene from "./Scene.js"; 4 | import SandGame from "../SandGame"; 5 | import ElementArea from "../ElementArea.js"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-20 11 | */ 12 | export default class SceneImplSnapshot extends Scene { 13 | 14 | /** 15 | * @type Snapshot 16 | */ 17 | #snapshot; 18 | 19 | /** 20 | * 21 | * @param snapshot {Snapshot} 22 | */ 23 | constructor(snapshot) { 24 | super(); 25 | this.#snapshot = snapshot; 26 | } 27 | 28 | countSize(prefWidth, prefHeight) { 29 | return [this.#snapshot.metadata.width, this.#snapshot.metadata.height]; 30 | } 31 | 32 | async createSandGame(prefWidth, prefHeight, defaults, context, rendererInitializer) { 33 | let elementArea = this.createElementArea(prefWidth, prefHeight, defaults.getDefaultElement()); 34 | return new SandGame(elementArea, this.#snapshot.metadata, defaults, context, rendererInitializer); 35 | } 36 | 37 | createElementArea(prefWidth, prefHeight, defaultElement) { 38 | return ElementArea.from( 39 | this.#snapshot.metadata.width, 40 | this.#snapshot.metadata.height, 41 | this.#snapshot.dataHeads, 42 | this.#snapshot.dataTails); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/scene/SceneImplTemplate.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Scene from "./Scene.js"; 4 | import SandGame from "../SandGame"; 5 | import ElementArea from "../ElementArea.js"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-20 11 | */ 12 | export default class SceneImplTemplate extends Scene { 13 | 14 | /** 15 | * @type ElementArea 16 | */ 17 | #elementArea; 18 | 19 | /** 20 | * 21 | * @param elementArea {ElementArea} 22 | */ 23 | constructor(elementArea) { 24 | super(); 25 | this.#elementArea = elementArea; 26 | } 27 | 28 | countSize(prefWidth, prefHeight) { 29 | return [this.#elementArea.getWidth(), this.#elementArea.getHeight()]; 30 | } 31 | 32 | async createSandGame(prefWidth, prefHeight, defaults, context, rendererInitializer) { 33 | let elementArea = this.createElementArea(prefWidth, prefHeight, defaults.getDefaultElement()); 34 | return new SandGame(elementArea, null, defaults, context, rendererInitializer); 35 | } 36 | 37 | createElementArea(prefWidth, prefHeight, defaultElement) { 38 | return ElementArea.from( 39 | this.#elementArea.getWidth(), 40 | this.#elementArea.getHeight(), 41 | this.#elementArea.getDataHeads(), 42 | this.#elementArea.getDataTails()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/scene/Scenes.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import SceneImplHardcoded from "./SceneImplHardcoded"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2024-03-24 9 | */ 10 | export default class Scenes { 11 | 12 | /** 13 | * 14 | * @param name {string} 15 | * @param func {function(SandGame):Promise|any} 16 | */ 17 | static custom(name, func) { 18 | return new SceneImplHardcoded({ 19 | name: name, 20 | apply: func 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/tool/ActionTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Tool from "./Tool"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-25 9 | */ 10 | export default class ActionTool extends Tool { 11 | 12 | /** @type function */ 13 | #handler; 14 | 15 | constructor(info, handler) { 16 | super(info); 17 | this.#handler = handler; 18 | } 19 | 20 | applyPoint(x, y, graphics, aldModifier) { 21 | this.#handler(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/core/tool/CursorDefinition.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @interface 5 | * 6 | * @author Patrik Harag 7 | * @version 2023-05-04 8 | */ 9 | export default class CursorDefinition { 10 | 11 | /** 12 | * 13 | * @return {number} 14 | */ 15 | getWidth() { 16 | throw 'Not implemented'; 17 | } 18 | 19 | /** 20 | * 21 | * @return {number} 22 | */ 23 | getHeight() { 24 | throw 'Not implemented'; 25 | } 26 | } -------------------------------------------------------------------------------- /src/core/tool/CursorDefinitionElementArea.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import CursorDefinition from "./CursorDefinition"; 4 | 5 | /** 6 | * @author Patrik Harag 7 | * @version 2023-05-04 8 | */ 9 | export default class CursorDefinitionElementArea extends CursorDefinition { 10 | 11 | /** @type ElementArea */ 12 | #elementArea; 13 | 14 | constructor(elementArea) { 15 | super(); 16 | this.#elementArea = elementArea; 17 | } 18 | 19 | getWidth() { 20 | return this.#elementArea.getWidth(); 21 | } 22 | 23 | getHeight() { 24 | return this.#elementArea.getHeight(); 25 | } 26 | 27 | getElementArea() { 28 | return this.#elementArea; 29 | } 30 | } -------------------------------------------------------------------------------- /src/core/tool/GlobalActionTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Tool from "./Tool"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2024-03-13 9 | */ 10 | export default class GlobalActionTool extends Tool { 11 | 12 | /** @type function(SandGame|null) */ 13 | #handler; 14 | 15 | constructor(info, handler) { 16 | super(info); 17 | this.#handler = handler; 18 | } 19 | 20 | /** 21 | * 22 | * @return {function((SandGame|null))} 23 | */ 24 | getHandler() { 25 | return this.#handler; 26 | } 27 | } -------------------------------------------------------------------------------- /src/core/tool/InsertElementAreaTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ElementArea from "../ElementArea"; 4 | import Brushes from "../brush/Brushes"; 5 | import CursorDefinitionElementArea from "./CursorDefinitionElementArea"; 6 | import Tool from "./Tool"; 7 | 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2023-12-28 12 | */ 13 | export default class InsertElementAreaTool extends Tool { 14 | 15 | static asElementArea(scene) { 16 | return scene.createElementArea(InsertElementAreaTool.DEFAULT_W, InsertElementAreaTool.DEFAULT_H, 17 | ElementArea.TRANSPARENT_ELEMENT); 18 | } 19 | 20 | 21 | static DEFAULT_W = 30; 22 | static DEFAULT_H = 30; 23 | 24 | /** @type ElementArea */ 25 | #elementArea; 26 | /** @type function */ 27 | #onInsertHandler; 28 | 29 | constructor(info, elementArea, onInsertHandler) { 30 | super(info); 31 | this.#elementArea = elementArea; 32 | this.#onInsertHandler = onInsertHandler; 33 | } 34 | 35 | applyPoint(x, y, graphics, aldModifier) { 36 | const elementArea = this.#elementArea; 37 | const offsetX = x - Math.trunc(elementArea.getWidth() / 2); 38 | const offsetY = y - Math.trunc(elementArea.getHeight() / 2); 39 | 40 | let brush = Brushes.custom((tx, ty) => { 41 | const element = elementArea.getElement(tx - offsetX, ty - offsetY); 42 | if (element.elementHead !== ElementArea.TRANSPARENT_ELEMENT.elementHead 43 | && element.elementTail !== ElementArea.TRANSPARENT_ELEMENT.elementTail) { 44 | 45 | return element; 46 | } 47 | return null; 48 | }); 49 | if (aldModifier) { 50 | brush = Brushes.gentle(brush); 51 | } 52 | 53 | for (let i = 0; i < elementArea.getWidth() && offsetX + i < graphics.getWidth(); i++) { 54 | const tx = offsetX + i; 55 | if (tx < 0) { 56 | continue; 57 | } 58 | 59 | for (let j = 0; j < elementArea.getHeight() && offsetY + j < graphics.getHeight(); j++) { 60 | const ty = offsetY + j; 61 | if (ty < 0) { 62 | continue; 63 | } 64 | 65 | graphics.draw(tx, ty, brush); 66 | } 67 | } 68 | 69 | if (this.#onInsertHandler !== undefined) { 70 | this.#onInsertHandler(); 71 | } 72 | } 73 | 74 | hasCursor() { 75 | return true; 76 | } 77 | 78 | createCursor() { 79 | return new CursorDefinitionElementArea(this.#elementArea); 80 | } 81 | } -------------------------------------------------------------------------------- /src/core/tool/InsertRandomSceneTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DeterministicRandom from "../DeterministicRandom"; 4 | import InsertElementAreaTool from "./InsertElementAreaTool"; 5 | import Tool from "./Tool"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-29 11 | */ 12 | export default class InsertRandomSceneTool extends Tool { 13 | 14 | /** @type Scene[] */ 15 | #scenes; 16 | 17 | #currentTool; 18 | 19 | /** @type function */ 20 | #onInsertHandler; 21 | 22 | constructor(info, scenes, onInsertHandler) { 23 | super(info); 24 | this.#scenes = scenes; 25 | this.#onInsertHandler = onInsertHandler; 26 | this.#initRandomTool(); 27 | } 28 | 29 | #initRandomTool() { 30 | if (this.#scenes.length === undefined || this.#scenes.length === 0) { 31 | throw 'Scenes not set'; 32 | } 33 | 34 | const i = DeterministicRandom.DEFAULT.nextInt(this.#scenes.length); 35 | const scene = this.#scenes[i]; 36 | const elementArea = InsertElementAreaTool.asElementArea(scene); 37 | this.#currentTool = new InsertElementAreaTool(this.getInfo(), elementArea, this.#onInsertHandler); 38 | } 39 | 40 | applyPoint(x, y, graphics, aldModifier) { 41 | this.#currentTool.applyPoint(x, y, graphics, aldModifier); 42 | this.#initRandomTool(); 43 | } 44 | 45 | hasCursor() { 46 | return true; 47 | } 48 | 49 | createCursor() { 50 | return this.#currentTool.createCursor(); 51 | } 52 | } -------------------------------------------------------------------------------- /src/core/tool/MeteorTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import BrushDefs from "../../def/BrushDefs"; 4 | import DeterministicRandom from "../DeterministicRandom"; 5 | import Tool from "./Tool"; 6 | 7 | // TODO: direct BrushDefs access >> Defaults 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2024-03-06 12 | */ 13 | export default class MeteorTool extends Tool { 14 | 15 | constructor(info) { 16 | super(info); 17 | } 18 | 19 | applyPoint(x, y, graphics, aldModifier) { 20 | const diffSlope4 = Math.trunc(y / 4); 21 | if (x < diffSlope4 + 10) { 22 | // right only 23 | graphics.draw(x + diffSlope4, 1, BrushDefs.METEOR_FROM_RIGHT); 24 | return; 25 | } 26 | if (x > graphics.getWidth() - diffSlope4 - 10) { 27 | // left only 28 | graphics.draw(x - diffSlope4, 1, BrushDefs.METEOR_FROM_LEFT); 29 | return; 30 | } 31 | 32 | if (x < graphics.getWidth() / 2) { 33 | if (DeterministicRandom.DEFAULT.next() < 0.8) { 34 | graphics.draw(x + diffSlope4, 1, BrushDefs.METEOR_FROM_RIGHT); 35 | } else { 36 | graphics.draw(x, 1, BrushDefs.METEOR); 37 | } 38 | } else { 39 | if (DeterministicRandom.DEFAULT.next() < 0.8) { 40 | graphics.draw(x - diffSlope4, 1, BrushDefs.METEOR_FROM_LEFT); 41 | } else { 42 | graphics.draw(x, 1, BrushDefs.METEOR); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/core/tool/Point2BrushTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Tool from "./Tool"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-25 9 | */ 10 | export default class Point2BrushTool extends Tool { 11 | 12 | /** @type Brush */ 13 | #brush1; 14 | /** @type Brush */ 15 | #brush2; 16 | 17 | constructor(info, brush1, brush2) { 18 | super(info); 19 | this.#brush1 = brush1; 20 | this.#brush2 = brush2; 21 | } 22 | 23 | applyPoint(x, y, graphics, aldModifier) { 24 | graphics.draw(x, y, this.#brush1); 25 | graphics.draw(x + 1, y, this.#brush2); 26 | } 27 | } -------------------------------------------------------------------------------- /src/core/tool/PointBrushTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Tool from "./Tool"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-25 9 | */ 10 | export default class PointBrushTool extends Tool { 11 | 12 | /** @type Brush */ 13 | #brush; 14 | 15 | constructor(info, brush) { 16 | super(info); 17 | this.#brush = brush; 18 | } 19 | 20 | applyPoint(x, y, graphics, aldModifier) { 21 | graphics.draw(x, y, this.#brush); 22 | } 23 | } -------------------------------------------------------------------------------- /src/core/tool/RoundBrushTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Brush from "../brush/Brush"; 4 | import Tool from "./Tool"; 5 | import Brushes from "../brush/Brushes"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-02-26 11 | */ 12 | export default class RoundBrushTool extends Tool { 13 | 14 | /** @type Brush */ 15 | #brush; 16 | 17 | /** @type number */ 18 | #size; 19 | 20 | constructor(info, brush, size) { 21 | super(info); 22 | this.#brush = brush; 23 | this.#size = size; 24 | } 25 | 26 | getBrush() { 27 | return this.#brush; 28 | } 29 | 30 | isLineModeEnabled() { 31 | return true; 32 | } 33 | 34 | isAreaModeEnabled() { 35 | return true; 36 | } 37 | 38 | isRepeatingEnabled() { 39 | return true; 40 | } 41 | 42 | applyPoint(x, y, graphics, altModifier) { 43 | this.applyStroke(x, y, x, y, graphics, altModifier); 44 | } 45 | 46 | applyStroke(x1, y1, x2, y2, graphics, altModifier) { 47 | const brush = this.#currentBrush(altModifier); 48 | graphics.drawLine(x1, y1, x2, y2, this.#size, brush, true); 49 | } 50 | 51 | applyArea(x1, y1, x2, y2, graphics, altModifier) { 52 | const brush = this.#currentBrush(altModifier); 53 | graphics.drawRectangle(x1, y1, x2, y2, brush); 54 | } 55 | 56 | applySpecial(x, y, graphics, altModifier) { 57 | const brush = this.#currentBrush(altModifier); 58 | graphics.floodFill(x, y, brush, 1); 59 | } 60 | 61 | #currentBrush(altModifier) { 62 | let brush = this.#brush; 63 | if (altModifier) { 64 | brush = Brushes.gentle(brush); 65 | } 66 | return brush; 67 | } 68 | } -------------------------------------------------------------------------------- /src/core/tool/RoundBrushToolForSolidBody.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Tool from "./Tool"; 4 | import Brush from "../brush/Brush"; 5 | import Brushes from "../brush/Brushes"; 6 | import ElementHead from "../ElementHead"; 7 | import Element from "../Element"; 8 | import PredicateDefs from "../../def/PredicateDefs"; 9 | 10 | /** 11 | * 12 | * @author Patrik Harag 13 | * @version 2024-02-26 14 | */ 15 | export default class RoundBrushToolForSolidBody extends Tool { 16 | 17 | /** @type Brush */ 18 | #brush; 19 | 20 | /** @type Brush */ 21 | #toSolidBodyBrush = Brushes.conditional(PredicateDefs.IS_STATIC, Brushes.toSolidBody(2, false)); // TODO: hardcoded 22 | 23 | /** @type number */ 24 | #size; 25 | 26 | #drag = false; 27 | #dragBuffer = []; 28 | 29 | constructor(info, brush, size) { 30 | super(info); 31 | this.#brush = brush; 32 | this.#size = size; 33 | } 34 | 35 | getBrush() { 36 | return this.#brush; 37 | } 38 | 39 | isLineModeEnabled() { 40 | return true; 41 | } 42 | 43 | isAreaModeEnabled() { 44 | return true; 45 | } 46 | 47 | isRepeatingEnabled() { 48 | return false; 49 | } 50 | 51 | applyPoint(x, y, graphics, altModifier) { 52 | // ignored 53 | } 54 | 55 | applyStroke(x1, y1, x2, y2, graphics, altModifier) { 56 | const brush = this.#currentBrush(altModifier); 57 | graphics.drawLine(x1, y1, x2, y2, this.#size, brush, true); 58 | } 59 | 60 | applyArea(x1, y1, x2, y2, graphics, altModifier) { 61 | const brush = this.#currentBrush(altModifier); 62 | graphics.drawRectangle(x1, y1, x2, y2, brush); 63 | } 64 | 65 | applySpecial(x, y, graphics, altModifier) { 66 | const brush = this.#currentBrush(altModifier); 67 | graphics.floodFill(x, y, brush, 1); 68 | } 69 | 70 | onDragStart(x, y, graphics, altModifier) { 71 | this.#drag = true; 72 | } 73 | 74 | onDragEnd(x, y, graphics, altModifier) { 75 | this.#drag = false; 76 | for (const [ex, ey] of this.#dragBuffer) { 77 | graphics.draw(ex, ey, this.#toSolidBodyBrush); 78 | } 79 | this.#dragBuffer = []; 80 | } 81 | 82 | #currentBrush(altModifier) { 83 | let brush = this.#brush; 84 | if (altModifier) { 85 | brush = Brushes.gentle(brush); 86 | } 87 | 88 | if (this.#drag) { 89 | return Brushes.custom((x, y, random, oldElement) => { 90 | const element = brush.apply(x, y, random, oldElement); 91 | if (element !== null) { 92 | const elementHead = ElementHead.setType(element.elementHead, ElementHead.TYPE_STATIC); 93 | this.#dragBuffer.push([x, y]); 94 | return new Element(elementHead, element.elementTail); 95 | } 96 | return null; 97 | }); 98 | } else { 99 | return brush; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/core/tool/TemplateSelectionFakeTool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Tool from "./Tool"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2024-02-08 9 | */ 10 | export default class TemplateSelectionFakeTool extends Tool { 11 | 12 | #templateDefinitions; 13 | 14 | constructor(info, templateDefinitions) { 15 | super(info); 16 | this.#templateDefinitions = templateDefinitions; 17 | } 18 | 19 | getTemplateDefinitions() { 20 | return this.#templateDefinitions; 21 | } 22 | } -------------------------------------------------------------------------------- /src/core/tool/Tool.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import CursorDefinition from "./CursorDefinition.js"; 4 | import ToolInfo from "./ToolInfo"; 5 | 6 | /** 7 | * @interface 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-02-08 11 | */ 12 | export default class Tool { 13 | 14 | /** @type ToolInfo|null */ 15 | #info; 16 | 17 | constructor(info = ToolInfo.NOT_DEFINED) { 18 | this.#info = info; 19 | } 20 | 21 | /** 22 | * 23 | * @returns {ToolInfo} 24 | */ 25 | getInfo() { 26 | return this.#info; 27 | } 28 | 29 | hasCursor() { 30 | return false; 31 | } 32 | 33 | /** 34 | * @return {CursorDefinition|null} 35 | */ 36 | createCursor() { 37 | // default cursor 38 | return null; 39 | } 40 | 41 | isRepeatingEnabled() { 42 | return false; 43 | } 44 | 45 | /** 46 | * 47 | * @param x {number} 48 | * @param y {number} 49 | * @param graphics {SandGameGraphics} 50 | * @param altModifier {boolean} 51 | * @return {void} 52 | */ 53 | applyPoint(x, y, graphics, altModifier) { 54 | // no action by default 55 | } 56 | 57 | isLineModeEnabled() { 58 | return false; 59 | } 60 | 61 | onDragStart(x, y, graphics, altModifier) { 62 | // no action by default 63 | } 64 | 65 | onDragEnd(x, y, graphics, altModifier) { 66 | // no action by default 67 | } 68 | 69 | /** 70 | * 71 | * @param x1 {number} 72 | * @param y1 {number} 73 | * @param x2 {number} 74 | * @param y2 {number} 75 | * @param graphics {SandGameGraphics} 76 | * @param altModifier {boolean} 77 | * @return {void} 78 | */ 79 | applyStroke(x1, y1, x2, y2, graphics, altModifier) { 80 | // no action by default 81 | } 82 | 83 | isAreaModeEnabled() { 84 | return false; 85 | } 86 | 87 | /** 88 | * 89 | * @param x1 {number} 90 | * @param y1 {number} 91 | * @param x2 {number} 92 | * @param y2 {number} 93 | * @param graphics {SandGameGraphics} 94 | * @param altModifier {boolean} 95 | * @return {void} 96 | */ 97 | applyArea(x1, y1, x2, y2, graphics, altModifier) { 98 | // no action by default 99 | } 100 | 101 | /** 102 | * 103 | * @param x {number} 104 | * @param y {number} 105 | * @param graphics {SandGameGraphics} 106 | * @param altModifier {boolean} 107 | * @return {void} 108 | */ 109 | applySpecial(x, y, graphics, altModifier) { 110 | // no action by default 111 | } 112 | 113 | isSecondaryActionEnabled() { 114 | return false; 115 | } 116 | 117 | /** 118 | * 119 | * @param x {number} 120 | * @param y {number} 121 | * @param graphics {SandGameGraphics} 122 | * @param altModifier {boolean} 123 | * @return {void} 124 | */ 125 | applySecondaryAction(x, y, graphics, altModifier) { 126 | // no action by default 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/core/tool/ToolInfo.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2024-02-08 7 | */ 8 | export default class ToolInfo { 9 | 10 | static NOT_DEFINED = new ToolInfo(); 11 | 12 | #info 13 | 14 | /** 15 | * 16 | * @param info {{ 17 | * codeName: string|undefined, 18 | * displayName: string|undefined, 19 | * category: string|undefined, 20 | * badgeStyle: CSSStyleDeclaration|undefined, 21 | * }} 22 | */ 23 | constructor(info = {}) { 24 | this.#info = info; 25 | } 26 | 27 | derive(info) { 28 | const derivedInfo = {}; 29 | Object.assign(derivedInfo, this.#info); 30 | Object.assign(derivedInfo, info); 31 | return new ToolInfo(derivedInfo); 32 | } 33 | 34 | /** 35 | * 36 | * @return {string|undefined} 37 | */ 38 | getCategory() { 39 | return this.#info.category; 40 | } 41 | 42 | /** 43 | * 44 | * @return {string|undefined} 45 | */ 46 | getDisplayName() { 47 | return this.#info.displayName; 48 | } 49 | 50 | /** 51 | * 52 | * @return {string|undefined} 53 | */ 54 | getCodeName() { 55 | return this.#info.codeName; 56 | } 57 | 58 | /** 59 | * 60 | * @return {CSSStyleDeclaration|undefined} 61 | */ 62 | getBadgeStyle() { 63 | return this.#info.badgeStyle; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/core/tool/Tools.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import RoundBrushTool from "./RoundBrushTool"; 4 | import RoundBrushToolForSolidBody from "./RoundBrushToolForSolidBody"; 5 | import PointBrushTool from "./PointBrushTool"; 6 | import Point2BrushTool from "./Point2BrushTool"; 7 | import MeteorTool from "./MeteorTool"; 8 | import InsertElementAreaTool from "./InsertElementAreaTool"; 9 | import InsertRandomSceneTool from "./InsertRandomSceneTool"; 10 | import ActionTool from "./ActionTool"; 11 | import MoveTool from "./MoveTool"; 12 | import TemplateSelectionFakeTool from "./TemplateSelectionFakeTool"; 13 | import GlobalActionTool from "./GlobalActionTool"; 14 | 15 | /** 16 | * 17 | * @author Patrik Harag 18 | * @version 2024-03-13 19 | */ 20 | export default class Tools { 21 | 22 | static roundBrushTool(brush, size, info) { 23 | return new RoundBrushTool(info, brush, size); 24 | } 25 | 26 | static roundBrushToolForSolidBody(brush, size, info) { 27 | return new RoundBrushToolForSolidBody(info, brush, size); 28 | } 29 | 30 | static pointBrushTool(brush, info) { 31 | return new PointBrushTool(info, brush); 32 | } 33 | 34 | static point2BrushTool(brush1, brush2, info) { 35 | return new Point2BrushTool(info, brush1, brush2); 36 | } 37 | 38 | static meteorTool(info) { 39 | return new MeteorTool(info); 40 | } 41 | 42 | static insertScenesTool(scenes, handler, info) { 43 | if (scenes.length === 1) { 44 | const elementArea = InsertElementAreaTool.asElementArea(scenes[0]); 45 | return new InsertElementAreaTool(info, elementArea, handler); 46 | } else { 47 | return new InsertRandomSceneTool(info, scenes, handler); 48 | } 49 | } 50 | 51 | static actionTool(handler, info) { 52 | return new ActionTool(info, handler); 53 | } 54 | 55 | /** 56 | * 57 | * @param handler {function(SandGame|null)} 58 | * @param info {ToolInfo} 59 | * @return {Tool} 60 | */ 61 | static globalActionTool(handler, info) { 62 | return new GlobalActionTool(info, handler); 63 | } 64 | 65 | static moveTool(size, info) { 66 | return new MoveTool(info, size); 67 | } 68 | 69 | static templateSelectionTool(templateDefinitions, info) { 70 | return new TemplateSelectionFakeTool(info, templateDefinitions); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/def/Defaults.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ProcessorDefaults from "../core/processing/ProcessorDefaults"; 4 | import BrushDefs from "./BrushDefs"; 5 | import StructureDefs from "./StructureDefs"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-17 11 | */ 12 | export default class Defaults extends ProcessorDefaults { 13 | 14 | static #DEFAULT_ELEMENT = BrushDefs.AIR.apply(0, 0, undefined); 15 | 16 | getDefaultElement() { 17 | return Defaults.#DEFAULT_ELEMENT; 18 | } 19 | 20 | // brushes 21 | 22 | getBrushWater() { 23 | return BrushDefs.WATER; 24 | } 25 | 26 | getBrushSteam() { 27 | return BrushDefs.STEAM; 28 | } 29 | 30 | getBrushGrass() { 31 | return BrushDefs.GRASS; 32 | } 33 | 34 | getBrushTree() { 35 | return BrushDefs.TREE; 36 | } 37 | 38 | getBrushFishHead() { 39 | return BrushDefs.FISH_HEAD; 40 | } 41 | 42 | getBrushFishBody() { 43 | return BrushDefs.FISH_BODY; 44 | } 45 | 46 | getBrushFishCorpse() { 47 | return BrushDefs.FISH_CORPSE; 48 | } 49 | 50 | getBrushFire() { 51 | return BrushDefs.FIRE; 52 | } 53 | 54 | getBrushAsh() { 55 | return BrushDefs.ASH; 56 | } 57 | 58 | // structures 59 | 60 | getTreeTrunkTemplates() { 61 | return StructureDefs.TREE_TRUNK_TEMPLATES; 62 | } 63 | 64 | getTreeLeafClusterTemplates() { 65 | return StructureDefs.TREE_CLUSTER_TEMPLATES; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/def/StructureDefs.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import _ASSET_TREE_TRUNK_TEMPLATES from './assets/structures/tree-trunk-templates.json' 4 | import _ASSET_TREE_CELL_TEMPLATES from './assets/structures/tree-leaf-cluster-templates.json' 5 | 6 | /** 7 | * 8 | * @author Patrik Harag 9 | * @version 2023-12-18 10 | */ 11 | export default class StructureDefs { 12 | 13 | /** @type {[]} */ 14 | static TREE_TRUNK_TEMPLATES = _ASSET_TREE_TRUNK_TEMPLATES; 15 | 16 | /** @type {[]} */ 17 | static TREE_CLUSTER_TEMPLATES = _ASSET_TREE_CELL_TEMPLATES; 18 | } 19 | -------------------------------------------------------------------------------- /src/def/assets/brushes/ash.palette.csv: -------------------------------------------------------------------------------- 1 | 131,131,131 2 | 131,131,131 3 | 131,131,131 4 | 131,131,131 5 | 131,131,131 6 | 131,131,131 7 | 135,135,135 8 | 135,135,135 9 | 135,135,135 10 | 135,135,135 11 | 135,135,135 12 | 135,135,135 13 | 145,145,145 14 | 145,145,145 15 | 145,145,145 16 | 145,145,145 17 | 145,145,145 18 | 145,145,145 19 | 148,148,148 20 | 148,148,148 21 | 148,148,148 22 | 148,148,148 23 | 148,148,148 24 | 148,148,148 25 | 160,160,160 26 | 160,160,160 27 | 160,160,160 28 | 160,160,160 29 | 160,160,160 30 | 160,160,160 31 | 114,114,114 32 | 193,193,193 -------------------------------------------------------------------------------- /src/def/assets/brushes/coal.palette.csv: -------------------------------------------------------------------------------- 1 | 31,31,31 2 | 46,44,41 3 | 13,13,13 4 | 17,17,15 -------------------------------------------------------------------------------- /src/def/assets/brushes/gravel.palette.csv: -------------------------------------------------------------------------------- 1 | 97,94,88 2 | 111,110,106 3 | 117,116,112 4 | 117,117,113 5 | 120,118,115 6 | 104,102,97 7 | 113,112,107 8 | 129,128,125 9 | 124,124,121 10 | 81,80,75 11 | 80,76,69 12 | 123,119,111 13 | 105,104,99 14 | 84,82,78 15 | 77,74,69 16 | 91,88,82 17 | 68,65,60 18 | 79,75,69 19 | 85,82,77 20 | 98,94,88 21 | 105,102,96 22 | 104,97,86 23 | 60,55,47 24 | 93,89,81 -------------------------------------------------------------------------------- /src/def/assets/brushes/sand.palette.csv: -------------------------------------------------------------------------------- 1 | 214,212,154 2 | 214,212,154 3 | 214,212,154 4 | 214,212,154 5 | 225,217,171 6 | 225,217,171 7 | 225,217,171 8 | 225,217,171 9 | 203,201,142 10 | 203,201,142 11 | 203,201,142 12 | 203,201,142 13 | 195,194,134 14 | 195,194,134 15 | 218,211,165 16 | 218,211,165 17 | 223,232,201 18 | 186,183,128 -------------------------------------------------------------------------------- /src/def/assets/brushes/soil.palette.csv: -------------------------------------------------------------------------------- 1 | 142,104,72 2 | 142,104,72 3 | 142,104,72 4 | 142,104,72 5 | 142,104,72 6 | 142,104,72 7 | 114,81,58 8 | 114,81,58 9 | 114,81,58 10 | 114,81,58 11 | 114,81,58 12 | 114,81,58 13 | 82,64,30 14 | 82,64,30 15 | 82,64,30 16 | 177,133,87 17 | 177,133,87 18 | 177,133,87 19 | 102,102,102 -------------------------------------------------------------------------------- /src/def/assets/brushes/steam.palette.csv: -------------------------------------------------------------------------------- 1 | 147,182,217 2 | 163,188,212 -------------------------------------------------------------------------------- /src/def/assets/brushes/thermite-1.palette.csv: -------------------------------------------------------------------------------- 1 | 68,42,41 2 | 68,42,41 3 | 68,42,41 4 | 68,42,41 5 | 68,42,41 6 | 50,28,27 7 | 50,28,27 8 | 50,28,27 9 | 83,55,55 10 | 47,28,28 -------------------------------------------------------------------------------- /src/def/assets/brushes/thermite-2.palette.csv: -------------------------------------------------------------------------------- 1 | 137,86,89 2 | 137,86,89 3 | 137,86,89 4 | 137,86,89 5 | 137,86,89 6 | 137,86,89 7 | 125,70,72 8 | 125,70,72 9 | 147,93,96 10 | 147,93,96 11 | 138,84,86 12 | 138,84,86 13 | 118,72,75 14 | 118,72,75 15 | 101,61,65 16 | 151,104,106 -------------------------------------------------------------------------------- /src/def/assets/brushes/tree-leaf-dark.palette.csv: -------------------------------------------------------------------------------- 1 | 74,86,47 2 | 74,86,47 3 | 74,86,47 4 | 74,86,47 5 | 68,77,40 6 | 68,77,40 7 | 68,77,40 8 | 70,82,42 9 | 70,82,42 10 | 70,82,42 11 | 72,82,46 12 | 72,82,46 13 | 78,90,48 14 | 78,90,48 15 | 95,106,60 16 | 88,100,57 17 | 66,74,36 18 | 66,67,35 19 | 76,87,52 20 | 86,100,53 21 | 75,89,47 -------------------------------------------------------------------------------- /src/def/assets/brushes/tree-leaf-light.palette.csv: -------------------------------------------------------------------------------- 1 | 128,137,79 2 | 128,137,79 3 | 128,137,79 4 | 128,137,79 5 | 128,137,79 6 | 141,149,91 7 | 141,149,91 8 | 117,128,71 9 | 99,110,65 10 | 111,123,68 11 | 121,132,73 12 | 111,123,74 -------------------------------------------------------------------------------- /src/def/assets/brushes/tree-root.palette.csv: -------------------------------------------------------------------------------- 1 | 75,54,31 2 | 67,53,38 -------------------------------------------------------------------------------- /src/def/assets/brushes/tree-wood-dark.palette.csv: -------------------------------------------------------------------------------- 1 | 94,70,42 2 | 109,82,53 -------------------------------------------------------------------------------- /src/def/assets/brushes/tree-wood-light.palette.csv: -------------------------------------------------------------------------------- 1 | 133,108,80 2 | 124,97,67 -------------------------------------------------------------------------------- /src/def/assets/brushes/wall.palette.csv: -------------------------------------------------------------------------------- 1 | 55,55,55 2 | 57,57,57 -------------------------------------------------------------------------------- /src/def/assets/brushes/water.palette.csv: -------------------------------------------------------------------------------- 1 | 4,135,186 2 | 5,138,189 -------------------------------------------------------------------------------- /src/def/assets/templates/rock-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-1.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-2.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-3.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-4.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-5.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-6.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-icon.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-lg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-lg-1.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-lg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-lg-2.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-lg-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-lg-icon.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-sm-1.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-sm-2.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-sm-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-sm-3.png -------------------------------------------------------------------------------- /src/def/assets/templates/rock-sm-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/rock-sm-icon.png -------------------------------------------------------------------------------- /src/def/assets/templates/sand-castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/sand-castle.png -------------------------------------------------------------------------------- /src/def/assets/templates/wooden-house-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/wooden-house-icon.png -------------------------------------------------------------------------------- /src/def/assets/templates/wooden-house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/def/assets/templates/wooden-house.png -------------------------------------------------------------------------------- /src/gui/ServiceToolManager.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Tool from "../core/tool/Tool.js"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2024-01-15 9 | */ 10 | export default class ServiceToolManager { 11 | 12 | #primaryTool; 13 | #secondaryTool; 14 | #tertiaryTool; 15 | 16 | /** @type function(Tool)[] */ 17 | #onPrimaryToolChanged = []; 18 | 19 | 20 | /** @type boolean */ 21 | #inputDisabled; 22 | /** @type function(boolean)[] */ 23 | #onInputDisabledChanged = []; 24 | 25 | constructor(primaryTool, secondaryTool, tertiaryTool) { 26 | this.#primaryTool = primaryTool; 27 | this.#secondaryTool = secondaryTool; 28 | this.#tertiaryTool = tertiaryTool; 29 | } 30 | 31 | /** 32 | * 33 | * @param tool {Tool} 34 | * @returns void 35 | */ 36 | setPrimaryTool(tool) { 37 | this.#primaryTool = tool; 38 | for (let handler of this.#onPrimaryToolChanged) { 39 | handler(tool); 40 | } 41 | } 42 | 43 | /** 44 | * 45 | * @param tool {Tool} 46 | * @returns void 47 | */ 48 | setSecondaryTool(tool) { 49 | this.#secondaryTool = tool; 50 | } 51 | 52 | /** 53 | * 54 | * @param tool {Tool} 55 | * @returns void 56 | */ 57 | setTertiaryTool(tool) { 58 | this.#tertiaryTool = tool; 59 | } 60 | 61 | /** 62 | * @returns {Tool} 63 | */ 64 | getPrimaryTool() { 65 | return this.#primaryTool; 66 | } 67 | 68 | /** 69 | * @returns {Tool} 70 | */ 71 | getSecondaryTool() { 72 | return this.#secondaryTool; 73 | } 74 | 75 | /** 76 | * @returns {Tool} 77 | */ 78 | getTertiaryTool() { 79 | return this.#tertiaryTool; 80 | } 81 | 82 | /** 83 | * 84 | * @param handler {function(Tool)} 85 | */ 86 | addOnPrimaryToolChanged(handler) { 87 | this.#onPrimaryToolChanged.push(handler); 88 | } 89 | 90 | /** 91 | * 92 | * @returns {boolean} 93 | */ 94 | isInputDisabled() { 95 | return this.#inputDisabled; 96 | } 97 | 98 | /** 99 | * 100 | * @param handler {function(boolean)} 101 | */ 102 | addOnInputDisabledChanged(handler) { 103 | this.#onInputDisabledChanged.push(handler); 104 | } 105 | 106 | setInputDisabled(disabled) { 107 | if (this.#inputDisabled !== disabled) { 108 | this.#inputDisabled = disabled; 109 | for (let handler of this.#onInputDisabledChanged) { 110 | handler(disabled); 111 | } 112 | } 113 | } 114 | 115 | createRevertAction() { 116 | const oldPrimary = this.getPrimaryTool(); 117 | const oldSecondary = this.getSecondaryTool(); 118 | const oldTertiary = this.getTertiaryTool(); 119 | return () => { 120 | this.setPrimaryTool(oldPrimary); 121 | this.setSecondaryTool(oldSecondary); 122 | this.setTertiaryTool(oldTertiary); 123 | }; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/gui/SizeUtils.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * 5 | * @author Patrik Harag 6 | * @version 2024-03-24 7 | */ 8 | export default class SizeUtils { 9 | 10 | static #determineSize(root, maxWidthPx = 1400, maxHeightPx = 800) { 11 | let parentWidth; 12 | if (window.innerWidth <= 575) { 13 | parentWidth = window.innerWidth; // no margins 14 | } else { 15 | parentWidth = root.clientWidth; // including padding 16 | } 17 | 18 | let width = Math.min(maxWidthPx, parentWidth); 19 | let height = Math.min(maxHeightPx, Math.trunc(window.innerHeight * 0.70)); 20 | if (width / height < 0.75) { 21 | height = Math.trunc(width / 0.75); 22 | } 23 | return {width, height}; 24 | } 25 | 26 | static #determineMaxNumberOfPoints() { 27 | if (navigator.maxTouchPoints || 'ontouchstart' in document.documentElement) { 28 | // probably a smartphone 29 | return 75000; 30 | } else { 31 | // bigger screen => usually more powerful (or newer) computer 32 | if (window.screen.width >= 2560 && window.screen.height >= 1440) { 33 | return 200000; // >= QHD 34 | } else if (window.screen.width >= 2048 && window.screen.height >= 1080) { 35 | return 175000; // >= 2k 36 | } else if (window.screen.width >= 1920 && window.screen.height >= 1080) { 37 | return 150000; 38 | } else { 39 | return 125000; 40 | } 41 | } 42 | } 43 | 44 | static #determineOptimalScale(width, height, maxPoints) { 45 | function countPointsWithScale(scale) { 46 | return Math.trunc(width * scale) * Math.trunc(height * scale); 47 | } 48 | 49 | if (countPointsWithScale(0.750) < maxPoints) { 50 | return 0.750; 51 | } else if (countPointsWithScale(0.5) < maxPoints) { 52 | return 0.5; 53 | } else if (countPointsWithScale(0.375) < maxPoints) { 54 | return 0.375; 55 | } else { 56 | return 0.25; 57 | } 58 | } 59 | 60 | static determineOptimalSizes(parentNode, canvasConfig = undefined) { 61 | const maxWidthPx = (canvasConfig !== undefined) ? canvasConfig.maxWidthPx : undefined; 62 | const maxHeightPx = (canvasConfig !== undefined) ? canvasConfig.maxHeightPx : undefined; 63 | 64 | const {width, height} = SizeUtils.#determineSize(parentNode, maxWidthPx, maxHeightPx); 65 | 66 | const scaleOverride = (canvasConfig !== undefined) ? canvasConfig.scale : undefined; 67 | const scale = (scaleOverride === undefined) 68 | ? SizeUtils.#determineOptimalScale(width, height, SizeUtils.#determineMaxNumberOfPoints()) 69 | : scaleOverride; 70 | return { width, height, scale }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/gui/action/Action.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @interface 5 | * 6 | * @author Patrik Harag 7 | * @version 2023-08-19 8 | */ 9 | export default class Action { 10 | 11 | /** 12 | * 13 | * @param controller {Controller} 14 | * @returns void 15 | */ 16 | performAction(controller) { 17 | throw 'Not implemented'; 18 | } 19 | 20 | /** 21 | * 22 | * @param func {function(controller:Controller):void} 23 | * @returns {ActionAnonymous} 24 | */ 25 | static create(func) { 26 | return new ActionAnonymous(func); 27 | } 28 | 29 | /** 30 | * 31 | * @param def {boolean} 32 | * @param func {function(controller:Controller,v:boolean):void} 33 | * @returns {ActionAnonymous} 34 | */ 35 | static createToggle(def, func) { 36 | let state = def; 37 | return new ActionAnonymous(function (c) { 38 | state = !state; 39 | func(c, state); 40 | }); 41 | } 42 | } 43 | 44 | class ActionAnonymous extends Action { 45 | #func; 46 | 47 | constructor(func) { 48 | super(); 49 | this.#func = func; 50 | } 51 | 52 | performAction(controller) { 53 | this.#func(controller); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/gui/action/ActionDialogChangeCanvasSize.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Action from "./Action"; 5 | import Analytics from "../../Analytics"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-08-19 11 | */ 12 | export default class ActionDialogChangeCanvasSize extends Action { 13 | 14 | performAction(controller) { 15 | let formBuilder = DomBuilder.bootstrapSimpleFormBuilder(); 16 | formBuilder.addInput('Width', 'width', '' + controller.getCurrentWidthPoints()); 17 | formBuilder.addInput('Height', 'height', '' + controller.getCurrentHeightPoints()); 18 | formBuilder.addInput('Scale', 'scale', '' + controller.getCurrentScale()); 19 | 20 | let dialog = DomBuilder.bootstrapDialogBuilder(); 21 | dialog.setHeaderContent('Change canvas size manually'); 22 | dialog.setBodyContent(formBuilder.createNode()); 23 | dialog.addSubmitButton('Submit', () => { 24 | let data = formBuilder.getData(); 25 | let w = Number.parseInt(data['width']); 26 | let h = Number.parseInt(data['height']); 27 | let s = Number.parseFloat(data['scale']); 28 | controller.changeCanvasSize(w, h, s); 29 | Analytics.triggerFeatureUsed(Analytics.FEATURE_CANVAS_SIZE_CHANGE); 30 | }); 31 | dialog.addCloseButton('Close'); 32 | dialog.show(controller.getDialogAnchor()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/gui/action/ActionDialogChangeElementSize.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Action from "./Action"; 5 | import ActionDialogChangeCanvasSize from "./ActionDialogChangeCanvasSize"; 6 | import ComponentViewElementSizeSelection from "../component/ComponentViewElementSizeSelection"; 7 | 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2023-08-19 12 | */ 13 | export default class ActionDialogChangeElementSize extends Action { 14 | 15 | performAction(controller) { 16 | let elementSizeComponent = new ComponentViewElementSizeSelection(); 17 | 18 | let dialog = DomBuilder.bootstrapDialogBuilder(); 19 | dialog.setHeaderContent('Adjust Scale'); 20 | dialog.setBodyContent(DomBuilder.div({ class: 'sand-game-component' }, [ 21 | elementSizeComponent.createNode(controller) 22 | ])); 23 | dialog.addSubmitButton("Set size manually", () => { 24 | new ActionDialogChangeCanvasSize().performAction(controller); 25 | }); 26 | dialog.addCloseButton('Close'); 27 | dialog.show(controller.getDialogAnchor()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/gui/action/ActionDialogTemplateSelection.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Action from "./Action"; 5 | import ComponentViewTemplateSelection from "../component/ComponentViewTemplateSelection"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-02-08 11 | */ 12 | export default class ActionDialogTemplateSelection extends Action { 13 | 14 | #templateDefinitions; 15 | #additionalInfo; 16 | 17 | constructor(templateDefinitions, additionalInfo = null) { 18 | super(); 19 | this.#templateDefinitions = templateDefinitions; 20 | this.#additionalInfo = additionalInfo; 21 | } 22 | 23 | performAction(controller) { 24 | let templatesComponent = new ComponentViewTemplateSelection(this.#templateDefinitions); 25 | 26 | let dialog = DomBuilder.bootstrapDialogBuilder(); 27 | dialog.setHeaderContent('Templates'); 28 | dialog.setBodyContent(DomBuilder.div({ class: 'sand-game-component' }, [ 29 | DomBuilder.par(null, "Select a template"), 30 | templatesComponent.createNode(controller), 31 | this.#additionalInfo 32 | ])); 33 | dialog.addCloseButton('Close'); 34 | dialog.show(controller.getDialogAnchor()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/gui/action/ActionFill.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "./Action"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-12-05 9 | */ 10 | export default class ActionFill extends Action { 11 | 12 | performAction(controller) { 13 | const sandGame = controller.getSandGame(); 14 | if (sandGame === null) { 15 | return; 16 | } 17 | const primaryTool = controller.getToolManager().getPrimaryTool(); 18 | if (!primaryTool.isAreaModeEnabled()) { 19 | return; 20 | } 21 | primaryTool.applyArea(0, 0, sandGame.getWidth(), sandGame.getHeight(), sandGame.graphics(), false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/gui/action/ActionIOExport.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "./Action"; 4 | import Analytics from "../../Analytics"; 5 | import Resources from "../../io/Resources"; 6 | import FileSaver from 'file-saver'; 7 | 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2023-08-19 12 | */ 13 | export default class ActionIOExport extends Action { 14 | 15 | performAction(controller) { 16 | const snapshot = controller.createSnapshot(); 17 | const bytes = Resources.createResourceFromSnapshot(snapshot); 18 | FileSaver.saveAs(new Blob([bytes]), this.#createFilename()); 19 | Analytics.triggerFeatureUsed(Analytics.FEATURE_IO_EXPORT); 20 | } 21 | 22 | #createFilename() { 23 | let date = new Date().toISOString().slice(0, 10); 24 | return `sand-game-js_${date}.sgjs`; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/gui/action/ActionIOImport.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "./Action"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2023-08-19 9 | */ 10 | export default class ActionIOImport extends Action { 11 | 12 | performAction(controller) { 13 | let input = document.createElement('input'); 14 | input.type = 'file'; 15 | input.onchange = e => { 16 | controller.getIOManager().loadFromFiles(e.target.files); 17 | } 18 | input.click(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/gui/action/ActionRecord.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "./Action"; 4 | import FileSaver from 'file-saver'; 5 | import { zipSync } from "fflate"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-02-02 11 | */ 12 | export default class ActionRecord extends Action { 13 | 14 | #controllerHandlersRegistered = false; 15 | #sandGameHandlersRegistered = false; 16 | 17 | #zipData = null; 18 | 19 | performAction(controller) { 20 | if (!this.#controllerHandlersRegistered) { 21 | controller.addOnBeforeClosed(() => { 22 | if (this.#zipData !== null) { 23 | this.#stopRecording(); 24 | } 25 | }); 26 | controller.addOnInitialized(sandGame => { 27 | this.#sandGameHandlersRegistered = false; 28 | }) 29 | this.#controllerHandlersRegistered = true; 30 | } 31 | 32 | if (this.#zipData === null) { 33 | // start recording 34 | this.#zipData = {}; 35 | 36 | if (!this.#sandGameHandlersRegistered) { 37 | let processed = false; 38 | let lastIteration = 0; 39 | let frameInProgress = false; 40 | controller.getSandGame().addOnProcessed((iteration) => { 41 | processed = true; 42 | lastIteration = iteration; 43 | }); 44 | controller.getSandGame().addOnRendered(() => { 45 | if (this.#zipData !== null && processed && !frameInProgress) { 46 | frameInProgress = true; 47 | this.#addFrame(controller, lastIteration, () => frameInProgress = false); 48 | processed = false; 49 | } 50 | }); 51 | this.#sandGameHandlersRegistered = true; 52 | } 53 | } else { 54 | this.#stopRecording(); 55 | } 56 | } 57 | 58 | #addFrame(controller, iteration, completed) { 59 | const canvas = controller.getCanvas(); 60 | if (canvas !== null) { 61 | canvas.toBlob((blob) => { 62 | blob.arrayBuffer().then(arrayBuffer => { 63 | const array = new Uint8Array(arrayBuffer); 64 | if (this.#zipData !== null) { 65 | this.#zipData[`iteration_${String(iteration).padStart(6, '0')}.png`] = array; 66 | } 67 | }).finally(() => { 68 | completed(); 69 | }) 70 | }); 71 | } 72 | } 73 | 74 | #stopRecording() { 75 | this.#download(this.#zipData, 'frames.zip'); 76 | this.#zipData = null; 77 | } 78 | 79 | #download(zipData, filename) { 80 | const bytes = zipSync(zipData, { level: 0 }); 81 | FileSaver.saveAs(new Blob([bytes]), filename); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/gui/action/ActionReportProblem.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "./Action"; 4 | import DomBuilder from "../DomBuilder"; 5 | 6 | /** 7 | * 8 | * @author Patrik Harag 9 | * @version 2024-02-04 10 | */ 11 | export default class ActionReportProblem extends Action { 12 | 13 | /** @type function(type:string,message:string,controller:Controller) */ 14 | #handler; 15 | 16 | constructor(handler) { 17 | super(); 18 | this.#handler = handler; 19 | } 20 | 21 | performAction(controller) { 22 | let formBuilder = DomBuilder.bootstrapSimpleFormBuilder(); 23 | formBuilder.addTextArea('Message', 'message'); 24 | 25 | let dialog = DomBuilder.bootstrapDialogBuilder(); 26 | dialog.setHeaderContent('Report a problem'); 27 | dialog.setBodyContent(formBuilder.createNode()); 28 | dialog.addSubmitButton('Submit', () => { 29 | let data = formBuilder.getData(); 30 | let message = data['message']; 31 | if (message.trim() !== '') { 32 | this.#handler("user", message, controller); 33 | } 34 | }); 35 | dialog.addCloseButton('Close'); 36 | dialog.show(controller.getDialogAnchor()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/gui/action/ActionRestart.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "./Action"; 4 | import DomBuilder from "../DomBuilder"; 5 | import Analytics from "../../Analytics"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-01-17 11 | */ 12 | export default class ActionRestart extends Action { 13 | 14 | performAction(controller) { 15 | let dialog = DomBuilder.bootstrapDialogBuilder(); 16 | dialog.setHeaderContent('Restart'); 17 | dialog.setBodyContent([ 18 | DomBuilder.par(null, "Are you sure?") 19 | ]); 20 | dialog.addSubmitButton('Restart', () => { 21 | const scene = controller.getInitialScene(); 22 | controller.openScene(scene); 23 | Analytics.triggerFeatureUsed(Analytics.FEATURE_RESTART_SCENE); 24 | }); 25 | dialog.addCloseButton('Close'); 26 | dialog.show(controller.getDialogAnchor()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/gui/action/ActionScreenshot.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "./Action"; 4 | import FileSaver from 'file-saver'; 5 | 6 | /** 7 | * 8 | * @author Patrik Harag 9 | * @version 2023-11-20 10 | */ 11 | export default class ActionScreenshot extends Action { 12 | 13 | performAction(controller) { 14 | const canvas = controller.getCanvas(); 15 | if (canvas !== null) { 16 | canvas.toBlob((blob) => { 17 | FileSaver.saveAs(blob, this.#formatDate(new Date()) + '.png'); 18 | }); 19 | } 20 | } 21 | 22 | #formatDate(date) { 23 | let dd = String(date.getDate()).padStart(2, '0'); 24 | let MM = String(date.getMonth() + 1).padStart(2, '0'); // January is 0! 25 | let yyyy = date.getFullYear(); 26 | 27 | let hh = String(date.getHours()).padStart(2, '0'); 28 | let mm = String(date.getMinutes()).padStart(2, '0'); 29 | 30 | return `${yyyy}-${MM}-${dd}_${hh}-${mm}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/gui/action/ActionsTest.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import BrushDefs from "../../def/BrushDefs"; 4 | import StructureDefs from "../../def/StructureDefs"; 5 | 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-19 11 | */ 12 | export default class ActionsTest { 13 | 14 | static ALL_MATERIALS = function (controller) { 15 | let sandGame = controller.getSandGame(); 16 | if (sandGame === null) { 17 | return; 18 | } 19 | 20 | const brushes = [ 21 | 'ash', 22 | 'sand', 23 | 'soil', 24 | 'gravel', 25 | 'wall', 26 | 'rock', 27 | 'metal', 28 | 'wood', 29 | 'water' 30 | ]; 31 | 32 | let segment = Math.ceil(sandGame.getWidth() / brushes.length); 33 | for (let i = 0; i < brushes.length; i++) { 34 | const brush = BrushDefs.byCodeName(brushes[i]); 35 | sandGame.graphics().drawRectangle(i * segment, 0, (i + 1) * segment, -1, brush, true); 36 | } 37 | } 38 | 39 | static TREE_SPAWN_TEST = function (controller) { 40 | let sandGame = controller.getSandGame(); 41 | if (sandGame === null) { 42 | return; 43 | } 44 | 45 | sandGame.graphics().fill(BrushDefs.AIR); 46 | 47 | sandGame.layeredTemplate() 48 | .layer(Math.trunc(sandGame.getHeight() / 2), false, BrushDefs.AIR) 49 | .layer(1, true, BrushDefs.WALL) 50 | .layer(10, true, BrushDefs.SOIL) 51 | .grass(); 52 | 53 | sandGame.layeredTemplate() 54 | .layer(1, true, BrushDefs.WALL) 55 | .layer(10, true, BrushDefs.SOIL) 56 | .grass(); 57 | } 58 | 59 | static treeGrowTest(level = -1) { 60 | return function (controller) { 61 | let sandGame = controller.getSandGame(); 62 | if (sandGame === null) { 63 | return; 64 | } 65 | 66 | sandGame.graphics().fill(BrushDefs.AIR); 67 | 68 | let count = StructureDefs.TREE_TRUNK_TEMPLATES.length; 69 | let segment = Math.trunc(sandGame.getWidth() / 8); 70 | 71 | const template1 = sandGame.layeredTemplate(); 72 | template1.layer(Math.trunc(sandGame.getHeight() / 2), false, BrushDefs.AIR); 73 | template1.layer(1, true, BrushDefs.WALL); 74 | template1.layer(10, true, BrushDefs.SOIL); 75 | for (let i = 0; i < 8; i++) { 76 | template1.tree(Math.trunc((i + 0.5) * segment), i % count, level); 77 | } 78 | template1.grass(); 79 | 80 | const template2 = sandGame.layeredTemplate(); 81 | template2.layer(1, true, BrushDefs.WALL); 82 | template2.layer(10, true, BrushDefs.SOIL); 83 | for (let i = 0; i < 8; i++) { 84 | template2.tree(Math.trunc((i + 0.5) * segment), (i + 8) % count, level); 85 | } 86 | template2.grass(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/gui/component/Component.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | /** 4 | * @interface 5 | * 6 | * @author Patrik Harag 7 | * @version 2023-12-22 8 | */ 9 | export default class Component { 10 | 11 | /** 12 | * 13 | * @param controller {Controller} 14 | * @return {HTMLElement} 15 | */ 16 | createNode(controller) { 17 | throw 'Not implemented'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/gui/component/ComponentButton.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Component from "./Component"; 4 | import DomBuilder from "../DomBuilder"; 5 | import Action from "../action/Action"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2023-12-05 11 | */ 12 | export default class ComponentButton extends Component { 13 | 14 | static CLASS_PRIMARY = 'btn-primary'; 15 | static CLASS_SECONDARY = 'btn-secondary'; 16 | static CLASS_INFO = 'btn-info'; 17 | static CLASS_SUCCESS = 'btn-success'; 18 | static CLASS_WARNING = 'btn-warning'; 19 | static CLASS_LIGHT = 'btn-light'; 20 | 21 | static CLASS_OUTLINE_PRIMARY = 'btn-outline-primary'; 22 | static CLASS_OUTLINE_SECONDARY = 'btn-outline-secondary'; 23 | static CLASS_OUTLINE_INFO = 'btn-outline-info'; 24 | static CLASS_OUTLINE_SUCCESS = 'btn-outline-success'; 25 | static CLASS_OUTLINE_WARNING = 'btn-outline-warning'; 26 | static CLASS_OUTLINE_LIGHT = 'btn-outline-light'; 27 | 28 | 29 | #label; 30 | #action; 31 | #cssClass; 32 | 33 | /** 34 | * 35 | * @param label {string|HTMLElement|HTMLElement[]} 36 | * @param cssClass {string|null} 37 | * @param action {Action|function} 38 | */ 39 | constructor(label, cssClass, action) { 40 | super(); 41 | this.#label = label; 42 | this.#action = (typeof action === "function" ? Action.create(action) : action); 43 | this.#cssClass = (cssClass == null ? ComponentButton.CLASS_PRIMARY : cssClass); 44 | } 45 | 46 | createNode(controller) { 47 | return DomBuilder.button(this.#label, { class: 'btn ' + this.#cssClass }, e => { 48 | this.#action.performAction(controller); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/gui/component/ComponentButtonAdjustScale.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Component from "./Component"; 4 | import DomBuilder from "../DomBuilder"; 5 | import ActionDialogChangeElementSize from "../action/ActionDialogChangeElementSize"; 6 | 7 | import _ASSET_SVG_ADJUST_SCALE from "./assets/icon-adjust-scale.svg"; 8 | 9 | /** 10 | * 11 | * @author Patrik Harag 12 | * @version 2023-08-19 13 | */ 14 | export default class ComponentButtonAdjustScale extends Component { 15 | 16 | createNode(controller) { 17 | return DomBuilder.button(DomBuilder.create(_ASSET_SVG_ADJUST_SCALE), { 18 | class: 'btn btn-outline-secondary adjust-scale', 19 | 'aria-label': 'Adjust scale' 20 | }, () => { 21 | new ActionDialogChangeElementSize().performAction(controller); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/gui/component/ComponentButtonReport.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ComponentButton from "./ComponentButton"; 4 | import ActionReportProblem from "../action/ActionReportProblem"; 5 | import DomBuilder from "../DomBuilder"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-03-23 11 | */ 12 | export default class ComponentButtonReport extends ComponentButton { 13 | 14 | constructor(cssClass, errorReporter) { 15 | const label = [ 16 | 'Report', 17 | DomBuilder.span(' a\xa0problem', { class: 'visible-on-big-screen-only' }) 18 | ]; 19 | super(label, cssClass, new ActionReportProblem(errorReporter)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/gui/component/ComponentButtonRestart.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import ComponentButton from "./ComponentButton"; 4 | import DomBuilder from "../DomBuilder"; 5 | import ActionRestart from "../action/ActionRestart"; 6 | 7 | import _ASSET_SVG_RESTART from "./assets/icon-reset.svg"; 8 | 9 | /** 10 | * 11 | * @author Patrik Harag 12 | * @version 2024-01-17 13 | */ 14 | export default class ComponentButtonRestart extends ComponentButton { 15 | 16 | constructor(cssClass) { 17 | super('', cssClass, new ActionRestart()); 18 | } 19 | 20 | createNode(controller) { 21 | const btn = super.createNode(controller); 22 | DomBuilder.setContent(btn, [DomBuilder.create(_ASSET_SVG_RESTART), 'Restart']); 23 | return btn; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/gui/component/ComponentButtonStartStop.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Action from "../action/Action"; 4 | import ComponentButton from "./ComponentButton"; 5 | import Analytics from "../../Analytics"; 6 | import DomBuilder from "../DomBuilder"; 7 | 8 | import _ASSET_SVG_PAUSE from "./assets/icon-pause.svg"; 9 | import _ASSET_SVG_PLAY from "./assets/icon-play.svg"; 10 | 11 | /** 12 | * 13 | * @author Patrik Harag 14 | * @version 2024-01-10 15 | */ 16 | export default class ComponentButtonStartStop extends ComponentButton { 17 | 18 | constructor(cssClass) { 19 | super('', cssClass, Action.create(controller => { 20 | controller.switchStartStop(); 21 | Analytics.triggerFeatureUsed(Analytics.FEATURE_PAUSE); 22 | })); 23 | } 24 | 25 | createNode(controller) { 26 | const btn = super.createNode(controller); 27 | DomBuilder.setContent(btn, [DomBuilder.create(_ASSET_SVG_PLAY), 'Start']); 28 | 29 | controller.addOnStarted(() => { 30 | DomBuilder.setContent(btn, [DomBuilder.create(_ASSET_SVG_PAUSE), 'Pause']); 31 | }); 32 | controller.addOnStopped(() => { 33 | DomBuilder.setContent(btn, [DomBuilder.create(_ASSET_SVG_PLAY), 'Start']); 34 | }); 35 | 36 | return btn; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/gui/component/ComponentContainer.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Component from "./Component"; 5 | 6 | /** 7 | * 8 | * @author Patrik Harag 9 | * @version 2024-01-04 10 | */ 11 | export default class ComponentContainer extends Component { 12 | 13 | #cssClass; 14 | #components; 15 | 16 | /** 17 | * 18 | * @param cssClass {string|null} 19 | * @param components {Component[]} 20 | */ 21 | constructor(cssClass, components) { 22 | super(); 23 | this.#cssClass = cssClass; 24 | this.#components = components; 25 | } 26 | 27 | createNode(controller) { 28 | const content = DomBuilder.div({ class: this.#cssClass }, []); 29 | for (let component of this.#components) { 30 | if (component !== null) { 31 | content.append(component.createNode(controller)); 32 | } 33 | } 34 | return content; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/gui/component/ComponentSimple.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Component from "./Component"; 4 | 5 | /** 6 | * 7 | * @author Patrik Harag 8 | * @version 2024-01-04 9 | */ 10 | export default class ComponentSimple extends Component { 11 | 12 | #node; 13 | 14 | /** 15 | * 16 | * @param node {HTMLElement} 17 | */ 18 | constructor(node) { 19 | super(); 20 | this.#node = node; 21 | } 22 | 23 | createNode(controller) { 24 | return this.#node; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/gui/component/ComponentViewCanvas.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Component from "./Component"; 5 | import ComponentViewCanvasInner from "./ComponentViewCanvasInner"; 6 | 7 | /** 8 | * 9 | * @author Patrik Harag 10 | * @version 2024-01-15 11 | */ 12 | export default class ComponentViewCanvas extends Component { 13 | 14 | /** @type {HTMLElement} */ 15 | #canvasHolderNode = DomBuilder.div({ class: 'sand-game-canvas-holder' }); 16 | /** @type {ComponentViewCanvasInner} */ 17 | #currentCanvas = null; 18 | 19 | createNode(controller) { 20 | controller.registerCanvasProvider({ 21 | initialize: () => { 22 | this.#canvasHolderNode.innerHTML = ''; 23 | 24 | this.#currentCanvas = new ComponentViewCanvasInner(controller); 25 | this.#canvasHolderNode.append(this.#currentCanvas.createNode(controller)); 26 | return this.#currentCanvas.getCanvasNode(); 27 | }, 28 | getCanvasNode: () => { 29 | if (this.#currentCanvas !== null) { 30 | return this.#currentCanvas.getCanvasNode(); 31 | } 32 | return null; 33 | } 34 | }); 35 | 36 | controller.addOnImageRenderingStyleChanged((imageRenderingStyle) => { 37 | if (this.#currentCanvas !== null) { 38 | this.#currentCanvas.setImageRenderingStyle(imageRenderingStyle) 39 | } 40 | }); 41 | 42 | controller.addOnInitialized((sandGame) => { 43 | if (this.#currentCanvas === null) { 44 | throw 'Illegal state: canvas is not initialized'; 45 | } 46 | 47 | // register mouse handling and overlays 48 | this.#currentCanvas.register(sandGame); 49 | }) 50 | 51 | controller.addOnBeforeClosed(() => { 52 | this.#currentCanvas = null; 53 | this.#canvasHolderNode.innerHTML = ''; 54 | }) 55 | 56 | controller.getToolManager().addOnInputDisabledChanged(disabled => { 57 | if (this.#currentCanvas !== null) { 58 | this.#currentCanvas.onInputDisabledChanged(disabled); 59 | } 60 | }); 61 | 62 | return this.#canvasHolderNode; 63 | } 64 | } -------------------------------------------------------------------------------- /src/gui/component/ComponentViewCanvasOverlayMarker.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Component from "./Component"; 4 | import DomBuilder from "../DomBuilder"; 5 | 6 | /** 7 | * 8 | * @author Patrik Harag 9 | * @version 2024-01-17 10 | */ 11 | export default class ComponentViewCanvasOverlayMarker extends Component { 12 | 13 | /** @type Controller */ 14 | #controller; 15 | 16 | #nodeOverlay; 17 | 18 | #w; 19 | #h; 20 | #scale; 21 | 22 | constructor(w, h, scale, controller) { 23 | super(); 24 | this.#w = w; 25 | this.#h = h; 26 | this.#scale = scale; 27 | const wPx = w / scale; 28 | const hPx = h / scale; 29 | this.#nodeOverlay = DomBuilder.div({ 30 | style: { 31 | display: 'none', // hidden by default 32 | position: 'absolute', 33 | left: '0', 34 | top: '0', 35 | width: `${wPx}px`, 36 | height: `${hPx}px` 37 | }, 38 | class: 'sand-game-canvas-overlay', 39 | width: w + 'px', 40 | height: h + 'px', 41 | }); 42 | this.#controller = controller; 43 | } 44 | 45 | /** 46 | * 47 | * @param overlay {SandGameOverlay} 48 | */ 49 | register(overlay) { 50 | const markers = overlay.getMarkers(); 51 | if (markers.length > 0) { 52 | for (const marker of markers) { 53 | this.#nodeOverlay.append(this.#createMarkerNode(marker)); 54 | } 55 | this.#nodeOverlay.style.display = 'initial'; 56 | } 57 | 58 | // future markers 59 | overlay.addOnMarkerAdded((marker) => { 60 | this.#nodeOverlay.append(this.#createMarkerNode(marker)); 61 | this.#nodeOverlay.style.display = 'initial'; 62 | }); 63 | } 64 | 65 | /** 66 | * 67 | * @param marker {Marker} 68 | * @returns {HTMLElement} 69 | */ 70 | #createMarkerNode(marker) { 71 | const config = marker.getConfig(); 72 | 73 | const [x1, y1, x2, y2] = marker.getPosition(); 74 | const rectangle = this.#createRectangle(x1, y1, x2, y2, config.label); 75 | if (typeof config.style === 'object') { 76 | for (const [key, value] of Object.entries(config.style)) { 77 | rectangle.style[key] = value; 78 | } 79 | } 80 | if (!marker.isVisible()) { 81 | rectangle.style.display = 'none'; 82 | } 83 | marker.addOnVisibleChanged((visible) => { 84 | rectangle.style.display = visible ? 'initial' : 'none'; 85 | }); 86 | return rectangle; 87 | } 88 | 89 | #createRectangle(x1, y1, x2, y2, content = null) { 90 | const xPx = x1 / this.#scale; 91 | const yPx = y1 / this.#scale; 92 | const wPx = (x2 - x1) / this.#scale; 93 | const hPx = (y2 - y1) / this.#scale; 94 | 95 | const attributes = { 96 | class: 'sand-game-marker', 97 | style: { 98 | left: xPx + 'px', 99 | top: yPx + 'px', 100 | width: wPx + 'px', 101 | height: hPx + 'px', 102 | position: 'absolute', 103 | } 104 | }; 105 | 106 | return DomBuilder.div(attributes, content); 107 | } 108 | 109 | createNode(controller) { 110 | return this.#nodeOverlay; 111 | } 112 | } -------------------------------------------------------------------------------- /src/gui/component/ComponentViewElementSizeSelection.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Component from "./Component"; 5 | import Analytics from "../../Analytics"; 6 | 7 | import _ASSET_IMG_ELEMENT_SIZE_1 from './assets/element-size-1.png' 8 | import _ASSET_IMG_ELEMENT_SIZE_2 from './assets/element-size-2.png' 9 | import _ASSET_IMG_ELEMENT_SIZE_3 from './assets/element-size-3.png' 10 | import _ASSET_IMG_ELEMENT_SIZE_4 from './assets/element-size-4.png' 11 | 12 | /** 13 | * 14 | * @author Patrik Harag 15 | * @version 2023-12-22 16 | */ 17 | export default class ComponentViewElementSizeSelection extends Component { 18 | 19 | static CLASS_SELECTED = 'selected-size'; 20 | 21 | static SIZES = [ 22 | { scale: 0.75, image: _ASSET_IMG_ELEMENT_SIZE_1, description: 'Very small elements' }, 23 | { scale: 0.5, image: _ASSET_IMG_ELEMENT_SIZE_2, description: 'Small elements' }, 24 | { scale: 0.375, image: _ASSET_IMG_ELEMENT_SIZE_3, description: 'Medium elements' }, 25 | { scale: 0.25, image: _ASSET_IMG_ELEMENT_SIZE_4, description: 'Big elements' }, 26 | ]; 27 | 28 | 29 | #nodes = []; 30 | 31 | #selected = null; 32 | #selectedScale = null; 33 | 34 | createNode(controller) { 35 | for (let sizeDef of ComponentViewElementSizeSelection.SIZES) { 36 | let node = this.#createSizeCard(sizeDef.scale, sizeDef.image, sizeDef.description); 37 | 38 | // initial scale 39 | if (sizeDef.scale === controller.getCurrentScale()) { 40 | this.#mark(node, sizeDef.scale); 41 | } 42 | 43 | node.addEventListener('click', e => { 44 | this.#select(node, sizeDef.scale, controller); 45 | }) 46 | 47 | this.#nodes.push(node); 48 | } 49 | 50 | return DomBuilder.div(null, [ 51 | DomBuilder.par(null, "Increasing the size of the elements will result in the top and right" + 52 | " parts of the canvas being clipped."), 53 | DomBuilder.par(null, "Reducing the size of the elements will result in an expansion of" + 54 | " the canvas in the upper and right parts."), 55 | DomBuilder.par(null, "Only the scale of the current scene and the initial setting for new" + 56 | " scenes will be changed. Scene can be regenerated by clicking on the scene card."), 57 | 58 | DomBuilder.div({ class: 'element-size-options' }, this.#nodes) 59 | ]); 60 | } 61 | 62 | #select(node, newScale, controller) { 63 | if (this.#selectedScale === newScale) { 64 | return; // already selected 65 | } 66 | 67 | // mark selected 68 | if (this.#selected) { 69 | this.#selected.classList.remove(ComponentViewElementSizeSelection.CLASS_SELECTED); 70 | } 71 | this.#mark(node, newScale); 72 | 73 | // change scale 74 | let w = Math.trunc(controller.getCurrentWidthPoints() / controller.getCurrentScale() * newScale); 75 | let h = Math.trunc(controller.getCurrentHeightPoints() / controller.getCurrentScale() * newScale); 76 | controller.changeCanvasSize(w, h, newScale); 77 | 78 | Analytics.triggerFeatureUsed(Analytics.FEATURE_SWITCH_SCALE); 79 | } 80 | 81 | #mark(node, scale) { 82 | node.classList.add(ComponentViewElementSizeSelection.CLASS_SELECTED); 83 | this.#selected = node; 84 | this.#selectedScale = scale; 85 | } 86 | 87 | /** 88 | * 89 | * @param scale {number} 90 | * @param image {string} 91 | * @param description {string} 92 | */ 93 | #createSizeCard(scale, image, description) { 94 | return DomBuilder.div({ class: 'card' }, [ 95 | DomBuilder.element('img', { src: image, alt: description }) 96 | ]); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/gui/component/ComponentViewTemplateSelection.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Resources from "../../io/Resources"; 5 | import Tools from "../../core/tool/Tools"; 6 | import Component from "./Component"; 7 | 8 | /** 9 | * 10 | * @author Patrik Harag 11 | * @version 2024-02-08 12 | */ 13 | export default class ComponentViewTemplateSelection extends Component { 14 | 15 | #templateDefinitions; 16 | 17 | constructor(templateDefinitions) { 18 | super(); 19 | this.#templateDefinitions = templateDefinitions; 20 | } 21 | 22 | createNode(controller) { 23 | let buttons = []; 24 | 25 | for (const toolDefinition of this.#templateDefinitions) { 26 | const name = toolDefinition.info.displayName; 27 | let loadedTool = null; 28 | 29 | let button = DomBuilder.button(name, { class: 'btn btn-light template-button', 'data-bs-dismiss': 'modal'}, () => { 30 | if (loadedTool !== null) { 31 | 32 | const toolManager = controller.getToolManager(); 33 | const revert = toolManager.createRevertAction(); 34 | 35 | toolManager.setPrimaryTool(loadedTool); 36 | toolManager.setSecondaryTool(Tools.actionTool(revert)); 37 | 38 | } else { 39 | // this should not happen 40 | } 41 | }); 42 | if (toolDefinition.info.icon !== undefined) { 43 | button.style.backgroundImage = `url(${ toolDefinition.info.icon.imageData })`; 44 | } 45 | 46 | buttons.push(button); 47 | 48 | Resources.parseToolDefinition(toolDefinition).then(tool => { 49 | loadedTool = tool; 50 | }).catch(e => { 51 | console.warn('Template loading failed: ' + e); 52 | }); 53 | } 54 | 55 | return DomBuilder.div({ class: 'sand-game-templates' }, buttons); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/gui/component/ComponentViewTestTools.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Action from "../action/Action"; 5 | import ActionsTest from "../action/ActionsTest"; 6 | import ActionBenchmark from "../action/ActionBenchmark"; 7 | import Component from "./Component"; 8 | import ComponentButton from "./ComponentButton"; 9 | import ToolDefs from "../../def/ToolDefs"; 10 | import RendererInitializer from "../../core/rendering/RendererInitializer"; 11 | import ActionScreenshot from "../action/ActionScreenshot"; 12 | import ActionRecord from "../action/ActionRecord"; 13 | import ActionFill from "../action/ActionFill"; 14 | 15 | /** 16 | * 17 | * @author Patrik Harag 18 | * @version 2024-02-02 19 | */ 20 | export default class ComponentViewTestTools extends Component { 21 | 22 | static #BTN_SCENE = ComponentButton.CLASS_OUTLINE_SECONDARY; 23 | static #BTN_RENDERING = ComponentButton.CLASS_OUTLINE_INFO; 24 | static #BTN_TOOL = ComponentButton.CLASS_OUTLINE_PRIMARY; 25 | static #BTN_BRUSH = ComponentButton.CLASS_OUTLINE_SUCCESS; 26 | 27 | static COMPONENTS = [ 28 | new ComponentButton("All materials", ComponentViewTestTools.#BTN_SCENE, ActionsTest.ALL_MATERIALS), 29 | new ComponentButton("Tree spawn", ComponentViewTestTools.#BTN_SCENE, ActionsTest.TREE_SPAWN_TEST), 30 | new ComponentButton("Tree grow", ComponentViewTestTools.#BTN_SCENE, ActionsTest.treeGrowTest(0)), 31 | new ComponentButton("Tree grown", ComponentViewTestTools.#BTN_SCENE, ActionsTest.treeGrowTest(-1)), 32 | new ComponentButton("Fill", ComponentViewTestTools.#BTN_SCENE, new ActionFill()), 33 | 34 | new ComponentButton("Benchmark", ComponentViewTestTools.#BTN_TOOL, new ActionBenchmark()), 35 | new ComponentButton("Screenshot", ComponentViewTestTools.#BTN_TOOL, new ActionScreenshot()), 36 | new ComponentButton("Record (start/stop)", ComponentViewTestTools.#BTN_TOOL, new ActionRecord()), 37 | 38 | new ComponentButton("Chunks", ComponentViewTestTools.#BTN_RENDERING, 39 | Action.createToggle(false, (c, v) => c.setShowActiveChunks(v))), 40 | new ComponentButton("M/webgl", ComponentViewTestTools.#BTN_RENDERING, 41 | Action.create(c => c.setRendererInitializer(RendererInitializer.canvasWebGL()))), 42 | new ComponentButton("M/classic", ComponentViewTestTools.#BTN_RENDERING, 43 | Action.create(c => c.setRendererInitializer(RendererInitializer.canvas2d()))), 44 | new ComponentButton("M/heatmap", ComponentViewTestTools.#BTN_RENDERING, 45 | Action.create(c => c.setRendererInitializer(RendererInitializer.canvas2dHeatmap()))), 46 | new ComponentButton("M/type", ComponentViewTestTools.#BTN_RENDERING, 47 | Action.create(c => c.setRendererInitializer(RendererInitializer.canvas2dElementType()))), 48 | new ComponentButton("M/null", ComponentViewTestTools.#BTN_RENDERING, 49 | Action.create(c => c.setRendererInitializer(RendererInitializer.nullRenderer()))), 50 | new ComponentButton("Pixelated", ComponentViewTestTools.#BTN_RENDERING, 51 | Action.createToggle(true, (c, v) => c.setCanvasImageRenderingStyle(v ? 'pixelated' : 'auto'))), 52 | ]; 53 | 54 | 55 | createNode(controller) { 56 | let content = DomBuilder.div({ class: 'test-tools' }, []); 57 | 58 | let components = [...ComponentViewTestTools.COMPONENTS]; 59 | for (let tool of ToolDefs.TEST_TOOLS) { 60 | let action = Action.create(c => c.getToolManager().setPrimaryTool(tool)); 61 | components.push(new ComponentButton(tool.getInfo().getDisplayName(), ComponentViewTestTools.#BTN_BRUSH, action)); 62 | } 63 | 64 | for (let component of components) { 65 | content.append(component.createNode(controller)); 66 | } 67 | return content; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/gui/component/ComponentViewTools.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import DomBuilder from "../DomBuilder"; 4 | import Component from "./Component"; 5 | import ActionDialogTemplateSelection from "../action/ActionDialogTemplateSelection"; 6 | import ToolDefs from "../../def/ToolDefs"; 7 | import TemplateSelectionFakeTool from "../../core/tool/TemplateSelectionFakeTool"; 8 | import GlobalActionTool from "../../core/tool/GlobalActionTool"; 9 | 10 | /** 11 | * 12 | * @author Patrik Harag 13 | * @version 2024-03-13 14 | */ 15 | export default class ComponentViewTools extends Component { 16 | 17 | /** @type Tool[] */ 18 | #tools; 19 | /** @type boolean */ 20 | #importEnabled; 21 | 22 | /** 23 | * @param tools {Tool[]} 24 | * @param importEnabled {boolean} 25 | */ 26 | constructor(tools, importEnabled = false) { 27 | super(); 28 | this.#tools = tools; 29 | this.#importEnabled = importEnabled; 30 | } 31 | 32 | createNode(controller) { 33 | let buttons = []; 34 | 35 | for (let tool of this.#tools) { 36 | let cssName = tool.getInfo().getCodeName(); 37 | let displayName = tool.getInfo().getDisplayName(); 38 | let badgeStyle = tool.getInfo().getBadgeStyle(); 39 | 40 | let attributes = { 41 | class: 'btn btn-secondary btn-sand-game-tool ' + cssName, 42 | style: badgeStyle 43 | }; 44 | let button = DomBuilder.button(displayName, attributes, () => { 45 | this.#selectTool(tool, controller); 46 | }); 47 | 48 | controller.getToolManager().addOnPrimaryToolChanged(newTool => { 49 | if (newTool === tool) { 50 | button.classList.add('selected'); 51 | } else { 52 | button.classList.remove('selected'); 53 | } 54 | }); 55 | 56 | // initial select 57 | if (tool === controller.getToolManager().getPrimaryTool()) { 58 | button.classList.add('selected'); 59 | } 60 | 61 | controller.getToolManager().addOnInputDisabledChanged(disabled => { 62 | button.disabled = disabled; 63 | }); 64 | 65 | buttons.push(button); 66 | } 67 | 68 | return DomBuilder.div({ class: 'sand-game-tools' }, buttons); 69 | } 70 | 71 | #selectTool(tool, controller) { 72 | if (tool instanceof TemplateSelectionFakeTool) { 73 | let additionalInfo = null; 74 | if (this.#importEnabled) { 75 | additionalInfo = DomBuilder.div(null, [ 76 | DomBuilder.par(null, ""), 77 | DomBuilder.par(null, "You can also create your own template using an image. See the Import button.") 78 | ]); 79 | } 80 | const action = new ActionDialogTemplateSelection(tool.getTemplateDefinitions(), additionalInfo); 81 | action.performAction(controller); 82 | } else if (tool instanceof GlobalActionTool) { 83 | const handler = tool.getHandler(); 84 | handler(controller.getSandGame()); 85 | } else { 86 | controller.getToolManager().setPrimaryTool(tool); 87 | controller.getToolManager().setSecondaryTool(ToolDefs.ERASE); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/gui/component/assets/element-size-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/gui/component/assets/element-size-1.png -------------------------------------------------------------------------------- /src/gui/component/assets/element-size-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/gui/component/assets/element-size-2.png -------------------------------------------------------------------------------- /src/gui/component/assets/element-size-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/gui/component/assets/element-size-3.png -------------------------------------------------------------------------------- /src/gui/component/assets/element-size-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/src/gui/component/assets/element-size-4.png -------------------------------------------------------------------------------- /src/gui/component/assets/icon-adjust-scale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/gui/component/assets/icon-pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/gui/component/assets/icon-play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/gui/component/assets/icon-reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/gui/component/assets/icon-square-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/gui/component/assets/icon-square-dotted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/gui/component/assets/icon-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/io/ResourceUtils.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Assets from "../Assets"; 4 | import Brush from "../core/brush/Brush"; 5 | import ElementArea from "../core/ElementArea"; 6 | import ElementTail from "../core/ElementTail"; 7 | import Scene from "../core/scene/Scene"; 8 | import SceneImplTemplate from "../core/scene/SceneImplTemplate"; 9 | import DeterministicRandom from "../core/DeterministicRandom"; 10 | 11 | /** 12 | * 13 | * @author Patrik Harag 14 | * @version 2023-12-09 15 | */ 16 | export default class ResourceUtils { 17 | 18 | /** 19 | * 20 | * @param objectUrl 21 | * @param maxWidth 22 | * @param maxHeight 23 | * @returns {Promise} 24 | */ 25 | static loadImageData(objectUrl, maxWidth, maxHeight) { 26 | // TODO: security - it will also fetch an external image 27 | return Assets.asImageData(objectUrl, maxWidth, maxHeight); 28 | } 29 | 30 | /** 31 | * 32 | * @param data {ArrayBuffer} ArrayBuffer 33 | * @param type {string} type 34 | * @returns {string} 35 | */ 36 | static asObjectUrl(data, type='image/png') { 37 | // https://gist.github.com/candycode/f18ae1767b2b0aba568e 38 | const arrayBufferView = new Uint8Array(data); 39 | const blob = new Blob([ arrayBufferView ], { type: type }); 40 | const urlCreator = window.URL || window.webkitURL; 41 | return urlCreator.createObjectURL(blob); 42 | } 43 | 44 | /** 45 | * 46 | * @param imageData {ImageData} 47 | * @param brush {Brush} 48 | * @param defaultElement {Element} 49 | * @param threshold {number} 0-255 50 | * @returns Scene 51 | */ 52 | static createSceneFromImageTemplate(imageData, brush, defaultElement, threshold) { 53 | const width = imageData.width; 54 | const height = imageData.height; 55 | 56 | const elementArea = ElementArea.create(width, height, defaultElement); 57 | 58 | const random = new DeterministicRandom(0); 59 | 60 | for (let y = 0; y < height; y++) { 61 | for (let x = 0; x < width; x++) { 62 | const index = (y * width + x) * 4; 63 | 64 | let red = imageData.data[index]; 65 | let green = imageData.data[index + 1]; 66 | let blue = imageData.data[index + 2]; 67 | const alpha = imageData.data[index + 3]; 68 | 69 | // perform alpha blending if needed 70 | if (alpha !== 0xFF) { 71 | red = Math.trunc((red * alpha) / 0xFF) + 0xFF - alpha; 72 | green = Math.trunc((green * alpha) / 0xFF) + 0xFF - alpha; 73 | blue = Math.trunc((blue * alpha) / 0xFF) + 0xFF - alpha; 74 | } 75 | 76 | // filter out background 77 | if (red > 0xFF-threshold && green > 0xFF-threshold && blue > 0xFF-threshold) { 78 | continue; // white 79 | } 80 | 81 | const element = brush.apply(x, y, random); 82 | const elementHead = element.elementHead; 83 | const elementTail = ElementTail.setColor(element.elementTail, red, green, blue); 84 | elementArea.setElementHeadAndTail(x, y, elementHead, elementTail); 85 | } 86 | } 87 | 88 | return new SceneImplTemplate(elementArea); 89 | } 90 | 91 | static getImageTypeOrNull(filename) { 92 | filename = filename.toLowerCase(); 93 | if (filename.endsWith('.png')) { 94 | return 'image/png' 95 | } 96 | if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) { 97 | return 'image/jpg' 98 | } 99 | if (filename.endsWith('.bmp')) { 100 | return 'image/bmp' 101 | } 102 | if (filename.endsWith('.gif')) { 103 | return 'image/gif' 104 | } 105 | return null; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/io/Resources.js: -------------------------------------------------------------------------------- 1 | // Sand Game JS; Patrik Harag, https://harag.cz; all rights reserved 2 | 3 | import Snapshot from "../core/Snapshot"; 4 | import Scene from "../core/scene/Scene"; 5 | import SceneImplSnapshot from "../core/scene/SceneImplSnapshot"; 6 | import Tool from "../core/tool/Tool"; 7 | import { strFromU8, unzipSync } from 'fflate'; 8 | import ResourceSnapshot from "./ResourceSnapshot"; 9 | import ResourceTool from "./ResourceTool"; 10 | 11 | /** 12 | * 13 | * @author Patrik Harag 14 | * @version 2023-12-09 15 | */ 16 | export default class Resources { 17 | 18 | static JSON_RESOURCE_TYPE_FIELD = 'resourceType'; 19 | 20 | /** 21 | * 22 | * @param snapshot {Snapshot} 23 | * @returns Uint8Array 24 | */ 25 | static createResourceFromSnapshot(snapshot) { 26 | return ResourceSnapshot.createZip(snapshot); 27 | } 28 | 29 | /** 30 | * 31 | * @param content {ArrayBuffer} 32 | * @returns Promise 33 | */ 34 | static async parseZipResource(content) { 35 | const zip = unzipSync(new Uint8Array(content)); 36 | 37 | function parseJson(fileName) { 38 | return JSON.parse(strFromU8(zip[fileName])); 39 | } 40 | 41 | if (zip[ResourceSnapshot.METADATA_JSON_NAME]) { 42 | // snapshot 43 | const metadataJson = parseJson(ResourceSnapshot.METADATA_JSON_NAME); 44 | const snapshot = ResourceSnapshot.parse(metadataJson, zip); 45 | return new SceneImplSnapshot(snapshot); 46 | } 47 | if (zip[ResourceSnapshot.LEGACY_METADATA_JSON_NAME]) { 48 | // legacy snapshot 49 | const metadataJson = parseJson(ResourceSnapshot.LEGACY_METADATA_JSON_NAME); 50 | const snapshot = ResourceSnapshot.parse(metadataJson, zip); 51 | return new SceneImplSnapshot(snapshot); 52 | } 53 | if (zip[ResourceTool.METADATA_JSON_NAME]) { 54 | // legacy snapshot 55 | const metadataJson = parseJson(ResourceTool.METADATA_JSON_NAME); 56 | return await ResourceTool.parse(metadataJson, zip); 57 | } 58 | throw 'Wrong format'; 59 | } 60 | 61 | /** 62 | * 63 | * @param content {ArrayBuffer} 64 | * @returns Promise 65 | */ 66 | static async parseJsonResource(content) { 67 | const text = strFromU8(new Uint8Array(content)); 68 | const metadataJson = JSON.parse(text); 69 | 70 | const type = metadataJson[Resources.JSON_RESOURCE_TYPE_FIELD]; 71 | if (type === 'tool') { 72 | return await ResourceTool.parse(metadataJson, null); 73 | } 74 | throw 'Wrong json format'; 75 | } 76 | 77 | /** 78 | * 79 | * @param json 80 | * @returns {Promise} 81 | */ 82 | static async parseToolDefinition(json) { 83 | return ResourceTool.parse(json, null); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | assert.equal(1+1, 2); 4 | console.log(`\u001B[32m✓\u001B[39m true`); 5 | -------------------------------------------------------------------------------- /tools/palette-designer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sand Game JS - Palette Designer 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 |
26 |

Sand Game JS - Palette Designer

27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /tools/palette-designer/tool-palette-designer.css: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @author Patrik Harag 4 | * @version 2024-03-12 5 | */ 6 | 7 | .palette-designer { 8 | position: relative; 9 | } 10 | 11 | .palette-designer .control-panel { 12 | position: absolute; 13 | right: 1em; 14 | top: 3em; 15 | 16 | width: 20em; 17 | height: 80%; 18 | 19 | padding: 1em; 20 | border-radius: 10px; 21 | background-color: rgba(255, 255, 255, 60%); 22 | } 23 | 24 | .palette-designer textarea.form-control { 25 | height: 80%; 26 | 27 | font-family: 'Consolas', monospace; 28 | font-size: 11pt; 29 | } -------------------------------------------------------------------------------- /tools/perlin-texture-designer/examples/a.js: -------------------------------------------------------------------------------- 1 | Brushes.join([ 2 | Brushes.color(155, 155, 155, BrushDefs.WALL), 3 | Brushes.colorNoise([ 4 | { seed: 40, factor: 60, threshold: 0.4, force: 0.80 }, 5 | { seed: 40, factor: 30, threshold: 0.5, force: 0.4 }, 6 | { seed: 40, factor: 15, threshold: 0.4, force: 0.2 }, 7 | { seed: 40, factor: 5, threshold: 0.4, force: 0.1 }, 8 | ], 135, 135, 135), 9 | Brushes.colorNoise([ 10 | { seed: 41, factor: 60, threshold: 0.4, force: 0.80 }, 11 | { seed: 41, factor: 30, threshold: 0.5, force: 0.4 }, 12 | { seed: 41, factor: 15, threshold: 0.4, force: 0.2 }, 13 | { seed: 41, factor: 5, threshold: 0.3, force: 0.1 }, 14 | ], 130, 130, 130) 15 | ]); -------------------------------------------------------------------------------- /tools/perlin-texture-designer/examples/b.js: -------------------------------------------------------------------------------- 1 | Brushes.join([ 2 | Brushes.color(45, 45, 45, BrushDefs.WALL), 3 | Brushes.colorNoise([ 4 | { seed: 40, factor: 60, threshold: 0.4, force: 0.80 }, 5 | { seed: 40, factor: 30, threshold: 0.5, force: 0.4 }, 6 | { seed: 40, factor: 15, threshold: 0.4, force: 0.2 }, 7 | { seed: 40, factor: 5, threshold: 0.4, force: 0.1 }, 8 | ], 79, 69, 63), 9 | Brushes.colorNoise([ 10 | { seed: 41, factor: 60, threshold: 0.4, force: 0.80 }, 11 | { seed: 41, factor: 30, threshold: 0.5, force: 0.4 }, 12 | { seed: 41, factor: 15, threshold: 0.4, force: 0.2 }, 13 | { seed: 41, factor: 5, threshold: 0.4, force: 0.1 }, 14 | ], 35, 35, 35), 15 | ]); -------------------------------------------------------------------------------- /tools/perlin-texture-designer/examples/c.js: -------------------------------------------------------------------------------- 1 | Brushes.join([ 2 | Brushes.color(45, 45, 45, BrushDefs.WALL), 3 | Brushes.colorNoise([ 4 | { seed: 40, factor: 60, threshold: 0.4, force: 0.80 }, 5 | { seed: 40, factor: 30, threshold: 0.5, force: 0.4 }, 6 | { seed: 40, factor: 15, threshold: 0.4, force: 0.2 }, 7 | { seed: 40, factor: 5, threshold: 0.4, force: 0.1 }, 8 | ], 79, 69, 63) 9 | ]); -------------------------------------------------------------------------------- /tools/perlin-texture-designer/examples/metal.js: -------------------------------------------------------------------------------- 1 | Brushes.join([ 2 | Brushes.color(155, 155, 155, BrushDefs.WALL), 3 | Brushes.colorNoise([ 4 | { seed: 40, factor: 40, threshold: 0.4, force: 0.75 }, 5 | { seed: 40, factor: 20, threshold: 0.5, force: 0.4 }, 6 | { seed: 40, factor: 10, threshold: 0.4, force: 0.2 }, 7 | { seed: 40, factor: 5, threshold: 0.4, force: 0.1 }, 8 | ], 135, 135, 135), 9 | Brushes.colorNoise([ 10 | { seed: 41, factor: 10, threshold: 0.4, force: 0.4 }, 11 | { seed: 41, factor: 6, threshold: 0.3, force: 0.3 }, 12 | { seed: 41, factor: 4, threshold: 0.5, force: 0.3 }, 13 | ], 130, 130, 130) 14 | ]); -------------------------------------------------------------------------------- /tools/perlin-texture-designer/examples/rock.js: -------------------------------------------------------------------------------- 1 | Brushes.join([ 2 | Brushes.color(155, 155, 155, BrushDefs.WALL), 3 | Brushes.colorNoise([ 4 | { seed: 40, factor: 60, threshold: 0.4, force: 0.9 }, 5 | { seed: 41, factor: 30, threshold: 0.4, force: 0.9 }, 6 | { seed: 42, factor: 15, threshold: 0.4, force: 0.5 }, 7 | { seed: 43, factor: 3, threshold: 0.1, force: 0.1 }, 8 | ], 79, 69, 63), 9 | Brushes.colorNoise([ 10 | { seed: 51, factor: 30, threshold: 0.4, force: 0.9 }, 11 | { seed: 52, factor: 15, threshold: 0.4, force: 0.5 }, 12 | { seed: 53, factor: 3, threshold: 0.1, force: 0.1 }, 13 | ], 55, 48, 46), 14 | Brushes.colorNoise([ 15 | { seed: 61, factor: 30, threshold: 0.4, force: 0.9 }, 16 | { seed: 62, factor: 15, threshold: 0.4, force: 0.5 }, 17 | { seed: 63, factor: 3, threshold: 0.1, force: 0.1 }, 18 | ], 33, 29, 28) 19 | ]); -------------------------------------------------------------------------------- /tools/perlin-texture-designer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sand Game JS - Perlin Texture Designer 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 |
26 |

Sand Game JS - Perlin Texture Designer

27 |
28 |
29 | 30 | 31 |
32 |

33 |     
34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /tools/perlin-texture-designer/script.js: -------------------------------------------------------------------------------- 1 | 2 | const sandGameRoot = document.getElementById('sand-game-root'); 3 | 4 | var sandGame; 5 | var Brushes; 6 | var BrushDefs; 7 | 8 | const initScript = ` 9 | Brushes.join([ 10 | Brushes.color(155, 155, 155, BrushDefs.WALL), 11 | Brushes.colorNoise([ 12 | { seed: 40, factor: 40, threshold: 0.4, force: 0.75 }, 13 | { seed: 40, factor: 20, threshold: 0.5, force: 0.4 }, 14 | { seed: 40, factor: 10, threshold: 0.4, force: 0.2 }, 15 | { seed: 40, factor: 5, threshold: 0.4, force: 0.1 }, 16 | ], 135, 135, 135), 17 | Brushes.colorNoise([ 18 | { seed: 41, factor: 10, threshold: 0.4, force: 0.4 }, 19 | { seed: 41, factor: 6, threshold: 0.3, force: 0.3 }, 20 | { seed: 41, factor: 4, threshold: 0.5, force: 0.3 }, 21 | ], 130, 130, 130) 22 | ]); 23 | `.trim(); 24 | 25 | document.addEventListener('DOMContentLoaded', () => { 26 | const codeArea = document.getElementById('code-area'); 27 | codeArea.value = initScript; 28 | 29 | // init drag and drop 30 | document.getElementById('main').ondrop = function(e) { 31 | e.preventDefault(); 32 | 33 | let reader = new FileReader(); 34 | let file = e.dataTransfer.files[0]; 35 | reader.onload = function(event) { 36 | codeArea.value = event.target.result; 37 | evaluateCode(); 38 | }; 39 | reader.readAsText(file); 40 | 41 | return false; 42 | }; 43 | 44 | const SandGameJS = window.SandGameJS; 45 | 46 | if (SandGameJS !== undefined) { 47 | 48 | const config = { 49 | version: 'dev', 50 | debug: false, 51 | scene: { 52 | init: (s) => { 53 | sandGame = s; 54 | } 55 | }, 56 | autoStart: false, 57 | tools: [], 58 | primaryTool: SandGameJS.ToolDefs.NONE, 59 | secondaryTool: SandGameJS.ToolDefs.NONE, 60 | tertiaryTool: SandGameJS.ToolDefs.NONE, 61 | disableImport: true, 62 | disableExport: true, 63 | disableSizeChange: true, 64 | disableSceneSelection: true, 65 | disableStartStop: true, 66 | disableRestart: true 67 | }; 68 | 69 | const controller = SandGameJS.init(sandGameRoot, config); 70 | 71 | Brushes = SandGameJS.Brushes; 72 | BrushDefs = SandGameJS.BrushDefs; 73 | 74 | evaluateCode(); 75 | 76 | } else { 77 | sandGameRoot.innerHTML = '

Failed to load the game.

'; 78 | } 79 | 80 | }); 81 | 82 | function evaluateCode() { 83 | const code = document.getElementById('code-area').value; 84 | 85 | try { 86 | const result = eval(code); 87 | 88 | if (typeof result === "object") { 89 | sandGame.graphics().fill(result); 90 | } 91 | 92 | // Display the result 93 | document.getElementById('result').innerText = result; 94 | } catch (error) { 95 | // Display any errors that occurred during evaluation 96 | document.getElementById('result').innerText = "Error: " + error.message; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tools/tree-template-builder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tree template builder 7 | 8 | 17 | 18 | 19 | 20 |
21 |

Tree template builder

22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tools/tree-template-builder/leaf-cluster-template-builder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Leaf cluster template builder 7 | 8 | 17 | 18 | 19 | 20 |
21 |

Leaf cluster template builder

22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tools/tree-template-builder/leaf-cluster-template-builder/templates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/leaf-cluster-template-builder/templates.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tree trunk template builder 7 | 8 | 17 | 18 | 19 | 20 |
21 |

Tree trunk template builder

22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-00.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-01.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-02.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-03.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-04.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-05.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-06.png -------------------------------------------------------------------------------- /tools/tree-template-builder/trunk-template-builder/template-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hartrik/sand-game-js/732c258a3c94a279793e26bc43a0642d720379a1/tools/tree-template-builder/trunk-template-builder/template-07.png --------------------------------------------------------------------------------