@downdraft/engine 0.1.1 → 0.1.2-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/README.md +11 -0
- package/app/src/renderer/bootstrap.ts +295 -19
- package/app/src/renderer/game-module.ts +60 -12
- package/app/src/renderer/hooks.ts +9 -1
- package/app/src/renderer/hot-reload.ts +9 -9
- package/app/src/renderer/mcp-harness.ts +2 -2
- package/app/src/renderer/standard-automation-tools.ts +104 -3
- package/app/src/shared/feature-log.ts +2 -1
- package/app/src/shared/mcp-discovery.ts +8 -3
- package/app/src/shared/mcp-proxy.ts +2 -2
- package/asset-bake/README.md +22 -0
- package/core/README.md +25 -0
- package/core/src/animation/skeleton-animator.ts +23 -5
- package/core/src/assets/manager.ts +4 -4
- package/core/src/ecs/events.ts +1 -1
- package/core/src/ecs/schedule.ts +1 -1
- package/core/src/ecs/world.ts +1 -1
- package/core/src/index.ts +3 -4
- package/core/src/library/host.ts +27 -13
- package/core/src/library/library.ts +13 -5
- package/core/src/module/cross-thread.ts +6 -0
- package/core/src/module/diagnostics.ts +10 -6
- package/core/src/module/host.ts +65 -18
- package/core/src/module/index.ts +1 -0
- package/core/src/module/module.ts +11 -5
- package/core/src/module/renderer-module.ts +15 -0
- package/core/src/physics/safety.ts +1 -1
- package/core/src/platform/bun-preload.ts +7 -2
- package/core/src/platform/glob-polyfill.ts +17 -8
- package/core/src/platform/pkg-root.ts +27 -0
- package/core/src/platform/runtime.ts +29 -0
- package/core/src/plugin/discovery.ts +1 -1
- package/core/src/plugin/extension-loaders.ts +6 -4
- package/core/src/plugin/host.ts +13 -13
- package/core/src/plugin/loader-asset.ts +5 -5
- package/core/src/plugin/loader-quickjs.ts +1 -1
- package/core/src/plugin/loader-wasm.ts +3 -3
- package/core/src/plugin/loader-worker.ts +1 -1
- package/core/src/plugin/manifest.ts +8 -3
- package/core/src/plugin/workshop.ts +3 -3
- package/core/src/profiling/index.ts +1 -1
- package/core/src/profiling/profiling-sab.ts +19 -0
- package/core/src/profiling/warnings.ts +1 -1
- package/core/src/profiling/worker-prelude.ts +1 -1
- package/core/src/render/bindless/frame-bindings.ts +1 -1
- package/core/src/render/bindless/material-manager.ts +2 -2
- package/core/src/render/bindless/texture-registry.ts +2 -2
- package/core/src/render/device.ts +5 -5
- package/core/src/render/frame-graph.ts +2 -2
- package/core/src/render/game-renderer.ts +42 -18
- package/core/src/render/hot-reload.ts +3 -3
- package/core/src/render/renderer-module-host.ts +32 -12
- package/core/src/render/shader-validator.ts +7 -7
- package/core/src/render/surface.ts +1 -1
- package/core/src/sab/errors.ts +1 -1
- package/core/src/sab/sab-polyfill.ts +1 -1
- package/core/src/save/migration-registry.ts +4 -4
- package/core/src/save/schema.ts +1 -1
- package/core/src/scene/prefab.ts +1 -1
- package/core/src/scene/scene-manager.ts +2 -2
- package/core/src/scene/scene.ts +4 -4
- package/core/src/scene/streaming.ts +2 -2
- package/core/src/scripting/hot-reload.ts +2 -2
- package/core/src/sim/hot-reload-pipeline.ts +30 -6
- package/core/src/sim/sim-state-helper.ts +1 -1
- package/core/src/sim/transient-state-registry.ts +1 -1
- package/core/src/sim/worker-loop.ts +2 -2
- package/core/src/telemetry/gpu-profiler.ts +3 -3
- package/core/src/util/index.ts +1 -1
- package/core/src/util/logger.ts +36 -5
- package/core/src/version.ts +13 -0
- package/core/src/worker/base-worker-host.ts +87 -25
- package/core/src/worker/buffer-sync.ts +1 -1
- package/core/src/worker/crash-recovery.ts +2 -2
- package/core/src/worker/instrumented-worker-host.ts +3 -3
- package/core/src/worker/rpc.ts +62 -11
- package/core/src/worker/sim-worker-base.ts +3 -8
- package/core/src/worker/task-pool.ts +2 -2
- package/dist/.dd-manifest.json +4119 -0
- package/dist/app/src/index.js +9 -0
- package/dist/app/src/renderer/bootstrap.js +446 -0
- package/dist/app/src/renderer/downdraft-base.css +93 -0
- package/dist/app/src/renderer/feature-log.js +180 -0
- package/dist/app/src/renderer/game-module.js +562 -0
- package/dist/app/src/renderer/hooks.js +82 -0
- package/dist/app/src/renderer/hot-reload.js +158 -0
- package/dist/app/src/renderer/import-cache.js +77 -0
- package/dist/app/src/renderer/index.js +149 -0
- package/dist/app/src/renderer/mcp-harness.js +112 -0
- package/dist/app/src/renderer/save-store-factory.js +180 -0
- package/dist/app/src/renderer/sim-bridge.js +138 -0
- package/dist/app/src/renderer/standard-automation-tools.js +720 -0
- package/dist/app/src/shared/feature-log.js +54 -0
- package/dist/app/src/shared/gpu-info.js +74 -0
- package/dist/app/src/shared/import-cache-store.js +123 -0
- package/dist/app/src/shared/index.js +4 -0
- package/dist/app/src/shared/mcp-discovery.js +55 -0
- package/dist/app/src/shared/mcp-proxy.js +71 -0
- package/dist/app/src/shared/types.js +13 -0
- package/dist/asset-bake/src/bake-audio.js +103 -0
- package/dist/asset-bake/src/bake-gltf.js +141 -0
- package/dist/asset-bake/src/bake.js +63 -0
- package/dist/asset-bake/src/cache.js +147 -0
- package/dist/asset-bake/src/config.js +93 -0
- package/dist/asset-bake/src/index.js +25 -0
- package/dist/core/src/animation/animation-event.js +24 -0
- package/dist/core/src/animation/bone-mask.js +103 -0
- package/dist/core/src/animation/clip.js +210 -0
- package/dist/core/src/animation/index.js +13 -0
- package/dist/core/src/animation/mixamo.js +78 -0
- package/dist/core/src/animation/morph-target.js +37 -0
- package/dist/core/src/animation/player.js +621 -0
- package/dist/core/src/animation/retarget.js +101 -0
- package/dist/core/src/animation/skeleton-animator.js +1361 -0
- package/dist/core/src/animation/skeleton.js +166 -0
- package/dist/core/src/animation/state-machine.js +252 -0
- package/dist/core/src/assets/blob-store.js +14 -0
- package/dist/core/src/assets/cache.js +168 -0
- package/dist/core/src/assets/cubemap-converter.js +129 -0
- package/dist/core/src/assets/environment-manager.js +84 -0
- package/dist/core/src/assets/import-cache.js +36 -0
- package/dist/core/src/assets/import-settings.js +34 -0
- package/dist/core/src/assets/importer.js +78 -0
- package/dist/core/src/assets/index.js +29 -0
- package/dist/core/src/assets/irradiance-generator.js +179 -0
- package/dist/core/src/assets/loader-audio.js +70 -0
- package/dist/core/src/assets/loader-cubemap.js +141 -0
- package/dist/core/src/assets/loader-dds.js +185 -0
- package/dist/core/src/assets/loader-hdr.js +430 -0
- package/dist/core/src/assets/loader-mesh.js +47 -0
- package/dist/core/src/assets/loader-shader.js +61 -0
- package/dist/core/src/assets/loader-texture.js +264 -0
- package/dist/core/src/assets/lod.js +150 -0
- package/dist/core/src/assets/manager.js +401 -0
- package/dist/core/src/assets/manifest.js +57 -0
- package/dist/core/src/assets/material-bridge.js +160 -0
- package/dist/core/src/assets/model-importer.js +32 -0
- package/dist/core/src/assets/model-normalizer.js +213 -0
- package/dist/core/src/assets/model-to-gpu.js +44 -0
- package/dist/core/src/assets/model-to-mesh.js +171 -0
- package/dist/core/src/assets/prefilter-generator.js +220 -0
- package/dist/core/src/audio/engine.js +153 -0
- package/dist/core/src/audio/interface.js +8 -0
- package/dist/core/src/audio/listener.js +19 -0
- package/dist/core/src/audio/mixer.js +124 -0
- package/dist/core/src/audio/registry.js +43 -0
- package/dist/core/src/audio/sab.js +74 -0
- package/dist/core/src/audio/source.js +60 -0
- package/dist/core/src/change-detection/atomics.js +1 -0
- package/dist/core/src/change-detection/tracker.js +26 -0
- package/dist/core/src/debug-draw/lines.js +27 -0
- package/dist/core/src/debug-draw/points.js +9 -0
- package/dist/core/src/debug-draw/queue.js +77 -0
- package/dist/core/src/debug-draw/text.js +12 -0
- package/dist/core/src/ecs/archetype.js +271 -0
- package/dist/core/src/ecs/component.js +109 -0
- package/dist/core/src/ecs/entity.js +11 -0
- package/dist/core/src/ecs/events.js +73 -0
- package/dist/core/src/ecs/hierarchy.js +104 -0
- package/dist/core/src/ecs/hmr-swap-registry.js +15 -0
- package/dist/core/src/ecs/index.js +14 -0
- package/dist/core/src/ecs/query.js +110 -0
- package/dist/core/src/ecs/resource.js +3 -0
- package/dist/core/src/ecs/schedule.js +157 -0
- package/dist/core/src/ecs/system-params.js +45 -0
- package/dist/core/src/ecs/system.js +18 -0
- package/dist/core/src/ecs/world.js +292 -0
- package/dist/core/src/index.js +191 -0
- package/dist/core/src/input/context.js +69 -0
- package/dist/core/src/input/dom-handler.js +288 -0
- package/dist/core/src/input/local-player-manager.js +145 -0
- package/dist/core/src/input/mapping.js +36 -0
- package/dist/core/src/input/multi-sab-bridge.js +224 -0
- package/dist/core/src/input/multi-state.js +48 -0
- package/dist/core/src/input/sab-bridge.js +66 -0
- package/dist/core/src/input/state.js +108 -0
- package/dist/core/src/input/ui-router.js +76 -0
- package/dist/core/src/library/host.js +291 -0
- package/dist/core/src/library/index.js +1 -0
- package/dist/core/src/library/library.js +20 -0
- package/dist/core/src/material/compiler.js +73 -0
- package/dist/core/src/material/graph-bridge.js +119 -0
- package/dist/core/src/material/index.js +6 -0
- package/dist/core/src/material/library.js +414 -0
- package/dist/core/src/material/material.js +120 -0
- package/dist/core/src/material/variants.js +37 -0
- package/dist/core/src/math/mat4.js +198 -0
- package/dist/core/src/math/perlin-noise-2d.js +66 -0
- package/dist/core/src/math/perlin-noise-3d.js +76 -0
- package/dist/core/src/math/quat.js +29 -0
- package/dist/core/src/math/rng.js +25 -0
- package/dist/core/src/math/trig.js +42 -0
- package/dist/core/src/math/value-noise.js +49 -0
- package/dist/core/src/mesh/builder.js +137 -0
- package/dist/core/src/mesh/csg.js +250 -0
- package/dist/core/src/mesh/greased-line.js +102 -0
- package/dist/core/src/mesh/mesh.js +6 -0
- package/dist/core/src/mesh/parametric.js +392 -0
- package/dist/core/src/mesh/skinning.js +30 -0
- package/dist/core/src/mesh/vertex-layout.js +65 -0
- package/dist/core/src/module/cross-thread.js +126 -0
- package/dist/core/src/module/diagnostics.js +99 -0
- package/dist/core/src/module/host.js +449 -0
- package/dist/core/src/module/index.js +6 -0
- package/dist/core/src/module/module.js +1 -0
- package/dist/core/src/module/registry.js +83 -0
- package/dist/core/src/module/renderer-module.js +13 -0
- package/dist/core/src/module/ts-loader.js +20 -0
- package/dist/core/src/particles/compute-pass.js +329 -0
- package/dist/core/src/particles/emitter.js +113 -0
- package/dist/core/src/particles/index.js +7 -0
- package/dist/core/src/particles/particle-data.js +33 -0
- package/dist/core/src/particles/render-pass.js +338 -0
- package/dist/core/src/particles/simulator.js +224 -0
- package/dist/core/src/particles/system.js +157 -0
- package/dist/core/src/physics/body.js +25 -0
- package/dist/core/src/physics/ccd-heuristic.js +46 -0
- package/dist/core/src/physics/character.js +184 -0
- package/dist/core/src/physics/collider.js +36 -0
- package/dist/core/src/physics/collision-system.js +62 -0
- package/dist/core/src/physics/events.js +74 -0
- package/dist/core/src/physics/index.js +24 -0
- package/dist/core/src/physics/interface.js +14 -0
- package/dist/core/src/physics/interpolation-buffer.js +182 -0
- package/dist/core/src/physics/lifecycle.js +66 -0
- package/dist/core/src/physics/load-shedder.js +155 -0
- package/dist/core/src/physics/physics-accumulator.js +94 -0
- package/dist/core/src/physics/physics-system.js +142 -0
- package/dist/core/src/physics/ragdoll-presets.js +248 -0
- package/dist/core/src/physics/ragdoll-system.js +193 -0
- package/dist/core/src/physics/ragdoll.js +117 -0
- package/dist/core/src/physics/raycast.js +97 -0
- package/dist/core/src/physics/realm-manager.js +431 -0
- package/dist/core/src/physics/realm-worker-pool.js +181 -0
- package/dist/core/src/physics/realm-worker.js +91 -0
- package/dist/core/src/physics/realm.js +137 -0
- package/dist/core/src/physics/registry.js +43 -0
- package/dist/core/src/physics/safety.js +211 -0
- package/dist/core/src/physics/snapshot-manager.js +118 -0
- package/dist/core/src/physics/worker-protocol.js +1 -0
- package/dist/core/src/platform/bun-preload.js +175 -0
- package/dist/core/src/platform/css-stub.js +4 -0
- package/dist/core/src/platform/glob-polyfill.js +137 -0
- package/dist/core/src/platform/hidpi.js +52 -0
- package/dist/core/src/platform/pkg-root.js +27 -0
- package/dist/core/src/platform/render-surface.js +15 -0
- package/dist/core/src/platform/rpc.js +82 -0
- package/dist/core/src/platform/runtime.js +93 -0
- package/dist/core/src/plugin/context.js +144 -0
- package/dist/core/src/plugin/diagnostics.js +20 -0
- package/dist/core/src/plugin/discovery.js +124 -0
- package/dist/core/src/plugin/extension-loaders.js +111 -0
- package/dist/core/src/plugin/host.js +542 -0
- package/dist/core/src/plugin/index.js +19 -0
- package/dist/core/src/plugin/loader-asset.js +105 -0
- package/dist/core/src/plugin/loader-quickjs.js +67 -0
- package/dist/core/src/plugin/loader-wasm.js +385 -0
- package/dist/core/src/plugin/loader-worker.js +228 -0
- package/dist/core/src/plugin/manifest.js +424 -0
- package/dist/core/src/plugin/material-registry.js +44 -0
- package/dist/core/src/plugin/mcp-tools.js +147 -0
- package/dist/core/src/plugin/permissions.js +152 -0
- package/dist/core/src/plugin/quickjs-bridge.js +219 -0
- package/dist/core/src/plugin/registry.js +92 -0
- package/dist/core/src/plugin/sandbox-shim.js +79 -0
- package/dist/core/src/plugin/sandbox-worker.js +194 -0
- package/dist/core/src/plugin/wasm-abi.js +62 -0
- package/dist/core/src/plugin/wasm-worker.js +234 -0
- package/dist/core/src/plugin/workshop.js +131 -0
- package/dist/core/src/profiling/event-loop.js +164 -0
- package/dist/core/src/profiling/index.js +23 -0
- package/dist/core/src/profiling/iops/idb-patch.js +211 -0
- package/dist/core/src/profiling/iops/opfs-patch.js +206 -0
- package/dist/core/src/profiling/profiling-sab.js +632 -0
- package/dist/core/src/profiling/task-latency.js +118 -0
- package/dist/core/src/profiling/thread-metrics.js +103 -0
- package/dist/core/src/profiling/trace-event-writer.js +165 -0
- package/dist/core/src/profiling/warnings.js +304 -0
- package/dist/core/src/profiling/worker-prelude.js +281 -0
- package/dist/core/src/render/bind-group.js +33 -0
- package/dist/core/src/render/bindless/bindless-struct.js +39 -0
- package/dist/core/src/render/bindless/bindless.wgsl.js +163 -0
- package/dist/core/src/render/bindless/frame-bindings.js +142 -0
- package/dist/core/src/render/bindless/index.js +6 -0
- package/dist/core/src/render/bindless/material-manager.js +182 -0
- package/dist/core/src/render/bindless/texture-registry.js +498 -0
- package/dist/core/src/render/buffer.js +74 -0
- package/dist/core/src/render/camera-2d.js +151 -0
- package/dist/core/src/render/camera-controller.js +370 -0
- package/dist/core/src/render/camera.js +332 -0
- package/dist/core/src/render/canvas-resize-watcher.js +36 -0
- package/dist/core/src/render/compute-kernel.js +130 -0
- package/dist/core/src/render/constants.js +2 -0
- package/dist/core/src/render/device.js +91 -0
- package/dist/core/src/render/frame-graph.js +594 -0
- package/dist/core/src/render/frustum.js +152 -0
- package/dist/core/src/render/fullscreen-vs.js +24 -0
- package/dist/core/src/render/g-buffer.js +156 -0
- package/dist/core/src/render/game-renderer.js +1165 -0
- package/dist/core/src/render/gpu-driven/cull-pass.js +352 -0
- package/dist/core/src/render/gpu-driven/hzb.js +183 -0
- package/dist/core/src/render/gpu-driven/index.js +6 -0
- package/dist/core/src/render/gpu-driven/indirect-draw-pass.js +300 -0
- package/dist/core/src/render/gpu-driven/instance-buffer.js +150 -0
- package/dist/core/src/render/gpu-driven/mesh-table.js +130 -0
- package/dist/core/src/render/gpu-driven/passes/hzb-build-pass.js +39 -0
- package/dist/core/src/render/gpu-utils.js +72 -0
- package/dist/core/src/render/hdr.js +69 -0
- package/dist/core/src/render/hot-reload.js +236 -0
- package/dist/core/src/render/ibl-bind-group.js +232 -0
- package/dist/core/src/render/ibl.js +148 -0
- package/dist/core/src/render/index.js +86 -0
- package/dist/core/src/render/input-manager.js +224 -0
- package/dist/core/src/render/lighting/area-light.js +142 -0
- package/dist/core/src/render/lighting/cluster-grid.js +255 -0
- package/dist/core/src/render/lighting/cluster-types.js +87 -0
- package/dist/core/src/render/lighting/ies-parser.js +111 -0
- package/dist/core/src/render/lighting.js +300 -0
- package/dist/core/src/render/material-types/depth.wgsl +44 -0
- package/dist/core/src/render/material-types/line.wgsl +50 -0
- package/dist/core/src/render/material-types/matcap.wgsl +51 -0
- package/dist/core/src/render/material-types/normal.wgsl +41 -0
- package/dist/core/src/render/material-types/physical.wgsl +152 -0
- package/dist/core/src/render/material-types/sprite.wgsl +46 -0
- package/dist/core/src/render/material-types/sss.wgsl +79 -0
- package/dist/core/src/render/material-types/toon.wgsl +79 -0
- package/dist/core/src/render/passes/README.md +121 -0
- package/dist/core/src/render/passes/atmosphere.js +107 -0
- package/dist/core/src/render/passes/cluster-lighting-pass.js +441 -0
- package/dist/core/src/render/passes/csm.js +338 -0
- package/dist/core/src/render/passes/cubemap-capture.js +93 -0
- package/dist/core/src/render/passes/debug-viz.js +248 -0
- package/dist/core/src/render/passes/debug.js +500 -0
- package/dist/core/src/render/passes/decal-mesh.js +86 -0
- package/dist/core/src/render/passes/decal-pass.js +344 -0
- package/dist/core/src/render/passes/deferred-lighting.js +406 -0
- package/dist/core/src/render/passes/depth-prepass.js +148 -0
- package/dist/core/src/render/passes/dome-360.js +68 -0
- package/dist/core/src/render/passes/fluid-render.js +116 -0
- package/dist/core/src/render/passes/gi-types.js +78 -0
- package/dist/core/src/render/passes/graph-compute.js +251 -0
- package/dist/core/src/render/passes/greased-line-pass.js +218 -0
- package/dist/core/src/render/passes/morph-target.js +35 -0
- package/dist/core/src/render/passes/opaque.js +966 -0
- package/dist/core/src/render/passes/point-light-shadow.js +221 -0
- package/dist/core/src/render/passes/procedural-texture.js +142 -0
- package/dist/core/src/render/passes/reflection-probe.js +100 -0
- package/dist/core/src/render/passes/rsm-pass.js +228 -0
- package/dist/core/src/render/passes/sdf-text.js +114 -0
- package/dist/core/src/render/passes/shadow-map.js +121 -0
- package/dist/core/src/render/passes/shadow.js +191 -0
- package/dist/core/src/render/passes/skinning-vs.js +102 -0
- package/dist/core/src/render/passes/skinning.js +282 -0
- package/dist/core/src/render/passes/sky-dome.js +380 -0
- package/dist/core/src/render/passes/skybox.js +146 -0
- package/dist/core/src/render/passes/spot-light-shadow.js +223 -0
- package/dist/core/src/render/passes/terrain.js +303 -0
- package/dist/core/src/render/passes/transparent.js +323 -0
- package/dist/core/src/render/passes/underwater-fog.js +173 -0
- package/dist/core/src/render/passes/video-texture.js +58 -0
- package/dist/core/src/render/passes/volumetric-pass.js +278 -0
- package/dist/core/src/render/passes/volumetric-types.js +96 -0
- package/dist/core/src/render/passes/water.js +697 -0
- package/dist/core/src/render/pbr.js +257 -0
- package/dist/core/src/render/pipeline.js +49 -0
- package/dist/core/src/render/render-pass.js +13 -0
- package/dist/core/src/render/renderer-input-bus.js +176 -0
- package/dist/core/src/render/renderer-module-host.js +539 -0
- package/dist/core/src/render/resource-tracker.js +59 -0
- package/dist/core/src/render/sab-canvas-overlay.js +83 -0
- package/dist/core/src/render/shader-validator.js +172 -0
- package/dist/core/src/render/shaders/debug/debug-line.wgsl +27 -0
- package/dist/core/src/render/shaders/debug/debug-point.wgsl +30 -0
- package/dist/core/src/render/shaders/debug/debug-text.wgsl +42 -0
- package/dist/core/src/render/shaders/debug-viz/aabb.wgsl +20 -0
- package/dist/core/src/render/shaders/debug-viz/depth.wgsl +29 -0
- package/dist/core/src/render/shaders/debug-viz/lod.wgsl +38 -0
- package/dist/core/src/render/shaders/debug-viz/normals.wgsl +30 -0
- package/dist/core/src/render/shaders/debug-viz/overdraw.wgsl +21 -0
- package/dist/core/src/render/shaders/debug-viz/tangents.wgsl +29 -0
- package/dist/core/src/render/shaders/debug-viz/wireframe.wgsl +33 -0
- package/dist/core/src/render/shaders/shadow-chunks.js +133 -0
- package/dist/core/src/render/skybox.js +123 -0
- package/dist/core/src/render/splitscreen.js +60 -0
- package/dist/core/src/render/surface.js +63 -0
- package/dist/core/src/render/tonemap.js +171 -0
- package/dist/core/src/render/tracked-render-pass.js +115 -0
- package/dist/core/src/render/viewport.js +81 -0
- package/dist/core/src/render/wgsl-hmr.js +66 -0
- package/dist/core/src/sab/core-input-channel.js +22 -0
- package/dist/core/src/sab/define.js +127 -0
- package/dist/core/src/sab/errors.js +60 -0
- package/dist/core/src/sab/game-input.js +299 -0
- package/dist/core/src/sab/gamepad-devices.js +205 -0
- package/dist/core/src/sab/grid-buffer.js +91 -0
- package/dist/core/src/sab/input.js +38 -0
- package/dist/core/src/sab/layout.js +160 -0
- package/dist/core/src/sab/multi-input-channel.js +39 -0
- package/dist/core/src/sab/record-buffer.js +130 -0
- package/dist/core/src/sab/sab-polyfill.js +47 -0
- package/dist/core/src/sab/sim-channel.js +397 -0
- package/dist/core/src/sab/slot-buffer.js +140 -0
- package/dist/core/src/sab/types.js +1 -0
- package/dist/core/src/safety/bounds.js +71 -0
- package/dist/core/src/safety/json.js +70 -0
- package/dist/core/src/safety/path.js +151 -0
- package/dist/core/src/save/binary-format.js +95 -0
- package/dist/core/src/save/game-save-system.js +101 -0
- package/dist/core/src/save/grid-save-system.js +102 -0
- package/dist/core/src/save/migrate.js +35 -0
- package/dist/core/src/save/migration-registry.js +66 -0
- package/dist/core/src/save/persist-types.js +4 -0
- package/dist/core/src/save/schema.js +36 -0
- package/dist/core/src/save/serializer.js +139 -0
- package/dist/core/src/scene/builder.js +31 -0
- package/dist/core/src/scene/camera.js +118 -0
- package/dist/core/src/scene/checkpoint.js +84 -0
- package/dist/core/src/scene/game-loop.js +155 -0
- package/dist/core/src/scene/index.js +14 -0
- package/dist/core/src/scene/layer.js +7 -0
- package/dist/core/src/scene/prefab.js +97 -0
- package/dist/core/src/scene/scene-manager.js +141 -0
- package/dist/core/src/scene/scene.js +147 -0
- package/dist/core/src/scene/sky.js +179 -0
- package/dist/core/src/scene/spatial-grid.js +140 -0
- package/dist/core/src/scene/streaming.js +247 -0
- package/dist/core/src/scene/vegetation.js +107 -0
- package/dist/core/src/scene/world.js +66 -0
- package/dist/core/src/scripting/binding.js +85 -0
- package/dist/core/src/scripting/hot-reload.js +123 -0
- package/dist/core/src/scripting/script.js +68 -0
- package/dist/core/src/sim/hot-reload-pipeline.js +65 -0
- package/dist/core/src/sim/sim-state-helper.js +40 -0
- package/dist/core/src/sim/transient-state-registry.js +63 -0
- package/dist/core/src/sim/types.js +4 -0
- package/dist/core/src/sim/worker-loop.js +168 -0
- package/dist/core/src/stores/game-store.js +81 -0
- package/dist/core/src/stores/hot-reload-store.js +19 -0
- package/dist/core/src/telemetry/collector.js +303 -0
- package/dist/core/src/telemetry/gc-controller.js +365 -0
- package/dist/core/src/telemetry/gc-profiler.js +82 -0
- package/dist/core/src/telemetry/gc-tracker.js +63 -0
- package/dist/core/src/telemetry/gpu-profiler.js +554 -0
- package/dist/core/src/telemetry/gpu-resource-tracker.js +234 -0
- package/dist/core/src/telemetry/gpu-timer-pool.js +223 -0
- package/dist/core/src/telemetry/gpu-timer.js +117 -0
- package/dist/core/src/telemetry/gpu-timestamp-gate.js +22 -0
- package/dist/core/src/telemetry/reporter.js +56 -0
- package/dist/core/src/types/engine-types.js +21 -0
- package/dist/core/src/ui-font-scale.js +111 -0
- package/dist/core/src/util/broad-phase-grid.js +77 -0
- package/dist/core/src/util/feature-log.js +176 -0
- package/dist/core/src/util/index.js +4 -0
- package/dist/core/src/util/logger.js +596 -0
- package/dist/core/src/version.js +11 -0
- package/dist/core/src/webgpu-destroy-augmentation.d.ts +25 -0
- package/dist/core/src/worker/base-worker-host.js +212 -0
- package/dist/core/src/worker/buffer-sync.js +454 -0
- package/dist/core/src/worker/crash-recovery.js +153 -0
- package/dist/core/src/worker/entity-sim-worker-host.js +128 -0
- package/dist/core/src/worker/instrumented-worker-host.js +139 -0
- package/dist/core/src/worker/rpc.js +244 -0
- package/dist/core/src/worker/sim-worker-base.js +558 -0
- package/dist/core/src/worker/sim-worker-host.js +271 -0
- package/dist/core/src/worker/task-pool.js +205 -0
- package/dist/core/src/worker/task-worker.js +77 -0
- package/dist/libraries/asset-browser/src/index.js +10 -0
- package/dist/libraries/asset-browser/src/software-thumbnails.js +940 -0
- package/dist/libraries/audio-kira/src/backend.js +405 -0
- package/dist/libraries/audio-kira/src/index.js +4 -0
- package/dist/libraries/audio-kira/src/library.js +54 -0
- package/dist/libraries/blitz-ui/src/index.js +13 -0
- package/dist/libraries/blitz-ui/src/native-module.js +162 -0
- package/dist/libraries/blitz-ui/src/native-osr-ffi.js +409 -0
- package/dist/libraries/blitz-ui/src/router.js +82 -0
- package/dist/libraries/blitz-ui/src/shaders/ui-blit.wgsl.js +32 -0
- package/dist/libraries/blitz-ui/src/ui-blit-pass.js +108 -0
- package/dist/libraries/character/src/character-animator.js +323 -0
- package/dist/libraries/character/src/customization.js +464 -0
- package/dist/libraries/character/src/index.js +19 -0
- package/dist/libraries/character/src/inspectable.js +77 -0
- package/dist/libraries/character/src/loader.js +336 -0
- package/dist/libraries/character/src/locomotion.js +84 -0
- package/dist/libraries/character/src/preview.js +280 -0
- package/dist/libraries/devtools/src/backend.js +449 -0
- package/dist/libraries/devtools/src/cdp-bridge.js +293 -0
- package/dist/libraries/devtools/src/collectors.js +318 -0
- package/dist/libraries/devtools/src/index.js +31 -0
- package/dist/libraries/devtools/src/native-providers.js +749 -0
- package/dist/libraries/devtools/src/snapshot.js +35 -0
- package/dist/libraries/devtools/src/verify.js +139 -0
- package/dist/libraries/devtools/src/web/browser.js +94 -0
- package/dist/libraries/devtools/src/web/host.js +183 -0
- package/dist/libraries/devtools/src/web/mirror.js +26 -0
- package/dist/libraries/devtools/src/web/server.js +187 -0
- package/dist/libraries/entities/src/index.js +3 -0
- package/dist/libraries/entities/src/library.js +46 -0
- package/dist/libraries/entities/src/model-renderer.js +1307 -0
- package/dist/libraries/entities/src/shaders/model.wgsl +375 -0
- package/dist/libraries/gamepad/src/feed-input.js +54 -0
- package/dist/libraries/gamepad/src/filters.js +44 -0
- package/dist/libraries/gamepad/src/glyphs.js +83 -0
- package/dist/libraries/gamepad/src/hub.js +164 -0
- package/dist/libraries/gamepad/src/index.js +10 -0
- package/dist/libraries/gamepad/src/library.js +56 -0
- package/dist/libraries/gamepad/src/source-sab.js +62 -0
- package/dist/libraries/gamepad/src/sysfs-enrich.js +125 -0
- package/dist/libraries/gaussian-splats/src/gpu-sort.js +506 -0
- package/dist/libraries/gaussian-splats/src/index.js +8 -0
- package/dist/libraries/gaussian-splats/src/library.js +44 -0
- package/dist/libraries/gaussian-splats/src/parser.js +297 -0
- package/dist/libraries/gaussian-splats/src/renderer.js +368 -0
- package/dist/libraries/gaussian-splats/src/sh-eval.js +196 -0
- package/dist/libraries/gaussian-splats/src/sorter.js +76 -0
- package/dist/libraries/gaussian-splats/src/tile-raster.js +424 -0
- package/dist/libraries/gpu-kernels/src/index.js +8 -0
- package/dist/libraries/gpu-kernels/src/kernel.js +591 -0
- package/dist/libraries/gpu-kernels/src/transpile.js +896 -0
- package/dist/libraries/html-ui-kit/src/behaviors.js +231 -0
- package/dist/libraries/html-ui-kit/src/components.js +219 -0
- package/dist/libraries/html-ui-kit/src/index.js +17 -0
- package/dist/libraries/html-ui-kit/src/nav/nav-controller.js +215 -0
- package/dist/libraries/html-ui-kit/src/nav/nav-input.js +138 -0
- package/dist/libraries/html-ui-kit/src/nav/osk.js +135 -0
- package/dist/libraries/html-ui-kit/src/nav/router.js +152 -0
- package/dist/libraries/html-ui-kit/src/nav/vcursor.js +177 -0
- package/dist/libraries/html-ui-kit/src/ten-foot.js +138 -0
- package/dist/libraries/html-ui-kit/src/theme.js +235 -0
- package/dist/libraries/inspectable/src/index.js +6 -0
- package/dist/libraries/inspectable/src/inspectable.js +205 -0
- package/dist/libraries/lighting/src/index.js +4 -0
- package/dist/libraries/lighting/src/library.js +48 -0
- package/dist/libraries/lighting/src/light-system.js +395 -0
- package/dist/libraries/lighting/src/lighting-system.js +57 -0
- package/dist/libraries/lighting/src/shaders/light-debug.wgsl +30 -0
- package/dist/libraries/marching-cubes/src/chunked-field.js +130 -0
- package/dist/libraries/marching-cubes/src/deformation.js +41 -0
- package/dist/libraries/marching-cubes/src/generator.js +356 -0
- package/dist/libraries/marching-cubes/src/index.js +13 -0
- package/dist/libraries/marching-cubes/src/library.js +54 -0
- package/dist/libraries/marching-cubes/src/lod.js +95 -0
- package/dist/libraries/marching-cubes/src/marching-tetrahedra.js +261 -0
- package/dist/libraries/marching-cubes/src/sab.js +40 -0
- package/dist/libraries/marching-cubes/src/streaming-config.js +28 -0
- package/dist/libraries/marching-cubes/src/streaming-manager.js +490 -0
- package/dist/libraries/marching-cubes/src/tables.js +314 -0
- package/dist/libraries/marching-cubes/src/types.js +1 -0
- package/dist/libraries/models/src/bake-node-transforms.js +146 -0
- package/dist/libraries/models/src/bind-unskinned-meshes.js +145 -0
- package/dist/libraries/models/src/codecs/basisu-codec.js +134 -0
- package/dist/libraries/models/src/codecs/draco-codec.js +236 -0
- package/dist/libraries/models/src/codecs/draco3dgltf.d.ts +40 -0
- package/dist/libraries/models/src/codecs/index.js +5 -0
- package/dist/libraries/models/src/codecs/meshopt-codec.js +59 -0
- package/dist/libraries/models/src/codecs/registry.js +111 -0
- package/dist/libraries/models/src/dae.js +665 -0
- package/dist/libraries/models/src/default-part-selection.js +58 -0
- package/dist/libraries/models/src/dxf.js +235 -0
- package/dist/libraries/models/src/fbx/index.js +438 -0
- package/dist/libraries/models/src/fbx/interpreter/animation.js +369 -0
- package/dist/libraries/models/src/fbx/interpreter/blend-shapes.js +283 -0
- package/dist/libraries/models/src/fbx/interpreter/connections.js +158 -0
- package/dist/libraries/models/src/fbx/interpreter/diagnostics.js +48 -0
- package/dist/libraries/models/src/fbx/interpreter/geometry.js +430 -0
- package/dist/libraries/models/src/fbx/interpreter/global-settings.js +77 -0
- package/dist/libraries/models/src/fbx/interpreter/materials.js +231 -0
- package/dist/libraries/models/src/fbx/interpreter/nodes.js +168 -0
- package/dist/libraries/models/src/fbx/interpreter/skeleton.js +454 -0
- package/dist/libraries/models/src/fbx/parsers/binary-parser.js +217 -0
- package/dist/libraries/models/src/fbx/types.js +88 -0
- package/dist/libraries/models/src/gltf-extensions.js +121 -0
- package/dist/libraries/models/src/gltf.js +724 -0
- package/dist/libraries/models/src/index.js +46 -0
- package/dist/libraries/models/src/library.js +44 -0
- package/dist/libraries/models/src/loader.js +218 -0
- package/dist/libraries/models/src/material-adapter.js +82 -0
- package/dist/libraries/models/src/mini-xml.js +145 -0
- package/dist/libraries/models/src/normalize.js +168 -0
- package/dist/libraries/models/src/obj.js +247 -0
- package/dist/libraries/models/src/off.js +94 -0
- package/dist/libraries/models/src/ply.js +329 -0
- package/dist/libraries/models/src/pose-sampler.js +183 -0
- package/dist/libraries/models/src/sidecar/blender-extras.js +32 -0
- package/dist/libraries/models/src/sidecar/ddmeta.js +98 -0
- package/dist/libraries/models/src/sidecar/godot-import.js +76 -0
- package/dist/libraries/models/src/sidecar/resolver.js +112 -0
- package/dist/libraries/models/src/sidecar/types.js +8 -0
- package/dist/libraries/models/src/sidecar/unity-meta.js +46 -0
- package/dist/libraries/models/src/skeleton-synthesis.js +122 -0
- package/dist/libraries/models/src/stl.js +129 -0
- package/dist/libraries/models/src/threeds.js +312 -0
- package/dist/libraries/models/src/threemf.js +203 -0
- package/dist/libraries/models/src/types.js +33 -0
- package/dist/libraries/models/src/vtk.js +242 -0
- package/dist/libraries/navmesh/src/crowd-system.js +207 -0
- package/dist/libraries/navmesh/src/debug-viz.js +52 -0
- package/dist/libraries/navmesh/src/index.js +7 -0
- package/dist/libraries/navmesh/src/library.js +47 -0
- package/dist/libraries/navmesh/src/navmesh-generator.js +364 -0
- package/dist/libraries/navmesh/src/navmesh.js +141 -0
- package/dist/libraries/navmesh/src/pathfinder.js +221 -0
- package/dist/libraries/navmesh/src/types.js +1 -0
- package/dist/libraries/networking/src/authority.js +107 -0
- package/dist/libraries/networking/src/connection.js +107 -0
- package/dist/libraries/networking/src/discovery.js +314 -0
- package/dist/libraries/networking/src/enhanced-replication.js +282 -0
- package/dist/libraries/networking/src/epic-adapter.js +101 -0
- package/dist/libraries/networking/src/index.js +16 -0
- package/dist/libraries/networking/src/library.js +45 -0
- package/dist/libraries/networking/src/lobby.js +84 -0
- package/dist/libraries/networking/src/platform-adapter.js +92 -0
- package/dist/libraries/networking/src/rcs-adapter.js +100 -0
- package/dist/libraries/networking/src/remote-input.js +162 -0
- package/dist/libraries/networking/src/replication.js +273 -0
- package/dist/libraries/networking/src/rpc.js +172 -0
- package/dist/libraries/networking/src/session.js +129 -0
- package/dist/libraries/networking/src/transport.js +156 -0
- package/dist/libraries/networking/src/webrtc.js +266 -0
- package/dist/libraries/pathfinding-2d/src/astar-grid.js +255 -0
- package/dist/libraries/pathfinding-2d/src/index.js +9 -0
- package/dist/libraries/persistence/src/autosave-manager.js +94 -0
- package/dist/libraries/persistence/src/binary-record-store.js +205 -0
- package/dist/libraries/persistence/src/browser.js +16 -0
- package/dist/libraries/persistence/src/file-save-store.js +507 -0
- package/dist/libraries/persistence/src/hash-utils.js +54 -0
- package/dist/libraries/persistence/src/index.js +17 -0
- package/dist/libraries/persistence/src/library.js +51 -0
- package/dist/libraries/persistence/src/mock-opfs.js +114 -0
- package/dist/libraries/persistence/src/node-fs-directory-handle.js +168 -0
- package/dist/libraries/persistence/src/opfs-save-store.js +584 -0
- package/dist/libraries/persistence/src/save-worker-proxy.js +96 -0
- package/dist/libraries/persistence/src/save-worker.js +76 -0
- package/dist/libraries/persistence/src/xxh3-ts.d.ts +10 -0
- package/dist/libraries/physics-native/src/backend.js +960 -0
- package/dist/libraries/physics-native/src/broadphase.js +79 -0
- package/dist/libraries/physics-native/src/index.js +6 -0
- package/dist/libraries/physics-native/src/library.js +57 -0
- package/dist/libraries/physics-native/src/narrowphase-shapes.js +348 -0
- package/dist/libraries/physics-native/src/narrowphase.js +60 -0
- package/dist/libraries/physics-native/src/solver.js +105 -0
- package/dist/libraries/physics-native/src/types.js +4 -0
- package/dist/libraries/physics-rapier/src/api.js +352 -0
- package/dist/libraries/physics-rapier/src/auto-backend.js +54 -0
- package/dist/libraries/physics-rapier/src/backend.js +1066 -0
- package/dist/libraries/physics-rapier/src/bulk-ops.js +46 -0
- package/dist/libraries/physics-rapier/src/ffi-backend.js +17 -0
- package/dist/libraries/physics-rapier/src/ffi-lib.js +605 -0
- package/dist/libraries/physics-rapier/src/index.js +9 -0
- package/dist/libraries/physics-rapier/src/library.js +71 -0
- package/dist/libraries/physics-rapier/src/rapier-backend.js +1102 -0
- package/dist/libraries/physics-rapier/src/realm-worker-script.js +233 -0
- package/dist/libraries/postfx/src/index.js +14 -0
- package/dist/libraries/postfx/src/library.js +207 -0
- package/dist/libraries/postfx/src/post-process-stack.js +1641 -0
- package/dist/libraries/postfx/src/shaders/post-process/afterimage.wgsl +14 -0
- package/dist/libraries/postfx/src/shaders/post-process/ascii.wgsl +24 -0
- package/dist/libraries/postfx/src/shaders/post-process/blit.wgsl +12 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-blur.wgsl +21 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-bright.wgsl +15 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-composite.wgsl +14 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-downsample.wgsl +60 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-soft.wgsl +46 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-upsample.wgsl +51 -0
- package/dist/libraries/postfx/src/shaders/post-process/channel-mixer.wgsl +36 -0
- package/dist/libraries/postfx/src/shaders/post-process/chromatic-aberration.wgsl +37 -0
- package/dist/libraries/postfx/src/shaders/post-process/dithering.wgsl +64 -0
- package/dist/libraries/postfx/src/shaders/post-process/dof.wgsl +131 -0
- package/dist/libraries/postfx/src/shaders/post-process/edges.wgsl +45 -0
- package/dist/libraries/postfx/src/shaders/post-process/fullscreen-vs.wgsl +15 -0
- package/dist/libraries/postfx/src/shaders/post-process/fxaa.wgsl +102 -0
- package/dist/libraries/postfx/src/shaders/post-process/gaussian-blur.wgsl +37 -0
- package/dist/libraries/postfx/src/shaders/post-process/glow-blur.wgsl +33 -0
- package/dist/libraries/postfx/src/shaders/post-process/glow-composite.wgsl +25 -0
- package/dist/libraries/postfx/src/shaders/post-process/grain.wgsl +37 -0
- package/dist/libraries/postfx/src/shaders/post-process/halftone.wgsl +80 -0
- package/dist/libraries/postfx/src/shaders/post-process/highlight-blur.wgsl +33 -0
- package/dist/libraries/postfx/src/shaders/post-process/highlight-composite.wgsl +32 -0
- package/dist/libraries/postfx/src/shaders/post-process/lens-distortion.wgsl +43 -0
- package/dist/libraries/postfx/src/shaders/post-process/lens-flare.wgsl +68 -0
- package/dist/libraries/postfx/src/shaders/post-process/lut.wgsl +30 -0
- package/dist/libraries/postfx/src/shaders/post-process/motion-blur.wgsl +55 -0
- package/dist/libraries/postfx/src/shaders/post-process/occluder-chunk.wgsl +28 -0
- package/dist/libraries/postfx/src/shaders/post-process/outline.wgsl +51 -0
- package/dist/libraries/postfx/src/shaders/post-process/pixelation.wgsl +47 -0
- package/dist/libraries/postfx/src/shaders/post-process/sharpen.wgsl +32 -0
- package/dist/libraries/postfx/src/shaders/post-process/sobel.wgsl +26 -0
- package/dist/libraries/postfx/src/shaders/post-process/split-tone.wgsl +36 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssao-blur.wgsl +38 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssao-composite.wgsl +25 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssao.wgsl +147 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssr.wgsl +155 -0
- package/dist/libraries/postfx/src/shaders/post-process/taa.wgsl +98 -0
- package/dist/libraries/postfx/src/shaders/post-process/tonemap.wgsl +40 -0
- package/dist/libraries/postfx/src/shaders/post-process/watercolor.wgsl +82 -0
- package/dist/libraries/postfx/src/shaders/post-process/white-balance.wgsl +41 -0
- package/dist/libraries/recast/src/backend.js +124 -0
- package/dist/libraries/recast/src/crowd-system.js +310 -0
- package/dist/libraries/recast/src/index.js +4 -0
- package/dist/libraries/recast/src/library.js +59 -0
- package/dist/libraries/recast/src/types.js +4 -0
- package/dist/libraries/sand/src/cell.js +32 -0
- package/dist/libraries/sand/src/fields.js +27 -0
- package/dist/libraries/sand/src/fluid-grid.js +267 -0
- package/dist/libraries/sand/src/grid-dims.js +47 -0
- package/dist/libraries/sand/src/grid-sim-buffer.js +184 -0
- package/dist/libraries/sand/src/index.js +20 -0
- package/dist/libraries/sand/src/library.js +67 -0
- package/dist/libraries/sand/src/materials.js +437 -0
- package/dist/libraries/sand/src/palette.js +87 -0
- package/dist/libraries/sand/src/particles.js +163 -0
- package/dist/libraries/sand/src/render/sand-grid-pass.js +377 -0
- package/dist/libraries/sand/src/rng.js +44 -0
- package/dist/libraries/sand/src/rules/builtin-rules.js +125 -0
- package/dist/libraries/sand/src/rules/rule-compiler.js +164 -0
- package/dist/libraries/sand/src/rules/rule-engine.js +210 -0
- package/dist/libraries/sand/src/rules/rule-types.js +27 -0
- package/dist/libraries/sand/src/sand-step-pool.js +458 -0
- package/dist/libraries/sand/src/sand-step-worker.js +101 -0
- package/dist/libraries/sand/src/sand-world.js +3382 -0
- package/dist/libraries/stickman/src/index.js +7 -0
- package/dist/libraries/stickman/src/library.js +44 -0
- package/dist/libraries/stickman/src/proportions.js +40 -0
- package/dist/libraries/stickman/src/shader.js +113 -0
- package/dist/libraries/stickman/src/skeleton.js +163 -0
- package/dist/libraries/stickman/src/stickman-pass.js +210 -0
- package/dist/libraries/stickman/src/thick-line.js +120 -0
- package/dist/libraries/surface-nets/src/chunked-field.js +5 -0
- package/dist/libraries/surface-nets/src/deformation.js +29 -0
- package/dist/libraries/surface-nets/src/index.js +15 -0
- package/dist/libraries/surface-nets/src/library.js +41 -0
- package/dist/libraries/surface-nets/src/lod.js +6 -0
- package/dist/libraries/surface-nets/src/sab.js +4 -0
- package/dist/libraries/surface-nets/src/streaming-config.js +4 -0
- package/dist/libraries/surface-nets/src/streaming-manager.js +9 -0
- package/dist/libraries/surface-nets/src/surface-nets.js +361 -0
- package/dist/libraries/surface-nets/src/types.js +1 -0
- package/dist/libraries/water/src/buoyancy.js +80 -0
- package/dist/libraries/water/src/index.js +12 -0
- package/dist/libraries/water/src/library.js +65 -0
- package/dist/libraries/water/src/shore-damping.js +70 -0
- package/dist/libraries/water/src/water-buffer.js +78 -0
- package/dist/libraries/water/src/water-chunks.js +5 -0
- package/dist/libraries/water/src/water-physics.js +315 -0
- package/dist/libraries/water/src/water-renderer.js +736 -0
- package/dist/libraries/water/src/water-sab.js +138 -0
- package/dist/libraries/water/src/wave-sources.js +45 -0
- package/dist/libraries/weather/src/index.js +5 -0
- package/dist/libraries/weather/src/library.js +48 -0
- package/dist/libraries/weather/src/types.js +29 -0
- package/dist/libraries/weather/src/weather-blend.js +108 -0
- package/dist/libraries/weather/src/weather-system.js +275 -0
- package/dist/libraries/weatherfx/src/cloud-provider.js +5 -0
- package/dist/libraries/weatherfx/src/cloud-system.js +294 -0
- package/dist/libraries/weatherfx/src/index.js +4 -0
- package/dist/libraries/weatherfx/src/library.js +41 -0
- package/dist/libraries/weatherfx/src/particle-system.js +350 -0
- package/dist/libraries/weatherfx/src/shaders/cloud.wgsl +111 -0
- package/dist/libraries/weatherfx/src/shaders/particle-compute.wgsl +187 -0
- package/dist/libraries/weatherfx/src/shaders/particle-render.wgsl +95 -0
- package/dist/mcp/src/client.js +669 -0
- package/dist/mcp/src/engine-context.js +99 -0
- package/dist/mcp/src/http-transport.js +423 -0
- package/dist/mcp/src/index.js +5 -0
- package/dist/mcp/src/prompts/add-entity.js +39 -0
- package/dist/mcp/src/prompts/create-scene.js +41 -0
- package/dist/mcp/src/prompts/debug-frame.js +36 -0
- package/dist/mcp/src/prompts/index.js +10 -0
- package/dist/mcp/src/resources/asset-list.js +28 -0
- package/dist/mcp/src/resources/checkpoint-list.js +29 -0
- package/dist/mcp/src/resources/entity-state.js +45 -0
- package/dist/mcp/src/resources/gpu-info.js +90 -0
- package/dist/mcp/src/resources/index.js +51 -0
- package/dist/mcp/src/resources/performance.js +33 -0
- package/dist/mcp/src/resources/scene-tree.js +37 -0
- package/dist/mcp/src/server.js +307 -0
- package/dist/mcp/src/tools/animation.js +102 -0
- package/dist/mcp/src/tools/asset.js +146 -0
- package/dist/mcp/src/tools/audio.js +80 -0
- package/dist/mcp/src/tools/build.js +128 -0
- package/dist/mcp/src/tools/camera.js +157 -0
- package/dist/mcp/src/tools/checkpoint.js +90 -0
- package/dist/mcp/src/tools/component.js +176 -0
- package/dist/mcp/src/tools/debug.js +273 -0
- package/dist/mcp/src/tools/entity.js +309 -0
- package/dist/mcp/src/tools/inspect.js +192 -0
- package/dist/mcp/src/tools/lighting.js +239 -0
- package/dist/mcp/src/tools/material.js +225 -0
- package/dist/mcp/src/tools/mesh.js +210 -0
- package/dist/mcp/src/tools/physics.js +251 -0
- package/dist/mcp/src/tools/scene.js +139 -0
- package/dist/mcp/src/tools/script.js +114 -0
- package/dist/mcp/src/types.js +9 -0
- package/dist/mcp/src/undo-redo.js +64 -0
- package/dist/modules/camera-controls/src/index.js +116 -0
- package/dist/modules/controller-ui/src/index.js +149 -0
- package/dist/modules/devtools/src/api.js +317 -0
- package/dist/modules/devtools/src/asset-utils.js +102 -0
- package/dist/modules/devtools/src/bind-debug-store.js +34 -0
- package/dist/modules/devtools/src/blitz/host.js +391 -0
- package/dist/modules/devtools/src/blitz/module.js +65 -0
- package/dist/modules/devtools/src/blitz/panels/console.js +217 -0
- package/dist/modules/devtools/src/blitz/panels/elements.js +79 -0
- package/dist/modules/devtools/src/blitz/panels/gpu.js +61 -0
- package/dist/modules/devtools/src/blitz/panels/performance.js +113 -0
- package/dist/modules/devtools/src/blitz/panels/snapshot.js +141 -0
- package/dist/modules/devtools/src/blitz/panels/types.js +55 -0
- package/dist/modules/devtools/src/blitz/theme.js +167 -0
- package/dist/modules/devtools/src/data-bridge.js +247 -0
- package/dist/modules/devtools/src/debug-overlay.js +181 -0
- package/dist/modules/devtools/src/debug-raycast.js +296 -0
- package/dist/modules/devtools/src/debug-store.js +28 -0
- package/dist/modules/devtools/src/debug-view-descriptors.js +24 -0
- package/dist/modules/devtools/src/doctor-panel.js +163 -0
- package/dist/modules/devtools/src/gizmo-module.js +95 -0
- package/dist/modules/devtools/src/graph-model.js +387 -0
- package/dist/modules/devtools/src/grid-renderer.js +145 -0
- package/dist/modules/devtools/src/height-ruler-renderer.js +189 -0
- package/dist/modules/devtools/src/index.js +46 -0
- package/dist/modules/devtools/src/init.js +204 -0
- package/dist/modules/devtools/src/label-overlay.js +129 -0
- package/dist/modules/devtools/src/material-stats-panel.js +171 -0
- package/dist/modules/devtools/src/profiling-bridge.js +288 -0
- package/dist/modules/devtools/src/profiling-hooks.js +63 -0
- package/dist/modules/devtools/src/renderer-adapter.js +61 -0
- package/dist/modules/devtools/src/scene-inspector.js +571 -0
- package/dist/modules/devtools/src/scene-store.js +212 -0
- package/dist/modules/devtools/src/scene-sync.js +92 -0
- package/dist/modules/devtools/src/shaders/transform-gizmo.wgsl +38 -0
- package/dist/modules/devtools/src/sim-stats-panel.js +250 -0
- package/dist/modules/devtools/src/sim-stats-provider.js +73 -0
- package/dist/modules/devtools/src/skeleton-renderer.js +326 -0
- package/dist/modules/devtools/src/transform-gizmo.js +677 -0
- package/dist/modules/devtools/src/types.js +5 -0
- package/dist/modules/devtools/src/worker-expose.js +59 -0
- package/dist/modules/devtools/src/worker-sync.js +115 -0
- package/dist/modules/editor/src/commands/component.js +151 -0
- package/dist/modules/editor/src/commands/entity.js +253 -0
- package/dist/modules/editor/src/commands/hierarchy.js +79 -0
- package/dist/modules/editor/src/commands/index.js +57 -0
- package/dist/modules/editor/src/commands/prefab.js +149 -0
- package/dist/modules/editor/src/commands/registry.js +246 -0
- package/dist/modules/editor/src/commands/scene-io.js +206 -0
- package/dist/modules/editor/src/commands/selection.js +71 -0
- package/dist/modules/editor/src/commands/transform.js +88 -0
- package/dist/modules/editor/src/ddscene.js +189 -0
- package/dist/modules/editor/src/document.js +96 -0
- package/dist/modules/editor/src/editor-context.js +115 -0
- package/dist/modules/editor/src/index.js +11 -0
- package/dist/modules/editor/src/mcp.js +91 -0
- package/dist/modules/editor/src/module.js +115 -0
- package/dist/modules/editor/src/selection.js +60 -0
- package/dist/modules/editor/src/shell.js +397 -0
- package/dist/modules/editor/src/viewport.js +186 -0
- package/dist/modules/html-ui/src/composite.js +116 -0
- package/dist/modules/html-ui/src/create-html-ui.js +137 -0
- package/dist/modules/html-ui/src/doc-backend.js +518 -0
- package/dist/modules/html-ui/src/host.js +819 -0
- package/dist/modules/html-ui/src/index.js +5 -0
- package/dist/modules/html-ui/src/jsx-dev-runtime.js +9 -0
- package/dist/modules/html-ui/src/jsx-runtime.js +121 -0
- package/dist/modules/html-ui/src/protocol.js +6 -0
- package/dist/modules/html-ui/src/ui-worker.js +10 -0
- package/dist/modules/mcp/src/index.js +62 -0
- package/dist/modules/movement-2d/src/grid-character.js +345 -0
- package/dist/modules/movement-2d/src/index.js +27 -0
- package/dist/modules/movement-3d/src/character-motor.js +166 -0
- package/dist/modules/movement-3d/src/index.js +27 -0
- package/dist/modules/native-osr/src/index.js +12 -0
- package/dist/modules/native-osr/src/input-router.js +335 -0
- package/dist/modules/native-osr/src/native-osr-manager.js +210 -0
- package/dist/modules/native-osr/src/native-texture-source.js +66 -0
- package/dist/modules/native-osr/src/types.js +8 -0
- package/dist/modules/native-osr/src/world-space-ui-pass.js +313 -0
- package/dist/modules/sailing/src/index.js +39 -0
- package/dist/modules/terrain/src/index.js +44 -0
- package/dist/modules/vitals/src/index.js +25 -0
- package/dist/modules/vitals/src/vitals.js +195 -0
- package/dist/modules/xr/src/camera-rig.js +119 -0
- package/dist/modules/xr/src/debug.js +45 -0
- package/dist/modules/xr/src/frame-loop.js +139 -0
- package/dist/modules/xr/src/index.js +10 -0
- package/dist/modules/xr/src/input.js +164 -0
- package/dist/modules/xr/src/layer.js +100 -0
- package/dist/modules/xr/src/session.js +133 -0
- package/dist/modules/xr/src/sim-module.js +21 -0
- package/dist/modules/xr/src/types.js +4 -0
- package/dist/modules/xr/src/webxr.d.ts +182 -0
- package/dist/modules/xr/src/xr-module.js +134 -0
- package/dist/shader-graph/src/chunks.js +340 -0
- package/dist/shader-graph/src/compiler.js +639 -0
- package/dist/shader-graph/src/compute-compiler.js +384 -0
- package/dist/shader-graph/src/compute-graph.js +55 -0
- package/dist/shader-graph/src/compute-profiles.js +39 -0
- package/dist/shader-graph/src/graph.js +38 -0
- package/dist/shader-graph/src/index.js +15 -0
- package/dist/shader-graph/src/profiles.js +305 -0
- package/dist/shader-graph/src/validator.js +42 -0
- package/dist/shader-graph/src/wgsl-binding-validator.js +110 -0
- package/dist/shader-graph/src/wgsl-struct-validator.js +222 -0
- package/dist/shader-graph/src/wgsl-struct.js +251 -0
- package/dist/test/src/index.js +1 -0
- package/dist/test/src/vision-test.js +228 -0
- package/libraries/asset-browser/src/software-thumbnails.ts +2 -2
- package/libraries/audio-kira/src/backend.ts +2 -1
- package/libraries/blitz-ui/native/src/shell.rs +35 -1
- package/libraries/blitz-ui/native-osr/Cargo.toml +8 -0
- package/libraries/blitz-ui/native-osr/src/lib.rs +213 -22
- package/libraries/blitz-ui/native-osr/src/mutate.rs +109 -14
- package/libraries/blitz-ui/src/native-osr-ffi.ts +71 -8
- package/libraries/blitz-ui/src/router.ts +3 -1
- package/libraries/blitz-ui/src/ui-blit-pass.ts +1 -1
- package/libraries/character/src/character-animator.ts +1 -1
- package/libraries/character/src/customization.ts +513 -0
- package/libraries/character/src/index.ts +6 -0
- package/libraries/character/src/inspectable.ts +111 -0
- package/libraries/character/src/loader.ts +38 -13
- package/libraries/character/src/locomotion.ts +13 -1
- package/libraries/character/src/preview.ts +94 -24
- package/libraries/devtools/README.md +16 -18
- package/libraries/devtools/src/backend.ts +505 -0
- package/libraries/devtools/src/cdp-bridge.ts +8 -8
- package/libraries/devtools/src/collectors.ts +41 -37
- package/libraries/devtools/src/index.ts +32 -14
- package/libraries/devtools/src/native-providers.ts +20 -12
- package/libraries/devtools/src/snapshot.ts +77 -0
- package/libraries/devtools/src/web/host.ts +11 -12
- package/libraries/devtools/src/web/mirror.ts +14 -405
- package/libraries/devtools/src/web/server.ts +4 -3
- package/libraries/entities/src/model-renderer.ts +96 -23
- package/libraries/gamepad/src/library.ts +2 -1
- package/libraries/inspectable/src/index.ts +11 -0
- package/libraries/inspectable/src/inspectable.ts +277 -0
- package/libraries/marching-cubes/src/streaming-manager.ts +4 -4
- package/libraries/models/src/bind-unskinned-meshes.ts +131 -0
- package/libraries/models/src/index.ts +1 -0
- package/libraries/models/src/types.ts +22 -0
- package/libraries/persistence/src/autosave-manager.ts +1 -1
- package/libraries/persistence/src/binary-record-store.ts +1 -1
- package/libraries/persistence/src/file-save-store.ts +4 -4
- package/libraries/persistence/src/library.ts +1 -1
- package/libraries/persistence/src/opfs-save-store.ts +5 -5
- package/libraries/persistence/src/save-worker.ts +13 -14
- package/libraries/physics-rapier/src/ffi-lib.ts +2 -1
- package/libraries/postfx/src/post-process-stack.ts +1 -1
- package/libraries/sand/src/sand-step-pool.ts +5 -0
- package/libraries/water/src/water-renderer.ts +1 -1
- package/libraries/weatherfx/src/cloud-system.ts +3 -3
- package/mcp/README.md +20 -0
- package/mcp/src/client.ts +75 -22
- package/mcp/src/undo-redo.ts +2 -2
- package/modules/devtools/src/blitz/host.ts +430 -0
- package/modules/devtools/src/blitz/module.ts +101 -0
- package/modules/devtools/src/blitz/panels/console.ts +223 -0
- package/modules/devtools/src/blitz/panels/elements.ts +92 -0
- package/modules/devtools/src/blitz/panels/gpu.ts +70 -0
- package/modules/devtools/src/blitz/panels/performance.ts +120 -0
- package/modules/devtools/src/blitz/panels/snapshot.ts +152 -0
- package/modules/devtools/src/blitz/panels/types.ts +120 -0
- package/modules/devtools/src/blitz/theme.ts +168 -0
- package/modules/devtools/src/data-bridge.ts +1 -1
- package/modules/devtools/src/index.ts +6 -0
- package/modules/devtools/src/profiling-bridge.ts +17 -13
- package/modules/devtools/src/profiling-hooks.ts +22 -13
- package/modules/devtools/src/scene-inspector.ts +1 -1
- package/modules/devtools/src/worker-sync.ts +9 -1
- package/modules/editor/src/commands/registry.ts +2 -2
- package/modules/editor/src/module.ts +1 -1
- package/modules/html-ui/src/composite.ts +1 -1
- package/modules/html-ui/src/create-html-ui.ts +21 -9
- package/modules/html-ui/src/doc-backend.ts +189 -10
- package/modules/html-ui/src/host.ts +354 -40
- package/modules/html-ui/src/index.ts +1 -1
- package/modules/html-ui/src/jsx-runtime.ts +1 -1
- package/modules/html-ui/src/protocol.ts +53 -6
- package/modules/mcp/README.md +6 -0
- package/modules/mcp/src/index.ts +70 -3
- package/modules/movement-3d/src/character-motor.ts +83 -10
- package/package.json +838 -165
- package/shader-graph/README.md +25 -0
- package/test/README.md +16 -0
- package/libraries/blitz-ui/native-osr-spike.ts +0 -70
- package/libraries/devtools/native/Cargo.toml +0 -12
- package/libraries/devtools/native/src/lib.rs +0 -1646
- package/libraries/devtools/native/src/panels/console.rs +0 -338
- package/libraries/devtools/native/src/panels/dom_tree.rs +0 -47
- package/libraries/devtools/native/src/panels/generic.rs +0 -317
- package/libraries/devtools/native/src/panels/gpu.rs +0 -107
- package/libraries/devtools/native/src/panels/input.rs +0 -88
- package/libraries/devtools/native/src/panels/mod.rs +0 -154
- package/libraries/devtools/native/src/panels/perf_metrics.rs +0 -91
- package/libraries/devtools/native/src/panels/perf_recorder.rs +0 -158
- package/libraries/devtools/native/src/panels/scene.rs +0 -72
- package/libraries/devtools/native/src/state.rs +0 -394
- package/libraries/devtools/src/egui-ffi.ts +0 -804
- package/libraries/devtools/src/egui-renderer.ts +0 -407
- package/libraries/devtools/src/host.ts +0 -363
- package/libraries/devtools/src/mirror.ts +0 -794
- package/libraries/models/test-fixtures/fbx/LICENSE-notice.txt +0 -24
- package/libraries/models/test-fixtures/fbx/README.md +0 -46
- package/libraries/models/test-fixtures/fbx/m01_cube_phong.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m02_geo_ngons.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m03_normals.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m04_material_properties.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m05_external_diffuse.png +0 -0
- package/libraries/models/test-fixtures/fbx/m05_textures.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m06_uv_transform.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m07_multimaterial.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m08_transforms.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m09_skinning.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m10_morph.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m11_node_anim.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m12_skeletal_anim.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m13_morph_anim.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m14_multiclip.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m15_camera_lights.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m16_axis_yup.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m16_axis_zup.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m16_units_254.fbx +0 -0
- package/libraries/physics-native/src/ffi-spike-worker.ts +0 -71
- package/libraries/physics-rapier/bench/ffi-bench.ts +0 -79
- package/libraries/physics-rapier/bench/wasm-baseline.ts +0 -90
|
@@ -0,0 +1,3382 @@
|
|
|
1
|
+
import { FLAG_ANCHORED, FLAG_POPPED, FLAG_SPARK, FLAG_UPDATED, FLAG_UPDATED_BIT, pack, packCell, SHADE_MASK, unpack } from "./cell.js";
|
|
2
|
+
import { DEFAULT_GRAVITY, DEFAULT_TEMP, FIELD } from "./fields.js";
|
|
3
|
+
import { FluidGrid } from "./fluid-grid.js";
|
|
4
|
+
import { IS_ACID_IMMUNE, IS_FIRE, IS_HOT, MAT_DENSITY, MAT_FLAGS, MAT_FLAMMABLE, MAT_GAS, MAT_GRAVITY, MAT_GRAVITY_DIR, MAT_HAS_REACTIONS, MAT_LIFETIME, MAT_LIQUID, MAT_SOLID, Material } from "./materials.js";
|
|
5
|
+
import { PARTICLE_TYPES, ParticleSystem } from "./particles.js";
|
|
6
|
+
import { SandRNG } from "./rng.js";
|
|
7
|
+
import { RuleEngine } from "./rules/rule-engine.js";
|
|
8
|
+
export { MAT_GRAVITY, MAT_GRAVITY_DIR } from "./materials.js";
|
|
9
|
+
function initialLifetime(mat) {
|
|
10
|
+
return MAT_LIFETIME[mat];
|
|
11
|
+
}
|
|
12
|
+
/** Ticks a Gravel cell must remain stationary (and stably supported) before
|
|
13
|
+
* re-settling to Stone. Long so mined debris stays loose and collectible for
|
|
14
|
+
* a while instead of visibly snapping back to solid stone within a second.
|
|
15
|
+
* At 60Hz, 600 ticks ≈ 10 seconds. */
|
|
16
|
+
export const GRAVEL_SETTLE_TICKS = 600;
|
|
17
|
+
/** Settle ticks used when a Gravel/Stone is disturbed by adjacent gravel
|
|
18
|
+
* movement. Short so the cell re-settles quickly if still supported, but
|
|
19
|
+
* gives it time to start falling if gravel flowed out from under it. */
|
|
20
|
+
export const GRAVEL_DISTURB_SETTLE_TICKS = 2;
|
|
21
|
+
// 4-neighbor offsets for disturbAdjacent
|
|
22
|
+
const DIR_DX = [0, 0, -1, 1];
|
|
23
|
+
const DIR_DY = [-1, 1, 0, 0];
|
|
24
|
+
export class SandWorld {
|
|
25
|
+
W;
|
|
26
|
+
H;
|
|
27
|
+
grid;
|
|
28
|
+
// Per-cell physics fields: 4 bytes per cell [gravity:u8, temp:u8, windX:i8, windY:i8]
|
|
29
|
+
fields;
|
|
30
|
+
frame = 0;
|
|
31
|
+
// --- Strip mode (multi-threaded sand step) ---
|
|
32
|
+
// When strip mode is enabled, this SandWorld only processes cells in
|
|
33
|
+
// [stripStartX, stripEndX). It can READ the full grid (backed by SAB) but
|
|
34
|
+
// only WRITES to cells in [writeXMin, writeXMax). Cross-strip writes are
|
|
35
|
+
// skipped (trySwap/tryShove return false), and the coordinator's boundary
|
|
36
|
+
// cleanup pass handles them after all workers finish.
|
|
37
|
+
// Defaults: full grid (no restriction) — zero overhead when strip mode is off.
|
|
38
|
+
stripStartX = 0;
|
|
39
|
+
stripEndX = 0; // set to W in constructor
|
|
40
|
+
writeXMin = 0;
|
|
41
|
+
writeXMax = 0; // set to W in constructor
|
|
42
|
+
// Fast xorshift32 PRNG — replaces Math.random() in hot loops.
|
|
43
|
+
rng;
|
|
44
|
+
// Data-driven rule engine — replaces hardcoded if-else in applyReactions().
|
|
45
|
+
ruleEngine;
|
|
46
|
+
// Particle system for explosion visuals (flying debris, blast rings).
|
|
47
|
+
particles;
|
|
48
|
+
// Global impulse settings (not spatial)
|
|
49
|
+
horizontalImpulseChance = 0.02;
|
|
50
|
+
horizontalImpulseStrength = 1;
|
|
51
|
+
// Interlace mode: process every Nth row per frame (halves movement cost
|
|
52
|
+
// for low-end devices). The row offset alternates each frame so all rows
|
|
53
|
+
// are processed over N frames. Default 1 = disabled (process all rows).
|
|
54
|
+
interlaceScale = 1;
|
|
55
|
+
interlaceEnabled = false;
|
|
56
|
+
// Additional flag bits (in the flags byte, bits 16-23) that consumers want
|
|
57
|
+
// preserved across the per-frame FLAG_UPDATED clear in buildActiveListAndClearFlags.
|
|
58
|
+
// The physics engine only uses bits 0-3 and 5 (shade 0-1, FLAG_UPDATED 2,
|
|
59
|
+
// FLAG_SPARK 3, FLAG_POPPED 5); bits 4, 6-7 are available for game-specific
|
|
60
|
+
// flags (e.g. a game's FLAG_DETACHED at bit 4).
|
|
61
|
+
// Set this mask so those bits survive the clear. Defaults to 0 (no extra bits).
|
|
62
|
+
preserveFlagsMask = 0;
|
|
63
|
+
// Flag bits OR'd into the flags of cells disturbed by disturbAdjacent()
|
|
64
|
+
// (Stone→LooseStone conversions when gravel flows out from under them).
|
|
65
|
+
// Disturbed cells also get FLAG_UPDATED, but applyAging clears FLAG_UPDATED
|
|
66
|
+
// before the chunk-world's expireWakeTicks() can detect them in frozen
|
|
67
|
+
// chunks. Setting a persistent flag here (e.g. a game's FLAG_DETACHED)
|
|
68
|
+
// ensures expireWakeTicks can re-activate frozen chunks that contain
|
|
69
|
+
// disturbed cells. Defaults to 0 (no extra flags). Must be a subset of
|
|
70
|
+
// preserveFlagsMask so the flag survives the per-frame clear.
|
|
71
|
+
disturbedFlags = 0;
|
|
72
|
+
// --- Reusable per-frame buffers (avoid allocations in hot paths) ---
|
|
73
|
+
// fireSources: marks cells that are fire/lava at the start of applyCombustion.
|
|
74
|
+
// visitedFrame: frame-tagged marker used in place of per-frame Sets for the
|
|
75
|
+
// ignitedFuse / ignitedOil de-duplication. A cell is "visited this frame"
|
|
76
|
+
// when visitedFrame[i] === this.frame. Shared between the fuse and
|
|
77
|
+
// burning-oil passes — safe because the two passes target different
|
|
78
|
+
// materials (Fuse vs Oil), so a cell marked in one pass is guarded out by
|
|
79
|
+
// the material-type check in the other before the visited check runs.
|
|
80
|
+
fireSources;
|
|
81
|
+
visitedFrame;
|
|
82
|
+
// Separate frame-tagged visited array for C4 flood-fill (detonateC4).
|
|
83
|
+
// Avoids clobbering the combustion pass's visitedFrame and avoids
|
|
84
|
+
// allocating a Set on every detonation.
|
|
85
|
+
visitedC4Frame;
|
|
86
|
+
// Separate frame-tagged visited array for antimatter annihilation flood-fill.
|
|
87
|
+
// Avoids clobbering the C4 / combustion visited arrays.
|
|
88
|
+
visitedAntimatterFrame;
|
|
89
|
+
// --- Sparse active-cell tracking ---
|
|
90
|
+
// activeCells holds the grid indices of all non-empty cells. activeCount is
|
|
91
|
+
// the number of valid entries. Rebuilt each frame (twice — once at the start
|
|
92
|
+
// to replace the FLAG_UPDATED clear scan, once before applyAging to capture
|
|
93
|
+
// cells created by combustion/reactions). Iterating the active list is
|
|
94
|
+
// O(active) instead of O(W*H) — a massive win when the grid is mostly empty.
|
|
95
|
+
activeCells;
|
|
96
|
+
activeCount = 0;
|
|
97
|
+
// --- Active Y bounds ---
|
|
98
|
+
// min/max rows that contain non-empty cells. The movement pass only
|
|
99
|
+
// iterates [minActiveY, maxActiveY] instead of [0, H-1]. For a sparse
|
|
100
|
+
// grid (e.g. a 50-cell-tall sand pile in a 512×512 world), this cuts
|
|
101
|
+
// movement from O(W*H) to O(W*activeHeight).
|
|
102
|
+
minActiveY = 0;
|
|
103
|
+
maxActiveY = 0;
|
|
104
|
+
// --- Chunk dirty bitmap ---
|
|
105
|
+
// 1 bit per CHUNK_SIZE×CHUNK_SIZE block. Set when a cell in that chunk
|
|
106
|
+
// changes. The movement pass skips chunks that aren't dirty, avoiding
|
|
107
|
+
// scanning large empty regions within the active Y bounds.
|
|
108
|
+
static CHUNK_SIZE = 16;
|
|
109
|
+
numChunksX = 0;
|
|
110
|
+
numChunksY = 0;
|
|
111
|
+
chunkDirty;
|
|
112
|
+
// --- Coarse-grid fluid simulation ---
|
|
113
|
+
// Replaces the old per-cell wind field system. Provides pressure relaxation
|
|
114
|
+
// and sustained airflow at 1/4 resolution. Per-cell wind is sampled from
|
|
115
|
+
// this grid via bilinear interpolation in tryMove().
|
|
116
|
+
fluid;
|
|
117
|
+
// --- Chunk freeze support ---
|
|
118
|
+
// Optional per-cell skip mask (length = W*H). When set, cells whose skipMask
|
|
119
|
+
// entry is non-zero are excluded from the active-cell list and the movement
|
|
120
|
+
// pass — they are "frozen" and do not participate in the simulation this
|
|
121
|
+
// frame. Used by the game's chunk world to skip inactive (frozen) chunks.
|
|
122
|
+
// null = no skipping (backward compatible with the original single-grid sim).
|
|
123
|
+
skipMask = null;
|
|
124
|
+
// Per-column active cell count — SAB-backed when multi-threaded.
|
|
125
|
+
// Written by each worker during buildActiveListAndClearFlags (only its own
|
|
126
|
+
// strip's columns). Read by the coordinator after all workers finish to
|
|
127
|
+
// rebalance strip boundaries for load balancing.
|
|
128
|
+
histogram = null;
|
|
129
|
+
// Per-cell "deferred move" marker (length = W*H, SAB-backed when
|
|
130
|
+
// multi-threaded). Set by movement functions (trySwap/tryFlow/etc.) when a
|
|
131
|
+
// move is blocked SOLELY by the strip write guard — i.e. the destination is
|
|
132
|
+
// in an adjacent strip and the move would have been valid if not for the
|
|
133
|
+
// guard. The coordinator's boundary cleanup pass reads this to know which
|
|
134
|
+
// cells need a cross-strip move attempt, and clears it after processing.
|
|
135
|
+
//
|
|
136
|
+
// Without this, the boundary cleanup would re-run the FULL tryMove on every
|
|
137
|
+
// boundary cell — re-rolling friction/flicker/impulse RNG gates and giving
|
|
138
|
+
// boundary cells a second movement chance that interior cells don't get.
|
|
139
|
+
// This caused "fast falling" artifacts at strip boundaries: sand in boundary
|
|
140
|
+
// columns fell ~1.3× faster because edge friction (30% skip/frame) was
|
|
141
|
+
// effectively rolled twice (9% skip).
|
|
142
|
+
//
|
|
143
|
+
// null = no deferred tracking (single-threaded / non-SAB mode — no boundary
|
|
144
|
+
// cleanup needed).
|
|
145
|
+
deferredMask = null;
|
|
146
|
+
// When true, tryMove is running in the boundary cleanup pass. This skips all
|
|
147
|
+
// RNG-based movement gates (friction, gas flicker, horizontal impulse, wind)
|
|
148
|
+
// — those already had their chance during the worker pass — and only
|
|
149
|
+
// processes cells marked in deferredMask. Write bounds are restricted to the
|
|
150
|
+
// adjacent strip so only cross-strip moves are attempted.
|
|
151
|
+
boundaryPass = false;
|
|
152
|
+
constructor(w, h, options) {
|
|
153
|
+
this.W = w;
|
|
154
|
+
this.H = h;
|
|
155
|
+
const cells = w * h;
|
|
156
|
+
this.rng = new SandRNG();
|
|
157
|
+
this.ruleEngine = new RuleEngine();
|
|
158
|
+
this.particles = new ParticleSystem(512);
|
|
159
|
+
this.fluid = new FluidGrid(w, h);
|
|
160
|
+
// Grid + fields can be backed by a SharedArrayBuffer for multi-threaded
|
|
161
|
+
// sand step. When SAB is provided, all workers share the same grid data.
|
|
162
|
+
// Otherwise, regular ArrayBuffer (single-threaded, tests, backward compat).
|
|
163
|
+
if (options?.sab) {
|
|
164
|
+
const gridBytes = cells * 4;
|
|
165
|
+
this.grid = new Uint32Array(options.sab, options.gridOffset ?? 0, cells);
|
|
166
|
+
this.fields = new Uint8Array(options.sab, options.fieldsOffset ?? gridBytes, cells * 4);
|
|
167
|
+
// Optional SAB-backed skip mask — shared between coordinator and workers.
|
|
168
|
+
if (options.skipMaskOffset !== undefined) {
|
|
169
|
+
this.skipMask = new Uint8Array(options.sab, options.skipMaskOffset, cells);
|
|
170
|
+
}
|
|
171
|
+
// Optional SAB-backed deferred-move mask — shared between workers and
|
|
172
|
+
// the coordinator. Each worker writes only to its own strip's cells
|
|
173
|
+
// (the source cell of a blocked move is always within the strip), so
|
|
174
|
+
// there are no races. The coordinator reads it in the boundary cleanup.
|
|
175
|
+
if (options.deferredMaskOffset !== undefined) {
|
|
176
|
+
this.deferredMask = new Uint8Array(options.sab, options.deferredMaskOffset, cells);
|
|
177
|
+
}
|
|
178
|
+
// Optional SAB-backed per-column active cell histogram — used by the
|
|
179
|
+
// coordinator to rebalance strip boundaries for load balancing.
|
|
180
|
+
// Each worker writes only to its own strip's columns (no races).
|
|
181
|
+
if (options.histogramOffset !== undefined) {
|
|
182
|
+
this.histogram = new Uint32Array(options.sab, options.histogramOffset, w);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
this.grid = new Uint32Array(cells);
|
|
187
|
+
this.fields = new Uint8Array(cells * 4);
|
|
188
|
+
}
|
|
189
|
+
// Strip mode defaults: full grid (no restriction). Set by SandStepPool
|
|
190
|
+
// when creating per-worker SandWorld instances.
|
|
191
|
+
this.stripStartX = 0;
|
|
192
|
+
this.stripEndX = w;
|
|
193
|
+
this.writeXMin = 0;
|
|
194
|
+
this.writeXMax = w;
|
|
195
|
+
// Per-worker local arrays (not shared — each worker has its own)
|
|
196
|
+
this.fireSources = new Uint8Array(cells);
|
|
197
|
+
this.visitedFrame = new Uint32Array(cells);
|
|
198
|
+
this.visitedC4Frame = new Uint32Array(cells);
|
|
199
|
+
this.visitedAntimatterFrame = new Uint32Array(cells);
|
|
200
|
+
this.activeCells = new Uint32Array(cells);
|
|
201
|
+
this.activeCount = 0;
|
|
202
|
+
// Chunk dirty bitmap — 1 byte per chunk (not bit-packed for simplicity).
|
|
203
|
+
this.numChunksX = Math.ceil(w / SandWorld.CHUNK_SIZE);
|
|
204
|
+
this.numChunksY = Math.ceil(h / SandWorld.CHUNK_SIZE);
|
|
205
|
+
this.chunkDirty = new Uint8Array(this.numChunksX * this.numChunksY);
|
|
206
|
+
// Initialize fields to defaults
|
|
207
|
+
for (let i = 0; i < cells * 4; i += 4) {
|
|
208
|
+
this.fields[i + FIELD.GRAVITY] = DEFAULT_GRAVITY;
|
|
209
|
+
this.fields[i + FIELD.TEMP] = DEFAULT_TEMP;
|
|
210
|
+
}
|
|
211
|
+
if (!options?.skipStoneFloor) {
|
|
212
|
+
// Stone floor
|
|
213
|
+
for (let x = 0; x < w; x++) {
|
|
214
|
+
for (let y = h - 4; y < h; y++) {
|
|
215
|
+
this.grid[y * w + x] = pack({ mat: Material.Stone, lifetime: 0, flags: 0 });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Mark the stone floor chunks as dirty so the first movement pass processes them.
|
|
219
|
+
this.minActiveY = h - 4;
|
|
220
|
+
this.maxActiveY = h - 1;
|
|
221
|
+
for (let cy = 0; cy < this.numChunksY; cy++) {
|
|
222
|
+
for (let cx = 0; cx < this.numChunksX; cx++) {
|
|
223
|
+
this.chunkDirty[cy * this.numChunksX + cx] = 1;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Configure strip mode for multi-threaded sand step. */
|
|
229
|
+
setStripBounds(stripStartX, stripEndX) {
|
|
230
|
+
this.stripStartX = stripStartX;
|
|
231
|
+
this.stripEndX = stripEndX;
|
|
232
|
+
this.writeXMin = stripStartX;
|
|
233
|
+
this.writeXMax = stripEndX;
|
|
234
|
+
}
|
|
235
|
+
/** Check if a cell at X coordinate can be written (within write bounds). */
|
|
236
|
+
canWriteX(x) {
|
|
237
|
+
return x >= this.writeXMin && x < this.writeXMax;
|
|
238
|
+
}
|
|
239
|
+
/** Check if a grid index can be written (within strip write bounds). */
|
|
240
|
+
canWriteIdx(idx) {
|
|
241
|
+
const x = idx % this.W;
|
|
242
|
+
return x >= this.writeXMin && x < this.writeXMax;
|
|
243
|
+
}
|
|
244
|
+
/** Get the active Y bounds [minY, maxY] — used by SandStepPool for boundary cleanup. */
|
|
245
|
+
getActiveYBounds() {
|
|
246
|
+
return { minY: this.minActiveY, maxY: this.maxActiveY };
|
|
247
|
+
}
|
|
248
|
+
/** Re-seed the PRNG for deterministic test mode. */
|
|
249
|
+
reseed(seed) {
|
|
250
|
+
this.rng.reseed(seed);
|
|
251
|
+
}
|
|
252
|
+
/** PRNG accessor for the rule engine. */
|
|
253
|
+
getRng() {
|
|
254
|
+
return this.rng;
|
|
255
|
+
}
|
|
256
|
+
/** Active cell list accessor for the rule engine. */
|
|
257
|
+
getActiveCells() {
|
|
258
|
+
return this.activeCells;
|
|
259
|
+
}
|
|
260
|
+
/** Active cell count accessor for the rule engine. */
|
|
261
|
+
getActiveCount() {
|
|
262
|
+
return this.activeCount;
|
|
263
|
+
}
|
|
264
|
+
/** Particle system accessor for the worker to transfer to SAB. */
|
|
265
|
+
getParticles() {
|
|
266
|
+
return this.particles;
|
|
267
|
+
}
|
|
268
|
+
/** Mark the chunk containing (x, y) as dirty. Called on every cell write. */
|
|
269
|
+
markChunkDirty(x, y) {
|
|
270
|
+
const cx = (x / SandWorld.CHUNK_SIZE) | 0;
|
|
271
|
+
const cy = (y / SandWorld.CHUNK_SIZE) | 0;
|
|
272
|
+
this.chunkDirty[cy * this.numChunksX + cx] = 1;
|
|
273
|
+
// Update Y bounds — the movement pass only scans [minActiveY, maxActiveY].
|
|
274
|
+
if (y < this.minActiveY)
|
|
275
|
+
this.minActiveY = y;
|
|
276
|
+
if (y > this.maxActiveY)
|
|
277
|
+
this.maxActiveY = y;
|
|
278
|
+
}
|
|
279
|
+
// --- Field accessors ---
|
|
280
|
+
// gravity: u8 0-255, 128 = 1.0×. Returns multiplier 0-2.
|
|
281
|
+
getGravity(x, y) {
|
|
282
|
+
if (x < 0 || x >= this.W || y < 0 || y >= this.H)
|
|
283
|
+
return 1.0;
|
|
284
|
+
return this.fields[(y * this.W + x) * 4 + FIELD.GRAVITY] / 128;
|
|
285
|
+
}
|
|
286
|
+
// temp: u8 0-255, 128 = 1.0. Returns 0-2.
|
|
287
|
+
getTemperature(x, y) {
|
|
288
|
+
if (x < 0 || x >= this.W || y < 0 || y >= this.H)
|
|
289
|
+
return 1.0;
|
|
290
|
+
return this.fields[(y * this.W + x) * 4 + FIELD.TEMP] / 128;
|
|
291
|
+
}
|
|
292
|
+
// Wind velocity from the coarse-grid fluid simulation. Returns cells/frame.
|
|
293
|
+
getWindX(x, y) {
|
|
294
|
+
if (x < 0 || x >= this.W || y < 0 || y >= this.H)
|
|
295
|
+
return 0;
|
|
296
|
+
return this.fluid.sampleVelX(x, y);
|
|
297
|
+
}
|
|
298
|
+
getWindY(x, y) {
|
|
299
|
+
if (x < 0 || x >= this.W || y < 0 || y >= this.H)
|
|
300
|
+
return 0;
|
|
301
|
+
return this.fluid.sampleVelY(x, y);
|
|
302
|
+
}
|
|
303
|
+
// --- Field painting ---
|
|
304
|
+
paintField(cx, cy, fieldType, value, radius) {
|
|
305
|
+
for (let y = cy - radius; y <= cy + radius; y++) {
|
|
306
|
+
for (let x = cx - radius; x <= cx + radius; x++) {
|
|
307
|
+
if ((x - cx) * (x - cx) + (y - cy) * (y - cy) > radius * radius)
|
|
308
|
+
continue;
|
|
309
|
+
if (x < 0 || x >= this.W || y < 0 || y >= this.H)
|
|
310
|
+
continue;
|
|
311
|
+
this.fields[(y * this.W + x) * 4 + fieldType] = value & 0xff;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
paintFieldLine(x0, y0, x1, y1, fieldType, value, radius) {
|
|
316
|
+
const dx = Math.abs(x1 - x0);
|
|
317
|
+
const dy = Math.abs(y1 - y0);
|
|
318
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
319
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
320
|
+
let err = dx - dy;
|
|
321
|
+
let x = x0, y = y0;
|
|
322
|
+
while (true) {
|
|
323
|
+
this.paintField(x, y, fieldType, value, radius);
|
|
324
|
+
if (x === x1 && y === y1)
|
|
325
|
+
break;
|
|
326
|
+
const e2 = 2 * err;
|
|
327
|
+
if (e2 > -dy) {
|
|
328
|
+
err -= dy;
|
|
329
|
+
x += sx;
|
|
330
|
+
}
|
|
331
|
+
if (e2 < dx) {
|
|
332
|
+
err += dx;
|
|
333
|
+
y += sy;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
getCell(x, y) {
|
|
338
|
+
if (x < 0 || x >= this.W || y < 0 || y >= this.H)
|
|
339
|
+
return { mat: Material.Empty, lifetime: 0, flags: 0 };
|
|
340
|
+
return unpack(this.grid[y * this.W + x]);
|
|
341
|
+
}
|
|
342
|
+
setCell(x, y, cell) {
|
|
343
|
+
if (x < 0 || x >= this.W || y < 0 || y >= this.H)
|
|
344
|
+
return;
|
|
345
|
+
this.grid[y * this.W + x] = pack(cell);
|
|
346
|
+
}
|
|
347
|
+
paintMaterial(cx, cy, mat, radius) {
|
|
348
|
+
const W = this.W, H = this.H;
|
|
349
|
+
const grid = this.grid;
|
|
350
|
+
const r2 = radius * radius;
|
|
351
|
+
const lt = initialLifetime(mat);
|
|
352
|
+
for (let y = cy - radius; y <= cy + radius; y++) {
|
|
353
|
+
if (y < 0 || y >= H)
|
|
354
|
+
continue;
|
|
355
|
+
for (let x = cx - radius; x <= cx + radius; x++) {
|
|
356
|
+
if (x < 0 || x >= W)
|
|
357
|
+
continue;
|
|
358
|
+
const ddx = x - cx, ddy = y - cy;
|
|
359
|
+
if (ddx * ddx + ddy * ddy > r2)
|
|
360
|
+
continue;
|
|
361
|
+
const curMat = grid[y * W + x] & 0xff;
|
|
362
|
+
if (curMat === Material.Stone || curMat === Material.Wall)
|
|
363
|
+
continue;
|
|
364
|
+
grid[y * W + x] = packCell(mat, lt, this.rng.randomShade());
|
|
365
|
+
this.markChunkDirty(x, y);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
paintLine(x0, y0, x1, y1, mat, radius) {
|
|
370
|
+
const dx = Math.abs(x1 - x0);
|
|
371
|
+
const dy = Math.abs(y1 - y0);
|
|
372
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
373
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
374
|
+
let err = dx - dy;
|
|
375
|
+
let x = x0, y = y0;
|
|
376
|
+
while (true) {
|
|
377
|
+
this.paintMaterial(x, y, mat, radius);
|
|
378
|
+
if (x === x1 && y === y1)
|
|
379
|
+
break;
|
|
380
|
+
const e2 = 2 * err;
|
|
381
|
+
if (e2 > -dy) {
|
|
382
|
+
err -= dy;
|
|
383
|
+
x += sx;
|
|
384
|
+
}
|
|
385
|
+
if (e2 < dx) {
|
|
386
|
+
err += dx;
|
|
387
|
+
y += sy;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
igniteLine(x0, y0, x1, y1, radius) {
|
|
392
|
+
const dx = Math.abs(x1 - x0);
|
|
393
|
+
const dy = Math.abs(y1 - y0);
|
|
394
|
+
const sx = x0 < x1 ? 1 : -1;
|
|
395
|
+
const sy = y0 < y1 ? 1 : -1;
|
|
396
|
+
let err = dx - dy;
|
|
397
|
+
let x = x0, y = y0;
|
|
398
|
+
while (true) {
|
|
399
|
+
this.ignite(x, y, radius);
|
|
400
|
+
if (x === x1 && y === y1)
|
|
401
|
+
break;
|
|
402
|
+
const e2 = 2 * err;
|
|
403
|
+
if (e2 > -dy) {
|
|
404
|
+
err -= dy;
|
|
405
|
+
x += sx;
|
|
406
|
+
}
|
|
407
|
+
if (e2 < dx) {
|
|
408
|
+
err += dx;
|
|
409
|
+
y += sy;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
ignite(cx, cy, radius) {
|
|
414
|
+
const W = this.W, H = this.H;
|
|
415
|
+
const grid = this.grid;
|
|
416
|
+
const r2 = radius * radius;
|
|
417
|
+
for (let y = cy - radius; y <= cy + radius; y++) {
|
|
418
|
+
if (y < 0 || y >= H)
|
|
419
|
+
continue;
|
|
420
|
+
for (let x = cx - radius; x <= cx + radius; x++) {
|
|
421
|
+
if (x < 0 || x >= W)
|
|
422
|
+
continue;
|
|
423
|
+
const ddx = x - cx, ddy = y - cy;
|
|
424
|
+
if (ddx * ddx + ddy * ddy > r2)
|
|
425
|
+
continue;
|
|
426
|
+
const mat = grid[y * W + x] & 0xff;
|
|
427
|
+
if (!(MAT_FLAGS[mat] & MAT_FLAMMABLE))
|
|
428
|
+
continue;
|
|
429
|
+
// Fuse gets FuseFire (yellow, stays put, deterministic spread)
|
|
430
|
+
if (mat === Material.Fuse) {
|
|
431
|
+
grid[y * W + x] = packCell(Material.FuseFire, 15, this.rng.randomShade());
|
|
432
|
+
}
|
|
433
|
+
else if (mat === Material.Oil) {
|
|
434
|
+
// Oil only ignites if exposed (has an empty/gas neighbor so fire
|
|
435
|
+
// can reach it). Buried oil stays inert.
|
|
436
|
+
if (!this.isExposed(x, y))
|
|
437
|
+
continue;
|
|
438
|
+
// Oil → BurningOil (flows like oil, slow decay, slow spread)
|
|
439
|
+
grid[y * W + x] = packCell(Material.BurningOil, MAT_LIFETIME[Material.BurningOil], this.rng.randomShade());
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
grid[y * W + x] = packCell(Material.Fire, 30, this.rng.randomShade());
|
|
443
|
+
}
|
|
444
|
+
this.markChunkDirty(x, y);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// ===========================================================================
|
|
449
|
+
// Main step
|
|
450
|
+
// ===========================================================================
|
|
451
|
+
step() {
|
|
452
|
+
this.frame++;
|
|
453
|
+
const W = this.W, H = this.H;
|
|
454
|
+
// Pass 1: Clear FLAG_UPDATED (preserving shade + spark bits) AND build the
|
|
455
|
+
// active-cell list in a single full-grid scan. This replaces the old
|
|
456
|
+
// separate FLAG_UPDATED clear loop — the scan is very cheap (bit-and +
|
|
457
|
+
// comparison per cell, no unpack or MATERIALS lookup).
|
|
458
|
+
this.buildActiveListAndClearFlags();
|
|
459
|
+
// Pass 2-3: Reactions iterate only the active list (O(active) instead of
|
|
460
|
+
// O(W*H)). Cells created during these passes won't be in the active list —
|
|
461
|
+
// same semantics as the old full-grid scan (one pass per frame).
|
|
462
|
+
// Pass 2: Data-driven rule engine (replaces the old applyReactions()).
|
|
463
|
+
this.ruleEngine.execute(this);
|
|
464
|
+
// Pass 3: Special reactions (complex logic not yet migrated to rules).
|
|
465
|
+
this.applySpecialReactions();
|
|
466
|
+
// Pass 4: Movement — must iterate in spatial order (bottom-to-top,
|
|
467
|
+
// alternating L/R) for correct falling-sand physics. We skip empty rows
|
|
468
|
+
// outside [minActiveY, maxActiveY] and empty chunks within those bounds.
|
|
469
|
+
// In interlace mode, we process every Nth row (offset by frame % N) to
|
|
470
|
+
// halve movement cost on low-end devices.
|
|
471
|
+
// Strip mode: iterate only [stripStartX, stripEndX) in the X dimension.
|
|
472
|
+
const leftToRight = this.frame % 2 === 0;
|
|
473
|
+
const chunkSize = SandWorld.CHUNK_SIZE;
|
|
474
|
+
const numChunksX = this.numChunksX;
|
|
475
|
+
const chunkDirty = this.chunkDirty;
|
|
476
|
+
const interlace = this.interlaceEnabled ? this.interlaceScale : 1;
|
|
477
|
+
const interlaceOffset = this.frame % interlace;
|
|
478
|
+
const moveX0 = this.stripStartX;
|
|
479
|
+
const moveX1 = this.stripEndX;
|
|
480
|
+
for (let y = this.maxActiveY; y >= this.minActiveY; y--) {
|
|
481
|
+
// Interlace: skip rows not in this frame's subset
|
|
482
|
+
if (interlace > 1 && (y % interlace) !== interlaceOffset)
|
|
483
|
+
continue;
|
|
484
|
+
// Skip entire rows that are in non-dirty chunks.
|
|
485
|
+
const chunkY = (y / chunkSize) | 0;
|
|
486
|
+
const rowBase = chunkY * numChunksX;
|
|
487
|
+
let rowHasDirty = false;
|
|
488
|
+
for (let cx = 0; cx < numChunksX; cx++) {
|
|
489
|
+
if (chunkDirty[rowBase + cx]) {
|
|
490
|
+
rowHasDirty = true;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!rowHasDirty)
|
|
495
|
+
continue;
|
|
496
|
+
if (leftToRight) {
|
|
497
|
+
for (let x = moveX0; x < moveX1; x++) {
|
|
498
|
+
if (!chunkDirty[rowBase + ((x / chunkSize) | 0)])
|
|
499
|
+
continue;
|
|
500
|
+
this.tryMove(x, y);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
for (let x = moveX1 - 1; x >= moveX0; x--) {
|
|
505
|
+
if (!chunkDirty[rowBase + ((x / chunkSize) | 0)])
|
|
506
|
+
continue;
|
|
507
|
+
this.tryMove(x, y);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Pass 5: Combustion — builds fireSources from the active list.
|
|
512
|
+
this.applyCombustion();
|
|
513
|
+
// Rebuild active list to capture cells created/destroyed by reactions,
|
|
514
|
+
// movement, and combustion. Uses the Y bounds from Pass 1 (expanded by a
|
|
515
|
+
// margin) to avoid a full O(W*H) grid scan — cells can only move a few
|
|
516
|
+
// positions per frame, so the active Y range can't expand beyond that.
|
|
517
|
+
this.buildActiveList();
|
|
518
|
+
// Pass 6: Aging — iterates the rebuilt active list.
|
|
519
|
+
this.applyAging();
|
|
520
|
+
// Pass 7: Fluid grid step — pressure relaxation + velocity update.
|
|
521
|
+
// Skipped entirely when no impulses/pressure exist (dirty flag).
|
|
522
|
+
this.fluid.step();
|
|
523
|
+
// Pass 8: Update explosion particles.
|
|
524
|
+
this.particles.update();
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Boundary cleanup movement pass — used by SandStepPool after all strip
|
|
528
|
+
* workers finish. Re-runs tryMove on the specified columns so cross-strip
|
|
529
|
+
* writes (which were skipped by write guards during the parallel step) get
|
|
530
|
+
* a chance to execute.
|
|
531
|
+
*
|
|
532
|
+
* When boundaryPass is true on this world, tryMove only processes cells
|
|
533
|
+
* marked in deferredMask (cells whose cross-strip move was blocked by the
|
|
534
|
+
* write guard) and skips all RNG-based movement gates. The caller must set
|
|
535
|
+
* writeXMin/writeXMax to the adjacent strip's bounds so only cross-strip
|
|
536
|
+
* moves are attempted.
|
|
537
|
+
*
|
|
538
|
+
* This is a movement-only pass — reactions, combustion, and aging already
|
|
539
|
+
* ran in the workers. We only handle the cross-strip movement writes that
|
|
540
|
+
* were skipped at the boundaries.
|
|
541
|
+
*/
|
|
542
|
+
runBoundaryMovement(cols, minY, maxY, leftToRight) {
|
|
543
|
+
// Sort columns in the iteration order matching the main movement pass.
|
|
544
|
+
const sorted = cols.slice().sort((a, b) => leftToRight ? a - b : b - a);
|
|
545
|
+
for (let y = maxY; y >= minY; y--) {
|
|
546
|
+
sorted.forEach((x) => {
|
|
547
|
+
this.tryMove(x, y);
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Build the active-cell list (indices of all non-empty cells) and clear
|
|
553
|
+
* FLAG_UPDATED in a single pass. Merges the old separate FLAG_UPDATED clear
|
|
554
|
+
* loop with active-list construction. Also detects non-zero wind fields so
|
|
555
|
+
* that externally-set wind (e.g. by tests or future field-painting code) is
|
|
556
|
+
* detected even when applyImpulse wasn't called.
|
|
557
|
+
*
|
|
558
|
+
* Static solids (MAT_GRAVITY_DIR === 0: Stone, Wall, Dirt, etc.) are
|
|
559
|
+
* excluded from the active list and chunkDirty bitmap. They can never move,
|
|
560
|
+
* have no reactions, and no lifetimes — including them bloats the active
|
|
561
|
+
* list to ~300k cells in a terrain-heavy world, making every active-list
|
|
562
|
+
* iteration (ruleEngine, applySpecialReactions, applyAging, applyCombustion)
|
|
563
|
+
* 5-10× slower. The tryMove loop also benefits: chunks with only static
|
|
564
|
+
* solids aren't marked dirty, so the loop skips them entirely instead of
|
|
565
|
+
* visiting every cell just to early-return at gravityDir === 0.
|
|
566
|
+
*
|
|
567
|
+
* FLAG_UPDATED is still cleared for ALL cells (including static solids) —
|
|
568
|
+
* the clearMask operation is outside the filter and runs unconditionally.
|
|
569
|
+
*/
|
|
570
|
+
buildActiveListAndClearFlags() {
|
|
571
|
+
const grid = this.grid;
|
|
572
|
+
const fields = this.fields;
|
|
573
|
+
const active = this.activeCells;
|
|
574
|
+
const W = this.W, H = this.H;
|
|
575
|
+
const clearMask = ~((0xff & ~(SHADE_MASK | FLAG_SPARK | FLAG_ANCHORED | FLAG_POPPED | this.preserveFlagsMask)) << 16);
|
|
576
|
+
const skip = this.skipMask;
|
|
577
|
+
const chunkDirty = this.chunkDirty;
|
|
578
|
+
const numChunksX = this.numChunksX;
|
|
579
|
+
const chunkSize = SandWorld.CHUNK_SIZE;
|
|
580
|
+
// Strip mode: only scan [stripStartX, stripEndX) in the X dimension.
|
|
581
|
+
// Flag clearing covers [stripStartX-1, stripEndX+1) to handle halo cells
|
|
582
|
+
// at strip boundaries (overlap ensures all flags are cleared).
|
|
583
|
+
const sx0 = this.stripStartX;
|
|
584
|
+
const sx1 = this.stripEndX;
|
|
585
|
+
const clearX0 = sx0 > 0 ? sx0 - 1 : 0;
|
|
586
|
+
const clearX1 = sx1 < W ? sx1 + 1 : W;
|
|
587
|
+
// Reset chunk dirty bitmap and Y bounds — rebuilt from the scan.
|
|
588
|
+
chunkDirty.fill(0);
|
|
589
|
+
// Per-column active cell histogram — clear our strip's columns then
|
|
590
|
+
// count during the scan. SAB-backed; each worker only writes its own
|
|
591
|
+
// columns so there are no races. The coordinator reads it after all
|
|
592
|
+
// workers finish to rebalance strip boundaries.
|
|
593
|
+
const histogram = this.histogram;
|
|
594
|
+
if (histogram !== null) {
|
|
595
|
+
for (let x = sx0; x < sx1; x++)
|
|
596
|
+
histogram[x] = 0;
|
|
597
|
+
}
|
|
598
|
+
// Clear the deferred-move mask for this strip's columns. Each worker only
|
|
599
|
+
// clears its own strip (no cross-strip writes). The mask is set during the
|
|
600
|
+
// movement pass and read by the coordinator's boundary cleanup after all
|
|
601
|
+
// workers finish.
|
|
602
|
+
const deferred = this.deferredMask;
|
|
603
|
+
let minY = H, maxY = 0;
|
|
604
|
+
let count = 0;
|
|
605
|
+
for (let y = 0; y < H; y++) {
|
|
606
|
+
const rowBase = y * W;
|
|
607
|
+
// Clear flags for the extended range (strip + 1-cell halo)
|
|
608
|
+
for (let x = clearX0; x < clearX1; x++) {
|
|
609
|
+
grid[rowBase + x] &= clearMask;
|
|
610
|
+
}
|
|
611
|
+
// Build active list only for the strip range
|
|
612
|
+
for (let x = sx0; x < sx1; x++) {
|
|
613
|
+
const i = rowBase + x;
|
|
614
|
+
if (deferred !== null)
|
|
615
|
+
deferred[i] = 0;
|
|
616
|
+
if (grid[i] !== 0 && !(skip !== null && skip[i] !== 0)) {
|
|
617
|
+
const mat = grid[i] & 0xff;
|
|
618
|
+
// Static solids (gravityDir=0) are included if their lifetime field
|
|
619
|
+
// is non-zero (loosened by mining — lifetime is the settle timer).
|
|
620
|
+
if (MAT_GRAVITY_DIR[mat] === 0 && !MAT_HAS_REACTIONS[mat]) {
|
|
621
|
+
if (((grid[i] >> 8) & 0xff) === 0)
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
active[count++] = i;
|
|
625
|
+
if (y < minY)
|
|
626
|
+
minY = y;
|
|
627
|
+
if (y > maxY)
|
|
628
|
+
maxY = y;
|
|
629
|
+
const cx = (x / chunkSize) | 0;
|
|
630
|
+
const cy = (y / chunkSize) | 0;
|
|
631
|
+
chunkDirty[cy * numChunksX + cx] = 1;
|
|
632
|
+
if (histogram !== null)
|
|
633
|
+
histogram[x]++;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
this.activeCount = count;
|
|
638
|
+
this.minActiveY = minY;
|
|
639
|
+
this.maxActiveY = maxY;
|
|
640
|
+
}
|
|
641
|
+
/** Build the active-cell list without clearing flags (for mid-frame rebuild).
|
|
642
|
+
* Like buildActiveListAndClearFlags, excludes static solids (gravityDir=0)
|
|
643
|
+
* from the active list and chunkDirty bitmap.
|
|
644
|
+
*
|
|
645
|
+
* Optimized: only scans rows within [minActiveY - margin, maxActiveY + margin]
|
|
646
|
+
* instead of the full grid. The margin covers cells that moved during the
|
|
647
|
+
* movement pass (particles move at most ~5 cells/frame; reactions affect
|
|
648
|
+
* only 8-connected neighbors). This reduces the scan from O(W*H) to
|
|
649
|
+
* O(W * activeHeight) — typically a 2-4x speedup. */
|
|
650
|
+
buildActiveList() {
|
|
651
|
+
const grid = this.grid;
|
|
652
|
+
const active = this.activeCells;
|
|
653
|
+
const W = this.W, H = this.H;
|
|
654
|
+
const skip = this.skipMask;
|
|
655
|
+
const chunkDirty = this.chunkDirty;
|
|
656
|
+
const numChunksX = this.numChunksX;
|
|
657
|
+
const chunkSize = SandWorld.CHUNK_SIZE;
|
|
658
|
+
// Reset chunk dirty bitmap and Y bounds — rebuilt from the scan.
|
|
659
|
+
chunkDirty.fill(0);
|
|
660
|
+
let minY = H, maxY = 0;
|
|
661
|
+
let count = 0;
|
|
662
|
+
// Only scan rows near the Pass 1 active Y range. Cells can move at most
|
|
663
|
+
// ~5 cells per frame (liquid flow) and reactions affect 8-connected
|
|
664
|
+
// neighbors (1 cell). A margin of 8 covers all cases with room to spare.
|
|
665
|
+
const margin = 8;
|
|
666
|
+
const startY = Math.max(0, this.minActiveY - margin);
|
|
667
|
+
const endY = Math.min(H - 1, this.maxActiveY + margin);
|
|
668
|
+
for (let y = startY; y <= endY; y++) {
|
|
669
|
+
const rowBase = y * W;
|
|
670
|
+
for (let x = this.stripStartX; x < this.stripEndX; x++) {
|
|
671
|
+
const i = rowBase + x;
|
|
672
|
+
if (grid[i] !== 0 && !(skip !== null && skip[i] !== 0)) {
|
|
673
|
+
const mat = grid[i] & 0xff;
|
|
674
|
+
if (MAT_GRAVITY_DIR[mat] === 0 && !MAT_HAS_REACTIONS[mat]) {
|
|
675
|
+
if (((grid[i] >> 8) & 0xff) === 0)
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
active[count++] = i;
|
|
679
|
+
if (y < minY)
|
|
680
|
+
minY = y;
|
|
681
|
+
if (y > maxY)
|
|
682
|
+
maxY = y;
|
|
683
|
+
const cx = (x / chunkSize) | 0;
|
|
684
|
+
const cy = (y / chunkSize) | 0;
|
|
685
|
+
chunkDirty[cy * numChunksX + cx] = 1;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
this.activeCount = count;
|
|
690
|
+
this.minActiveY = minY;
|
|
691
|
+
this.maxActiveY = maxY;
|
|
692
|
+
}
|
|
693
|
+
tryMove(x, y) {
|
|
694
|
+
const W = this.W, H = this.H;
|
|
695
|
+
const idx = y * W + x;
|
|
696
|
+
const packed = this.grid[idx];
|
|
697
|
+
if (packed === 0)
|
|
698
|
+
return;
|
|
699
|
+
// Frozen cells (chunk freeze optimization) do not initiate movement.
|
|
700
|
+
const skip = this.skipMask;
|
|
701
|
+
if (skip !== null && skip[idx] !== 0)
|
|
702
|
+
return;
|
|
703
|
+
// Inline unpack — avoid allocating a Cell object on every cell visit
|
|
704
|
+
const mat = packed & 0xff;
|
|
705
|
+
if (mat === Material.Empty)
|
|
706
|
+
return;
|
|
707
|
+
const flags = (packed >> 16) & 0xff;
|
|
708
|
+
if (flags & FLAG_UPDATED)
|
|
709
|
+
return;
|
|
710
|
+
// Boundary cleanup pass: only process cells whose cross-strip move was
|
|
711
|
+
// deferred by the write guard during the worker pass. Cells that didn't
|
|
712
|
+
// attempt a cross-strip move (friction-blocked, obstacle-blocked, or
|
|
713
|
+
// already moved) have no deferred marker and are skipped — this prevents
|
|
714
|
+
// the cleanup from re-rolling RNG gates and giving boundary cells an
|
|
715
|
+
// unfair second movement chance.
|
|
716
|
+
if (this.boundaryPass) {
|
|
717
|
+
const deferred = this.deferredMask;
|
|
718
|
+
if (deferred === null || deferred[idx] === 0)
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
// Spark-flagged fire (visual flames emitted by BurningOil) rises straight
|
|
722
|
+
// up without horizontal drift — the fire should stay above its fuel source
|
|
723
|
+
// (the BurningOil cell), not scatter sideways. Horizontal wind, impulse,
|
|
724
|
+
// and gas drift are skipped for these particles.
|
|
725
|
+
const isSparkFire = mat === Material.Fire && (flags & FLAG_SPARK) !== 0;
|
|
726
|
+
// Typed-array lookups instead of MATERIALS[mat]?.property
|
|
727
|
+
const gravityDir = MAT_GRAVITY_DIR[mat];
|
|
728
|
+
// Static solids (gravityDir === 0: Stone, Wall, Concrete) are frozen
|
|
729
|
+
// UNLESS their lifetime field is non-zero (set by mining as a settle
|
|
730
|
+
// timer). This lets mined Stone fall as Stone — no material conversion.
|
|
731
|
+
// The lifetime field is repurposed as both the "loosened" flag and the
|
|
732
|
+
// settle timer: lifetime > 0 = loosened (can fall, will re-freeze when
|
|
733
|
+
// it lands); lifetime === 0 = frozen static solid.
|
|
734
|
+
if (gravityDir === 0) {
|
|
735
|
+
const lifetime = (packed >> 8) & 0xff;
|
|
736
|
+
if (lifetime === 0)
|
|
737
|
+
return;
|
|
738
|
+
// Duplicator stores the locked material id in its lifetime field, but
|
|
739
|
+
// it must NEVER fall — it's a permanent static solid. Skip the
|
|
740
|
+
// "loosened static solid" fall path entirely for Duplicator.
|
|
741
|
+
// Void is also a permanent static solid that must never fall.
|
|
742
|
+
if (mat === Material.Duplicator || mat === Material.Void)
|
|
743
|
+
return;
|
|
744
|
+
// Loosened static solid — fall downward (default direction)
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// Normal falling material — check gravity field below
|
|
748
|
+
}
|
|
749
|
+
// FuseFire stays put so it can deterministically spread to adjacent fuse
|
|
750
|
+
// cells. Without this, the fire gas floats away before it can propagate.
|
|
751
|
+
if (mat === Material.FuseFire)
|
|
752
|
+
return;
|
|
753
|
+
// Anchored fire (FLAG_ANCHORED) stays put on the fuel surface (e.g. wax)
|
|
754
|
+
// so it can keep spreading to adjacent fuel. Without this, the fire gas
|
|
755
|
+
// rises and drifts away before it can propagate.
|
|
756
|
+
if (mat === Material.Fire && (flags & FLAG_ANCHORED) !== 0)
|
|
757
|
+
return;
|
|
758
|
+
// BurningOil flows like a liquid (gravity: 1, density: 0.8). The
|
|
759
|
+
// burning-oil pass in applyCombustion handles controlled spread to
|
|
760
|
+
// adjacent oil cells, and visual flames are emitted as separate Fire
|
|
761
|
+
// particles (FLAG_SPARK) above the BurningOil — those rise straight up
|
|
762
|
+
// (wind/diagonal movement skipped for spark fire) so they don't scatter
|
|
763
|
+
// even when the BurningOil itself flows.
|
|
764
|
+
// Inline field reads — avoid 4× bounds-checked method calls per cell.
|
|
765
|
+
// tryMove is only called within [0,W)×[0,H) so bounds checks are redundant.
|
|
766
|
+
const fi = idx * 4;
|
|
767
|
+
const gravity = this.fields[fi + FIELD.GRAVITY] / 128;
|
|
768
|
+
// Sample wind from the coarse-grid fluid simulation (bilinear interpolation).
|
|
769
|
+
const windX = this.fluid.sampleVelX(x, y);
|
|
770
|
+
const windY = this.fluid.sampleVelY(x, y);
|
|
771
|
+
// Apply gravity multiplier — at 0 gravity, nothing falls
|
|
772
|
+
if (gravity <= 0)
|
|
773
|
+
return;
|
|
774
|
+
const matFlags = MAT_FLAGS[mat];
|
|
775
|
+
// For loosened static solids (gravityDir=0 but lifetime>0), fall downward.
|
|
776
|
+
const dy = gravityDir !== 0 ? gravityDir : 1;
|
|
777
|
+
const isLiquid = (matFlags & MAT_LIQUID) !== 0;
|
|
778
|
+
const isGas = (matFlags & MAT_GAS) !== 0;
|
|
779
|
+
const matGravity = MAT_GRAVITY[mat];
|
|
780
|
+
// In the boundary cleanup pass, skip all RNG-based movement gates (wind,
|
|
781
|
+
// friction, gas flicker, horizontal impulse). These already had their
|
|
782
|
+
// chance during the worker pass — re-rolling them would give boundary
|
|
783
|
+
// cells an unfair second movement attempt. The cell is only here because
|
|
784
|
+
// a cross-strip move was deferred by the write guard; we attempt only
|
|
785
|
+
// gravity/diagonal/flow moves into the adjacent strip.
|
|
786
|
+
if (!this.boundaryPass) {
|
|
787
|
+
// --- Wind: apply horizontal/vertical force from the fluid grid ---
|
|
788
|
+
// The fluid grid provides float velocities; scale to cell-frame units.
|
|
789
|
+
// A velocity of ~1.0 means "move every frame" (100% chance).
|
|
790
|
+
// Spark fire (visual flames from BurningOil/fuse) skips wind entirely —
|
|
791
|
+
// the emit impulse gives it a fixed horizontal component at birth, and
|
|
792
|
+
// sampling that back here moves it diagonally (up-left/up-right) in a
|
|
793
|
+
// straight streak for its whole short life. Sparks rise straight up via
|
|
794
|
+
// gas gravity instead. The upward impulse still pushes neighboring
|
|
795
|
+
// smoke/gas up via the fluid grid; it just doesn't steer the spark itself.
|
|
796
|
+
// BurningOil also skips wind — its own spark emission creates upward wind
|
|
797
|
+
// in the fluid grid above it, which would blow the BurningOil itself
|
|
798
|
+
// upward off the oil surface. BurningOil flows via gravity only (sinks,
|
|
799
|
+
// spreads horizontally on the oil surface).
|
|
800
|
+
const windMag = Math.abs(windX) + Math.abs(windY);
|
|
801
|
+
if (!isSparkFire && mat !== Material.BurningOil && windMag > 0.05) {
|
|
802
|
+
const wdx = windX > 0 ? 1 : windX < 0 ? -1 : 0;
|
|
803
|
+
const wdy = windY > 0 ? 1 : windY < 0 ? -1 : 0;
|
|
804
|
+
// Scale chance with wind magnitude: 1.0 = 100% move chance.
|
|
805
|
+
const windChance = Math.min(1, windMag);
|
|
806
|
+
if (this.rng.random() < windChance) {
|
|
807
|
+
// Strong wind can shove into occupied cells (displace liquids/gases)
|
|
808
|
+
if (windMag >= 1.5) {
|
|
809
|
+
if (this.tryShove(x, y, x + wdx, y + wdy, packed))
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
if (this.trySwap(x, y, x + wdx, y + wdy, packed, mat, matGravity, isGas))
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// --- Edge friction ---
|
|
819
|
+
const hasLeft = x > 0 && this.grid[y * W + (x - 1)] !== 0;
|
|
820
|
+
const hasRight = x < W - 1 && this.grid[y * W + (x + 1)] !== 0;
|
|
821
|
+
const isExterior = !hasLeft || !hasRight;
|
|
822
|
+
// Honey: very thick — high friction, barely flows
|
|
823
|
+
if (mat === Material.Honey && isExterior && this.rng.random() < 0.7)
|
|
824
|
+
return;
|
|
825
|
+
// Tar: extremely viscous — even higher friction than honey, oozes slowly.
|
|
826
|
+
if (mat === Material.Tar && isExterior && this.rng.random() < 0.85)
|
|
827
|
+
return;
|
|
828
|
+
// Exterior particles have a chance to skip falling (friction).
|
|
829
|
+
if (isExterior && !isGas) {
|
|
830
|
+
const frictionChance = 0.3 / Math.max(1, matGravity);
|
|
831
|
+
if (this.rng.random() < frictionChance)
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
// --- Gas flicker: random chance to not move at all ---
|
|
835
|
+
// Prevents gasses from rising in uniform horizontal lines. Each particle
|
|
836
|
+
// has a chance to "flicker" in place, creating organic, non-uniform spread.
|
|
837
|
+
if (isGas) {
|
|
838
|
+
const flickerChance = (mat === Material.Fire || mat === Material.FuseFire) ? 0.35 : 0.25;
|
|
839
|
+
if (this.rng.random() < flickerChance)
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
// --- Density-scaled horizontal impulse ---
|
|
843
|
+
// Lighter materials (low gravity) get more impulse; denser materials get less.
|
|
844
|
+
// Sand (gravity 1) → full impulse, Water (gravity 2) → half, Lava (gravity 3) → third
|
|
845
|
+
// Gasses (fire/smoke/steam) also get impulse so they drift sideways while rising.
|
|
846
|
+
// Spark fire (visual flames from BurningOil) skips this — no horizontal drift.
|
|
847
|
+
if (this.horizontalImpulseChance > 0 && !isSparkFire) {
|
|
848
|
+
const scaledChance = this.horizontalImpulseChance / Math.max(1, matGravity);
|
|
849
|
+
if (this.rng.random() < scaledChance) {
|
|
850
|
+
const nudgeDir = this.rng.random() < 0.5 ? -1 : 1;
|
|
851
|
+
const nudge = nudgeDir * Math.max(1, Math.round(this.horizontalImpulseStrength));
|
|
852
|
+
if (this.trySwap(x, y, x + nudge, y + dy, packed, mat, matGravity, isGas))
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} // end if (!this.boundaryPass)
|
|
857
|
+
// 1. Try gravity direction
|
|
858
|
+
if (this.trySwap(x, y, x, y + dy, packed, mat, matGravity, isGas)) {
|
|
859
|
+
if (mat === Material.Gravel && ((packed >> 8) & 0xff) !== GRAVEL_DISTURB_SETTLE_TICKS)
|
|
860
|
+
this.disturbAdjacent(x, y);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
// Rubber: bouncy — try to bounce upward when blocked from below
|
|
864
|
+
if (mat === Material.Rubber) {
|
|
865
|
+
// Check if we're resting on something (can't fall)
|
|
866
|
+
const belowY = y + dy;
|
|
867
|
+
const blocked = belowY < 0 || belowY >= H || this.grid[belowY * W + x] !== 0;
|
|
868
|
+
if (blocked) {
|
|
869
|
+
// Bounce: try to move up or sideways
|
|
870
|
+
if (this.rng.random() < 0.5) {
|
|
871
|
+
if (this.trySwap(x, y, x, y - dy, packed, mat, matGravity, isGas))
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const bounceDir = this.rng.random() < 0.5 ? -1 : 1;
|
|
875
|
+
if (this.trySwap(x, y, x + bounceDir * 2, y, packed, mat, matGravity, isGas))
|
|
876
|
+
return;
|
|
877
|
+
if (this.trySwap(x, y, x + bounceDir, y, packed, mat, matGravity, isGas))
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
// Diagonal movement (gravity direction + horizontal). Spark fire (visual
|
|
882
|
+
// flames from BurningOil) skips this — if it can't rise straight up, it
|
|
883
|
+
// stays put and decays via lifetime rather than sliding sideways.
|
|
884
|
+
if (!isSparkFire) {
|
|
885
|
+
const dir = this.rng.random() < 0.5 ? -1 : 1;
|
|
886
|
+
if (this.trySwap(x, y, x + dir, y + dy, packed, mat, matGravity, isGas)) {
|
|
887
|
+
if (mat === Material.Gravel && ((packed >> 8) & 0xff) !== GRAVEL_DISTURB_SETTLE_TICKS)
|
|
888
|
+
this.disturbAdjacent(x, y);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (this.trySwap(x, y, x - dir, y + dy, packed, mat, matGravity, isGas)) {
|
|
892
|
+
if (mat === Material.Gravel && ((packed >> 8) & 0xff) !== GRAVEL_DISTURB_SETTLE_TICKS)
|
|
893
|
+
this.disturbAdjacent(x, y);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// Gravel: flows horizontally like a liquid when unsupported, but settles
|
|
898
|
+
// firmly in place when supported from below. Unlike a true liquid, gravel
|
|
899
|
+
// only flows when there's empty space below the adjacent cell (it "spills"
|
|
900
|
+
// downhill), and stops as soon as it's resting on something. This creates
|
|
901
|
+
// realistic pile behavior — gravel spreads into low spots then freezes.
|
|
902
|
+
if (mat === Material.Gravel) {
|
|
903
|
+
// Only "real" gravel (from mining, lifetime=GRAVEL_SETTLE_TICKS or 0)
|
|
904
|
+
// disturbs adjacent Stone when it moves. Freshly-disturbed Stone that
|
|
905
|
+
// became Gravel (lifetime=GRAVEL_DISTURB_SETTLE_TICKS) does NOT disturb
|
|
906
|
+
// — otherwise each falling disturbed cell would disturb its neighbors,
|
|
907
|
+
// which would become Gravel, fall, disturb THEIR neighbors, causing an
|
|
908
|
+
// unbounded cascade of stone destruction across the map.
|
|
909
|
+
const canDisturb = ((packed >> 8) & 0xff) !== GRAVEL_DISTURB_SETTLE_TICKS;
|
|
910
|
+
const flowDir = this.rng.random() < 0.5 ? -1 : 1;
|
|
911
|
+
if (this.tryGravelFlow(x, y, flowDir)) {
|
|
912
|
+
if (canDisturb)
|
|
913
|
+
this.disturbAdjacent(x, y);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (this.tryGravelFlow(x, y, -flowDir)) {
|
|
917
|
+
if (canDisturb)
|
|
918
|
+
this.disturbAdjacent(x, y);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
// Gravel is settled (supported below, can't spread) — stop moving.
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (isLiquid) {
|
|
925
|
+
// Buoyancy: a less-dense liquid rises through a denser liquid above it.
|
|
926
|
+
// This is the mirror of the "heavy liquid sinks through lighter liquid
|
|
927
|
+
// below" code at the end of tryMove. Without active rising, oil dropped
|
|
928
|
+
// into water spreads horizontally at the bottom but never reaches the
|
|
929
|
+
// surface — the passive sinking mechanism is too slow because the
|
|
930
|
+
// bottom-to-top movement pass lets oil spread sideways before the water
|
|
931
|
+
// above gets a chance to sink through it.
|
|
932
|
+
if (dy > 0) {
|
|
933
|
+
const above = y - 1;
|
|
934
|
+
if (above >= 0) {
|
|
935
|
+
const aboveIdx = above * W + x;
|
|
936
|
+
const abovePacked = this.grid[aboveIdx];
|
|
937
|
+
if (abovePacked !== 0) {
|
|
938
|
+
const aboveMat = abovePacked & 0xff;
|
|
939
|
+
if (!((abovePacked >> 16) & FLAG_UPDATED) &&
|
|
940
|
+
(MAT_FLAGS[aboveMat] & MAT_LIQUID) &&
|
|
941
|
+
MAT_DENSITY[aboveMat] > MAT_DENSITY[mat]) {
|
|
942
|
+
this.grid[aboveIdx] = packed | FLAG_UPDATED_BIT;
|
|
943
|
+
this.grid[idx] = abovePacked | FLAG_UPDATED_BIT;
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const flowDir = this.rng.random() < 0.5 ? -1 : 1;
|
|
950
|
+
if (this.tryFlow(x, y, flowDir, 5))
|
|
951
|
+
return;
|
|
952
|
+
if (this.tryFlow(x, y, -flowDir, 5))
|
|
953
|
+
return;
|
|
954
|
+
// Density-based horizontal spread: a less-dense liquid (e.g. oil) can
|
|
955
|
+
// push through a denser liquid (e.g. water) to spread across its
|
|
956
|
+
// surface. Without this, oil rises to the top of water in a column but
|
|
957
|
+
// can't spread sideways because tryFlow only enters empty cells.
|
|
958
|
+
if (this.tryDensityFlow(x, y, flowDir, mat, packed))
|
|
959
|
+
return;
|
|
960
|
+
if (this.tryDensityFlow(x, y, -flowDir, mat, packed))
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
// Gas: wider horizontal drift (up to 3 cells) for organic spread.
|
|
964
|
+
if (isGas) {
|
|
965
|
+
const driftDir = this.rng.random() < 0.5 ? -1 : 1;
|
|
966
|
+
if (this.tryFlow(x, y, driftDir, 3))
|
|
967
|
+
return;
|
|
968
|
+
if (this.tryFlow(x, y, -driftDir, 3))
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// 5. Liquids: sink through gas below (heavy liquid displaces light gas upward)
|
|
972
|
+
if (isLiquid && dy > 0) {
|
|
973
|
+
const below = y + 1;
|
|
974
|
+
if (below < H) {
|
|
975
|
+
const belowIdx = below * W + x;
|
|
976
|
+
const belowPacked = this.grid[belowIdx];
|
|
977
|
+
if (belowPacked !== 0) {
|
|
978
|
+
const belowMat = belowPacked & 0xff;
|
|
979
|
+
const belowFlags = MAT_FLAGS[belowMat];
|
|
980
|
+
// Heavy liquid sinks through a lighter gas or a less-dense liquid.
|
|
981
|
+
// Density is a separate property from gravity (movement speed):
|
|
982
|
+
// mercury (13.5) sinks through water (1.0), lava (3.0) through
|
|
983
|
+
// water, honey (1.4) through water, etc.
|
|
984
|
+
if (!((belowPacked >> 16) & FLAG_UPDATED) &&
|
|
985
|
+
((belowFlags & MAT_GAS) || ((belowFlags & MAT_LIQUID) && MAT_DENSITY[mat] > MAT_DENSITY[belowMat]))) {
|
|
986
|
+
this.grid[belowIdx] = packed | FLAG_UPDATED_BIT;
|
|
987
|
+
this.grid[idx] = belowPacked | FLAG_UPDATED_BIT;
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// Glitter: suspends in water (tries to float up when in water)
|
|
994
|
+
if (mat === Material.Glitter) {
|
|
995
|
+
if (y + 1 < H && (this.grid[(y + 1) * W + x] & 0xff) === Material.Water) {
|
|
996
|
+
// Stay suspended — don't sink further
|
|
997
|
+
if (this.rng.random() < 0.8)
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Try to move/swap the cell at (x,y) into (nx,ny).
|
|
1004
|
+
*
|
|
1005
|
+
* The caller (tryMove) passes the already-read `srcPacked` plus pre-fetched
|
|
1006
|
+
* srcMat/srcGravity/srcIsGas so the source cell is not re-read or re-unpacked
|
|
1007
|
+
* on every call — tryMove calls this up to ~6 times per cell. The moved cell
|
|
1008
|
+
* is written with FLAG_UPDATED set via a direct bit-OR on `srcPacked` (no
|
|
1009
|
+
* object spread / re-pack).
|
|
1010
|
+
*/
|
|
1011
|
+
trySwap(x, y, nx, ny, srcPacked, srcMat, srcGravity, srcIsGas) {
|
|
1012
|
+
const W = this.W, H = this.H;
|
|
1013
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1014
|
+
return false;
|
|
1015
|
+
// Strip mode write guard: don't write to cells outside our strip.
|
|
1016
|
+
// The coordinator's boundary cleanup handles deferred cross-strip moves.
|
|
1017
|
+
if (nx < this.writeXMin || nx >= this.writeXMax) {
|
|
1018
|
+
// Mark the source cell as deferred so the boundary cleanup knows to
|
|
1019
|
+
// re-attempt this cross-strip move. Only set during the worker pass
|
|
1020
|
+
// (not during the boundary cleanup itself).
|
|
1021
|
+
if (!this.boundaryPass) {
|
|
1022
|
+
const dm = this.deferredMask;
|
|
1023
|
+
if (dm !== null)
|
|
1024
|
+
dm[y * W + x] = 1;
|
|
1025
|
+
}
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
const destIdx = ny * W + nx;
|
|
1029
|
+
const srcIdx = y * W + x;
|
|
1030
|
+
const destPacked = this.grid[destIdx];
|
|
1031
|
+
if (destPacked === 0) {
|
|
1032
|
+
// Empty: simple move
|
|
1033
|
+
this.grid[destIdx] = srcPacked | FLAG_UPDATED_BIT;
|
|
1034
|
+
this.grid[srcIdx] = 0;
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
// Non-gas displacing gas: solids and liquids are denser than gas, so a
|
|
1038
|
+
// falling (or rising) solid/liquid pushes the gas aside and the gas swaps
|
|
1039
|
+
// into the source cell. Without this, smoke/steam would block sand and
|
|
1040
|
+
// water from falling — the gas has no gravity-driven way to yield. A gas
|
|
1041
|
+
// that already moved this frame is left alone (it will yield next frame).
|
|
1042
|
+
if (!srcIsGas) {
|
|
1043
|
+
const destMat = destPacked & 0xff;
|
|
1044
|
+
if ((MAT_FLAGS[destMat] & MAT_GAS) && !((destPacked >> 16) & FLAG_UPDATED)) {
|
|
1045
|
+
this.grid[destIdx] = srcPacked | FLAG_UPDATED_BIT;
|
|
1046
|
+
this.grid[srcIdx] = destPacked | FLAG_UPDATED_BIT;
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// Gas-to-gas displacement: a lighter gas (higher gravity for upward, i.e.
|
|
1051
|
+
// rises faster) can push through a slower gas. This lets fire (gravity 4)
|
|
1052
|
+
// rise through smoke (gravity 3) so they separate instead of mixing.
|
|
1053
|
+
// Without this, smoke from a bottom-up burn would overtake the fire front
|
|
1054
|
+
// and suffocate it (fire trapped below its own smoke, can't reach fuel).
|
|
1055
|
+
if (srcIsGas) {
|
|
1056
|
+
const destMat = destPacked & 0xff;
|
|
1057
|
+
if ((MAT_FLAGS[destMat] & MAT_GAS) && srcGravity > MAT_GRAVITY[destMat]) {
|
|
1058
|
+
// Swap: source moves to dest, dest moves to source
|
|
1059
|
+
this.grid[destIdx] = srcPacked | FLAG_UPDATED_BIT;
|
|
1060
|
+
this.grid[srcIdx] = destPacked | FLAG_UPDATED_BIT;
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
// Solid-liquid density displacement: a denser material sinks through a
|
|
1065
|
+
// less-dense one. Sand (density 2.0) sinks through water (1.0) but not
|
|
1066
|
+
// through mercury (13.5). Water (1.0) sinks through wood (0.6), making
|
|
1067
|
+
// wood float. Only applies to solid-liquid pairs — solids don't flow
|
|
1068
|
+
// through each other, and gas displacement is handled above. Structural
|
|
1069
|
+
// barriers (static solids with density >= 2.0, e.g. stone/wall/concrete)
|
|
1070
|
+
// are immovable — nothing sinks through them regardless of density.
|
|
1071
|
+
if (!srcIsGas) {
|
|
1072
|
+
const destMat = destPacked & 0xff;
|
|
1073
|
+
const destFlags = MAT_FLAGS[destMat];
|
|
1074
|
+
if ((destFlags & (MAT_SOLID | MAT_LIQUID)) && !((destPacked >> 16) & FLAG_UPDATED)) {
|
|
1075
|
+
const srcIsSolid = (MAT_FLAGS[srcMat] & MAT_SOLID) !== 0;
|
|
1076
|
+
const destIsSolid = (destFlags & MAT_SOLID) !== 0;
|
|
1077
|
+
// One must be solid, the other liquid (not solid-solid or liquid-liquid).
|
|
1078
|
+
// Structural barriers (static + dense) are immovable. A loosened
|
|
1079
|
+
// static solid (lifetime > 0, set by mining) is NOT a barrier — it
|
|
1080
|
+
// can fall, so other cells can sink through it.
|
|
1081
|
+
const destIsBarrier = destIsSolid && MAT_GRAVITY_DIR[destMat] === 0 &&
|
|
1082
|
+
MAT_DENSITY[destMat] >= 2.0 &&
|
|
1083
|
+
((destPacked >> 8) & 0xff) === 0;
|
|
1084
|
+
if (srcIsSolid !== destIsSolid &&
|
|
1085
|
+
!destIsBarrier &&
|
|
1086
|
+
MAT_DENSITY[srcMat] > MAT_DENSITY[destMat]) {
|
|
1087
|
+
this.grid[destIdx] = srcPacked | FLAG_UPDATED_BIT;
|
|
1088
|
+
this.grid[srcIdx] = destPacked | FLAG_UPDATED_BIT;
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Like trySwap, but can also displace liquids/gases: if the destination is
|
|
1097
|
+
* occupied by a liquid or gas, the two cells swap (the pushed particle moves
|
|
1098
|
+
* into the source cell). Solids and walls block the shove. Used by strong
|
|
1099
|
+
* wind/impulse forces to push particles through dense media.
|
|
1100
|
+
*
|
|
1101
|
+
* The caller passes the already-read `srcPacked` so the source is not
|
|
1102
|
+
* re-read; the moved cell is written via a direct bit-OR (no spread/re-pack).
|
|
1103
|
+
*/
|
|
1104
|
+
tryShove(x, y, nx, ny, srcPacked) {
|
|
1105
|
+
const W = this.W, H = this.H;
|
|
1106
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1107
|
+
return false;
|
|
1108
|
+
// Strip mode write guard
|
|
1109
|
+
if (nx < this.writeXMin || nx >= this.writeXMax) {
|
|
1110
|
+
if (!this.boundaryPass) {
|
|
1111
|
+
const dm = this.deferredMask;
|
|
1112
|
+
if (dm !== null)
|
|
1113
|
+
dm[y * W + x] = 1;
|
|
1114
|
+
}
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
const destIdx = ny * W + nx;
|
|
1118
|
+
const srcIdx = y * W + x;
|
|
1119
|
+
const destPacked = this.grid[destIdx];
|
|
1120
|
+
// Empty destination: normal swap
|
|
1121
|
+
if (destPacked === 0) {
|
|
1122
|
+
this.grid[destIdx] = srcPacked | FLAG_UPDATED_BIT;
|
|
1123
|
+
this.grid[srcIdx] = 0;
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
// Occupied: only displace liquids/gases (not solids, walls, or already-updated cells)
|
|
1127
|
+
const destMat = destPacked & 0xff;
|
|
1128
|
+
const destFlags = MAT_FLAGS[destMat];
|
|
1129
|
+
if (!(destFlags & (MAT_LIQUID | MAT_GAS)))
|
|
1130
|
+
return false;
|
|
1131
|
+
if ((destPacked >> 16) & FLAG_UPDATED)
|
|
1132
|
+
return false;
|
|
1133
|
+
// Swap the two cells
|
|
1134
|
+
this.grid[destIdx] = srcPacked | FLAG_UPDATED_BIT;
|
|
1135
|
+
this.grid[srcIdx] = destPacked | FLAG_UPDATED_BIT;
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
tryFlow(x, y, dir, maxSteps) {
|
|
1139
|
+
const W = this.W, H = this.H;
|
|
1140
|
+
const srcIdx = y * W + x;
|
|
1141
|
+
// Read the source once and pre-compute the moved value (FLAG_UPDATED set).
|
|
1142
|
+
// The old code re-unpacked the source twice per move via
|
|
1143
|
+
// `pack({ ...unpack(packed), flags: unpack(packed).flags | FLAG_UPDATED })`.
|
|
1144
|
+
const movedPacked = this.grid[srcIdx] | FLAG_UPDATED_BIT;
|
|
1145
|
+
for (let step = 1; step <= maxSteps; step++) {
|
|
1146
|
+
const nx = x + dir * step;
|
|
1147
|
+
if (nx < 0 || nx >= W)
|
|
1148
|
+
return false;
|
|
1149
|
+
// Strip mode write guard
|
|
1150
|
+
if (nx < this.writeXMin || nx >= this.writeXMax) {
|
|
1151
|
+
if (!this.boundaryPass) {
|
|
1152
|
+
const dm = this.deferredMask;
|
|
1153
|
+
if (dm !== null)
|
|
1154
|
+
dm[srcIdx] = 1;
|
|
1155
|
+
}
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
const destIdx = y * W + nx;
|
|
1159
|
+
if (this.grid[destIdx] !== 0)
|
|
1160
|
+
return false;
|
|
1161
|
+
const belowY = y + 1;
|
|
1162
|
+
if (belowY < H) {
|
|
1163
|
+
const belowIdx = belowY * W + nx;
|
|
1164
|
+
if (this.grid[belowIdx] === 0) {
|
|
1165
|
+
this.grid[belowIdx] = movedPacked;
|
|
1166
|
+
this.grid[srcIdx] = 0;
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (step === maxSteps) {
|
|
1171
|
+
this.grid[destIdx] = movedPacked;
|
|
1172
|
+
this.grid[srcIdx] = 0;
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Density-based horizontal flow for liquids: a less-dense liquid can swap
|
|
1180
|
+
* with an adjacent denser liquid. This lets oil (density 0.8) spread across
|
|
1181
|
+
* the surface of water (density 1.0). Without this, oil rises to the top of
|
|
1182
|
+
* water in a vertical column but can't spread horizontally because tryFlow
|
|
1183
|
+
* only moves into empty cells, and the adjacent cells at the surface are
|
|
1184
|
+
* water, not empty.
|
|
1185
|
+
*
|
|
1186
|
+
* Only fires when the source is strictly less dense than the destination
|
|
1187
|
+
* (so same-density liquids don't churn). Solids, gases, and already-updated
|
|
1188
|
+
* cells are not displaced.
|
|
1189
|
+
*/
|
|
1190
|
+
tryDensityFlow(x, y, dir, srcMat, srcPacked) {
|
|
1191
|
+
const W = this.W;
|
|
1192
|
+
const nx = x + dir;
|
|
1193
|
+
if (nx < 0 || nx >= W)
|
|
1194
|
+
return false;
|
|
1195
|
+
// Strip mode write guard
|
|
1196
|
+
if (nx < this.writeXMin || nx >= this.writeXMax) {
|
|
1197
|
+
if (!this.boundaryPass) {
|
|
1198
|
+
const dm = this.deferredMask;
|
|
1199
|
+
if (dm !== null)
|
|
1200
|
+
dm[y * W + x] = 1;
|
|
1201
|
+
}
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
const destIdx = y * W + nx;
|
|
1205
|
+
const destPacked = this.grid[destIdx];
|
|
1206
|
+
if (destPacked === 0)
|
|
1207
|
+
return false; // empty — tryFlow already handled this
|
|
1208
|
+
const destMat = destPacked & 0xff;
|
|
1209
|
+
// Only displace liquids (not solids, gases, or already-updated cells)
|
|
1210
|
+
if (!(MAT_FLAGS[destMat] & MAT_LIQUID))
|
|
1211
|
+
return false;
|
|
1212
|
+
if ((destPacked >> 16) & FLAG_UPDATED)
|
|
1213
|
+
return false;
|
|
1214
|
+
// Source must be strictly less dense than destination
|
|
1215
|
+
if (MAT_DENSITY[srcMat] >= MAT_DENSITY[destMat])
|
|
1216
|
+
return false;
|
|
1217
|
+
// Swap: less-dense liquid moves sideways, denser liquid takes its place
|
|
1218
|
+
this.grid[destIdx] = srcPacked | FLAG_UPDATED_BIT;
|
|
1219
|
+
this.grid[y * W + x] = destPacked | FLAG_UPDATED_BIT;
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* When a gravel cell vacates position (x, y), disturb adjacent Stone and
|
|
1224
|
+
* LooseStone cells so they don't float in the air when their support flows
|
|
1225
|
+
* away.
|
|
1226
|
+
*
|
|
1227
|
+
* - Stone (re-settled or natural) → gravity field set + short settle timer
|
|
1228
|
+
* in the lifetime field, so it falls as Stone if unsupported. No material
|
|
1229
|
+
* conversion — it stays Stone and re-freezes (gravity cleared) when it
|
|
1230
|
+
* lands.
|
|
1231
|
+
* - LooseStone (legacy cells from old saves) → settle timer reset to the
|
|
1232
|
+
* short disturbed value, gravity ensured, so it keeps falling instead of
|
|
1233
|
+
* re-settling prematurely.
|
|
1234
|
+
*
|
|
1235
|
+
* The disturbed settle timer is GRAVEL_DISTURB_SETTLE_TICKS (2 ticks). If the
|
|
1236
|
+
* cell is still supported after gravel moves, it re-settles (gravity cleared)
|
|
1237
|
+
* in 2 ticks. If gravel removed its support, the cell starts falling
|
|
1238
|
+
* (FLAG_UPDATED keeps the timer reset to 2 via applyAging) and won't settle
|
|
1239
|
+
* until it lands.
|
|
1240
|
+
*/
|
|
1241
|
+
disturbAdjacent(x, y) {
|
|
1242
|
+
const W = this.W, H = this.H;
|
|
1243
|
+
// Check 4-neighbors (the cell above is most critical — it lost support)
|
|
1244
|
+
// Set disturbedFlags (in addition to FLAG_UPDATED) so the chunk-world's
|
|
1245
|
+
// expireWakeTicks can detect disturbed cells in frozen chunks even after
|
|
1246
|
+
// applyAging clears FLAG_UPDATED. disturbedFlags is persistent (included
|
|
1247
|
+
// in preserveFlagsMask by the game).
|
|
1248
|
+
const extra = this.disturbedFlags | FLAG_UPDATED;
|
|
1249
|
+
for (let i = 0; i < 4; i++) {
|
|
1250
|
+
const nx = x + DIR_DX[i];
|
|
1251
|
+
const ny = y + DIR_DY[i];
|
|
1252
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1253
|
+
continue;
|
|
1254
|
+
// Strip mode write guard: skip neighbors outside our write bounds.
|
|
1255
|
+
if (nx < this.writeXMin || nx >= this.writeXMax)
|
|
1256
|
+
continue;
|
|
1257
|
+
const nIdx = ny * W + nx;
|
|
1258
|
+
const nPacked = this.grid[nIdx];
|
|
1259
|
+
if (nPacked === 0)
|
|
1260
|
+
continue;
|
|
1261
|
+
const nMat = nPacked & 0xff;
|
|
1262
|
+
const nFlags = (nPacked >> 16) & 0xff;
|
|
1263
|
+
const nFi = nIdx * 4;
|
|
1264
|
+
if (nMat === Material.Stone) {
|
|
1265
|
+
// Set gravity field + settle timer so Stone falls as Stone if
|
|
1266
|
+
// unsupported. No material conversion. Re-freezes when it lands.
|
|
1267
|
+
this.grid[nIdx] = (nMat & 0xff) | ((GRAVEL_DISTURB_SETTLE_TICKS & 0xff) << 8) | (((nFlags | extra) & 0xff) << 16);
|
|
1268
|
+
this.fields[nFi + FIELD.GRAVITY] = DEFAULT_GRAVITY;
|
|
1269
|
+
}
|
|
1270
|
+
else if (nMat === Material.LooseStone) {
|
|
1271
|
+
// Legacy LooseStone (old saves): reset settle timer to short value
|
|
1272
|
+
const shade = nFlags & SHADE_MASK;
|
|
1273
|
+
this.grid[nIdx] = packCell(Material.LooseStone, GRAVEL_DISTURB_SETTLE_TICKS, shade | extra);
|
|
1274
|
+
this.fields[nFi + FIELD.GRAVITY] = DEFAULT_GRAVITY;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Gravel flow: like liquid flow, but gravel only moves horizontally to an
|
|
1280
|
+
* adjacent cell if that cell is empty AND the cell below the destination is
|
|
1281
|
+
* also empty (i.e. it "spills downhill" into a low spot). If the destination
|
|
1282
|
+
* at the same level is empty but supported below, gravel still moves there
|
|
1283
|
+
* (spreading along a flat surface), but only for a limited distance so it
|
|
1284
|
+
* forms realistic piles rather than spreading infinitely like water.
|
|
1285
|
+
*
|
|
1286
|
+
* This creates the "flows like a liquid but settles in place" behavior:
|
|
1287
|
+
* gravel pours into gaps, spreads to fill low spots, then stops once it's
|
|
1288
|
+
* resting on something with no downhill path.
|
|
1289
|
+
*/
|
|
1290
|
+
tryGravelFlow(x, y, dir) {
|
|
1291
|
+
const W = this.W, H = this.H;
|
|
1292
|
+
const srcIdx = y * W + x;
|
|
1293
|
+
const movedPacked = this.grid[srcIdx] | FLAG_UPDATED_BIT;
|
|
1294
|
+
// Step 1: try to spill into the adjacent cell at the same level
|
|
1295
|
+
const nx = x + dir;
|
|
1296
|
+
if (nx < 0 || nx >= W)
|
|
1297
|
+
return false;
|
|
1298
|
+
// Strip mode write guard
|
|
1299
|
+
if (nx < this.writeXMin || nx >= this.writeXMax) {
|
|
1300
|
+
if (!this.boundaryPass) {
|
|
1301
|
+
const dm = this.deferredMask;
|
|
1302
|
+
if (dm !== null)
|
|
1303
|
+
dm[srcIdx] = 1;
|
|
1304
|
+
}
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
const destIdx = y * W + nx;
|
|
1308
|
+
if (this.grid[destIdx] !== 0)
|
|
1309
|
+
return false;
|
|
1310
|
+
// Check if there's a drop below the destination (downhill spill).
|
|
1311
|
+
// If so, gravel falls into the gap — this is the primary flow mode.
|
|
1312
|
+
const belowY = y + 1;
|
|
1313
|
+
if (belowY < H) {
|
|
1314
|
+
const belowDestIdx = belowY * W + nx;
|
|
1315
|
+
if (this.grid[belowDestIdx] === 0) {
|
|
1316
|
+
// Downhill: gravel spills into the gap and falls
|
|
1317
|
+
this.grid[belowDestIdx] = movedPacked;
|
|
1318
|
+
this.grid[srcIdx] = 0;
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
// Same-level spread: gravel moves to an adjacent empty cell that's
|
|
1323
|
+
// supported below. Limit spread distance to 2 cells so gravel forms
|
|
1324
|
+
// piles rather than spreading infinitely like a liquid.
|
|
1325
|
+
// Only spread if the gravel is NOT supported directly below (i.e. it's
|
|
1326
|
+
// on an edge and could fall off). This prevents gravel on a flat floor
|
|
1327
|
+
// from spreading forever.
|
|
1328
|
+
const srcBelowIdx = (y + 1) * W + x;
|
|
1329
|
+
const srcSupported = (y + 1 >= H) || this.grid[srcBelowIdx] !== 0;
|
|
1330
|
+
if (srcSupported) {
|
|
1331
|
+
// Gravel is supported below — only spread if the destination is also
|
|
1332
|
+
// NOT supported (gravel flows off the edge of a pile).
|
|
1333
|
+
if (belowY < H) {
|
|
1334
|
+
const belowDestIdx = belowY * W + nx;
|
|
1335
|
+
if (this.grid[belowDestIdx] !== 0) {
|
|
1336
|
+
// Destination is supported too — no flow needed, gravel settles
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// Spread to the adjacent cell
|
|
1342
|
+
this.grid[destIdx] = movedPacked;
|
|
1343
|
+
this.grid[srcIdx] = 0;
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
// ===========================================================================
|
|
1347
|
+
// Special reactions for new materials
|
|
1348
|
+
// ===========================================================================
|
|
1349
|
+
applySpecialReactions() {
|
|
1350
|
+
const W = this.W, H = this.H;
|
|
1351
|
+
const grid = this.grid;
|
|
1352
|
+
const fields = this.fields;
|
|
1353
|
+
const active = this.activeCells;
|
|
1354
|
+
const count = this.activeCount;
|
|
1355
|
+
// Strip mode write bounds — neighbor writes outside this range are skipped.
|
|
1356
|
+
const wxMin = this.writeXMin, wxMax = this.writeXMax;
|
|
1357
|
+
for (let a = 0; a < count; a++) {
|
|
1358
|
+
const idx = active[a];
|
|
1359
|
+
const packed = grid[idx];
|
|
1360
|
+
if (packed === 0)
|
|
1361
|
+
continue; // cell was destroyed earlier this frame
|
|
1362
|
+
const mat = packed & 0xff;
|
|
1363
|
+
if (mat === Material.Empty)
|
|
1364
|
+
continue;
|
|
1365
|
+
const lifetime = (packed >> 8) & 0xff;
|
|
1366
|
+
const flags = (packed >> 16) & 0xff;
|
|
1367
|
+
// Inline field read — no bounds check needed (idx is always valid)
|
|
1368
|
+
const fi = idx * 4;
|
|
1369
|
+
const temp = fields[fi + FIELD.TEMP] / 128;
|
|
1370
|
+
const x = idx % W;
|
|
1371
|
+
const y = (idx / W) | 0;
|
|
1372
|
+
// --- Antimatter: on contact with any non-empty, non-antimatter,
|
|
1373
|
+
// non-wall, non-duplicator material, annihilates the ENTIRE contiguous
|
|
1374
|
+
// antimatter cluster PLUS all contiguous other material (8-connected
|
|
1375
|
+
// flood-fill through non-empty cells). Wall and Duplicator are barriers
|
|
1376
|
+
// (not annihilated, flood-fill stops at them). Empty cells stop the
|
|
1377
|
+
// flood-fill. A fire explosion marks the contact point. ---
|
|
1378
|
+
if (mat === Material.Antimatter) {
|
|
1379
|
+
// Find a contact neighbor (the trigger for annihilation).
|
|
1380
|
+
let contactX = -1, contactY = -1;
|
|
1381
|
+
for (let dy = -1; dy <= 1 && contactX < 0; dy++) {
|
|
1382
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1383
|
+
if (dx === 0 && dy === 0)
|
|
1384
|
+
continue;
|
|
1385
|
+
const nx = x + dx, ny = y + dy;
|
|
1386
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1387
|
+
continue;
|
|
1388
|
+
if (nx < wxMin || nx >= wxMax)
|
|
1389
|
+
continue;
|
|
1390
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
1391
|
+
if (nMat !== Material.Empty && nMat !== Material.Antimatter &&
|
|
1392
|
+
nMat !== Material.Wall && nMat !== Material.Duplicator &&
|
|
1393
|
+
nMat !== Material.Void) {
|
|
1394
|
+
contactX = nx;
|
|
1395
|
+
contactY = ny;
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (contactX >= 0) {
|
|
1401
|
+
this.annihilateAntimatter(x, y, contactX, contactY);
|
|
1402
|
+
}
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
// --- Plasma: damages everything around, decays quickly ---
|
|
1406
|
+
if (mat === Material.Plasma) {
|
|
1407
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1408
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1409
|
+
if (dx === 0 && dy === 0)
|
|
1410
|
+
continue;
|
|
1411
|
+
const nx = x + dx, ny = y + dy;
|
|
1412
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1413
|
+
continue;
|
|
1414
|
+
if (nx < wxMin || nx >= wxMax)
|
|
1415
|
+
continue;
|
|
1416
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
1417
|
+
if (nMat !== Material.Empty && nMat !== Material.Plasma && nMat !== Material.Wall) {
|
|
1418
|
+
if (this.rng.random() < 0.3) {
|
|
1419
|
+
grid[ny * W + nx] = packCell(Material.Fire, 10, this.rng.randomShade());
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
// --- Mystery (???): oscillating reaction ---
|
|
1427
|
+
// Changes color/behavior in a sine wave pattern based on frame count
|
|
1428
|
+
if (mat === Material.Mystery) {
|
|
1429
|
+
const phase = Math.sin(this.frame * 0.1 + x * 0.3 + y * 0.2);
|
|
1430
|
+
if (phase > 0.9 && this.rng.random() < 0.1) {
|
|
1431
|
+
// Emit plasma occasionally (same X, always in strip)
|
|
1432
|
+
if (y > 0 && grid[(y - 1) * W + x] === 0) {
|
|
1433
|
+
grid[(y - 1) * W + x] = packCell(Material.Plasma, 20, this.rng.randomShade());
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (phase < -0.9 && this.rng.random() < 0.05) {
|
|
1437
|
+
// Absorb nearby particles
|
|
1438
|
+
const MYSTERY_DIRS = [1, 0, -1, 0, 0, 1, 0, -1]; // [dx0,dy0, dx1,dy1, ...]
|
|
1439
|
+
const di = Math.floor(this.rng.random() * 4) * 2;
|
|
1440
|
+
const dx = MYSTERY_DIRS[di], dy = MYSTERY_DIRS[di + 1];
|
|
1441
|
+
const nx = x + dx, ny = y + dy;
|
|
1442
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax) {
|
|
1443
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
1444
|
+
if (nMat !== Material.Empty && nMat !== Material.Wall && nMat !== Material.Mystery) {
|
|
1445
|
+
grid[ny * W + nx] = 0;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
// --- Gasoline: slowly vaporizes into gas vapor ---
|
|
1452
|
+
if (mat === Material.Gasoline) {
|
|
1453
|
+
if (this.rng.random() < 0.005) {
|
|
1454
|
+
// Same X as source, always in strip
|
|
1455
|
+
if (y > 0 && grid[(y - 1) * W + x] === 0) {
|
|
1456
|
+
grid[(y - 1) * W + x] = packCell(Material.GasVapor, 200, this.rng.randomShade());
|
|
1457
|
+
// Small chance to consume the gasoline
|
|
1458
|
+
if (this.rng.random() < 0.3)
|
|
1459
|
+
grid[idx] = 0;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
// --- Salt + Water → Brine ---
|
|
1465
|
+
if (mat === Material.Salt) {
|
|
1466
|
+
// Single 8-neighbor scan for water + snow
|
|
1467
|
+
let waterNi = -1, hasSnow = false;
|
|
1468
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1469
|
+
const ny = y + dy;
|
|
1470
|
+
if (ny < 0 || ny >= H)
|
|
1471
|
+
continue;
|
|
1472
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1473
|
+
if (dx === 0 && dy === 0)
|
|
1474
|
+
continue;
|
|
1475
|
+
const nx = x + dx;
|
|
1476
|
+
if (nx < 0 || nx >= W)
|
|
1477
|
+
continue;
|
|
1478
|
+
if (nx < wxMin || nx >= wxMax)
|
|
1479
|
+
continue;
|
|
1480
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
1481
|
+
if (nMat === Material.Water) {
|
|
1482
|
+
waterNi = ny * W + nx;
|
|
1483
|
+
}
|
|
1484
|
+
else if (nMat === Material.Snow) {
|
|
1485
|
+
hasSnow = true;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (waterNi >= 0 && this.rng.random() < 0.1) {
|
|
1490
|
+
grid[idx] = packCell(Material.Brine, 0, this.rng.randomShade());
|
|
1491
|
+
grid[waterNi] = packCell(Material.Brine, 0, this.rng.randomShade());
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
// Salt dissolves snow: 1 salt grain melts up to 10 snow cells nearby
|
|
1495
|
+
if (hasSnow) {
|
|
1496
|
+
// Remove the salt grain
|
|
1497
|
+
grid[idx] = 0;
|
|
1498
|
+
// Dissolve up to 10 snow cells in a radius
|
|
1499
|
+
let dissolved = 0;
|
|
1500
|
+
for (let ry = y - 3; ry <= y + 3 && dissolved < 10; ry++) {
|
|
1501
|
+
for (let rx = x - 3; rx <= x + 3 && dissolved < 10; rx++) {
|
|
1502
|
+
if (rx < 0 || rx >= W || ry < 0 || ry >= H)
|
|
1503
|
+
continue;
|
|
1504
|
+
if (rx < wxMin || rx >= wxMax)
|
|
1505
|
+
continue;
|
|
1506
|
+
const ridx = ry * W + rx;
|
|
1507
|
+
if ((grid[ridx] & 0xff) === Material.Snow) {
|
|
1508
|
+
grid[ridx] = packCell(Material.Water, 0, this.rng.randomShade());
|
|
1509
|
+
dissolved++;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
// High temp: salt → molten salt (destructive liquid)
|
|
1516
|
+
if (temp > 1.8 && this.rng.random() < 0.02) {
|
|
1517
|
+
grid[idx] = packCell(Material.MoltenSalt, 0, this.rng.randomShade());
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// --- Molten Salt: destroys neighbors, cools to salt in low temp ---
|
|
1522
|
+
if (mat === Material.MoltenSalt) {
|
|
1523
|
+
if (temp < 0.5 && this.rng.random() < 0.05) {
|
|
1524
|
+
grid[idx] = packCell(Material.Salt, 0, this.rng.randomShade());
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
// Single 8-neighbor scan for water + dry ice
|
|
1528
|
+
let waterNi = -1, dryIceNi = -1;
|
|
1529
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1530
|
+
const ny = y + dy;
|
|
1531
|
+
if (ny < 0 || ny >= H)
|
|
1532
|
+
continue;
|
|
1533
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1534
|
+
if (dx === 0 && dy === 0)
|
|
1535
|
+
continue;
|
|
1536
|
+
const nx = x + dx;
|
|
1537
|
+
if (nx < 0 || nx >= W)
|
|
1538
|
+
continue;
|
|
1539
|
+
if (nx < wxMin || nx >= wxMax)
|
|
1540
|
+
continue;
|
|
1541
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
1542
|
+
if (nMat === Material.Water) {
|
|
1543
|
+
waterNi = ny * W + nx;
|
|
1544
|
+
}
|
|
1545
|
+
else if (nMat === Material.DryIce) {
|
|
1546
|
+
dryIceNi = ny * W + nx;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
// Contact with water → violent impulse explosion (steam pressure, not destruction)
|
|
1551
|
+
if (waterNi >= 0) {
|
|
1552
|
+
this.applyImpulse(x, y, 6, 100);
|
|
1553
|
+
// Convert water to steam, molten salt cools to salt
|
|
1554
|
+
grid[waterNi] = packCell(Material.Steam, 80, this.rng.randomShade());
|
|
1555
|
+
if (this.rng.random() < 0.3) {
|
|
1556
|
+
grid[idx] = packCell(Material.Salt, 0, this.rng.randomShade());
|
|
1557
|
+
}
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
// Contact with dry ice → violent impulse explosion (thermal shock)
|
|
1561
|
+
if (dryIceNi >= 0) {
|
|
1562
|
+
this.applyImpulse(x, y, 6, 100);
|
|
1563
|
+
// Consume the dry ice, cool molten salt to salt
|
|
1564
|
+
grid[dryIceNi] = 0;
|
|
1565
|
+
if (this.rng.random() < 0.5) {
|
|
1566
|
+
grid[idx] = packCell(Material.Salt, 0, this.rng.randomShade());
|
|
1567
|
+
}
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
// Ignite flammable neighbors on contact (molten salt is very hot)
|
|
1571
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1572
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1573
|
+
if (dx === 0 && dy === 0)
|
|
1574
|
+
continue;
|
|
1575
|
+
const nx = x + dx, ny = y + dy;
|
|
1576
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1577
|
+
continue;
|
|
1578
|
+
if (nx < wxMin || nx >= wxMax)
|
|
1579
|
+
continue;
|
|
1580
|
+
const ni = ny * W + nx;
|
|
1581
|
+
const nMat = grid[ni] & 0xff;
|
|
1582
|
+
if ((MAT_FLAGS[nMat] & MAT_FLAMMABLE) && this.rng.random() < 0.15) {
|
|
1583
|
+
if (nMat === Material.Oil) {
|
|
1584
|
+
// Oil needs air exposure to ignite — molten salt touching
|
|
1585
|
+
// buried oil heats it but can't sustain a flame without oxygen.
|
|
1586
|
+
if (!this.isExposed(nx, ny))
|
|
1587
|
+
continue;
|
|
1588
|
+
grid[ni] = packCell(Material.BurningOil, MAT_LIFETIME[Material.BurningOil], this.rng.randomShade());
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
grid[ni] = packCell(Material.Fire, 30, this.rng.randomShade());
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
// --- Concrete Powder + Water → Concrete ---
|
|
1599
|
+
if (mat === Material.ConcretePowder) {
|
|
1600
|
+
let waterNi = -1;
|
|
1601
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1602
|
+
const ny = y + dy;
|
|
1603
|
+
if (ny < 0 || ny >= H)
|
|
1604
|
+
continue;
|
|
1605
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1606
|
+
if (dx === 0 && dy === 0)
|
|
1607
|
+
continue;
|
|
1608
|
+
const nx = x + dx;
|
|
1609
|
+
if (nx < 0 || nx >= W)
|
|
1610
|
+
continue;
|
|
1611
|
+
if (nx < wxMin || nx >= wxMax)
|
|
1612
|
+
continue;
|
|
1613
|
+
if ((grid[ny * W + nx] & 0xff) === Material.Water) {
|
|
1614
|
+
waterNi = ny * W + nx;
|
|
1615
|
+
break;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (waterNi >= 0)
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
if (waterNi >= 0 && this.rng.random() < 0.2) {
|
|
1622
|
+
grid[idx] = packCell(Material.Concrete, 0, this.rng.randomShade());
|
|
1623
|
+
// Consume the water
|
|
1624
|
+
grid[waterNi] = 0;
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
// --- Snow: melts to water when in contact with hot materials or at
|
|
1629
|
+
// high ambient temperature. Hot neighbors: fire, fusefire, burning
|
|
1630
|
+
// oil, lava, molten salt, plasma. ---
|
|
1631
|
+
if (mat === Material.Snow) {
|
|
1632
|
+
// Single 8-neighbor scan using IS_HOT lookup table
|
|
1633
|
+
let hasHot = false;
|
|
1634
|
+
for (let dy = -1; dy <= 1 && !hasHot; dy++) {
|
|
1635
|
+
const ny = y + dy;
|
|
1636
|
+
if (ny < 0 || ny >= H)
|
|
1637
|
+
continue;
|
|
1638
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1639
|
+
if (dx === 0 && dy === 0)
|
|
1640
|
+
continue;
|
|
1641
|
+
const nx = x + dx;
|
|
1642
|
+
if (nx < 0 || nx >= W)
|
|
1643
|
+
continue;
|
|
1644
|
+
if (IS_HOT[grid[ny * W + nx] & 0xff]) {
|
|
1645
|
+
hasHot = true;
|
|
1646
|
+
break;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (hasHot) {
|
|
1651
|
+
// Direct contact with a hot material — melts quickly.
|
|
1652
|
+
if (this.rng.random() < 0.3) {
|
|
1653
|
+
grid[idx] = packCell(Material.Water, 0, this.rng.randomShade());
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
else if (temp > 1.3 && this.rng.random() < (temp - 1.3) * 0.05) {
|
|
1658
|
+
// High ambient temperature — melts gradually.
|
|
1659
|
+
grid[idx] = packCell(Material.Water, 0, this.rng.randomShade());
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
// --- Dry Ice: sublimates into CO2 gas (smoke-like, no water) ---
|
|
1664
|
+
if (mat === Material.DryIce) {
|
|
1665
|
+
if (this.rng.random() < 0.02) {
|
|
1666
|
+
if (y > 0 && grid[(y - 1) * W + x] === 0) {
|
|
1667
|
+
grid[(y - 1) * W + x] = packCell(Material.Smoke, 60, this.rng.randomShade());
|
|
1668
|
+
if (this.rng.random() < 0.5)
|
|
1669
|
+
grid[idx] = 0;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
// --- Liquid Nitrogen: cools neighbors, slowly evaporates into cold vapor ---
|
|
1675
|
+
if (mat === Material.LiquidNitrogen) {
|
|
1676
|
+
// Cool down nearby fire/lava
|
|
1677
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1678
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1679
|
+
if (dx === 0 && dy === 0)
|
|
1680
|
+
continue;
|
|
1681
|
+
const nx = x + dx, ny = y + dy;
|
|
1682
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1683
|
+
continue;
|
|
1684
|
+
const ni = ny * W + nx;
|
|
1685
|
+
const nMat = grid[ni] & 0xff;
|
|
1686
|
+
if (IS_HOT[nMat]) {
|
|
1687
|
+
grid[ni] = nMat === Material.Lava
|
|
1688
|
+
? packCell(Material.Stone, 0, this.rng.randomShade())
|
|
1689
|
+
: 0;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
// Slowly evaporate into ColdVapor — a visible white-ish gas that
|
|
1694
|
+
// doesn't rise (cold vapor is denser than air) and dissipates over
|
|
1695
|
+
// ~30 seconds. Low probability so the liquid phase lasts a while
|
|
1696
|
+
// before converting. Previously converted to Steam (which condensed
|
|
1697
|
+
// to Water) or dissipated directly to empty (invisible).
|
|
1698
|
+
if (this.rng.random() < 0.005) {
|
|
1699
|
+
grid[idx] = packCell(Material.ColdVapor, 255, this.rng.randomShade());
|
|
1700
|
+
}
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
// --- Seed: grows into tree on dirt, progressively from the bottom up ---
|
|
1704
|
+
// lifetime === 0: a normal falling seed. When it lands on dirt/grass,
|
|
1705
|
+
// it plants a Root and spawns a "growing tip" seed above with
|
|
1706
|
+
// lifetime = trunkHeight.
|
|
1707
|
+
// lifetime > 0: a growing tip. Each frame it places TreeWood at its
|
|
1708
|
+
// current position and moves one cell up (lifetime - 1). When
|
|
1709
|
+
// lifetime reaches 1 or the cell above is blocked, it places the
|
|
1710
|
+
// leaf canopy and is consumed — the tree is fully grown.
|
|
1711
|
+
if (mat === Material.Seed) {
|
|
1712
|
+
if (lifetime > 0) {
|
|
1713
|
+
// Growing tip — place TreeWood at current position
|
|
1714
|
+
grid[idx] = packCell(Material.TreeWood, 0, this.rng.randomShade());
|
|
1715
|
+
if (lifetime > 1 && y > 0 && grid[(y - 1) * W + x] === 0) {
|
|
1716
|
+
// Grow one cell higher next frame
|
|
1717
|
+
grid[(y - 1) * W + x] = packCell(Material.Seed, lifetime - 1, this.rng.randomShade());
|
|
1718
|
+
}
|
|
1719
|
+
else {
|
|
1720
|
+
// Reached target height or blocked — grow leaf canopy here
|
|
1721
|
+
this.growCanopy(x, y);
|
|
1722
|
+
}
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
// Falling seed — check if it landed on dirt/grass
|
|
1726
|
+
if (y + 1 < H) {
|
|
1727
|
+
const belowMat = grid[(y + 1) * W + x] & 0xff;
|
|
1728
|
+
if (belowMat === Material.Dirt || belowMat === Material.Grass) {
|
|
1729
|
+
if (this.rng.random() < 0.05) {
|
|
1730
|
+
// Plant root and start growing upward
|
|
1731
|
+
grid[idx] = packCell(Material.Root, 0, this.rng.randomShade());
|
|
1732
|
+
const trunkHeight = 8 + Math.floor(this.rng.random() * 8);
|
|
1733
|
+
if (y > 0 && grid[(y - 1) * W + x] === 0) {
|
|
1734
|
+
grid[(y - 1) * W + x] = packCell(Material.Seed, trunkHeight, this.rng.randomShade());
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
// Can't grow upward (blocked at base) — just grow a canopy
|
|
1738
|
+
this.growCanopy(x, y);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
// --- Grass: spreads on top of dirt ---
|
|
1746
|
+
if (mat === Material.Grass) {
|
|
1747
|
+
if (y + 1 < H && (grid[(y + 1) * W + x] & 0xff) === Material.Dirt) {
|
|
1748
|
+
// Spread sideways on dirt surface
|
|
1749
|
+
if (this.rng.random() < 0.02) {
|
|
1750
|
+
const dir = this.rng.random() < 0.5 ? -1 : 1;
|
|
1751
|
+
const nx = x + dir;
|
|
1752
|
+
if (nx >= 0 && nx < W) {
|
|
1753
|
+
if (y + 1 < H && (grid[(y + 1) * W + nx] & 0xff) === Material.Dirt && grid[y * W + nx] === 0) {
|
|
1754
|
+
grid[y * W + nx] = packCell(Material.Grass, 0, this.rng.randomShade());
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
// --- Nanobots: randomly move around and slowly eat through materials ---
|
|
1762
|
+
if (mat === Material.Nanobots) {
|
|
1763
|
+
// Pick a random direction to move/eat (inline the 8 dirs to avoid array alloc)
|
|
1764
|
+
const NANOBOT_DIRS = [1, 0, -1, 0, 0, 1, 0, -1, 1, 1, -1, -1, 1, -1, -1, 1];
|
|
1765
|
+
const di = Math.floor(this.rng.random() * 8) * 2;
|
|
1766
|
+
const dx = NANOBOT_DIRS[di], dy = NANOBOT_DIRS[di + 1];
|
|
1767
|
+
const nx = x + dx, ny = y + dy;
|
|
1768
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax) {
|
|
1769
|
+
const ni = ny * W + nx;
|
|
1770
|
+
const nMat = grid[ni] & 0xff;
|
|
1771
|
+
if (nMat === Material.Empty) {
|
|
1772
|
+
// Move into empty space
|
|
1773
|
+
grid[ni] = packCell(mat, lifetime, flags | FLAG_UPDATED);
|
|
1774
|
+
grid[idx] = 0;
|
|
1775
|
+
}
|
|
1776
|
+
else if (nMat !== Material.Nanobots && nMat !== Material.Wall &&
|
|
1777
|
+
nMat !== Material.Antimatter) {
|
|
1778
|
+
// Eat through the material — slowly destroy it (5% chance per frame)
|
|
1779
|
+
if (this.rng.random() < 0.05) {
|
|
1780
|
+
grid[ni] = 0;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
// --- Magic Powder: random color flicker, explodes on flesh, decays like fire ---
|
|
1787
|
+
if (mat === Material.MagicPowder) {
|
|
1788
|
+
// Random shade flicker each frame (independent per particle, no spatial pattern)
|
|
1789
|
+
if (this.rng.random() < 0.5) {
|
|
1790
|
+
const newShade = Math.floor(this.rng.random() * 4);
|
|
1791
|
+
grid[idx] = packCell(mat, lifetime, (flags & ~SHADE_MASK) | newShade | FLAG_UPDATED);
|
|
1792
|
+
}
|
|
1793
|
+
// Explodes on contact with flesh
|
|
1794
|
+
let hasFlesh = false;
|
|
1795
|
+
for (let dy = -1; dy <= 1 && !hasFlesh; dy++) {
|
|
1796
|
+
const ny = y + dy;
|
|
1797
|
+
if (ny < 0 || ny >= H)
|
|
1798
|
+
continue;
|
|
1799
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1800
|
+
if (dx === 0 && dy === 0)
|
|
1801
|
+
continue;
|
|
1802
|
+
const nx = x + dx;
|
|
1803
|
+
if (nx < 0 || nx >= W)
|
|
1804
|
+
continue;
|
|
1805
|
+
if ((grid[ny * W + nx] & 0xff) === Material.Flesh) {
|
|
1806
|
+
hasFlesh = true;
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (hasFlesh && this.rng.random() < 0.3) {
|
|
1812
|
+
this.explode(x, y, 4);
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
// Emit fireflies in a random direction (not always upward)
|
|
1816
|
+
if (this.rng.random() < 0.02) {
|
|
1817
|
+
const dx = Math.floor(this.rng.random() * 3) - 1;
|
|
1818
|
+
const dy = Math.floor(this.rng.random() * 3) - 1;
|
|
1819
|
+
const nx = x + dx, ny = y + dy;
|
|
1820
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax && grid[ny * W + nx] === 0) {
|
|
1821
|
+
grid[ny * W + nx] = packCell(Material.Fireflies, 255, this.rng.randomShade());
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
// --- Popcorn: pops like fireworks near fire/lava/molten salt or high heat ---
|
|
1827
|
+
if (mat === Material.Popcorn) {
|
|
1828
|
+
// Already-popped popcorn (FLAG_POPPED) never pops again — prevents an
|
|
1829
|
+
// infinite creation loop where scattered popcorn re-triggers the pop
|
|
1830
|
+
// reaction and spawns ever more popcorn.
|
|
1831
|
+
if (flags & FLAG_POPPED)
|
|
1832
|
+
continue;
|
|
1833
|
+
// Single 8-neighbor scan using IS_HOT lookup table
|
|
1834
|
+
let hasHot = temp > 1.3;
|
|
1835
|
+
for (let dy = -1; dy <= 1 && !hasHot; dy++) {
|
|
1836
|
+
const ny = y + dy;
|
|
1837
|
+
if (ny < 0 || ny >= H)
|
|
1838
|
+
continue;
|
|
1839
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1840
|
+
if (dx === 0 && dy === 0)
|
|
1841
|
+
continue;
|
|
1842
|
+
const nx = x + dx;
|
|
1843
|
+
if (nx < 0 || nx >= W)
|
|
1844
|
+
continue;
|
|
1845
|
+
if (IS_HOT[grid[ny * W + nx] & 0xff]) {
|
|
1846
|
+
hasHot = true;
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (hasHot && this.rng.random() < 0.3) {
|
|
1852
|
+
// Fireworks pop: radial impulse + scatter popcorn particles outward.
|
|
1853
|
+
// All resulting popcorn (scattered + original) is marked FLAG_POPPED
|
|
1854
|
+
// so it won't pop again — each kernel pops exactly once.
|
|
1855
|
+
this.applyImpulse(x, y, 4, 60);
|
|
1856
|
+
// Scatter popcorn particles in random directions (marked FLAG_POPPED)
|
|
1857
|
+
const POPCORN_DIRS = [-1, -1, 0, -1, 1, -1, -1, 0, 1, 0, -1, 1, 0, 1, 1, 1];
|
|
1858
|
+
for (let di = 0; di < 16; di += 2) {
|
|
1859
|
+
if (this.rng.random() < 0.6) {
|
|
1860
|
+
const nx = x + POPCORN_DIRS[di], ny = y + POPCORN_DIRS[di + 1];
|
|
1861
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax && grid[ny * W + nx] === 0) {
|
|
1862
|
+
grid[ny * W + nx] = packCell(Material.Popcorn, 0, FLAG_POPPED | this.rng.randomShade());
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
// Mark the original kernel as popped so it doesn't re-pop next frame.
|
|
1867
|
+
grid[idx] = packCell(Material.Popcorn, 0, (flags & ~FLAG_UPDATED) | FLAG_POPPED);
|
|
1868
|
+
// Sometimes launch a popped popcorn particle upward
|
|
1869
|
+
if (this.rng.random() < 0.5 && y > 0 && grid[(y - 1) * W + x] === 0) {
|
|
1870
|
+
grid[(y - 1) * W + x] = packCell(Material.Popcorn, 0, FLAG_POPPED | this.rng.randomShade());
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
// --- Dynamite: detonated by fire or fuse, chain-detonates connected sticks ---
|
|
1876
|
+
if (mat === Material.Dynamite) {
|
|
1877
|
+
// Single 8-neighbor scan for fire-class + fuse
|
|
1878
|
+
let hasFireOrFuse = false;
|
|
1879
|
+
for (let dy = -1; dy <= 1 && !hasFireOrFuse; dy++) {
|
|
1880
|
+
const ny = y + dy;
|
|
1881
|
+
if (ny < 0 || ny >= H)
|
|
1882
|
+
continue;
|
|
1883
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1884
|
+
if (dx === 0 && dy === 0)
|
|
1885
|
+
continue;
|
|
1886
|
+
const nx = x + dx;
|
|
1887
|
+
if (nx < 0 || nx >= W)
|
|
1888
|
+
continue;
|
|
1889
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
1890
|
+
if (IS_FIRE[nMat] || nMat === Material.Fuse) {
|
|
1891
|
+
hasFireOrFuse = true;
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
// 100% trigger chance — dynamite is unstable, it should detonate
|
|
1897
|
+
// immediately when lit, not sit there for frames while fire spreads.
|
|
1898
|
+
if (hasFireOrFuse) {
|
|
1899
|
+
this.detonateDynamite(x, y);
|
|
1900
|
+
}
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
// --- C4: only detonates when a burning fuse is adjacent.
|
|
1904
|
+
// C4 itself burns like wax (flammable, slow burn) but does NOT explode from fire alone.
|
|
1905
|
+
// Chain-detonates all connected C4 via flood fill. ---
|
|
1906
|
+
if (mat === Material.C4) {
|
|
1907
|
+
// C4 only detonates when adjacent to FuseFire (fire from a burning fuse).
|
|
1908
|
+
// Regular fire does NOT trigger C4 — only fuse fire does.
|
|
1909
|
+
let fuseBurning = false;
|
|
1910
|
+
for (let dy = -1; dy <= 1 && !fuseBurning; dy++) {
|
|
1911
|
+
for (let dx = -1; dx <= 1 && !fuseBurning; dx++) {
|
|
1912
|
+
if (dx === 0 && dy === 0)
|
|
1913
|
+
continue;
|
|
1914
|
+
const nx = x + dx, ny = y + dy;
|
|
1915
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
1916
|
+
continue;
|
|
1917
|
+
if ((grid[ny * W + nx] & 0xff) === Material.FuseFire) {
|
|
1918
|
+
fuseBurning = true;
|
|
1919
|
+
break;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
if (fuseBurning && this.rng.random() < 0.3) {
|
|
1924
|
+
this.detonateC4(x, y);
|
|
1925
|
+
}
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
// --- Acid: eats adjacent materials, 50% consumed per eat ---
|
|
1929
|
+
// Each frame, acid scans its 8 neighbors for a non-immune material.
|
|
1930
|
+
// If found, it destroys the neighbor and has a 50% chance of being
|
|
1931
|
+
// consumed itself. This gives a ~2:1 ratio (2 material eaten per 1
|
|
1932
|
+
// acid consumed), so 1000 sand + 1000 acid → ~500 acid remaining.
|
|
1933
|
+
// Acid does NOT eat: Empty, Wall, Acid, Base. Base is handled by the
|
|
1934
|
+
// neutralization reaction (base section below).
|
|
1935
|
+
if (mat === Material.Acid) {
|
|
1936
|
+
// First: check for adjacent Base — neutralization takes priority
|
|
1937
|
+
// (acid + base → salt + steam). Fast reaction — 50% chance per frame.
|
|
1938
|
+
let baseNi = -1;
|
|
1939
|
+
for (let dy = -1; dy <= 1 && baseNi < 0; dy++) {
|
|
1940
|
+
const ny = y + dy;
|
|
1941
|
+
if (ny < 0 || ny >= H)
|
|
1942
|
+
continue;
|
|
1943
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1944
|
+
if (dx === 0 && dy === 0)
|
|
1945
|
+
continue;
|
|
1946
|
+
const nx = x + dx;
|
|
1947
|
+
if (nx < 0 || nx >= W)
|
|
1948
|
+
continue;
|
|
1949
|
+
if (nx < wxMin || nx >= wxMax)
|
|
1950
|
+
continue;
|
|
1951
|
+
if ((grid[ny * W + nx] & 0xff) === Material.Base) {
|
|
1952
|
+
baseNi = ny * W + nx;
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (baseNi >= 0 && this.rng.random() < 0.5) {
|
|
1958
|
+
// Neutralization: acid + base → salt + steam
|
|
1959
|
+
grid[idx] = packCell(Material.Salt, 0, this.rng.randomShade());
|
|
1960
|
+
grid[baseNi] = packCell(Material.Steam, 80, this.rng.randomShade());
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
// Otherwise: eat an adjacent non-immune material
|
|
1964
|
+
if (this.rng.random() < 0.15) {
|
|
1965
|
+
// Pick a random neighbor to eat (avoids eating all 8 at once)
|
|
1966
|
+
const eatDir = Math.floor(this.rng.random() * 8);
|
|
1967
|
+
const EAT_DX = [-1, 0, 1, -1, 1, -1, 0, 1];
|
|
1968
|
+
const EAT_DY = [-1, -1, -1, 0, 0, 1, 1, 1];
|
|
1969
|
+
const nx = x + EAT_DX[eatDir];
|
|
1970
|
+
const ny = y + EAT_DY[eatDir];
|
|
1971
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax) {
|
|
1972
|
+
const ni = ny * W + nx;
|
|
1973
|
+
const nMat = grid[ni] & 0xff;
|
|
1974
|
+
if (!IS_ACID_IMMUNE[nMat]) {
|
|
1975
|
+
// Eat the neighbor
|
|
1976
|
+
grid[ni] = 0;
|
|
1977
|
+
// 50% chance the acid is consumed by the reaction
|
|
1978
|
+
if (this.rng.random() < 0.5) {
|
|
1979
|
+
grid[idx] = 0;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
// --- Base: neutralizes acid on contact (acid + base → salt + steam) ---
|
|
1987
|
+
// Mirrors the acid-side check so the reaction triggers from either
|
|
1988
|
+
// material's perspective. Without this, if only acid checks for base,
|
|
1989
|
+
// a base cell surrounded by acid might not react if none of the acid
|
|
1990
|
+
// cells happen to scan it this frame.
|
|
1991
|
+
if (mat === Material.Base) {
|
|
1992
|
+
let acidNi = -1;
|
|
1993
|
+
for (let dy = -1; dy <= 1 && acidNi < 0; dy++) {
|
|
1994
|
+
const ny = y + dy;
|
|
1995
|
+
if (ny < 0 || ny >= H)
|
|
1996
|
+
continue;
|
|
1997
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1998
|
+
if (dx === 0 && dy === 0)
|
|
1999
|
+
continue;
|
|
2000
|
+
const nx = x + dx;
|
|
2001
|
+
if (nx < 0 || nx >= W)
|
|
2002
|
+
continue;
|
|
2003
|
+
if (nx < wxMin || nx >= wxMax)
|
|
2004
|
+
continue;
|
|
2005
|
+
if ((grid[ny * W + nx] & 0xff) === Material.Acid) {
|
|
2006
|
+
acidNi = ny * W + nx;
|
|
2007
|
+
break;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (acidNi >= 0 && this.rng.random() < 0.5) {
|
|
2012
|
+
// Neutralization: base + acid → steam + salt
|
|
2013
|
+
grid[idx] = packCell(Material.Steam, 80, this.rng.randomShade());
|
|
2014
|
+
grid[acidNi] = packCell(Material.Salt, 0, this.rng.randomShade());
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
// --- Flour: dust explosion when suspended near fire ---
|
|
2019
|
+
if (mat === Material.Flour) {
|
|
2020
|
+
let hasFire = false;
|
|
2021
|
+
for (let dy = -1; dy <= 1 && !hasFire; dy++) {
|
|
2022
|
+
const ny = y + dy;
|
|
2023
|
+
if (ny < 0 || ny >= H)
|
|
2024
|
+
continue;
|
|
2025
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2026
|
+
if (dx === 0 && dy === 0)
|
|
2027
|
+
continue;
|
|
2028
|
+
const nx = x + dx;
|
|
2029
|
+
if (nx < 0 || nx >= W)
|
|
2030
|
+
continue;
|
|
2031
|
+
if (IS_FIRE[grid[ny * W + nx] & 0xff]) {
|
|
2032
|
+
hasFire = true;
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (hasFire && this.rng.random() < 0.15) {
|
|
2038
|
+
this.explode(x, y, 3);
|
|
2039
|
+
}
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
// --- Hydrogen: explodes near fire ---
|
|
2043
|
+
if (mat === Material.Hydrogen) {
|
|
2044
|
+
let hasFire = false;
|
|
2045
|
+
for (let dy = -1; dy <= 1 && !hasFire; dy++) {
|
|
2046
|
+
const ny = y + dy;
|
|
2047
|
+
if (ny < 0 || ny >= H)
|
|
2048
|
+
continue;
|
|
2049
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2050
|
+
if (dx === 0 && dy === 0)
|
|
2051
|
+
continue;
|
|
2052
|
+
const nx = x + dx;
|
|
2053
|
+
if (nx < 0 || nx >= W)
|
|
2054
|
+
continue;
|
|
2055
|
+
if (IS_FIRE[grid[ny * W + nx] & 0xff]) {
|
|
2056
|
+
hasFire = true;
|
|
2057
|
+
break;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
if (hasFire && this.rng.random() < 0.3) {
|
|
2062
|
+
this.explode(x, y, 4);
|
|
2063
|
+
}
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
// --- Glitter: suspends in water, floats in air ---
|
|
2067
|
+
// (handled in tryMove via low gravity — no special reaction needed)
|
|
2068
|
+
// --- Fireflies: random flicker + organic flight, spread out (no buoyancy) ---
|
|
2069
|
+
if (mat === Material.Fireflies) {
|
|
2070
|
+
// Flicker: each firefly independently picks a random shade each frame.
|
|
2071
|
+
// Uses cell.lifetime as a per-particle phase seed so neighbors don't sync.
|
|
2072
|
+
if (this.rng.random() < 0.5) {
|
|
2073
|
+
const newShade = Math.floor(this.rng.random() * 4);
|
|
2074
|
+
grid[idx] = packCell(mat, lifetime, (flags & ~SHADE_MASK) | newShade | FLAG_UPDATED);
|
|
2075
|
+
}
|
|
2076
|
+
// Flight: pure random walk in all 8 directions + occasional darts.
|
|
2077
|
+
// No gravity/buoyancy — they spread out evenly in all directions.
|
|
2078
|
+
if (this.rng.random() < 0.7) {
|
|
2079
|
+
let dx, dy;
|
|
2080
|
+
if (this.rng.random() < 0.2) {
|
|
2081
|
+
// Dart: 2-3 cell jump for organic burst movement
|
|
2082
|
+
dx = Math.floor(this.rng.random() * 7) - 3;
|
|
2083
|
+
dy = Math.floor(this.rng.random() * 7) - 3;
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
// Normal: 1-cell step in any of 8 directions (uniform)
|
|
2087
|
+
dx = Math.floor(this.rng.random() * 3) - 1;
|
|
2088
|
+
dy = Math.floor(this.rng.random() * 3) - 1;
|
|
2089
|
+
}
|
|
2090
|
+
const nx = x + dx, ny = y + dy;
|
|
2091
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && grid[ny * W + nx] === 0) {
|
|
2092
|
+
grid[ny * W + nx] = packCell(mat, lifetime, flags | FLAG_UPDATED);
|
|
2093
|
+
grid[idx] = 0;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
// --- Toast: made from bread near fire (future), burns like wood ---
|
|
2099
|
+
// --- Wax: slow burning (handled by combustion + aging) ---
|
|
2100
|
+
// --- Rubber: bouncy (handled in tryMove) ---
|
|
2101
|
+
// ===============================================================
|
|
2102
|
+
// Game-specific reactions
|
|
2103
|
+
// Minimal physical reactions; most "mixing" is analyzer-driven
|
|
2104
|
+
// effect-vector math in the game, not cell transforms here.
|
|
2105
|
+
// ===============================================================
|
|
2106
|
+
// --- Ether + Fire → EtherealVapor (glowing gas byproduct) ---
|
|
2107
|
+
if (mat === Material.Ether) {
|
|
2108
|
+
let fireNi = -1;
|
|
2109
|
+
for (let dy = -1; dy <= 1 && fireNi < 0; dy++) {
|
|
2110
|
+
const ny = y + dy;
|
|
2111
|
+
if (ny < 0 || ny >= H)
|
|
2112
|
+
continue;
|
|
2113
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2114
|
+
if (dx === 0 && dy === 0)
|
|
2115
|
+
continue;
|
|
2116
|
+
const nx = x + dx;
|
|
2117
|
+
if (nx < 0 || nx >= W)
|
|
2118
|
+
continue;
|
|
2119
|
+
if (IS_FIRE[grid[ny * W + nx] & 0xff]) {
|
|
2120
|
+
fireNi = ny * W + nx;
|
|
2121
|
+
break;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (fireNi >= 0 && this.rng.random() < 0.3) {
|
|
2126
|
+
grid[idx] = packCell(Material.EtherealVapor, 120, this.rng.randomShade());
|
|
2127
|
+
grid[fireNi] = packCell(Material.Smoke, 40, this.rng.randomShade());
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
// High temp: ether evaporates to ethereal vapor
|
|
2131
|
+
if (temp > 1.4 && this.rng.random() < (temp - 1.4) * 0.03) {
|
|
2132
|
+
grid[idx] = packCell(Material.EtherealVapor, 120, this.rng.randomShade());
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
// --- Sulfur + Water → corrosive reaction (produces AlchemicalSlag slowly) ---
|
|
2137
|
+
if (mat === Material.Sulfur) {
|
|
2138
|
+
let waterNi = -1;
|
|
2139
|
+
for (let dy = -1; dy <= 1 && waterNi < 0; dy++) {
|
|
2140
|
+
const ny = y + dy;
|
|
2141
|
+
if (ny < 0 || ny >= H)
|
|
2142
|
+
continue;
|
|
2143
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2144
|
+
if (dx === 0 && dy === 0)
|
|
2145
|
+
continue;
|
|
2146
|
+
const nx = x + dx;
|
|
2147
|
+
if (nx < 0 || nx >= W)
|
|
2148
|
+
continue;
|
|
2149
|
+
if ((grid[ny * W + nx] & 0xff) === Material.Water) {
|
|
2150
|
+
waterNi = ny * W + nx;
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
if (waterNi >= 0 && temp > 1.0 && this.rng.random() < 0.04) {
|
|
2156
|
+
grid[idx] = packCell(Material.AlchemicalSlag, 0, this.rng.randomShade());
|
|
2157
|
+
grid[waterNi] = 0; // consume the water
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
// --- Blood + BoneDust → necrotic reaction (slow, produces Slag) ---
|
|
2162
|
+
if (mat === Material.Blood) {
|
|
2163
|
+
let boneNi = -1;
|
|
2164
|
+
for (let dy = -1; dy <= 1 && boneNi < 0; dy++) {
|
|
2165
|
+
const ny = y + dy;
|
|
2166
|
+
if (ny < 0 || ny >= H)
|
|
2167
|
+
continue;
|
|
2168
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2169
|
+
if (dx === 0 && dy === 0)
|
|
2170
|
+
continue;
|
|
2171
|
+
const nx = x + dx;
|
|
2172
|
+
if (nx < 0 || nx >= W)
|
|
2173
|
+
continue;
|
|
2174
|
+
if ((grid[ny * W + nx] & 0xff) === Material.BoneDust) {
|
|
2175
|
+
boneNi = ny * W + nx;
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (boneNi >= 0 && this.rng.random() < 0.02) {
|
|
2181
|
+
grid[idx] = packCell(Material.AlchemicalSlag, 0, this.rng.randomShade());
|
|
2182
|
+
grid[boneNi] = 0;
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
// --- MushroomSpores + Water → grows into Plant (mild toxic reaction) ---
|
|
2187
|
+
if (mat === Material.MushroomSpores) {
|
|
2188
|
+
let waterNi = -1;
|
|
2189
|
+
for (let dy = -1; dy <= 1 && waterNi < 0; dy++) {
|
|
2190
|
+
const ny = y + dy;
|
|
2191
|
+
if (ny < 0 || ny >= H)
|
|
2192
|
+
continue;
|
|
2193
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2194
|
+
if (dx === 0 && dy === 0)
|
|
2195
|
+
continue;
|
|
2196
|
+
const nx = x + dx;
|
|
2197
|
+
if (nx < 0 || nx >= W)
|
|
2198
|
+
continue;
|
|
2199
|
+
if ((grid[ny * W + nx] & 0xff) === Material.Water) {
|
|
2200
|
+
waterNi = ny * W + nx;
|
|
2201
|
+
break;
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
if (waterNi >= 0 && this.rng.random() < 0.03) {
|
|
2206
|
+
grid[idx] = packCell(Material.Plant, 0, this.rng.randomShade());
|
|
2207
|
+
continue;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
// --- PhoenixFeather near fire/lava: glows brighter, resists burning ---
|
|
2211
|
+
// (flammable but long burnTime already set; here we just let it shimmer)
|
|
2212
|
+
// --- DragonScale: very dense, sinks through liquids (handled by density) ---
|
|
2213
|
+
// --- LiquidShadow + light source (fire/plasma) → dissipates to smoke ---
|
|
2214
|
+
if (mat === Material.LiquidShadow) {
|
|
2215
|
+
let lightNi = -1;
|
|
2216
|
+
for (let dy = -1; dy <= 1 && lightNi < 0; dy++) {
|
|
2217
|
+
const ny = y + dy;
|
|
2218
|
+
if (ny < 0 || ny >= H)
|
|
2219
|
+
continue;
|
|
2220
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2221
|
+
if (dx === 0 && dy === 0)
|
|
2222
|
+
continue;
|
|
2223
|
+
const nx = x + dx;
|
|
2224
|
+
if (nx < 0 || nx >= W)
|
|
2225
|
+
continue;
|
|
2226
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
2227
|
+
if (nMat === Material.Fire || nMat === Material.Plasma || nMat === Material.StarShard) {
|
|
2228
|
+
lightNi = ny * W + nx;
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (lightNi >= 0 && this.rng.random() < 0.08) {
|
|
2234
|
+
grid[idx] = packCell(Material.Smoke, 80, this.rng.randomShade());
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
// --- VoidEssence + any organic (Blood/Flesh/Plant) → violent annihilation ---
|
|
2239
|
+
if (mat === Material.VoidEssence) {
|
|
2240
|
+
let organicNi = -1;
|
|
2241
|
+
for (let dy = -1; dy <= 1 && organicNi < 0; dy++) {
|
|
2242
|
+
const ny = y + dy;
|
|
2243
|
+
if (ny < 0 || ny >= H)
|
|
2244
|
+
continue;
|
|
2245
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2246
|
+
if (dx === 0 && dy === 0)
|
|
2247
|
+
continue;
|
|
2248
|
+
const nx = x + dx;
|
|
2249
|
+
if (nx < 0 || nx >= W)
|
|
2250
|
+
continue;
|
|
2251
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
2252
|
+
if (nMat === Material.Blood || nMat === Material.Flesh || nMat === Material.Plant ||
|
|
2253
|
+
nMat === Material.TrollBlood) {
|
|
2254
|
+
organicNi = ny * W + nx;
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
if (organicNi >= 0 && this.rng.random() < 0.05) {
|
|
2260
|
+
grid[organicNi] = 0;
|
|
2261
|
+
grid[idx] = packCell(Material.AlchemicalSlag, 0, this.rng.randomShade());
|
|
2262
|
+
this.applyImpulse(x, y, 3, 40);
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
// --- TimeSand + high temp → brief plasma flash (reality-bending) ---
|
|
2267
|
+
if (mat === Material.TimeSand && temp > 1.6 && this.rng.random() < 0.02) {
|
|
2268
|
+
grid[idx] = packCell(Material.Plasma, 20, this.rng.randomShade());
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
// --- StarShard: emits fireflies when in contact with ether/ethereal vapor ---
|
|
2272
|
+
if (mat === Material.StarShard) {
|
|
2273
|
+
let etherNi = -1;
|
|
2274
|
+
for (let dy = -1; dy <= 1 && etherNi < 0; dy++) {
|
|
2275
|
+
const ny = y + dy;
|
|
2276
|
+
if (ny < 0 || ny >= H)
|
|
2277
|
+
continue;
|
|
2278
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2279
|
+
if (dx === 0 && dy === 0)
|
|
2280
|
+
continue;
|
|
2281
|
+
const nx = x + dx;
|
|
2282
|
+
if (nx < 0 || nx >= W)
|
|
2283
|
+
continue;
|
|
2284
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
2285
|
+
if (nMat === Material.Ether || nMat === Material.EtherealVapor) {
|
|
2286
|
+
etherNi = ny * W + nx;
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (etherNi >= 0 && this.rng.random() < 0.04) {
|
|
2292
|
+
// Emit a firefly into a random empty neighbor
|
|
2293
|
+
const rdx = Math.floor(this.rng.random() * 3) - 1;
|
|
2294
|
+
const rdy = Math.floor(this.rng.random() * 3) - 1;
|
|
2295
|
+
const ex = x + rdx, ey = y + rdy;
|
|
2296
|
+
if (ex >= 0 && ex < W && ey >= 0 && ey < H && ex >= wxMin && ex < wxMax && grid[ey * W + ex] === 0) {
|
|
2297
|
+
grid[ey * W + ex] = packCell(Material.Fireflies, 255, this.rng.randomShade());
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
// --- Spore: floating mold spore. Germinates into Mold when adjacent to
|
|
2302
|
+
// a food material (wood/plant/leaf/tree wood/root/grass). The spore is
|
|
2303
|
+
// consumed (becomes Mold at its current position). Otherwise it drifts
|
|
2304
|
+
// as a gas (handled by tryMove) and slowly dissipates via lifetime. ---
|
|
2305
|
+
if (mat === Material.Spore) {
|
|
2306
|
+
let hasFood = false;
|
|
2307
|
+
for (let dy = -1; dy <= 1 && !hasFood; dy++) {
|
|
2308
|
+
const ny = y + dy;
|
|
2309
|
+
if (ny < 0 || ny >= H)
|
|
2310
|
+
continue;
|
|
2311
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2312
|
+
if (dx === 0 && dy === 0)
|
|
2313
|
+
continue;
|
|
2314
|
+
const nx = x + dx;
|
|
2315
|
+
if (nx < 0 || nx >= W)
|
|
2316
|
+
continue;
|
|
2317
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
2318
|
+
if (nMat === Material.Wood || nMat === Material.Plant || nMat === Material.Leaf ||
|
|
2319
|
+
nMat === Material.TreeWood || nMat === Material.Root || nMat === Material.Grass) {
|
|
2320
|
+
hasFood = true;
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
if (hasFood && this.rng.random() < 0.2) {
|
|
2326
|
+
grid[idx] = packCell(Material.Mold, 0, this.rng.randomShade());
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
// --- Mold: grows on wood/plant/leaf/tree wood/root/grass. Spreads very
|
|
2332
|
+
// slowly to an adjacent food cell (converts it to Mold). When no food
|
|
2333
|
+
// remains adjacent, releases a spore cloud into surrounding empty cells
|
|
2334
|
+
// and dies (clears to empty). Static solid (gravityDir=0) kept in the
|
|
2335
|
+
// active list via MAT_HAS_REACTIONS. ---
|
|
2336
|
+
if (mat === Material.Mold) {
|
|
2337
|
+
// Collect food neighbors + empty/gas neighbors (for spore release).
|
|
2338
|
+
let foodNi = -1;
|
|
2339
|
+
let foodCount = 0;
|
|
2340
|
+
let emptyCount = 0;
|
|
2341
|
+
const MOLD_DIRS = [1, 0, -1, 0, 0, 1, 0, -1, 1, 1, -1, -1, 1, -1, -1, 1];
|
|
2342
|
+
for (let di = 0; di < 16; di += 2) {
|
|
2343
|
+
const nx = x + MOLD_DIRS[di], ny = y + MOLD_DIRS[di + 1];
|
|
2344
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2345
|
+
continue;
|
|
2346
|
+
if (nx < wxMin || nx >= wxMax)
|
|
2347
|
+
continue;
|
|
2348
|
+
const ni = ny * W + nx;
|
|
2349
|
+
const nMat = grid[ni] & 0xff;
|
|
2350
|
+
if (nMat === Material.Wood || nMat === Material.Plant || nMat === Material.Leaf ||
|
|
2351
|
+
nMat === Material.TreeWood || nMat === Material.Root || nMat === Material.Grass) {
|
|
2352
|
+
foodCount++;
|
|
2353
|
+
if (foodNi < 0)
|
|
2354
|
+
foodNi = ni;
|
|
2355
|
+
}
|
|
2356
|
+
else if (nMat === Material.Empty || (MAT_FLAGS[nMat] & MAT_GAS)) {
|
|
2357
|
+
emptyCount++;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (foodCount > 0) {
|
|
2361
|
+
// Spread slowly: ~2% chance per frame to convert one food neighbor
|
|
2362
|
+
// to Mold. At 60fps that's ~0.8 seconds per spread step — slow
|
|
2363
|
+
// enough to be visible as creeping growth, fast enough to spread
|
|
2364
|
+
// across a food block in reasonable time.
|
|
2365
|
+
if (foodNi >= 0 && this.rng.random() < 0.02) {
|
|
2366
|
+
grid[foodNi] = packCell(Material.Mold, 0, this.rng.randomShade());
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
else if (emptyCount > 0) {
|
|
2370
|
+
// No food left — release a spore cloud and die. Spawn a few Spore
|
|
2371
|
+
// particles into surrounding empty/gas cells, then clear self.
|
|
2372
|
+
for (let di = 0; di < 16; di += 2) {
|
|
2373
|
+
if (this.rng.random() < 0.6) {
|
|
2374
|
+
const nx = x + MOLD_DIRS[di], ny = y + MOLD_DIRS[di + 1];
|
|
2375
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2376
|
+
continue;
|
|
2377
|
+
if (nx < wxMin || nx >= wxMax)
|
|
2378
|
+
continue;
|
|
2379
|
+
const ni = ny * W + nx;
|
|
2380
|
+
const nMat = grid[ni] & 0xff;
|
|
2381
|
+
if (nMat === Material.Empty || (MAT_FLAGS[nMat] & MAT_GAS)) {
|
|
2382
|
+
grid[ni] = packCell(Material.Spore, MAT_LIFETIME[Material.Spore], this.rng.randomShade());
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
grid[idx] = 0;
|
|
2387
|
+
}
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
// --- Glitch: randomly swaps places with a neighboring non-empty cell.
|
|
2391
|
+
// Any non-empty neighbor (any material except Empty) is a valid swap
|
|
2392
|
+
// target. The swapped-in material takes the glitch's old position and
|
|
2393
|
+
// vice versa. Falls like a normal solid when it can't swap. ---
|
|
2394
|
+
if (mat === Material.Glitch) {
|
|
2395
|
+
if (this.rng.random() < 0.3) {
|
|
2396
|
+
// Pick a random 8-direction and swap with whatever is there.
|
|
2397
|
+
const GLITCH_DIRS = [1, 0, -1, 0, 0, 1, 0, -1, 1, 1, -1, -1, 1, -1, -1, 1];
|
|
2398
|
+
const di = Math.floor(this.rng.random() * 8) * 2;
|
|
2399
|
+
const nx = x + GLITCH_DIRS[di], ny = y + GLITCH_DIRS[di + 1];
|
|
2400
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax) {
|
|
2401
|
+
const ni = ny * W + nx;
|
|
2402
|
+
const nPacked = grid[ni];
|
|
2403
|
+
if (nPacked !== 0 && !((nPacked >> 16) & FLAG_UPDATED)) {
|
|
2404
|
+
// Swap: glitch moves to neighbor, neighbor moves to glitch's spot.
|
|
2405
|
+
// Both marked FLAG_UPDATED so neither is reprocessed this frame.
|
|
2406
|
+
grid[ni] = packed | FLAG_UPDATED_BIT;
|
|
2407
|
+
grid[idx] = nPacked | FLAG_UPDATED_BIT;
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
// --- Duplicator: static solid that clones the first material to touch
|
|
2415
|
+
// it. The locked material id is stored in the lifetime field
|
|
2416
|
+
// (0 = not yet locked). Each frame, if locked, it spawns the locked
|
|
2417
|
+
// material into an adjacent empty cell. Does not move, does not react
|
|
2418
|
+
// otherwise (acid-immune, skipped by antimatter). ---
|
|
2419
|
+
if (mat === Material.Duplicator) {
|
|
2420
|
+
const locked = lifetime; // 0 = not locked; otherwise = material id
|
|
2421
|
+
if (locked === 0) {
|
|
2422
|
+
// Not yet locked — scan neighbors for a material to lock onto.
|
|
2423
|
+
// Skip Empty, Wall, other Duplicators, Antimatter (would be
|
|
2424
|
+
// catastrophically destructive), Acid, and Base — the duplicator
|
|
2425
|
+
// should not react with these at all (like Wall). Cloning acid
|
|
2426
|
+
// would spread corrosion through the duplicator; cloning base is
|
|
2427
|
+
// equally undesirable.
|
|
2428
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2429
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2430
|
+
if (dx === 0 && dy === 0)
|
|
2431
|
+
continue;
|
|
2432
|
+
const nx = x + dx, ny = y + dy;
|
|
2433
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2434
|
+
continue;
|
|
2435
|
+
if (nx < wxMin || nx >= wxMax)
|
|
2436
|
+
continue;
|
|
2437
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
2438
|
+
if (nMat !== Material.Empty && nMat !== Material.Wall &&
|
|
2439
|
+
nMat !== Material.Duplicator && nMat !== Material.Antimatter &&
|
|
2440
|
+
nMat !== Material.Acid && nMat !== Material.Base) {
|
|
2441
|
+
// Lock onto this material (store its id in the lifetime field).
|
|
2442
|
+
grid[idx] = packCell(Material.Duplicator, nMat, flags & ~FLAG_UPDATED);
|
|
2443
|
+
break;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
if (((grid[idx] >> 8) & 0xff) !== 0)
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
else {
|
|
2451
|
+
// Locked — spawn the locked material into an adjacent empty cell,
|
|
2452
|
+
// and slowly propagate the lock to adjacent unlocked Duplicators.
|
|
2453
|
+
// Low chance per frame so it produces a steady trickle, not a flood.
|
|
2454
|
+
if (this.rng.random() < 0.15) {
|
|
2455
|
+
const DUP_DIRS = [1, 0, -1, 0, 0, 1, 0, -1, 1, 1, -1, -1, 1, -1, -1, 1];
|
|
2456
|
+
const di = Math.floor(this.rng.random() * 8) * 2;
|
|
2457
|
+
const nx = x + DUP_DIRS[di], ny = y + DUP_DIRS[di + 1];
|
|
2458
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax) {
|
|
2459
|
+
const ni = ny * W + nx;
|
|
2460
|
+
if (grid[ni] === 0) {
|
|
2461
|
+
const spawnMat = locked;
|
|
2462
|
+
grid[ni] = packCell(spawnMat, MAT_LIFETIME[spawnMat], this.rng.randomShade());
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
// Propagate lock to adjacent unlocked Duplicators — same low chance
|
|
2467
|
+
// so the lock creeps across a duplicator cluster slowly.
|
|
2468
|
+
if (this.rng.random() < 0.15) {
|
|
2469
|
+
const PROP_DIRS = [1, 0, -1, 0, 0, 1, 0, -1];
|
|
2470
|
+
const di = Math.floor(this.rng.random() * 4) * 2;
|
|
2471
|
+
const nx = x + PROP_DIRS[di], ny = y + PROP_DIRS[di + 1];
|
|
2472
|
+
if (nx >= 0 && nx < W && ny >= 0 && ny < H && nx >= wxMin && nx < wxMax) {
|
|
2473
|
+
const ni = ny * W + nx;
|
|
2474
|
+
const nPacked = grid[ni];
|
|
2475
|
+
// Only lock an adjacent Duplicator that is NOT yet locked
|
|
2476
|
+
// (lifetime === 0). Don't overwrite an existing lock — a
|
|
2477
|
+
// duplicator that already locked onto a different material
|
|
2478
|
+
// keeps its own lock.
|
|
2479
|
+
if ((nPacked & 0xff) === Material.Duplicator &&
|
|
2480
|
+
((nPacked >> 8) & 0xff) === 0) {
|
|
2481
|
+
grid[ni] = packCell(Material.Duplicator, locked, nPacked >> 16 & 0xff);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
// --- Void: a static solid that swallows up any material that touches
|
|
2489
|
+
// it. Each frame, every adjacent non-empty cell (except Wall, other
|
|
2490
|
+
// Void, and Duplicator — those are permanent barriers) is destroyed
|
|
2491
|
+
// (set to empty). The void itself is never consumed. Acid-immune,
|
|
2492
|
+
// antimatter barrier, never falls. MAT_HAS_REACTIONS keeps it active. ---
|
|
2493
|
+
if (mat === Material.Void) {
|
|
2494
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2495
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2496
|
+
if (dx === 0 && dy === 0)
|
|
2497
|
+
continue;
|
|
2498
|
+
const nx = x + dx, ny = y + dy;
|
|
2499
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2500
|
+
continue;
|
|
2501
|
+
if (nx < wxMin || nx >= wxMax)
|
|
2502
|
+
continue;
|
|
2503
|
+
const ni = ny * W + nx;
|
|
2504
|
+
const nMat = grid[ni] & 0xff;
|
|
2505
|
+
// Swallow everything except Empty, Wall, Void, Duplicator, and
|
|
2506
|
+
// Antimatter (those are permanent — void doesn't eat itself or
|
|
2507
|
+
// other exotic barriers).
|
|
2508
|
+
if (nMat !== Material.Empty && nMat !== Material.Wall &&
|
|
2509
|
+
nMat !== Material.Void && nMat !== Material.Duplicator &&
|
|
2510
|
+
nMat !== Material.Antimatter) {
|
|
2511
|
+
grid[ni] = 0;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Apply a radial impulse: immediately shoves particles outward from (cx, cy)
|
|
2521
|
+
* and sets per-cell wind fields for ongoing push. This pushes particles away
|
|
2522
|
+
* without destroying them. The fluid grid handles velocity decay via its step().
|
|
2523
|
+
*
|
|
2524
|
+
* Immediate displacement: process rings from outermost to innermost so outer
|
|
2525
|
+
* particles move first, creating space for inner ones. Each particle is
|
|
2526
|
+
* shoved one cell radially outward (displacing liquids/gases via tryShove).
|
|
2527
|
+
*
|
|
2528
|
+
* Wind field: sets radial wind with linear falloff for continued push over
|
|
2529
|
+
* the next several frames.
|
|
2530
|
+
*/
|
|
2531
|
+
applyImpulse(cx, cy, radius, strength) {
|
|
2532
|
+
const W = this.W, H = this.H;
|
|
2533
|
+
const grid = this.grid;
|
|
2534
|
+
const fields = this.fields;
|
|
2535
|
+
// --- Phase 1: Immediate radial displacement (outer ring → inner ring) ---
|
|
2536
|
+
// Process from the outermost ring inward so outer particles shove out first,
|
|
2537
|
+
// creating space for inner particles to follow.
|
|
2538
|
+
for (let ring = radius; ring >= 1; ring--) {
|
|
2539
|
+
for (let y = cy - ring; y <= cy + ring; y++) {
|
|
2540
|
+
for (let x = cx - ring; x <= cx + ring; x++) {
|
|
2541
|
+
// Only process cells on the current ring boundary (skip inner cells)
|
|
2542
|
+
const dx = x - cx, dy = y - cy;
|
|
2543
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
2544
|
+
if (Math.round(dist) !== ring)
|
|
2545
|
+
continue;
|
|
2546
|
+
if (x < 0 || x >= W || y < 0 || y >= H)
|
|
2547
|
+
continue;
|
|
2548
|
+
const idx = y * W + x;
|
|
2549
|
+
const packed = grid[idx];
|
|
2550
|
+
if (packed === 0)
|
|
2551
|
+
continue;
|
|
2552
|
+
const flags = (packed >> 16) & 0xff;
|
|
2553
|
+
if (flags & FLAG_UPDATED)
|
|
2554
|
+
continue;
|
|
2555
|
+
// Don't move walls or static solids
|
|
2556
|
+
const mat = packed & 0xff;
|
|
2557
|
+
if (mat === Material.Wall || mat === Material.Stone)
|
|
2558
|
+
continue;
|
|
2559
|
+
// Shove one step radially outward
|
|
2560
|
+
const stepX = dx === 0 ? 0 : dx > 0 ? 1 : -1;
|
|
2561
|
+
const stepY = dy === 0 ? 0 : dy > 0 ? 1 : -1;
|
|
2562
|
+
this.tryShove(x, y, x + stepX, y + stepY, packed);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
// --- Phase 2: Apply impulse to the fluid grid for ongoing push ---
|
|
2567
|
+
// The fluid grid handles pressure relaxation and velocity decay, replacing
|
|
2568
|
+
// the old per-cell wind field system.
|
|
2569
|
+
this.fluid.applyImpulse(cx, cy, radius, 0, 0); // pressure impulse for shockwave
|
|
2570
|
+
// Radial velocity impulse — each cell gets pushed outward
|
|
2571
|
+
for (let y = cy - radius; y <= cy + radius; y++) {
|
|
2572
|
+
for (let x = cx - radius; x <= cx + radius; x++) {
|
|
2573
|
+
if (x < 0 || x >= W || y < 0 || y >= H)
|
|
2574
|
+
continue;
|
|
2575
|
+
const dx = x - cx, dy = y - cy;
|
|
2576
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
2577
|
+
if (dist > radius || dist < 0.5)
|
|
2578
|
+
continue;
|
|
2579
|
+
const falloff = 1 - dist / radius;
|
|
2580
|
+
const mag = strength * falloff * 0.02; // scale to fluid grid velocity units
|
|
2581
|
+
const ux = dx / dist, uy = dy / dist;
|
|
2582
|
+
this.fluid.applyImpulse(x, y, 1, ux * mag, uy * mag);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
/** Create an explosion: fire + smoke in a radius */
|
|
2587
|
+
explode(cx, cy, radius) {
|
|
2588
|
+
const W = this.W, H = this.H;
|
|
2589
|
+
const grid = this.grid;
|
|
2590
|
+
const wxMin = this.writeXMin, wxMax = this.writeXMax;
|
|
2591
|
+
for (let y = cy - radius; y <= cy + radius; y++) {
|
|
2592
|
+
for (let x = cx - radius; x <= cx + radius; x++) {
|
|
2593
|
+
const dist = (x - cx) * (x - cx) + (y - cy) * (y - cy);
|
|
2594
|
+
if (dist > radius * radius)
|
|
2595
|
+
continue;
|
|
2596
|
+
if (x < 0 || x >= W || y < 0 || y >= H)
|
|
2597
|
+
continue;
|
|
2598
|
+
// Strip mode write guard — explosions don't cross strip boundaries.
|
|
2599
|
+
// The coordinator's boundary cleanup re-runs explosions that span strips.
|
|
2600
|
+
if (x < wxMin || x >= wxMax)
|
|
2601
|
+
continue;
|
|
2602
|
+
const mat = grid[y * W + x] & 0xff;
|
|
2603
|
+
if (mat === Material.Wall)
|
|
2604
|
+
continue;
|
|
2605
|
+
// Don't destroy C4 or Dynamite in the blast — let detonateC4 /
|
|
2606
|
+
// detonateDynamite handle chain reactions via flood-fill.
|
|
2607
|
+
if (mat === Material.C4 || mat === Material.Dynamite)
|
|
2608
|
+
continue;
|
|
2609
|
+
if (dist < radius * radius * 0.3) {
|
|
2610
|
+
// Core: fire
|
|
2611
|
+
grid[y * W + x] = packCell(Material.Fire, 20, this.rng.randomShade());
|
|
2612
|
+
}
|
|
2613
|
+
else {
|
|
2614
|
+
// Outer: smoke or empty
|
|
2615
|
+
if (this.rng.random() < 0.5) {
|
|
2616
|
+
grid[y * W + x] = packCell(Material.Smoke, 60, this.rng.randomShade());
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
grid[y * W + x] = 0;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
// --- Spawn explosion particles ---
|
|
2625
|
+
// Projectile particles: flying debris in all directions
|
|
2626
|
+
const numProjectiles = Math.min(8, 3 + radius);
|
|
2627
|
+
for (let i = 0; i < numProjectiles; i++) {
|
|
2628
|
+
const angle = (i / numProjectiles) * Math.PI * 2 + this.rng.random() * 0.5;
|
|
2629
|
+
const speed = 1 + this.rng.random() * 2;
|
|
2630
|
+
this.particles.spawn(cx, cy, PARTICLE_TYPES.PROJECTILE, 0xffaa3300, // orange-red
|
|
2631
|
+
1 + this.rng.random(), Math.cos(angle) * speed, Math.sin(angle) * speed, 20 + Math.floor(this.rng.random() * 20));
|
|
2632
|
+
}
|
|
2633
|
+
// Circle particles: expanding blast rings
|
|
2634
|
+
const numCircles = Math.min(3, 1 + Math.floor(radius / 2));
|
|
2635
|
+
for (let i = 0; i < numCircles; i++) {
|
|
2636
|
+
this.particles.spawn(cx, cy, PARTICLE_TYPES.CIRCLE, 0xffff6600, // bright orange
|
|
2637
|
+
radius * 0.5, 0, 0, 15 + i * 5);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
/** Annihilate antimatter: flood-fill all 8-connected non-empty cells from
|
|
2641
|
+
* the antimatter cluster at (ax, ay) through the contacted material at
|
|
2642
|
+
* (cx, cy). Destroys every connected non-empty cell except Wall and
|
|
2643
|
+
* Duplicator (which act as barriers and are left intact). Empty cells
|
|
2644
|
+
* stop the flood-fill. A fire explosion marks the contact point.
|
|
2645
|
+
*
|
|
2646
|
+
* This implements "eliminates all contiguous antimatter and all contiguous
|
|
2647
|
+
* other material, except empty" — the entire connected non-empty region
|
|
2648
|
+
* touching the antimatter cluster is annihilated. */
|
|
2649
|
+
annihilateAntimatter(ax, ay, cx, cy) {
|
|
2650
|
+
const W = this.W, H = this.H;
|
|
2651
|
+
const grid = this.grid;
|
|
2652
|
+
const visited = this.visitedAntimatterFrame;
|
|
2653
|
+
const frame = this.frame;
|
|
2654
|
+
const wxMin = this.writeXMin, wxMax = this.writeXMax;
|
|
2655
|
+
// Flood-fill from the antimatter seed cell through all 8-connected
|
|
2656
|
+
// non-empty cells. Wall and Duplicator are barriers (skipped, not
|
|
2657
|
+
// destroyed). Empty cells are not traversed.
|
|
2658
|
+
const stack = [ay * W + ax];
|
|
2659
|
+
const cells = [];
|
|
2660
|
+
while (stack.length > 0) {
|
|
2661
|
+
const idx = stack.pop();
|
|
2662
|
+
if (visited[idx] === frame)
|
|
2663
|
+
continue;
|
|
2664
|
+
visited[idx] = frame;
|
|
2665
|
+
const px = idx % W;
|
|
2666
|
+
const py = (idx / W) | 0;
|
|
2667
|
+
const m = grid[idx] & 0xff;
|
|
2668
|
+
// Stop at empty, wall, duplicator, or void (barriers)
|
|
2669
|
+
if (m === Material.Empty || m === Material.Wall || m === Material.Duplicator ||
|
|
2670
|
+
m === Material.Void)
|
|
2671
|
+
continue;
|
|
2672
|
+
cells.push(idx);
|
|
2673
|
+
// Push 8 neighbors (only those within write bounds — cross-strip
|
|
2674
|
+
// annihilation is handled by the coordinator's boundary cleanup)
|
|
2675
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2676
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2677
|
+
if (dx === 0 && dy === 0)
|
|
2678
|
+
continue;
|
|
2679
|
+
const nx = px + dx, ny = py + dy;
|
|
2680
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2681
|
+
continue;
|
|
2682
|
+
if (nx < wxMin || nx >= wxMax)
|
|
2683
|
+
continue;
|
|
2684
|
+
stack.push(ny * W + nx);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
// Annihilate every cell in the connected region.
|
|
2689
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2690
|
+
grid[cells[i]] = 0;
|
|
2691
|
+
}
|
|
2692
|
+
// Fire explosion at the contact point (visual + ignites nearby flammables).
|
|
2693
|
+
this.explode(cx, cy, 3);
|
|
2694
|
+
}
|
|
2695
|
+
/** Detonate C4 at (cx, cy) and flood-fill all connected C4 for chain reaction */
|
|
2696
|
+
detonateC4(cx, cy) {
|
|
2697
|
+
const W = this.W, H = this.H;
|
|
2698
|
+
const grid = this.grid;
|
|
2699
|
+
// Collect all connected C4 cells (8-connected flood fill).
|
|
2700
|
+
// Use a frame-tagged visited array instead of allocating a Set every call.
|
|
2701
|
+
// We use a dedicated visitedC4Frame array to avoid clobbering the
|
|
2702
|
+
// combustion pass's visitedFrame.
|
|
2703
|
+
const visited = this.visitedC4Frame;
|
|
2704
|
+
const frame = this.frame;
|
|
2705
|
+
const stack = [cy * W + cx];
|
|
2706
|
+
const c4Cells = [];
|
|
2707
|
+
while (stack.length > 0) {
|
|
2708
|
+
const idx = stack.pop();
|
|
2709
|
+
if (visited[idx] === frame)
|
|
2710
|
+
continue;
|
|
2711
|
+
visited[idx] = frame;
|
|
2712
|
+
const px = idx % W;
|
|
2713
|
+
const py = (idx / W) | 0;
|
|
2714
|
+
if ((grid[idx] & 0xff) !== Material.C4)
|
|
2715
|
+
continue;
|
|
2716
|
+
c4Cells.push(idx);
|
|
2717
|
+
// Check 8 neighbors
|
|
2718
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2719
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2720
|
+
if (dx === 0 && dy === 0)
|
|
2721
|
+
continue;
|
|
2722
|
+
const nx = px + dx, ny = py + dy;
|
|
2723
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2724
|
+
continue;
|
|
2725
|
+
stack.push(ny * W + nx);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
// Explode each C4 cell
|
|
2730
|
+
for (let i = 0; i < c4Cells.length; i++) {
|
|
2731
|
+
const idx = c4Cells[i];
|
|
2732
|
+
const px = idx % W;
|
|
2733
|
+
const py = (idx / W) | 0;
|
|
2734
|
+
this.explode(px, py, 8);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
/** Detonate Dynamite at (cx, cy) and flood-fill all connected Dynamite for
|
|
2738
|
+
* chain reaction. Collects all 8-connected Dynamite cells, explodes once at
|
|
2739
|
+
* the center of mass (radius scaled to the cluster size), then consumes all
|
|
2740
|
+
* cells. Exploding once instead of per-cell avoids massive lag on large
|
|
2741
|
+
* dynamite clusters. */
|
|
2742
|
+
detonateDynamite(cx, cy) {
|
|
2743
|
+
const W = this.W, H = this.H;
|
|
2744
|
+
const grid = this.grid;
|
|
2745
|
+
const visited = this.visitedC4Frame;
|
|
2746
|
+
const frame = this.frame;
|
|
2747
|
+
const stack = [cy * W + cx];
|
|
2748
|
+
const dynoCells = [];
|
|
2749
|
+
while (stack.length > 0) {
|
|
2750
|
+
const idx = stack.pop();
|
|
2751
|
+
if (visited[idx] === frame)
|
|
2752
|
+
continue;
|
|
2753
|
+
visited[idx] = frame;
|
|
2754
|
+
const px = idx % W;
|
|
2755
|
+
const py = (idx / W) | 0;
|
|
2756
|
+
if ((grid[idx] & 0xff) !== Material.Dynamite)
|
|
2757
|
+
continue;
|
|
2758
|
+
dynoCells.push(idx);
|
|
2759
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2760
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2761
|
+
if (dx === 0 && dy === 0)
|
|
2762
|
+
continue;
|
|
2763
|
+
const nx = px + dx, ny = py + dy;
|
|
2764
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2765
|
+
continue;
|
|
2766
|
+
stack.push(ny * W + nx);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
if (dynoCells.length === 0)
|
|
2771
|
+
return;
|
|
2772
|
+
// Explode once at the center of mass. Radius scales with cluster size
|
|
2773
|
+
// (6 base + 1 per extra stick, capped at 12) so a single big blast
|
|
2774
|
+
// covers the whole cluster — much cheaper than N separate explosions.
|
|
2775
|
+
let sumX = 0, sumY = 0;
|
|
2776
|
+
for (let i = 0; i < dynoCells.length; i++) {
|
|
2777
|
+
sumX += dynoCells[i] % W;
|
|
2778
|
+
sumY += (dynoCells[i] / W) | 0;
|
|
2779
|
+
}
|
|
2780
|
+
const centerX = Math.round(sumX / dynoCells.length);
|
|
2781
|
+
const centerY = Math.round(sumY / dynoCells.length);
|
|
2782
|
+
const radius = Math.min(12, 6 + dynoCells.length - 1);
|
|
2783
|
+
this.explode(centerX, centerY, radius);
|
|
2784
|
+
// Consume all dynamite cells — explode() skips them, so we must destroy
|
|
2785
|
+
// them explicitly. Convert to fire so the blast looks right.
|
|
2786
|
+
for (let i = 0; i < dynoCells.length; i++) {
|
|
2787
|
+
grid[dynoCells[i]] = packCell(Material.Fire, 20, this.rng.randomShade());
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
/** Grow a leaf canopy around (x, y) — the top of the tree trunk.
|
|
2791
|
+
* Called when the growing seed tip reaches its target height or is blocked. */
|
|
2792
|
+
growCanopy(x, y) {
|
|
2793
|
+
const W = this.W, H = this.H;
|
|
2794
|
+
const grid = this.grid;
|
|
2795
|
+
const wxMin = this.writeXMin, wxMax = this.writeXMax;
|
|
2796
|
+
const canopyRadius = 3 + Math.floor(this.rng.random() * 2);
|
|
2797
|
+
for (let dy = -canopyRadius; dy <= 0; dy++) {
|
|
2798
|
+
for (let dx = -canopyRadius; dx <= canopyRadius; dx++) {
|
|
2799
|
+
const lx = x + dx, ly = y + dy - canopyRadius;
|
|
2800
|
+
if (lx < 0 || lx >= W || ly < 0 || ly >= H)
|
|
2801
|
+
continue;
|
|
2802
|
+
if (lx < wxMin || lx >= wxMax)
|
|
2803
|
+
continue;
|
|
2804
|
+
const dist = dx * dx + dy * dy;
|
|
2805
|
+
if (dist > canopyRadius * canopyRadius)
|
|
2806
|
+
continue;
|
|
2807
|
+
if (grid[ly * W + lx] !== 0)
|
|
2808
|
+
continue;
|
|
2809
|
+
if (this.rng.random() < 0.7) {
|
|
2810
|
+
grid[ly * W + lx] = packCell(Material.Leaf, 0, this.rng.randomShade());
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
findNeighbor(x, y, mat) {
|
|
2816
|
+
const W = this.W, H = this.H;
|
|
2817
|
+
const grid = this.grid;
|
|
2818
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2819
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2820
|
+
if (dx === 0 && dy === 0)
|
|
2821
|
+
continue;
|
|
2822
|
+
const nx = x + dx, ny = y + dy;
|
|
2823
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2824
|
+
continue;
|
|
2825
|
+
if ((grid[ny * W + nx] & 0xff) === mat)
|
|
2826
|
+
return { x: nx, y: ny };
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
return null;
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Check if a cell is "exposed" — has at least one neighbor that is empty
|
|
2833
|
+
* or a gas (so fire/heat can reach it). Used to prevent buried oil from
|
|
2834
|
+
* igniting when it's encased in solid/liquid material.
|
|
2835
|
+
*/
|
|
2836
|
+
isExposed(x, y) {
|
|
2837
|
+
const W = this.W, H = this.H;
|
|
2838
|
+
const grid = this.grid;
|
|
2839
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2840
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2841
|
+
if (dx === 0 && dy === 0)
|
|
2842
|
+
continue;
|
|
2843
|
+
const nx = x + dx, ny = y + dy;
|
|
2844
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2845
|
+
continue;
|
|
2846
|
+
const nMat = grid[ny * W + nx] & 0xff;
|
|
2847
|
+
if (nMat === Material.Empty)
|
|
2848
|
+
return true;
|
|
2849
|
+
if (MAT_FLAGS[nMat] & MAT_GAS)
|
|
2850
|
+
return true;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
return false;
|
|
2854
|
+
}
|
|
2855
|
+
applyCombustion() {
|
|
2856
|
+
const W = this.W, H = this.H;
|
|
2857
|
+
const grid = this.grid;
|
|
2858
|
+
const fields = this.fields;
|
|
2859
|
+
const active = this.activeCells;
|
|
2860
|
+
const count = this.activeCount;
|
|
2861
|
+
// Strip mode write bounds
|
|
2862
|
+
const wxMin = this.writeXMin, wxMax = this.writeXMax;
|
|
2863
|
+
// Snapshot which cells are fire/lava at the start of this pass.
|
|
2864
|
+
// Only these cells can spread fire — newly ignited cells wait until next frame.
|
|
2865
|
+
// This prevents instant cascade through fuse/gunpowder chains in a single pass.
|
|
2866
|
+
// (Reused per-frame buffer — avoids a Uint8Array allocation every step.)
|
|
2867
|
+
// Build from the active list instead of scanning the full grid.
|
|
2868
|
+
const fireSources = this.fireSources;
|
|
2869
|
+
// Only clear the entries we'll use (the active cells). The rest are already 0
|
|
2870
|
+
// from the previous frame's clear, but to be safe we clear all active entries.
|
|
2871
|
+
for (let a = 0; a < count; a++)
|
|
2872
|
+
fireSources[active[a]] = 0;
|
|
2873
|
+
let fireCount = 0;
|
|
2874
|
+
for (let a = 0; a < count; a++) {
|
|
2875
|
+
const idx = active[a];
|
|
2876
|
+
const m = grid[idx] & 0xff;
|
|
2877
|
+
if (m === Material.Fire || m === Material.FuseFire || m === Material.BurningOil || m === Material.Lava) {
|
|
2878
|
+
fireSources[idx] = 1;
|
|
2879
|
+
fireCount++;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
// Early-out: no fire sources means no combustion spread, fuse burn, or
|
|
2883
|
+
// burning-oil passes needed.
|
|
2884
|
+
if (fireCount === 0)
|
|
2885
|
+
return;
|
|
2886
|
+
// --- Fire spread pass ---
|
|
2887
|
+
// Iterate only the fire-source cells (from the active list) instead of
|
|
2888
|
+
// scanning the full grid.
|
|
2889
|
+
for (let a = 0; a < count; a++) {
|
|
2890
|
+
const idx = active[a];
|
|
2891
|
+
if (!fireSources[idx])
|
|
2892
|
+
continue;
|
|
2893
|
+
const srcPacked = grid[idx];
|
|
2894
|
+
const srcMat = srcPacked & 0xff;
|
|
2895
|
+
// Spark-flagged fire (visual flames emitted by BurningOil/fuse) does NOT
|
|
2896
|
+
// ignite oil — the burning-oil pass handles controlled oil-to-oil spread.
|
|
2897
|
+
// Without this, the visual flames would ignite adjacent oil in all 8
|
|
2898
|
+
// directions (including diagonals), causing the fire to "burst" outward
|
|
2899
|
+
// instead of creeping slowly from the ignition site.
|
|
2900
|
+
const srcIsSpark = srcMat === Material.Fire && ((srcPacked >> 16) & FLAG_SPARK) !== 0;
|
|
2901
|
+
const x = idx % W;
|
|
2902
|
+
const y = (idx / W) | 0;
|
|
2903
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2904
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2905
|
+
if (dx === 0 && dy === 0)
|
|
2906
|
+
continue;
|
|
2907
|
+
const nx = x + dx, ny = y + dy;
|
|
2908
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
2909
|
+
continue;
|
|
2910
|
+
if (nx < wxMin || nx >= wxMax)
|
|
2911
|
+
continue;
|
|
2912
|
+
const ni = ny * W + nx;
|
|
2913
|
+
const nMat = grid[ni] & 0xff;
|
|
2914
|
+
if (!(MAT_FLAGS[nMat] & MAT_FLAMMABLE))
|
|
2915
|
+
continue;
|
|
2916
|
+
if (nMat === Material.Gunpowder) {
|
|
2917
|
+
grid[ni] = packCell(Material.Fire, 15, this.rng.randomShade());
|
|
2918
|
+
continue;
|
|
2919
|
+
}
|
|
2920
|
+
// Gas vapor and hydrogen: explosive
|
|
2921
|
+
if (nMat === Material.GasVapor || nMat === Material.Hydrogen) {
|
|
2922
|
+
grid[ni] = packCell(Material.Fire, 20, this.rng.randomShade());
|
|
2923
|
+
this.explode(nx, ny, 3);
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
// Flour: dust explosion
|
|
2927
|
+
if (nMat === Material.Flour) {
|
|
2928
|
+
this.explode(nx, ny, 3);
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
// Dynamite: chain detonate via flood-fill (same as applySpecialReactions)
|
|
2932
|
+
if (nMat === Material.Dynamite) {
|
|
2933
|
+
this.detonateDynamite(nx, ny);
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2936
|
+
const baseChance = srcMat === Material.Lava ? 0.1 : 0.08;
|
|
2937
|
+
// Rubber is hard to ignite — low continual burn/spread chance.
|
|
2938
|
+
// Oil is a slow-burning liquid fuel: low per-frame spread chance
|
|
2939
|
+
// so the flame creeps gradually across the surface rather than
|
|
2940
|
+
// flashing instantly. BurningOil→oil spread is handled by the
|
|
2941
|
+
// dedicated burning-oil pass below (with decay-linked chance),
|
|
2942
|
+
// so skip oil neighbors here when the source is BurningOil.
|
|
2943
|
+
// Spark-flagged fire (visual flames from BurningOil) also skips
|
|
2944
|
+
// oil — only "real" fire sources (lava, regular fire from burning
|
|
2945
|
+
// wood/gunpowder, etc.) can ignite oil via this pass.
|
|
2946
|
+
if ((srcMat === Material.BurningOil || srcIsSpark) && nMat === Material.Oil)
|
|
2947
|
+
continue;
|
|
2948
|
+
// Oil needs air exposure to ignite — fire/lava touching buried oil
|
|
2949
|
+
// heats it but can't sustain a flame without oxygen.
|
|
2950
|
+
if (nMat === Material.Oil && !this.isExposed(nx, ny))
|
|
2951
|
+
continue;
|
|
2952
|
+
const matMult = nMat === Material.Rubber ? 0.3 :
|
|
2953
|
+
nMat === Material.Oil ? 0.3 :
|
|
2954
|
+
nMat === Material.Wax ? 2.5 : // wax spreads fire extremely fast
|
|
2955
|
+
1.0;
|
|
2956
|
+
// Per-cell temperature scales fire spread rate (inline field read)
|
|
2957
|
+
const nTemp = fields[ni * 4 + FIELD.TEMP] / 128;
|
|
2958
|
+
const chance = baseChance * nTemp * matMult;
|
|
2959
|
+
if (this.rng.random() < chance) {
|
|
2960
|
+
// Fuse gets FuseFire (yellow, stays put, deterministic spread)
|
|
2961
|
+
if (nMat === Material.Fuse) {
|
|
2962
|
+
grid[ni] = packCell(Material.FuseFire, 15, this.rng.randomShade());
|
|
2963
|
+
}
|
|
2964
|
+
else if (nMat === Material.Oil) {
|
|
2965
|
+
// Oil → BurningOil (stays put, slow decay, slow spread)
|
|
2966
|
+
grid[ni] = packCell(Material.BurningOil, MAT_LIFETIME[Material.BurningOil], this.rng.randomShade());
|
|
2967
|
+
}
|
|
2968
|
+
else if (nMat === Material.Wax) {
|
|
2969
|
+
// Wax burns very slowly — long fire lifetime + FLAG_ANCHORED so
|
|
2970
|
+
// the flame stays put on the wax surface and keeps spreading.
|
|
2971
|
+
grid[ni] = packCell(Material.Fire, 200, FLAG_ANCHORED | this.rng.randomShade());
|
|
2972
|
+
}
|
|
2973
|
+
else {
|
|
2974
|
+
grid[ni] = packCell(Material.Fire, 30, this.rng.randomShade());
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
// --- Deterministic fuse burn ---
|
|
2981
|
+
// FuseFire deterministically ignites ALL adjacent fuse cells when its
|
|
2982
|
+
// lifetime drops to the threshold. The fire has a short lifetime
|
|
2983
|
+
// (FUSE_FIRE_LIFETIME) so the flame trail is brief. The burn speed is
|
|
2984
|
+
// controlled by the lifetime: each cell burns for FUSE_FIRE_LIFETIME frames
|
|
2985
|
+
// before passing the flame onward.
|
|
2986
|
+
//
|
|
2987
|
+
// FuseFire is a separate material (Material.FuseFire) so it's reliably
|
|
2988
|
+
// identified even at the end of the trail where all adjacent fuse has been
|
|
2989
|
+
// consumed. No flag bits needed.
|
|
2990
|
+
//
|
|
2991
|
+
// To prevent single-frame cascades, we use a frame-tagged visited array
|
|
2992
|
+
// (this.visitedFrame) instead of allocating a Set every step. A cell is
|
|
2993
|
+
// "visited this frame" when visitedFrame[i] === this.frame.
|
|
2994
|
+
const FUSE_FIRE_LIFETIME = 15;
|
|
2995
|
+
const FUSE_SPREAD_THRESHOLD = 3;
|
|
2996
|
+
const frame = this.frame;
|
|
2997
|
+
const visitedFrame = this.visitedFrame;
|
|
2998
|
+
for (let a = 0; a < count; a++) {
|
|
2999
|
+
const idx = active[a];
|
|
3000
|
+
const packed = grid[idx];
|
|
3001
|
+
const mat = packed & 0xff;
|
|
3002
|
+
if (mat !== Material.FuseFire)
|
|
3003
|
+
continue;
|
|
3004
|
+
const lifetime = (packed >> 8) & 0xff;
|
|
3005
|
+
if (lifetime === 0)
|
|
3006
|
+
continue;
|
|
3007
|
+
const x = idx % W;
|
|
3008
|
+
const y = (idx / W) | 0;
|
|
3009
|
+
// Emit sparks every frame — small fire particles that fly upward with
|
|
3010
|
+
// random spread. Sparks use Material.Fire (red) so they're visually
|
|
3011
|
+
// distinct from the yellow fuse fire and fly freely (FuseFire is
|
|
3012
|
+
// anchored, Fire is not).
|
|
3013
|
+
for (let s = 0; s < 3; s++) {
|
|
3014
|
+
if (this.rng.random() < 0.5) {
|
|
3015
|
+
const sx = x + Math.floor(this.rng.random() * 3) - 1;
|
|
3016
|
+
const sy = y - 1 - Math.floor(this.rng.random() * 2); // 1-2 cells above
|
|
3017
|
+
if (sx >= 0 && sx < W && sy >= 0 && sy < H && sx >= wxMin && sx < wxMax && grid[sy * W + sx] === 0) {
|
|
3018
|
+
// Sparks: very short lifetime, expire to empty (not smoke) so they
|
|
3019
|
+
// don't accumulate and suffocate the burn when going upward.
|
|
3020
|
+
grid[sy * W + sx] = packCell(Material.Fire, 6, this.rng.randomShade() | FLAG_SPARK);
|
|
3021
|
+
// Upward impulse only — sparks rise straight up (wind is skipped for
|
|
3022
|
+
// spark fire in tryMove). The impulse still pushes neighboring
|
|
3023
|
+
// smoke/gas upward via the fluid grid. No horizontal drift: a fixed
|
|
3024
|
+
// at-birth drift caused sparks to streak diagonally up-left/up-right.
|
|
3025
|
+
this.fluid.applyImpulse(sx, sy, 2, 0, -0.8); // strong upward
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
// Spread to adjacent fuse cells only when near end of lifetime
|
|
3030
|
+
if (lifetime > FUSE_SPREAD_THRESHOLD)
|
|
3031
|
+
continue;
|
|
3032
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
3033
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
3034
|
+
if (dx === 0 && dy === 0)
|
|
3035
|
+
continue;
|
|
3036
|
+
const nx = x + dx, ny = y + dy;
|
|
3037
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
3038
|
+
continue;
|
|
3039
|
+
if (nx < wxMin || nx >= wxMax)
|
|
3040
|
+
continue;
|
|
3041
|
+
const ni = ny * W + nx;
|
|
3042
|
+
if (visitedFrame[ni] === frame)
|
|
3043
|
+
continue;
|
|
3044
|
+
if ((grid[ni] & 0xff) === Material.Fuse) {
|
|
3045
|
+
grid[ni] = packCell(Material.FuseFire, FUSE_FIRE_LIFETIME, this.rng.randomShade());
|
|
3046
|
+
visitedFrame[ni] = frame;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
// --- Burning oil pass ---
|
|
3052
|
+
// BurningOil flows like a liquid (gravity: 1, density: 0.8) on the oil
|
|
3053
|
+
// surface. Each frame it:
|
|
3054
|
+
// 1. Emits normal Fire particles upward (visual flames that rise and
|
|
3055
|
+
// decay to smoke like regular fire).
|
|
3056
|
+
// 2. Slowly spreads to adjacent oil. The spread chance scales with
|
|
3057
|
+
// decay progress — early in its life it barely spreads, but as it
|
|
3058
|
+
// burns down the chance increases, so the fire creeps outward
|
|
3059
|
+
// gradually rather than flashing instantly.
|
|
3060
|
+
// The ignitedOil de-dup uses the same frame-tagged visitedFrame array as
|
|
3061
|
+
// the fuse pass above. Safe to share: the two passes target different
|
|
3062
|
+
// materials (Fuse vs Oil), so a cell marked in one pass is guarded out by
|
|
3063
|
+
// the material-type check in the other before the visited check runs.
|
|
3064
|
+
const BURNING_OIL_LIFETIME = MAT_LIFETIME[Material.BurningOil];
|
|
3065
|
+
for (let a = 0; a < count; a++) {
|
|
3066
|
+
const idx = active[a];
|
|
3067
|
+
const packed = grid[idx];
|
|
3068
|
+
const mat = packed & 0xff;
|
|
3069
|
+
if (mat !== Material.BurningOil)
|
|
3070
|
+
continue;
|
|
3071
|
+
const lifetime = (packed >> 8) & 0xff;
|
|
3072
|
+
const x = idx % W;
|
|
3073
|
+
const y = (idx / W) | 0;
|
|
3074
|
+
// Emit fire particles above: visual flames that rise from the burning
|
|
3075
|
+
// oil surface with slight horizontal variation for a natural look.
|
|
3076
|
+
// Marked with FLAG_SPARK so they:
|
|
3077
|
+
// 1. Expire to empty (not smoke) — smoke would accumulate and
|
|
3078
|
+
// suffocate the burn. The BurningOil itself already produces smoke
|
|
3079
|
+
// when it decays.
|
|
3080
|
+
// 2. Are skipped by the fire spread pass for oil ignition (see below)
|
|
3081
|
+
// — the burning-oil pass handles controlled oil-to-oil spread.
|
|
3082
|
+
// Without this, the visual flames would ignite adjacent oil in all
|
|
3083
|
+
// directions (including diagonals), causing the fire to "burst"
|
|
3084
|
+
// instead of creeping slowly outward.
|
|
3085
|
+
// 3. Skip diagonal rising in tryMove (isSparkFire) — they rise straight
|
|
3086
|
+
// up or drift horizontally via gas drift, but don't shoot off in
|
|
3087
|
+
// diagonal directions.
|
|
3088
|
+
for (let s = 0; s < 2; s++) {
|
|
3089
|
+
if (this.rng.random() < 0.4) {
|
|
3090
|
+
const sx = x + Math.floor(this.rng.random() * 3) - 1; // -1, 0, or +1
|
|
3091
|
+
const sy = y - 1 - Math.floor(this.rng.random() * 2); // 1-2 cells above
|
|
3092
|
+
if (sx >= 0 && sx < W && sy >= 0 && sy < H && sx >= wxMin && sx < wxMax && grid[sy * W + sx] === 0) {
|
|
3093
|
+
grid[sy * W + sx] = packCell(Material.Fire, 20, this.rng.randomShade() | FLAG_SPARK);
|
|
3094
|
+
// Upward impulse only — sparks rise straight up (wind is skipped for
|
|
3095
|
+
// spark fire in tryMove). The impulse still pushes neighboring
|
|
3096
|
+
// smoke/gas upward via the fluid grid. No horizontal drift: a fixed
|
|
3097
|
+
// at-birth drift caused sparks to streak diagonally up-left/up-right.
|
|
3098
|
+
this.fluid.applyImpulse(sx, sy, 2, 0, -0.5);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
// Spread to adjacent oil: slow chance that increases with decay.
|
|
3103
|
+
// decayProgress goes 0→1 as lifetime goes 200→0.
|
|
3104
|
+
const decayProgress = 1 - (lifetime / BURNING_OIL_LIFETIME);
|
|
3105
|
+
const spreadChance = 0.02 + 0.04 * decayProgress; // 2% early → 6% late
|
|
3106
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
3107
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
3108
|
+
if (dx === 0 && dy === 0)
|
|
3109
|
+
continue;
|
|
3110
|
+
const nx = x + dx, ny = y + dy;
|
|
3111
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H)
|
|
3112
|
+
continue;
|
|
3113
|
+
if (nx < wxMin || nx >= wxMax)
|
|
3114
|
+
continue;
|
|
3115
|
+
const ni = ny * W + nx;
|
|
3116
|
+
if (visitedFrame[ni] === frame)
|
|
3117
|
+
continue;
|
|
3118
|
+
// Oil only ignites if it has air exposure (empty/gas neighbor) —
|
|
3119
|
+
// burning oil flowing into buried oil won't ignite it without oxygen.
|
|
3120
|
+
if ((grid[ni] & 0xff) === Material.Oil && this.isExposed(nx, ny) && this.rng.random() < spreadChance) {
|
|
3121
|
+
grid[ni] = packCell(Material.BurningOil, BURNING_OIL_LIFETIME, this.rng.randomShade());
|
|
3122
|
+
visitedFrame[ni] = frame;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
// Clear fireSources for the active cells we marked (so next frame's
|
|
3128
|
+
// buildActiveListAndClearFlags doesn't inherit stale marks). We only
|
|
3129
|
+
// touch the active cells — the rest are already 0.
|
|
3130
|
+
for (let a = 0; a < count; a++)
|
|
3131
|
+
fireSources[active[a]] = 0;
|
|
3132
|
+
}
|
|
3133
|
+
applyAging() {
|
|
3134
|
+
const grid = this.grid;
|
|
3135
|
+
const active = this.activeCells;
|
|
3136
|
+
const count = this.activeCount;
|
|
3137
|
+
for (let a = 0; a < count; a++) {
|
|
3138
|
+
const i = active[a];
|
|
3139
|
+
const packed = grid[i];
|
|
3140
|
+
if (packed === 0)
|
|
3141
|
+
continue; // cell was destroyed earlier this frame
|
|
3142
|
+
// Inline unpack — avoid allocating a Cell object
|
|
3143
|
+
let mat = packed & 0xff;
|
|
3144
|
+
let lifetime = (packed >> 8) & 0xff;
|
|
3145
|
+
let flags = (packed >> 16) & 0xff;
|
|
3146
|
+
// Gravel (and legacy LooseStone): re-settle to Stone when stationary AND
|
|
3147
|
+
// supported by stable ground. The lifetime field is a settle timer, set
|
|
3148
|
+
// when the stone is dislodged by mining (GRAVEL_SETTLE_TICKS) or disturbed
|
|
3149
|
+
// by adjacent gravel movement (GRAVEL_DISTURB_SETTLE_TICKS). When the
|
|
3150
|
+
// cell moves (FLAG_UPDATED), the timer keeps its current value. When
|
|
3151
|
+
// stationary, the timer only counts down if the cell is supported from
|
|
3152
|
+
// below by a STABLE material — otherwise it's floating and must not
|
|
3153
|
+
// re-settle (it needs to keep trying to fall). This prevents Gravel from
|
|
3154
|
+
// freezing mid-air when friction or random chance prevents it from moving
|
|
3155
|
+
// for a few ticks.
|
|
3156
|
+
//
|
|
3157
|
+
// Stable support = a solid that won't move out from under us:
|
|
3158
|
+
// - Static solids (gravityDir === 0: Stone, Wall, Concrete) with
|
|
3159
|
+
// lifetime === 0 (not loosened).
|
|
3160
|
+
// - Gravity-affected solids (ore, dirt, etc.) at rest: gravity field
|
|
3161
|
+
// === 0 (not falling) and lifetime === 0 (not loosened/decaying).
|
|
3162
|
+
// Liquids, gases, and loosened/falling cells are NOT stable support —
|
|
3163
|
+
// they can flow or fall away, leaving re-settled stone floating.
|
|
3164
|
+
//
|
|
3165
|
+
// LooseStone (mat 63) is kept in the condition for backwards compat with
|
|
3166
|
+
// old saves that still contain LooseStone cells; no new LooseStone is
|
|
3167
|
+
// created — all stone-debris conversions produce Gravel.
|
|
3168
|
+
// Stone (mat 3) is included: when a Stone cell's gravity field is set
|
|
3169
|
+
// (by mining), it falls. When it lands and is stably supported, it
|
|
3170
|
+
// re-freezes (gravity field cleared) — no material conversion needed.
|
|
3171
|
+
if (mat === Material.Gravel || mat === Material.LooseStone || mat === Material.Stone) {
|
|
3172
|
+
if (!(flags & FLAG_UPDATED) && lifetime > 0) {
|
|
3173
|
+
// Check if supported from below by a stable cell or grid boundary.
|
|
3174
|
+
// A cell is stable support if it won't move out from under us:
|
|
3175
|
+
// - Static solids (gravityDir === 0: Stone, Wall, Concrete) with
|
|
3176
|
+
// lifetime === 0 (not loosened by mining).
|
|
3177
|
+
// - Gravity-affected solids (gravityDir !== 0 but solid: ore,
|
|
3178
|
+
// dirt, etc.) that are at rest — gravity field === 0 (not
|
|
3179
|
+
// falling) and lifetime === 0 (not loosened/decaying). Ore is
|
|
3180
|
+
// generated with gravity=0 so it stays embedded until mined;
|
|
3181
|
+
// without this, stone resting on ore never settles because ore
|
|
3182
|
+
// has gravityDir=1.
|
|
3183
|
+
// Liquids, gases, and loosened/falling cells do NOT count — they
|
|
3184
|
+
// can move out from under us.
|
|
3185
|
+
const belowIdx = i + this.W;
|
|
3186
|
+
let supported;
|
|
3187
|
+
if (belowIdx >= this.grid.length) {
|
|
3188
|
+
supported = true; // grid boundary = stable floor
|
|
3189
|
+
}
|
|
3190
|
+
else {
|
|
3191
|
+
const belowPacked = this.grid[belowIdx];
|
|
3192
|
+
if (belowPacked === 0) {
|
|
3193
|
+
supported = false;
|
|
3194
|
+
}
|
|
3195
|
+
else {
|
|
3196
|
+
const belowMat = belowPacked & 0xff;
|
|
3197
|
+
const belowLifetime = (belowPacked >> 8) & 0xff;
|
|
3198
|
+
if (MAT_GRAVITY_DIR[belowMat] === 0) {
|
|
3199
|
+
// Static solid — stable if not loosened
|
|
3200
|
+
supported = belowLifetime === 0;
|
|
3201
|
+
}
|
|
3202
|
+
else if (MAT_FLAGS[belowMat] & MAT_SOLID) {
|
|
3203
|
+
// Gravity-affected solid at rest — stable if not falling
|
|
3204
|
+
// (gravity field cleared) and not loosened/decaying.
|
|
3205
|
+
supported = this.fields[belowIdx * 4 + FIELD.GRAVITY] === 0 &&
|
|
3206
|
+
belowLifetime === 0;
|
|
3207
|
+
}
|
|
3208
|
+
else {
|
|
3209
|
+
// Liquid/gas — never stable support
|
|
3210
|
+
supported = false;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
if (supported) {
|
|
3215
|
+
lifetime--;
|
|
3216
|
+
if (lifetime === 0) {
|
|
3217
|
+
// Re-settle: convert back to Stone (static, no gravity).
|
|
3218
|
+
// For Gravel/LooseStone: change material to Stone.
|
|
3219
|
+
// For Stone: just clear the lifetime (already Stone, now frozen).
|
|
3220
|
+
// Also clear FLAG_DETACHED so the renderer stops drawing the
|
|
3221
|
+
// warm tint + amber outline, and the cell re-freezes (no longer
|
|
3222
|
+
// collectible / unfrozen). Without this, re-settled stone stays
|
|
3223
|
+
// visually distinct from original stone and gets absorbed into
|
|
3224
|
+
// the player's inventory when walked near.
|
|
3225
|
+
mat = Material.Stone;
|
|
3226
|
+
lifetime = 0;
|
|
3227
|
+
flags &= ~0x10; // clear FLAG_DETACHED (bit 4 of flags byte)
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
// If not stably supported, don't count down — keep trying to fall
|
|
3231
|
+
}
|
|
3232
|
+
const newFlags = flags & ~FLAG_UPDATED;
|
|
3233
|
+
const newPacked = (mat & 0xff) | ((lifetime & 0xff) << 8) | ((newFlags & 0xff) << 16);
|
|
3234
|
+
if (newPacked !== packed)
|
|
3235
|
+
grid[i] = newPacked;
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
3238
|
+
if (lifetime > 0) {
|
|
3239
|
+
// Randomized decay: fire/smoke/steam sometimes skip a tick so
|
|
3240
|
+
// individual particles last variable amounts of time.
|
|
3241
|
+
if (mat === Material.Fire) {
|
|
3242
|
+
// Anchored fire (from wax) decays very slowly so the flame
|
|
3243
|
+
// persists and keeps spreading. Normal fire decays fast.
|
|
3244
|
+
if (flags & FLAG_ANCHORED) {
|
|
3245
|
+
if (this.rng.random() < 0.15)
|
|
3246
|
+
lifetime--;
|
|
3247
|
+
}
|
|
3248
|
+
else {
|
|
3249
|
+
if (this.rng.random() < 0.7)
|
|
3250
|
+
lifetime--;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
else if (mat === Material.FuseFire) {
|
|
3254
|
+
// FuseFire decays deterministically for consistent burn speed
|
|
3255
|
+
lifetime--;
|
|
3256
|
+
}
|
|
3257
|
+
else if (mat === Material.BurningOil) {
|
|
3258
|
+
// BurningOil decays very slowly so the fire sits on the oil surface
|
|
3259
|
+
// for a long time, giving it time to spread to neighbors gradually.
|
|
3260
|
+
if (this.rng.random() < 0.15)
|
|
3261
|
+
lifetime--;
|
|
3262
|
+
}
|
|
3263
|
+
else if (mat === Material.Smoke) {
|
|
3264
|
+
if (this.rng.random() < 0.8)
|
|
3265
|
+
lifetime--;
|
|
3266
|
+
}
|
|
3267
|
+
else if (mat === Material.Steam) {
|
|
3268
|
+
if (this.rng.random() < 0.75)
|
|
3269
|
+
lifetime--;
|
|
3270
|
+
}
|
|
3271
|
+
else if (mat === Material.MagicPowder) {
|
|
3272
|
+
if (this.rng.random() < 0.7)
|
|
3273
|
+
lifetime--;
|
|
3274
|
+
}
|
|
3275
|
+
else if (mat === Material.ColdVapor) {
|
|
3276
|
+
// Cold vapor dissipates very slowly — ~30 seconds at 60fps.
|
|
3277
|
+
// lifetime 255 / 0.14 chance ≈ 1800 frames ≈ 30s.
|
|
3278
|
+
if (this.rng.random() < 0.14)
|
|
3279
|
+
lifetime--;
|
|
3280
|
+
}
|
|
3281
|
+
else if (mat === Material.Seed) {
|
|
3282
|
+
// Growing seeds (lifetime > 0) manage their own lifetime in the
|
|
3283
|
+
// seed growth reaction — don't decrement here.
|
|
3284
|
+
}
|
|
3285
|
+
else if (mat === Material.Duplicator) {
|
|
3286
|
+
// Duplicator's lifetime field stores the locked material id (0 = not
|
|
3287
|
+
// locked). It must NOT decay — the lock is permanent. Don't decrement.
|
|
3288
|
+
}
|
|
3289
|
+
else {
|
|
3290
|
+
lifetime--;
|
|
3291
|
+
}
|
|
3292
|
+
if (lifetime === 0) {
|
|
3293
|
+
if (mat === Material.Fire) {
|
|
3294
|
+
// Sparks and anchored fire (wax) expire to empty, not smoke.
|
|
3295
|
+
// Sparks: marked with FLAG_SPARK (bit 3).
|
|
3296
|
+
// Anchored fire: marked with FLAG_ANCHORED (bit 4) — smoke would
|
|
3297
|
+
// accumulate and suffocate the spreading flame.
|
|
3298
|
+
if (flags & (FLAG_SPARK | FLAG_ANCHORED)) {
|
|
3299
|
+
grid[i] = 0;
|
|
3300
|
+
continue;
|
|
3301
|
+
}
|
|
3302
|
+
mat = Material.Smoke;
|
|
3303
|
+
lifetime = 120;
|
|
3304
|
+
}
|
|
3305
|
+
else if (mat === Material.FuseFire) {
|
|
3306
|
+
// FuseFire expires to empty (not smoke) — smoke would accumulate
|
|
3307
|
+
// below the rising fire and suffocate the burn trail.
|
|
3308
|
+
grid[i] = 0;
|
|
3309
|
+
continue;
|
|
3310
|
+
}
|
|
3311
|
+
else if (mat === Material.BurningOil) {
|
|
3312
|
+
// BurningOil expires to smoke — the oil is consumed.
|
|
3313
|
+
mat = Material.Smoke;
|
|
3314
|
+
lifetime = 60;
|
|
3315
|
+
}
|
|
3316
|
+
else if (mat === Material.Smoke) {
|
|
3317
|
+
grid[i] = 0;
|
|
3318
|
+
continue;
|
|
3319
|
+
}
|
|
3320
|
+
else if (mat === Material.Steam) {
|
|
3321
|
+
if (this.rng.random() < 0.7) {
|
|
3322
|
+
mat = Material.Water;
|
|
3323
|
+
lifetime = 0;
|
|
3324
|
+
}
|
|
3325
|
+
else {
|
|
3326
|
+
grid[i] = 0;
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
else if (mat === Material.GasVapor) {
|
|
3331
|
+
// Expired vapor → empty
|
|
3332
|
+
grid[i] = 0;
|
|
3333
|
+
continue;
|
|
3334
|
+
}
|
|
3335
|
+
else if (mat === Material.ColdVapor) {
|
|
3336
|
+
// Cold vapor fully dissipated → empty
|
|
3337
|
+
grid[i] = 0;
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
else if (mat === Material.Hydrogen) {
|
|
3341
|
+
grid[i] = 0;
|
|
3342
|
+
continue;
|
|
3343
|
+
}
|
|
3344
|
+
else if (mat === Material.Spore) {
|
|
3345
|
+
// Spore that never germinated dissipates to empty.
|
|
3346
|
+
grid[i] = 0;
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
else if (mat === Material.Plasma) {
|
|
3350
|
+
// Plasma → fire then smoke
|
|
3351
|
+
mat = Material.Fire;
|
|
3352
|
+
lifetime = 15;
|
|
3353
|
+
}
|
|
3354
|
+
else if (mat === Material.Fireflies) {
|
|
3355
|
+
// Fireflies don't expire (lifetime stays at max)
|
|
3356
|
+
lifetime = 255;
|
|
3357
|
+
}
|
|
3358
|
+
else if (mat === Material.Nanobots) {
|
|
3359
|
+
lifetime = 255;
|
|
3360
|
+
}
|
|
3361
|
+
else if (mat === Material.Wood || mat === Material.Plant || mat === Material.Oil ||
|
|
3362
|
+
mat === Material.Flesh || mat === Material.Leaf || mat === Material.TreeWood ||
|
|
3363
|
+
mat === Material.Root || mat === Material.Grass || mat === Material.Toast ||
|
|
3364
|
+
mat === Material.Plastic || mat === Material.Wax || mat === Material.Fuse ||
|
|
3365
|
+
mat === Material.Rubber || mat === Material.C4 || mat === Material.Glitter ||
|
|
3366
|
+
mat === Material.MagicPowder) {
|
|
3367
|
+
mat = Material.Smoke;
|
|
3368
|
+
lifetime = 60;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
// Clear FLAG_UPDATED and write back. Only write if the packed value
|
|
3373
|
+
// actually changed — avoids unnecessary memory writes when nothing
|
|
3374
|
+
// happened (e.g. static solids with lifetime=0).
|
|
3375
|
+
const newFlags = flags & ~FLAG_UPDATED;
|
|
3376
|
+
const newPacked = (mat & 0xff) | ((lifetime & 0xff) << 8) | ((newFlags & 0xff) << 16);
|
|
3377
|
+
if (newPacked !== packed) {
|
|
3378
|
+
grid[i] = newPacked;
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
}
|