├── .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 | 
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 |
--------------------------------------------------------------------------------
/src/gui/component/assets/icon-pause.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/gui/component/assets/icon-play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/gui/component/assets/icon-reset.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/gui/component/assets/icon-square-check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/gui/component/assets/icon-square-dotted.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/gui/component/assets/icon-square.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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
--------------------------------------------------------------------------------