Initial commit: AtomicEngine v0.2
- Rust/WASM physics engine (grid, material, physics, render_buffer) - 16 material types (stone, sand, water, wood, fire, lava, glass, ice, oil, acid, steam, spark, smoke + player placeholder) - Fire system (fuel-based, flammability-scaled consumption, spread, smoke/sparks) - Lighting system (emission, flood-fill propagation, glow, reflection) - Per-material pixel texturing (wood grain, dirt specks, stone noise, flame variation) - WebGL2 renderer with camera (WASD move, mouse wheel zoom) - Native x86 debug binary (identical dimensions, FPS counter) - Large world 1024x768 with active-region simulation (camera + 200px margin) - UI material palette + FPS display
This commit is contained in:
@@ -0,0 +1,224 @@
|
|||||||
|
# AtomicEngine
|
||||||
|
|
||||||
|
> Eine 2D-Platformer-Gameengine bei der jeder Pixel wie ein Atom mit eigenen
|
||||||
|
> Eigenschaften und Anziehungskräften zu seinen Nachbarn agiert.
|
||||||
|
> Browser-basiert mit Rust/WASM für die Physik und WebGL2 fürs Rendering.
|
||||||
|
|
||||||
|
## Architektur-Übersicht
|
||||||
|
|
||||||
|
```
|
||||||
|
engine/ (Rust → WASM) web/ (TypeScript + Vite)
|
||||||
|
┌─────────────────────┐ ┌──────────────────────┐
|
||||||
|
│ grid.rs │ │ main.ts │
|
||||||
|
│ material.rs │ │ renderer.ts (WebGL2) │
|
||||||
|
│ physics.rs │ │ input.ts │
|
||||||
|
│ render_buffer.rs │ │ camera.ts │
|
||||||
|
│ lib.rs (wasm API) │ │ │
|
||||||
|
└─────────────────────┘ └──────────────────────┘
|
||||||
|
│ │
|
||||||
|
└── SharedArrayBuffer ──────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech-Stack
|
||||||
|
|
||||||
|
| Schicht | Technologie | Warum |
|
||||||
|
|---------------|--------------------------|------------------------------------|
|
||||||
|
| Physik | Rust → WASM | Performance, Sicherheit |
|
||||||
|
| WASM Bridge | wasm-bindgen, wasm-pack | Auto JS-Bindings |
|
||||||
|
| Renderer | WebGL2 (Canvas) | GPU-beschleunigt, 60fps |
|
||||||
|
| Web Shell | TypeScript, Vite | Build, Dev-Server, HMR |
|
||||||
|
| Speicher | SharedArrayBuffer | Zero-copy WASM ↔ JS |
|
||||||
|
|
||||||
|
## Welt-Modell
|
||||||
|
|
||||||
|
- **Große Welt**: 1024×768 Pixel (4×3 Chunks à 256×256)
|
||||||
|
- Nur Zellen im Sichtfeld + 80px Margin werden simuliert
|
||||||
|
- Rest eingefroren — keine CPU-Kosten für unsichtbare Bereiche
|
||||||
|
- Grid wächst auf 1024×768 = 786K Zellen (~3 MB Speicher)
|
||||||
|
|
||||||
|
### Datenstruktur Cell (pro Pixel)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Cell {
|
||||||
|
material: u8, // Material-ID (0 = Luft)
|
||||||
|
health: u8, // Lebenspunkte/Dichte
|
||||||
|
temp: u8, // Temperatur
|
||||||
|
flags: u8, // Bitflags (aktiv, Player, Flüssig, ...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Grid ist SoA (Structure of Arrays):
|
||||||
|
- `Vec<u8>` für materials, healths, temps, flags
|
||||||
|
- Breite × Höhe = Chunk-Größe
|
||||||
|
|
||||||
|
## Material-System
|
||||||
|
|
||||||
|
16 Materialien (0=Luft, 1=Stein, 2=Erde, 3=Sand, 4=Wasser, 5=Holz, 6=Lava,
|
||||||
|
7=Player, 8=Dampf, 9=Feuer, 10=Glas, 11=Eis, 12=Öl, 13=Säure, 14=Funken, 15=Rauch).
|
||||||
|
|
||||||
|
Properties pro Material: `MaterialProps` in `engine/src/material.rs`:
|
||||||
|
- `color`, `density`, Bewegung-Typ (`is_powder/is_liquid/is_gas/is_solid/is_static`)
|
||||||
|
- `flammability`, `melt_temp`, `boil_temp`, `heat_conduct`, `acid_resist`
|
||||||
|
- `light_emit` (0–255, wie viel Licht das Material abstrahlt)
|
||||||
|
- `light_block` (0–255, wie stark das Material Licht blockiert)
|
||||||
|
|
||||||
|
### Interaktions-System
|
||||||
|
|
||||||
|
**Heat-Transfer**: Temperatur-Diffusion über Moore-Nachbarn (`heat_conduct`-abhängig).
|
||||||
|
|
||||||
|
**Phase-Transformationen** (`material::transform`):
|
||||||
|
- Eis (11) → Wasser (4) bei temp ≥ 5°C
|
||||||
|
- Wasser (4) → Dampf (8) bei temp ≥ 100°C
|
||||||
|
- Dampf (8) → Wasser (4) bei temp ≤ 80°C
|
||||||
|
- Lava (6) → Stein (1) bei temp ≤ 5°C
|
||||||
|
- Holz (5) → Feuer (9) bei temp ≥ 240°C
|
||||||
|
- Öl (12) → Feuer (9) bei temp ≥ 120°C
|
||||||
|
|
||||||
|
**Reaktionen** (`physics::react`):
|
||||||
|
- Wasser + Lava → Dampf + Stein
|
||||||
|
- Wasser + Feuer → Dampf + Luft (löscht Feuer)
|
||||||
|
- Lava + Holz/Öl → entzündet Holz/Öl sofort zu Feuer
|
||||||
|
- Säure + Holz/Sand → zerstört Holz/Sand
|
||||||
|
- Säure + Stein/Erde → zerstört Stein/Erde (langsamer)
|
||||||
|
|
||||||
|
**Feuer-System**:
|
||||||
|
- Braucht Brennstoff (Holz, Öl) um zu überleben — prüft direkt unter sich + Moore-Nachbarn
|
||||||
|
- Feuer-Säule: Hat eine Zelle Feuer unter sich, gilt sie als versorgt (Ketten-Propagation)
|
||||||
|
- Mit Brennstoff: keine Health-Decay
|
||||||
|
- Ohne Brennstoff: −6hp/Frame → erlischt in ~7 Frames (~0.1s)
|
||||||
|
- Brenn-Rate `flammability`-abhängig: `300/flam` Frames zwischen −1hp-Konsum
|
||||||
|
- Holz (30): ~10 Frames/Konsum → ~42s pro Block
|
||||||
|
- Öl (80): ~3 Frames/Konsum → ~13s pro Block
|
||||||
|
- Ausbreitung: 1/250 Chance auf brennbare Nachbarn
|
||||||
|
- Wasser/Eis in Nachbarschaft → Feuer erlischt sofort
|
||||||
|
- Erzeugt Funken (14) und Rauch (15) beim Brennen
|
||||||
|
- **Flammen-Züngeln**: 1/5 Chance pro Zelle, Funken-Partikel nach oben zu feuern (4–12 Frames Lebensdauer)
|
||||||
|
|
||||||
|
**Ephemere Partikel** (Funken + Rauch):
|
||||||
|
- `update_ephemeral`: dekrementiert Health, bei 0 → Luft
|
||||||
|
- Flammen-Funken: 4–12 Frames, 1/5 Emission, kurzlebig für Flammenspitzen
|
||||||
|
- Funken: 30–80 Frames Lebensdauer, orange, 1/16 Emission/Frame
|
||||||
|
- Rauch: 140–220 Frames, hellgrau transparent, 1/6 Emission/Frame
|
||||||
|
- Gas-Pass separat (alle 2 Frames, top→bottom = max 1px/Frame Aufstieg)
|
||||||
|
|
||||||
|
## Physik-Loop (pro Frame)
|
||||||
|
|
||||||
|
Reihenfolge in `physics::update(cam_x, cam_y, rw, rh, zoom)`:
|
||||||
|
|
||||||
|
Nur Zellen innerhalb **Kamera-Sichtfeld + 80px Margin** werden simuliert.
|
||||||
|
Rest der Welt ist eingefroren (keine CPU-Kosten).
|
||||||
|
|
||||||
|
1. **update_fire** — Feuer-Update: Brennstoff-Verbrauch, Ausbreitung, Funken/Rauch-Emission
|
||||||
|
2. **Haupt-Loop** (bottom→top, alternierende Spalten, nur aktive Region):
|
||||||
|
- Heat-Transfer über Moore-Nachbarn
|
||||||
|
- Material-Reaktionen (Water+Lava, Säure+Holz, ...)
|
||||||
|
- Phasen-Transformation (Temp-basiert)
|
||||||
|
- Bewegung: Powders ↓, Liquids ↓↔, Solids ↓ (kein Gas!)
|
||||||
|
3. **update_gases** (top→bottom, nur jedes 2. Frame, nur aktive Region): Gas steigt 1px/Frame
|
||||||
|
4. **update_ephemeral** (nur aktive Region): Spark/Smoke Health dekrementieren, bei 0 → Luft
|
||||||
|
|
||||||
|
## Level-Format
|
||||||
|
|
||||||
|
RGBA-PNG, jeder Pixel = 1 Atom:
|
||||||
|
- R = Material-ID (0=Luft)
|
||||||
|
- G = Dichte/Gesundheit
|
||||||
|
- B = Temperatur
|
||||||
|
- A = Flags
|
||||||
|
|
||||||
|
Levels in Aseprite/Photoshop malbar. Große Welten = Raster von PNG-Dateien.
|
||||||
|
|
||||||
|
## Rendering
|
||||||
|
|
||||||
|
1. WASM schreibt sichtbaren Bereich als RGBA-Buffer
|
||||||
|
2. Buffer als Uint8Array aus WASM-Speicher gelesen
|
||||||
|
3. WebGL2 lädt als Textur → Fullscreen-Quad
|
||||||
|
4. Kamera-Matrix für Scroll/Zoom
|
||||||
|
|
||||||
|
### Textur-System
|
||||||
|
|
||||||
|
Jedes Material bekommt deterministische Pixel-Variation basierend auf Grid-Koordinaten:
|
||||||
|
- **Holz**: Vertikale Maserung (dünne Fasern alle ~5px, ±1px Welle)
|
||||||
|
- **Erde**: Grobkörnige Flecken (niederfrequentes Rauschen, ±20 RGB)
|
||||||
|
- **Stein/Glas**: Subtiles Rauschen (±10 RGB)
|
||||||
|
- **Sand**: Feines Granulat (±6 RGB)
|
||||||
|
- **Eis**: Minimales Rauschen (±5 RGB)
|
||||||
|
- **Flüssigkeiten/Gase**: Keine Textur
|
||||||
|
- **Feuer/Lava**: Kanal-getrennte Varianz (R ±6, G ±12, B ±8) — natürliche Gelb-/Orange-Mischung
|
||||||
|
|
||||||
|
### Beleuchtungs-System
|
||||||
|
|
||||||
|
**Light-Propagation** in `render_buffer.rs`:
|
||||||
|
1. Emissions-Pass: Jedes Pixel mit `light_emit > 0` strahlt Licht
|
||||||
|
2. Flood-Fill (max 12 Passes, early-termination): 8-Richtungs-Propagation
|
||||||
|
- Falloff: 2 (kardinal) / 3 (diagonal) + `light_block` (gecapped bei 200)
|
||||||
|
- Opaque Pixel blocken Weitergabe, werden aber selbst beleuchtet
|
||||||
|
3. Abwechselnde Scan-Richtung für gleichmäßige Verteilung
|
||||||
|
|
||||||
|
**Lichtquellen**: Feuer (255), Lava (200), Funken (180)
|
||||||
|
|
||||||
|
**Glow-Effekt**: Luft-Pixel mit Licht blenden von dunklem Hintergrund zu warmem Orange — quadratische Kurve (`t = lvl²/255`) für natürlichen Abfall. Nur direkt an der Quelle stark sichtbar.
|
||||||
|
|
||||||
|
**Reflexion**: Semi-transparente Materialien (Glas, Wasser, Eis — block 1–179) werfen 50% des empfangenen Lichts an Nachbar-Pixel zurück → indirekte Beleuchtung.
|
||||||
|
|
||||||
|
**Beleuchtungs-Formel** (Material-Pixel): Additiv: `r = min(255, r × 150/256 + light)` — Ambient-Basis 59% + Licht obendrauf.
|
||||||
|
|
||||||
|
### UI
|
||||||
|
- **Material-Palette**: Leiste am unteren Bildschirmrand, Farb-Swatch + Name + Taste
|
||||||
|
- **FPS-Anzeige**: Oben rechts, grün, alle 500ms aktualisiert
|
||||||
|
|
||||||
|
## Build & Entwicklung
|
||||||
|
|
||||||
|
**WICHTIG**: Native Debug-Binary und WASM/Web-Build müssen immer identisch laufen.
|
||||||
|
Der Debug-Build nutzt dieselben Module (`grid`, `material`, `physics`, `render_buffer`)
|
||||||
|
und dieselben Dimensionen (256×192 Grid, 320×180 Render-Buffer). Performance-Unterschiede
|
||||||
|
sind WASM-Overhead, nicht Logik-Abweichungen. Bei Änderungen an der Engine immer beide
|
||||||
|
Builds testen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rust → WASM bauen
|
||||||
|
cd engine && wasm-pack build --target web --out-dir ../web/pkg
|
||||||
|
|
||||||
|
# Native Debug-Binary (x86, ohne WASM)
|
||||||
|
cd engine && cargo run --bin atomic-debug --release
|
||||||
|
|
||||||
|
# Web Dev-Server
|
||||||
|
cd web && npm run dev
|
||||||
|
|
||||||
|
# Produktion
|
||||||
|
cd web && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datei-Index (was findet man wo)
|
||||||
|
|
||||||
|
| Was | Datei |
|
||||||
|
|----------------------------------|-----------------------------------|
|
||||||
|
| WASM öffentliche API | `engine/src/lib.rs` |
|
||||||
|
| Native Debug Binary | `engine/src/main.rs` |
|
||||||
|
| Cell + Chunk + Grid DS | `engine/src/grid.rs` |
|
||||||
|
| Material-Definitionen | `engine/src/material.rs` |
|
||||||
|
| Physik: Kräfte, Sand, Fluide | `engine/src/physics.rs` |
|
||||||
|
| RGBA-Buffer Export | `engine/src/render_buffer.rs` |
|
||||||
|
| Rust-Abhängigkeiten | `engine/Cargo.toml` |
|
||||||
|
| WebGL2 Renderer | `web/src/renderer.ts` |
|
||||||
|
| WASM-Bridge + Material-Konstanten| `web/src/engine.ts` |
|
||||||
|
| Game-Loop, Wiring | `web/src/main.ts` |
|
||||||
|
| Input (Keyboard, Maus) | `web/src/input.ts` |
|
||||||
|
| Kamera (Scroll, Zoom) | `web/src/camera.ts` |
|
||||||
|
| HTML Einstieg | `web/index.html` |
|
||||||
|
| Vite Konfiguration | `web/vite.config.ts` |
|
||||||
|
| TypeScript Konfiguration | `web/tsconfig.json` |
|
||||||
|
|
||||||
|
## Aktueller Status
|
||||||
|
|
||||||
|
- [x] Phase 0: Projekt-Struktur + WASM/WebGL End-to-End ← **fertig**
|
||||||
|
- [x] Phase 1: Sand-Physik (Pixel fällt nach unten) ← **fertig** (Sand + Flüssigkeiten)
|
||||||
|
- [x] Phase 2: Mehrere Materialien + Kräfte-Tabelle ← **fertig**
|
||||||
|
- [x] Phase 3: Chunk-System + große Welt + Kamera ← **fertig**
|
||||||
|
- [ ] Phase 4: Spieler als Atom-Cluster + Input
|
||||||
|
- [ ] Phase 5: Erweiterungen (Items, Player-Interaktion, Editor, ...)
|
||||||
|
|
||||||
|
## Namenskonventionen
|
||||||
|
|
||||||
|
- Rust: snake_case, English
|
||||||
|
- TypeScript: camelCase, English
|
||||||
|
- Keine Kommentare im Code (nur wenn explizit gewünscht)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
pkg/
|
||||||
Generated
+178
@@ -0,0 +1,178 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wee_alloc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memory_units"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wee_alloc"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 0.1.10",
|
||||||
|
"libc",
|
||||||
|
"memory_units",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "atomic-engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "atomic-debug"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
wee_alloc = "0.4"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
pub struct Grid {
|
||||||
|
pub materials: Vec<u8>,
|
||||||
|
pub healths: Vec<u8>,
|
||||||
|
pub temps: Vec<u8>,
|
||||||
|
pub flags: Vec<u8>,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FLAG_UPDATED: u8 = 0b0000_0001;
|
||||||
|
pub const FLAG_PLAYER: u8 = 0b0000_0010;
|
||||||
|
|
||||||
|
impl Grid {
|
||||||
|
pub fn new(width: u32, height: u32) -> Self {
|
||||||
|
let size = (width * height) as usize;
|
||||||
|
Self {
|
||||||
|
materials: vec![0; size],
|
||||||
|
healths: vec![255; size],
|
||||||
|
temps: vec![0; size],
|
||||||
|
flags: vec![0; size],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn index(&self, x: u32, y: u32) -> usize {
|
||||||
|
(y * self.width + x) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_bounds(&self, x: i32, y: i32) -> bool {
|
||||||
|
x >= 0 && x < self.width as i32 && y >= 0 && y < self.height as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get(&self, x: u32, y: u32) -> u8 {
|
||||||
|
self.materials[self.index(x, y)]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_signed(&self, x: i32, y: i32) -> u8 {
|
||||||
|
if self.in_bounds(x, y) {
|
||||||
|
self.materials[self.index(x as u32, y as u32)]
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set(&mut self, x: u32, y: u32, material: u8) {
|
||||||
|
let idx = self.index(x, y);
|
||||||
|
self.materials[idx] = material;
|
||||||
|
self.flags[idx] |= FLAG_UPDATED;
|
||||||
|
if material == 6 {
|
||||||
|
self.temps[idx] = 200;
|
||||||
|
self.healths[idx] = 255;
|
||||||
|
} else if material == 9 {
|
||||||
|
self.temps[idx] = 200;
|
||||||
|
self.healths[idx] = 200;
|
||||||
|
} else if material == 11 {
|
||||||
|
self.temps[idx] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn swap(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
|
||||||
|
let i1 = self.index(x1, y1);
|
||||||
|
let i2 = self.index(x2, y2);
|
||||||
|
self.materials.swap(i1, i2);
|
||||||
|
self.healths.swap(i1, i2);
|
||||||
|
self.temps.swap(i1, i2);
|
||||||
|
self.flags[i1] |= FLAG_UPDATED;
|
||||||
|
self.flags[i2] |= FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self, x: u32, y: u32) -> bool {
|
||||||
|
self.get(x, y) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_flags(&mut self) {
|
||||||
|
for f in &mut self.flags {
|
||||||
|
*f &= !FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, material: u8) {
|
||||||
|
for dy in 0..h {
|
||||||
|
for dx in 0..w {
|
||||||
|
let px = x + dx;
|
||||||
|
let py = y + dy;
|
||||||
|
if px < self.width && py < self.height {
|
||||||
|
self.set(px, py, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_circle(&mut self, cx: u32, cy: u32, radius: u32, material: u8) {
|
||||||
|
let r = radius as i32;
|
||||||
|
for dy in -r..=r {
|
||||||
|
for dx in -r..=r {
|
||||||
|
if dx * dx + dy * dy <= r * r {
|
||||||
|
let px = (cx as i32 + dx) as u32;
|
||||||
|
let py = (cy as i32 + dy) as u32;
|
||||||
|
if px < self.width && py < self.height {
|
||||||
|
self.set(px, py, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
pub mod grid;
|
||||||
|
pub mod material;
|
||||||
|
pub mod physics;
|
||||||
|
pub mod render_buffer;
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use grid::Grid;
|
||||||
|
use physics::Physics;
|
||||||
|
use render_buffer::RenderBuffer;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ENGINE: RefCell<Option<Engine>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Engine {
|
||||||
|
grid: Grid,
|
||||||
|
physics: Physics,
|
||||||
|
render_buffer: RenderBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub fn start() {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_engine<F, R>(f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Engine) -> R,
|
||||||
|
{
|
||||||
|
ENGINE.with(|cell| {
|
||||||
|
let mut opt = cell.borrow_mut();
|
||||||
|
let engine = opt.as_mut().expect("Engine not initialized. Call init() first.");
|
||||||
|
f(engine)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn init(width: u32, height: u32) {
|
||||||
|
let engine = Engine {
|
||||||
|
grid: Grid::new(width, height),
|
||||||
|
physics: Physics::new(),
|
||||||
|
render_buffer: RenderBuffer::new(320, 180),
|
||||||
|
};
|
||||||
|
ENGINE.with(|cell| {
|
||||||
|
*cell.borrow_mut() = Some(engine);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn get_width() -> u32 {
|
||||||
|
with_engine(|e| e.grid.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn get_height() -> u32 {
|
||||||
|
with_engine(|e| e.grid.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn fill_rect(x: u32, y: u32, w: u32, h: u32, material: u8) {
|
||||||
|
with_engine(|e| e.grid.fill_rect(x, y, w, h, material));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn fill_circle(x: u32, y: u32, radius: u32, material: u8) {
|
||||||
|
with_engine(|e| e.grid.fill_circle(x, y, radius, material));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn simulate(cam_x: i32, cam_y: i32, zoom: f32) {
|
||||||
|
with_engine(|e| {
|
||||||
|
let rw = e.render_buffer.width;
|
||||||
|
let rh = e.render_buffer.height;
|
||||||
|
e.physics.update(&mut e.grid, cam_x, cam_y, rw, rh, zoom);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn render_buffer_ptr() -> *const u8 {
|
||||||
|
ENGINE.with(|cell| {
|
||||||
|
let opt = cell.borrow();
|
||||||
|
let engine = opt.as_ref().expect("Engine not initialized.");
|
||||||
|
engine.render_buffer.pixels.as_ptr()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn render_buffer_len() -> usize {
|
||||||
|
ENGINE.with(|cell| {
|
||||||
|
let opt = cell.borrow();
|
||||||
|
let engine = opt.as_ref().expect("Engine not initialized.");
|
||||||
|
engine.render_buffer.pixels.len()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn render_width() -> u32 {
|
||||||
|
with_engine(|e| e.render_buffer.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn render_height() -> u32 {
|
||||||
|
with_engine(|e| e.render_buffer.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn update_render(cam_x: i32, cam_y: i32, zoom: f32) {
|
||||||
|
with_engine(|e| e.render_buffer.render(&e.grid, cam_x, cam_y, zoom));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn reset() {
|
||||||
|
with_engine(|e| {
|
||||||
|
e.grid = Grid::new(e.grid.width, e.grid.height);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
mod grid;
|
||||||
|
mod material;
|
||||||
|
mod physics;
|
||||||
|
mod render_buffer;
|
||||||
|
|
||||||
|
use grid::Grid;
|
||||||
|
use physics::Physics;
|
||||||
|
use render_buffer::RenderBuffer;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let width = 1024u32;
|
||||||
|
let height = 768u32;
|
||||||
|
let rw = 320u32;
|
||||||
|
let rh = 180u32;
|
||||||
|
let mut grid = Grid::new(width, height);
|
||||||
|
let mut physics = Physics::new();
|
||||||
|
let mut rb = RenderBuffer::new(rw, rh);
|
||||||
|
|
||||||
|
grid.fill_rect(440, 720, 144, 48, 1);
|
||||||
|
grid.fill_rect(460, 700, 104, 20, 2);
|
||||||
|
grid.fill_rect(470, 600, 84, 12, 5);
|
||||||
|
grid.fill_circle(490, 560, 20, 3);
|
||||||
|
grid.fill_rect(430, 650, 50, 30, 4);
|
||||||
|
grid.fill_rect(540, 650, 50, 30, 6);
|
||||||
|
grid.fill_rect(500, 640, 10, 18, 11);
|
||||||
|
grid.fill_rect(480, 630, 64, 24, 5);
|
||||||
|
grid.fill_rect(480, 618, 64, 12, 9);
|
||||||
|
|
||||||
|
let cam_x = 512i32;
|
||||||
|
let cam_y = 600i32;
|
||||||
|
let zoom = 2.0f32;
|
||||||
|
|
||||||
|
let mut frame = 0;
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
physics.update(&mut grid, cam_x, cam_y, rw, rh, zoom);
|
||||||
|
rb.render(&grid, cam_x, cam_y, zoom);
|
||||||
|
frame += 1;
|
||||||
|
|
||||||
|
if frame % 60 == 0 {
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
let fps = frame as f64 / elapsed.as_secs_f64();
|
||||||
|
let (fire, wood, spark, smoke) = count_mats(&grid);
|
||||||
|
|
||||||
|
println!("--- {}s (frame {}) fps: {:.0} ---", frame / 60, frame, fps);
|
||||||
|
println!("fire: {}, wood: {}, spark: {}, smoke: {}", fire, wood, spark, smoke);
|
||||||
|
|
||||||
|
if fire == 0 {
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
let fps = frame as f64 / elapsed.as_secs_f64();
|
||||||
|
println!("Fire died at frame {}, avg fps: {:.0}", frame, fps);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if frame >= 3600 {
|
||||||
|
println!("Timeout at 60s");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_mats(grid: &Grid) -> (u32, u32, u32, u32) {
|
||||||
|
let mut fire = 0u32;
|
||||||
|
let mut wood = 0u32;
|
||||||
|
let mut spark = 0u32;
|
||||||
|
let mut smoke = 0u32;
|
||||||
|
for y in 0..grid.height {
|
||||||
|
for x in 0..grid.width {
|
||||||
|
match grid.get(x, y) {
|
||||||
|
5 => wood += 1,
|
||||||
|
9 => fire += 1,
|
||||||
|
14 => spark += 1,
|
||||||
|
15 => smoke += 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(fire, wood, spark, smoke)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temp_range(grid: &Grid) -> (u8, u8) {
|
||||||
|
let mut min = 255u8;
|
||||||
|
let mut max = 0u8;
|
||||||
|
for y in 0..grid.height {
|
||||||
|
for x in 0..grid.width {
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
let t = grid.temps[idx];
|
||||||
|
if t > 0 {
|
||||||
|
min = min.min(t);
|
||||||
|
max = max.max(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min == 255 { min = 0; }
|
||||||
|
(min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_grid(grid: &Grid) {
|
||||||
|
for y in 0..grid.height {
|
||||||
|
for x in 0..grid.width {
|
||||||
|
let c = match grid.get(x, y) {
|
||||||
|
5 => 'W',
|
||||||
|
9 => 'F',
|
||||||
|
14 => '*',
|
||||||
|
15 => '%',
|
||||||
|
0 => '.',
|
||||||
|
1 => '#',
|
||||||
|
4 => '~',
|
||||||
|
6 => 'L',
|
||||||
|
8 => '\'',
|
||||||
|
_ => '?',
|
||||||
|
};
|
||||||
|
print!("{}", c);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Material {
|
||||||
|
Air = 0,
|
||||||
|
Stone = 1,
|
||||||
|
Dirt = 2,
|
||||||
|
Sand = 3,
|
||||||
|
Water = 4,
|
||||||
|
Wood = 5,
|
||||||
|
Lava = 6,
|
||||||
|
Player = 7,
|
||||||
|
Steam = 8,
|
||||||
|
Fire = 9,
|
||||||
|
Glass = 10,
|
||||||
|
Ice = 11,
|
||||||
|
Oil = 12,
|
||||||
|
Acid = 13,
|
||||||
|
Spark = 14,
|
||||||
|
Smoke = 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Material {
|
||||||
|
pub fn from_u8(val: u8) -> Material {
|
||||||
|
match val {
|
||||||
|
0 => Material::Air,
|
||||||
|
1 => Material::Stone,
|
||||||
|
2 => Material::Dirt,
|
||||||
|
3 => Material::Sand,
|
||||||
|
4 => Material::Water,
|
||||||
|
5 => Material::Wood,
|
||||||
|
6 => Material::Lava,
|
||||||
|
7 => Material::Player,
|
||||||
|
8 => Material::Steam,
|
||||||
|
9 => Material::Fire,
|
||||||
|
10 => Material::Glass,
|
||||||
|
11 => Material::Ice,
|
||||||
|
12 => Material::Oil,
|
||||||
|
13 => Material::Acid,
|
||||||
|
14 => Material::Spark,
|
||||||
|
15 => Material::Smoke,
|
||||||
|
_ => Material::Air,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct MaterialProps {
|
||||||
|
pub color: [u8; 4],
|
||||||
|
pub density: u8,
|
||||||
|
pub is_powder: bool,
|
||||||
|
pub is_liquid: bool,
|
||||||
|
pub is_gas: bool,
|
||||||
|
pub is_solid: bool,
|
||||||
|
pub is_static: bool,
|
||||||
|
pub flammability: u8,
|
||||||
|
pub melt_temp: u8,
|
||||||
|
pub boil_temp: u8,
|
||||||
|
pub heat_conduct: u8,
|
||||||
|
pub acid_resist: u8,
|
||||||
|
pub light_emit: u8,
|
||||||
|
pub light_block: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const M: usize = 16;
|
||||||
|
pub static PROPS: [MaterialProps; M] = [
|
||||||
|
MaterialProps { color: [0, 0, 0, 0], density: 0, is_powder: false, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 0, acid_resist: 255, light_emit: 0, light_block: 0 },
|
||||||
|
MaterialProps { color: [120, 120, 130, 255], density: 100, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: true, flammability: 0, melt_temp: 220, boil_temp: 255, heat_conduct: 3, acid_resist: 200, light_emit: 0, light_block: 255 },
|
||||||
|
MaterialProps { color: [101, 67, 33, 255], density: 60, is_powder: true, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 200, boil_temp: 255, heat_conduct: 2, acid_resist: 100, light_emit: 0, light_block: 255 },
|
||||||
|
MaterialProps { color: [194, 178, 128, 255], density: 40, is_powder: true, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 200, boil_temp: 255, heat_conduct: 5, acid_resist: 150, light_emit: 0, light_block: 200 },
|
||||||
|
MaterialProps { color: [30, 60, 200, 180], density: 10, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 0, boil_temp: 100, heat_conduct: 10, acid_resist: 200, light_emit: 0, light_block: 80 },
|
||||||
|
MaterialProps { color: [90, 60, 30, 255], density: 80, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: false, flammability: 30, melt_temp: 240, boil_temp: 255, heat_conduct: 1, acid_resist: 80, light_emit: 0, light_block: 255 },
|
||||||
|
MaterialProps { color: [255, 100, 0, 255], density: 50, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 200, boil_temp: 255, heat_conduct: 20, acid_resist: 255, light_emit: 200, light_block: 60 },
|
||||||
|
MaterialProps { color: [50, 200, 50, 255], density: 90, is_powder: false, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 5, acid_resist: 255, light_emit: 0, light_block: 100 },
|
||||||
|
MaterialProps { color: [200, 200, 220, 140], density: 1, is_powder: false, is_liquid: false, is_gas: true, is_solid: false, is_static: false, flammability: 0, melt_temp: 0, boil_temp: 0, heat_conduct: 5, acid_resist: 255, light_emit: 0, light_block: 20 },
|
||||||
|
MaterialProps { color: [255, 180, 30, 255], density: 2, is_powder: false, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 255, melt_temp: 255, boil_temp: 255, heat_conduct: 50, acid_resist: 255, light_emit: 255, light_block: 30 },
|
||||||
|
MaterialProps { color: [180, 210, 220, 255], density: 95, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: false, flammability: 0, melt_temp: 160, boil_temp: 255, heat_conduct: 1, acid_resist: 250, light_emit: 0, light_block: 40 },
|
||||||
|
MaterialProps { color: [180, 220, 255, 255], density: 85, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: false, flammability: 0, melt_temp: 5, boil_temp: 100, heat_conduct: 8, acid_resist: 200, light_emit: 0, light_block: 100 },
|
||||||
|
MaterialProps { color: [180, 140, 40, 200], density: 15, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 80, melt_temp: 0, boil_temp: 120, heat_conduct: 3, acid_resist: 200, light_emit: 0, light_block: 60 },
|
||||||
|
MaterialProps { color: [100, 255, 60, 200], density: 30, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 0, boil_temp: 150, heat_conduct: 15, acid_resist: 255, light_emit: 0, light_block: 80 },
|
||||||
|
MaterialProps { color: [255, 180, 40, 255], density: 1, is_powder: false, is_liquid: false, is_gas: true, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 30, acid_resist: 255, light_emit: 180, light_block: 30 },
|
||||||
|
MaterialProps { color: [160, 155, 150, 200], density: 1, is_powder: false, is_liquid: false, is_gas: true, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 5, acid_resist: 255, light_emit: 0, light_block: 80 },
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Interaction {
|
||||||
|
pub attraction: i8,
|
||||||
|
pub can_merge: bool,
|
||||||
|
pub bond_strength: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interaction {
|
||||||
|
const fn none() -> Self {
|
||||||
|
Interaction { attraction: 0, can_merge: false, bond_strength: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interact(a: u8, b: u8) -> Interaction {
|
||||||
|
match (a, b) {
|
||||||
|
(1, 4) | (4, 1) => Interaction { attraction: 5, can_merge: false, bond_strength: 3 },
|
||||||
|
(1, 6) | (6, 1) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
|
||||||
|
(3, 4) | (4, 3) => Interaction { attraction: 8, can_merge: false, bond_strength: 0 },
|
||||||
|
(4, 6) | (6, 4) => Interaction { attraction: -20, can_merge: false, bond_strength: 0 },
|
||||||
|
(5, 9) | (9, 5) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
|
||||||
|
(5, 4) | (4, 5) => Interaction { attraction: 10, can_merge: false, bond_strength: 2 },
|
||||||
|
(11, 4) | (4, 11) => Interaction { attraction: 15, can_merge: false, bond_strength: 5 },
|
||||||
|
(11, 6) | (6, 11) => Interaction { attraction: -30, can_merge: false, bond_strength: 0 },
|
||||||
|
(9, 4) | (4, 9) => Interaction { attraction: -10, can_merge: false, bond_strength: 0 },
|
||||||
|
(12, 9) | (9, 12) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
|
||||||
|
(13, 1) | (1, 13) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
|
||||||
|
(13, 3) | (3, 13) | (13, 5) | (5, 13) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
|
||||||
|
_ => {
|
||||||
|
if a == b {
|
||||||
|
if a == 4 || a == 6 || a == 12 || a == 13 {
|
||||||
|
Interaction { attraction: 20, can_merge: true, bond_strength: 1 }
|
||||||
|
} else if a == 3 || a == 2 {
|
||||||
|
Interaction { attraction: 5, can_merge: false, bond_strength: 0 }
|
||||||
|
} else if a == 1 || a == 10 || a == 11 {
|
||||||
|
Interaction { attraction: 30, can_merge: false, bond_strength: 10 }
|
||||||
|
} else if a == 5 {
|
||||||
|
Interaction { attraction: 15, can_merge: false, bond_strength: 8 }
|
||||||
|
} else {
|
||||||
|
Interaction::none()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Interaction::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform(temp: u8, material: u8) -> u8 {
|
||||||
|
match material {
|
||||||
|
4 => if temp >= 100 { 8 } else { 4 },
|
||||||
|
11 => if temp >= 5 { 4 } else { 11 },
|
||||||
|
6 => if temp <= 5 { 1 } else { 6 },
|
||||||
|
8 => if temp <= 80 { 4 } else { 8 },
|
||||||
|
5 => if temp >= 240 { 9 } else { 5 },
|
||||||
|
12 => if temp >= 120 { 9 } else { 12 },
|
||||||
|
_ => material,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,528 @@
|
|||||||
|
use crate::grid::Grid;
|
||||||
|
use crate::material::{self, PROPS};
|
||||||
|
|
||||||
|
const MOORE_X: [i32; 8] = [-1, 0, 1, -1, 1, -1, 0, 1];
|
||||||
|
const MOORE_Y: [i32; 8] = [-1, -1, -1, 0, 0, 1, 1, 1];
|
||||||
|
|
||||||
|
pub struct Physics {
|
||||||
|
rand_state: u32,
|
||||||
|
frame: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Physics {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { rand_state: 12345, frame: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rand(&mut self) -> u32 {
|
||||||
|
self.rand_state = self.rand_state.wrapping_mul(1103515245).wrapping_add(12345);
|
||||||
|
self.rand_state >> 16
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rand_dir(&mut self) -> i32 {
|
||||||
|
if self.rand() & 1 == 0 { -1 } else { 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, grid: &mut Grid, cam_x: i32, cam_y: i32, rw: u32, rh: u32, zoom: f32) {
|
||||||
|
self.frame = self.frame.wrapping_add(1);
|
||||||
|
let w = grid.width as i32;
|
||||||
|
let h = grid.height as i32;
|
||||||
|
|
||||||
|
let margin = 200;
|
||||||
|
let vw = (rw as f32 / zoom) as i32;
|
||||||
|
let vh = (rh as f32 / zoom) as i32;
|
||||||
|
let x0 = (cam_x - vw / 2 - margin).max(0) as u32;
|
||||||
|
let y0 = (cam_y - vh / 2 - margin).max(0) as u32;
|
||||||
|
let x1 = (cam_x + vw / 2 + margin).min(w) as u32;
|
||||||
|
let y1 = (cam_y + vh / 2 + margin).min(h) as u32;
|
||||||
|
|
||||||
|
self.update_fire(grid, x0, y0, x1, y1);
|
||||||
|
|
||||||
|
for y in (y0..y1).rev() {
|
||||||
|
let parity = (self.frame & 1) as u32;
|
||||||
|
let xs = if x0 & 1 == parity { x0 } else { x0 + 1 };
|
||||||
|
for x in (xs..x1).step_by(2) {
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
let mat = grid.materials[idx];
|
||||||
|
if mat == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let temp = grid.temps[idx];
|
||||||
|
|
||||||
|
self.transfer_heat(grid, x, y, mat, temp);
|
||||||
|
self.react(grid, x, y, mat);
|
||||||
|
|
||||||
|
let mat = grid.materials[grid.index(x, y)];
|
||||||
|
let temp = grid.temps[grid.index(x, y)];
|
||||||
|
self.transform(grid, x, y, mat, temp);
|
||||||
|
|
||||||
|
let mat = grid.materials[grid.index(x, y)];
|
||||||
|
if mat == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let props = &PROPS[mat as usize];
|
||||||
|
|
||||||
|
if props.is_powder {
|
||||||
|
self.update_powder(grid, x, y, mat);
|
||||||
|
} else if props.is_liquid {
|
||||||
|
self.update_liquid(grid, x, y, mat);
|
||||||
|
} else if props.is_solid && !props.is_static {
|
||||||
|
self.update_solid(grid, x, y, mat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.frame % 2 == 0 {
|
||||||
|
self.update_gases(grid, x0, y0, x1, y1);
|
||||||
|
}
|
||||||
|
self.update_ephemeral(grid, x0, y0, x1, y1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_heat(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8, temp: u8) {
|
||||||
|
let props = &PROPS[mat as usize];
|
||||||
|
if props.heat_conduct == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
for i in 0..8 {
|
||||||
|
let nx = x as i32 + MOORE_X[i];
|
||||||
|
let ny = y as i32 + MOORE_Y[i];
|
||||||
|
if !grid.in_bounds(nx, ny) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ni = grid.index(nx as u32, ny as u32);
|
||||||
|
let nmat = grid.materials[ni];
|
||||||
|
if nmat == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ntemp = grid.temps[ni];
|
||||||
|
if temp == ntemp {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diff = if temp > ntemp {
|
||||||
|
let d = temp - ntemp;
|
||||||
|
let rate = (props.heat_conduct as u32 * d as u32) / 255;
|
||||||
|
((rate + 1) / 2) as u8
|
||||||
|
} else {
|
||||||
|
let d = ntemp - temp;
|
||||||
|
let nprops = &PROPS[nmat as usize];
|
||||||
|
let rate = (nprops.heat_conduct as u32 * d as u32) / 255;
|
||||||
|
((rate + 1) / 2) as u8
|
||||||
|
};
|
||||||
|
|
||||||
|
if diff == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if temp > ntemp {
|
||||||
|
grid.temps[idx] = temp.saturating_sub(diff);
|
||||||
|
grid.temps[ni] = ntemp.saturating_add(diff);
|
||||||
|
} else {
|
||||||
|
grid.temps[idx] = temp.saturating_add(diff);
|
||||||
|
grid.temps[ni] = ntemp.saturating_sub(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn react(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8) {
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
|
||||||
|
for i in 0..8 {
|
||||||
|
let nx = x as i32 + MOORE_X[i];
|
||||||
|
let ny = y as i32 + MOORE_Y[i];
|
||||||
|
if !grid.in_bounds(nx, ny) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ni = grid.index(nx as u32, ny as u32);
|
||||||
|
let nmat = grid.materials[ni];
|
||||||
|
if nmat == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match (mat, nmat) {
|
||||||
|
(4, 6) | (6, 4) => {
|
||||||
|
if self.rand() % 4 == 0 {
|
||||||
|
if mat == 4 {
|
||||||
|
grid.materials[idx] = 8;
|
||||||
|
grid.materials[ni] = 1;
|
||||||
|
grid.temps[idx] = 100;
|
||||||
|
grid.temps[ni] = 10;
|
||||||
|
} else {
|
||||||
|
grid.materials[idx] = 1;
|
||||||
|
grid.materials[ni] = 8;
|
||||||
|
grid.temps[idx] = 10;
|
||||||
|
grid.temps[ni] = 100;
|
||||||
|
}
|
||||||
|
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(6, 5) | (5, 6) | (6, 12) | (12, 6) => {
|
||||||
|
if self.rand() % 2 == 0 {
|
||||||
|
if mat == 6 {
|
||||||
|
grid.materials[ni] = 9;
|
||||||
|
grid.temps[ni] = 200;
|
||||||
|
grid.healths[ni] = 200;
|
||||||
|
} else {
|
||||||
|
grid.materials[idx] = 9;
|
||||||
|
grid.temps[idx] = 200;
|
||||||
|
grid.healths[idx] = 200;
|
||||||
|
}
|
||||||
|
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(4, 9) => {
|
||||||
|
if self.rand() % 2 == 0 {
|
||||||
|
grid.materials[idx] = 8;
|
||||||
|
grid.materials[ni] = 0;
|
||||||
|
grid.temps[idx] = 90;
|
||||||
|
grid.temps[ni] = 0;
|
||||||
|
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(13, 5) | (13, 3) => {
|
||||||
|
if self.rand() % 4 == 0 {
|
||||||
|
grid.materials[ni] = 0;
|
||||||
|
grid.healths[idx] = grid.healths[idx].saturating_sub(5);
|
||||||
|
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(13, 1) | (13, 2) => {
|
||||||
|
if self.rand() % 8 == 0 {
|
||||||
|
grid.materials[ni] = 0;
|
||||||
|
grid.healths[idx] = grid.healths[idx].saturating_sub(2);
|
||||||
|
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8, temp: u8) {
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
let new_mat = material::transform(temp, mat);
|
||||||
|
if new_mat != mat {
|
||||||
|
grid.materials[idx] = new_mat;
|
||||||
|
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_fire(&mut self, grid: &mut Grid, x0: u32, y0: u32, x1: u32, y1: u32) {
|
||||||
|
for y in (y0..y1).rev() {
|
||||||
|
for x in x0..x1 {
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
if grid.materials[idx] != 9 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut extinguish = false;
|
||||||
|
for i in 0..8 {
|
||||||
|
let nx = x as i32 + MOORE_X[i];
|
||||||
|
let ny = y as i32 + MOORE_Y[i];
|
||||||
|
if !grid.in_bounds(nx, ny) { continue; }
|
||||||
|
let ni = grid.index(nx as u32, ny as u32);
|
||||||
|
let nmat = grid.materials[ni];
|
||||||
|
if nmat == 4 || nmat == 11 {
|
||||||
|
grid.materials[idx] = 0;
|
||||||
|
grid.temps[idx] = 0;
|
||||||
|
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
if nmat == 4 {
|
||||||
|
grid.materials[ni] = 8;
|
||||||
|
grid.temps[ni] = 90;
|
||||||
|
} else {
|
||||||
|
grid.materials[ni] = 4;
|
||||||
|
grid.temps[ni] = 5;
|
||||||
|
}
|
||||||
|
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
|
||||||
|
extinguish = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if extinguish {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut on_fuel = false;
|
||||||
|
if y + 1 < grid.height {
|
||||||
|
let below_idx = grid.index(x, y + 1);
|
||||||
|
let below_mat = grid.materials[below_idx];
|
||||||
|
if PROPS[below_mat as usize].flammability > 0 && below_mat != 9 {
|
||||||
|
on_fuel = true;
|
||||||
|
let flam = PROPS[below_mat as usize].flammability as u32;
|
||||||
|
let threshold = (300 / flam).max(3);
|
||||||
|
if self.rand() % threshold == 0 {
|
||||||
|
let bh = grid.healths[below_idx];
|
||||||
|
if bh <= 1 {
|
||||||
|
grid.materials[below_idx] = 0;
|
||||||
|
grid.flags[below_idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
} else {
|
||||||
|
grid.healths[below_idx] = bh - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if y > 0 && self.rand() % 6 == 0 {
|
||||||
|
for &sdx in &[0, -1, 1] {
|
||||||
|
let sx = x as i32 + sdx;
|
||||||
|
if grid.in_bounds(sx, y as i32 - 1) {
|
||||||
|
let si = grid.index(sx as u32, y - 1);
|
||||||
|
if grid.materials[si] == 0 {
|
||||||
|
grid.materials[si] = 15;
|
||||||
|
grid.temps[si] = 50;
|
||||||
|
grid.healths[si] = 140 + (self.rand() % 80) as u8;
|
||||||
|
grid.flags[si] |= crate::grid::FLAG_UPDATED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !on_fuel && y + 1 < grid.height && grid.get(x, y + 1) == 9 {
|
||||||
|
on_fuel = true;
|
||||||
|
}
|
||||||
|
if !on_fuel {
|
||||||
|
for i in 0..8 {
|
||||||
|
let nx = x as i32 + MOORE_X[i];
|
||||||
|
let ny = y as i32 + MOORE_Y[i];
|
||||||
|
if !grid.in_bounds(nx, ny) { continue; }
|
||||||
|
let ni = grid.index(nx as u32, ny as u32);
|
||||||
|
let nmat = grid.materials[ni];
|
||||||
|
if nmat == 9 { continue; }
|
||||||
|
if PROPS[nmat as usize].flammability > 0 {
|
||||||
|
on_fuel = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fh = grid.healths[idx];
|
||||||
|
if on_fuel {
|
||||||
|
grid.healths[idx] = fh;
|
||||||
|
} else {
|
||||||
|
grid.healths[idx] = fh.saturating_sub(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
if grid.healths[idx] == 0 {
|
||||||
|
grid.materials[idx] = 0;
|
||||||
|
grid.temps[idx] = 0;
|
||||||
|
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.temps[idx] = 200;
|
||||||
|
|
||||||
|
for i in 0..8 {
|
||||||
|
let nx = x as i32 + MOORE_X[i];
|
||||||
|
let ny = y as i32 + MOORE_Y[i];
|
||||||
|
if !grid.in_bounds(nx, ny) { continue; }
|
||||||
|
let ni = grid.index(nx as u32, ny as u32);
|
||||||
|
let nmat = grid.materials[ni];
|
||||||
|
if nmat == 0 { continue; }
|
||||||
|
|
||||||
|
if PROPS[nmat as usize].flammability > 0 && nmat != 9 && self.rand() % 250 == 0 {
|
||||||
|
grid.materials[ni] = 9;
|
||||||
|
grid.temps[ni] = 200;
|
||||||
|
grid.healths[ni] = 40;
|
||||||
|
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if y > 0 && self.rand() % 5 == 0 {
|
||||||
|
let above = grid.get(x, y - 1);
|
||||||
|
if above == 0 {
|
||||||
|
let ai = grid.index(x, y - 1);
|
||||||
|
grid.materials[ai] = 14;
|
||||||
|
grid.temps[ai] = 180;
|
||||||
|
grid.healths[ai] = 4 + (self.rand() % 8) as u8;
|
||||||
|
grid.flags[ai] |= crate::grid::FLAG_UPDATED;
|
||||||
|
} else {
|
||||||
|
let sdir = self.rand_dir();
|
||||||
|
let sx = x as i32 + sdir;
|
||||||
|
if grid.in_bounds(sx, y as i32 - 1) && grid.get(sx as u32, y - 1) == 0 {
|
||||||
|
let si = grid.index(sx as u32, y - 1);
|
||||||
|
grid.materials[si] = 14;
|
||||||
|
grid.temps[si] = 180;
|
||||||
|
grid.healths[si] = 4 + (self.rand() % 8) as u8;
|
||||||
|
grid.flags[si] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if y > 0 && self.rand() % 16 == 0 {
|
||||||
|
let above = grid.get(x, y - 1);
|
||||||
|
if above == 0 {
|
||||||
|
let ai = grid.index(x, y - 1);
|
||||||
|
grid.materials[ai] = 14;
|
||||||
|
grid.temps[ai] = 150;
|
||||||
|
grid.healths[ai] = 30 + (self.rand() % 50) as u8;
|
||||||
|
grid.flags[ai] |= crate::grid::FLAG_UPDATED;
|
||||||
|
} else {
|
||||||
|
let sdir = self.rand_dir();
|
||||||
|
let sx = x as i32 + sdir;
|
||||||
|
if grid.in_bounds(sx, y as i32 - 1) && grid.get(sx as u32, y - 1) == 0 {
|
||||||
|
let si = grid.index(sx as u32, y - 1);
|
||||||
|
grid.materials[si] = 14;
|
||||||
|
grid.temps[si] = 150;
|
||||||
|
grid.healths[si] = 30 + (self.rand() % 50) as u8;
|
||||||
|
grid.flags[si] |= crate::grid::FLAG_UPDATED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_powder(&mut self, grid: &mut Grid, x: u32, y: u32, _mat: u8) {
|
||||||
|
if y + 1 >= grid.height {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let below = grid.get(x, y + 1);
|
||||||
|
let bprops = &PROPS[below as usize];
|
||||||
|
if below == 0 || bprops.is_liquid || bprops.is_gas {
|
||||||
|
grid.swap(x, y, x, y + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = self.rand_dir();
|
||||||
|
let lx = x as i32 + dir;
|
||||||
|
let rx = x as i32 - dir;
|
||||||
|
|
||||||
|
if grid.in_bounds(lx, y as i32 + 1) && grid.get(lx as u32, y + 1) == 0 {
|
||||||
|
grid.swap(x, y, lx as u32, y + 1);
|
||||||
|
} else if grid.in_bounds(rx, y as i32 + 1) && grid.get(rx as u32, y + 1) == 0 {
|
||||||
|
grid.swap(x, y, rx as u32, y + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_liquid(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8) {
|
||||||
|
if y + 1 >= grid.height {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mprops = &PROPS[mat as usize];
|
||||||
|
|
||||||
|
let below = grid.get(x, y + 1);
|
||||||
|
let bprops = &PROPS[below as usize];
|
||||||
|
let can_fall = below == 0 || bprops.is_gas
|
||||||
|
|| (bprops.is_liquid && mprops.density > bprops.density);
|
||||||
|
|
||||||
|
if can_fall {
|
||||||
|
let was_liquid = bprops.is_liquid;
|
||||||
|
grid.swap(x, y, x, y + 1);
|
||||||
|
if was_liquid {
|
||||||
|
let pdir = self.rand_dir();
|
||||||
|
for dist in 1..=4i32 {
|
||||||
|
let sx = x as i32 + pdir * dist;
|
||||||
|
if !grid.in_bounds(sx, y as i32 + 1) { break; }
|
||||||
|
let sxu = sx as u32;
|
||||||
|
if grid.get(sxu, y + 1) == 0 {
|
||||||
|
grid.swap(x, y + 1, sxu, y + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = self.rand_dir();
|
||||||
|
for &d in &[dir, -dir] {
|
||||||
|
let dx = x as i32 + d;
|
||||||
|
if grid.in_bounds(dx, y as i32 + 1) {
|
||||||
|
let db = grid.get(dx as u32, y + 1);
|
||||||
|
let dp = &PROPS[db as usize];
|
||||||
|
if db == 0 || dp.is_gas || (dp.is_liquid && mprops.density > dp.density) {
|
||||||
|
grid.swap(x, y, dx as u32, y + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_spread = 10 + (self.rand() % 40) as i32;
|
||||||
|
for &d in &[dir, -dir] {
|
||||||
|
for dist in 1..=max_spread {
|
||||||
|
let sx = x as i32 + d * dist;
|
||||||
|
if !grid.in_bounds(sx, y as i32) { break; }
|
||||||
|
let sxu = sx as u32;
|
||||||
|
let cell = grid.get(sxu, y);
|
||||||
|
if cell != 0 && !PROPS[cell as usize].is_liquid { break; }
|
||||||
|
let below_cell = grid.get_signed(sx, y as i32 + 1);
|
||||||
|
if cell == 0 && below_cell != 0 {
|
||||||
|
grid.swap(x, y, sxu, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_solid(&mut self, grid: &mut Grid, x: u32, y: u32, _mat: u8) {
|
||||||
|
if y + 1 >= grid.height {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let below = grid.get(x, y + 1);
|
||||||
|
let bprops = &PROPS[below as usize];
|
||||||
|
if below == 0 || bprops.is_liquid || bprops.is_gas {
|
||||||
|
grid.swap(x, y, x, y + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_gases(&mut self, grid: &mut Grid, x0: u32, y0: u32, x1: u32, y1: u32) {
|
||||||
|
for y in y0..y1 {
|
||||||
|
for x in x0..x1 {
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
let mat = grid.materials[idx];
|
||||||
|
if mat == 0 { continue; }
|
||||||
|
let props = &PROPS[mat as usize];
|
||||||
|
if !props.is_gas { continue; }
|
||||||
|
if y == 0 { continue; }
|
||||||
|
|
||||||
|
let above = grid.get(x, y - 1);
|
||||||
|
if above == 0 {
|
||||||
|
grid.swap(x, y, x, y - 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = self.rand_dir();
|
||||||
|
let dx = x as i32 + dir;
|
||||||
|
if grid.in_bounds(dx, y as i32 - 1) {
|
||||||
|
let dau = dx as u32;
|
||||||
|
if grid.get(dau, y - 1) == 0 {
|
||||||
|
grid.swap(x, y, dau, y - 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let odx = x as i32 - dir;
|
||||||
|
if grid.in_bounds(odx, y as i32) && grid.get(odx as u32, y) == 0 {
|
||||||
|
grid.swap(x, y, odx as u32, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_ephemeral(&mut self, grid: &mut Grid, x0: u32, y0: u32, x1: u32, y1: u32) {
|
||||||
|
for y in y0..y1 {
|
||||||
|
for x in x0..x1 {
|
||||||
|
let idx = grid.index(x, y);
|
||||||
|
let mat = grid.materials[idx];
|
||||||
|
if mat != 14 && mat != 15 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let hh = grid.healths[idx];
|
||||||
|
if hh <= 1 {
|
||||||
|
grid.materials[idx] = 0;
|
||||||
|
grid.temps[idx] = 0;
|
||||||
|
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
|
||||||
|
} else {
|
||||||
|
grid.healths[idx] = hh - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
use crate::grid::Grid;
|
||||||
|
use crate::material::PROPS;
|
||||||
|
|
||||||
|
pub struct RenderBuffer {
|
||||||
|
pub pixels: Vec<u8>,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
light: Vec<u8>,
|
||||||
|
block: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderBuffer {
|
||||||
|
pub fn new(width: u32, height: u32) -> Self {
|
||||||
|
let size = (width * height * 4) as usize;
|
||||||
|
let lsize = (width * height) as usize;
|
||||||
|
Self {
|
||||||
|
pixels: vec![0; size],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
light: vec![0; lsize],
|
||||||
|
block: vec![0; lsize],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, grid: &Grid, cam_x: i32, cam_y: i32, zoom: f32) {
|
||||||
|
let rw = self.width as i32;
|
||||||
|
let rh = self.height as i32;
|
||||||
|
let total = (rw * rh) as usize;
|
||||||
|
|
||||||
|
for i in 0..total {
|
||||||
|
self.light[i] = 0;
|
||||||
|
self.block[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for py in 0..rh {
|
||||||
|
let gy = cam_y + ((py as f32 - rh as f32 / 2.0) / zoom) as i32;
|
||||||
|
for px in 0..rw {
|
||||||
|
let gx = cam_x + ((px as f32 - rw as f32 / 2.0) / zoom) as i32;
|
||||||
|
let idx = (py * rw + px) as usize;
|
||||||
|
if grid.in_bounds(gx, gy) {
|
||||||
|
let gi = grid.index(gx as u32, gy as u32);
|
||||||
|
let mat = grid.materials[gi];
|
||||||
|
if mat > 0 {
|
||||||
|
let props = &PROPS[mat as usize];
|
||||||
|
self.light[idx] = props.light_emit;
|
||||||
|
self.block[idx] = props.light_block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _pass in 0..12 {
|
||||||
|
let dir = _pass & 1;
|
||||||
|
let mut changed = false;
|
||||||
|
if dir == 0 {
|
||||||
|
for py in 0..rh {
|
||||||
|
for px in 0..rw {
|
||||||
|
if self.spread_light(py, px, rw, rh) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for py in (0..rh).rev() {
|
||||||
|
for px in (0..rw).rev() {
|
||||||
|
if self.spread_light(py, px, rw, rh) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for py in 0..rh {
|
||||||
|
for px in 0..rw {
|
||||||
|
let gx = cam_x + ((px as f32 - rw as f32 / 2.0) / zoom) as i32;
|
||||||
|
let gy = cam_y + ((py as f32 - rh as f32 / 2.0) / zoom) as i32;
|
||||||
|
|
||||||
|
let color = if grid.in_bounds(gx, gy) {
|
||||||
|
let gi = grid.index(gx as u32, gy as u32);
|
||||||
|
let mat = grid.materials[gi];
|
||||||
|
let li = (py * rw + px) as usize;
|
||||||
|
let lvl = self.light[li] as u32;
|
||||||
|
|
||||||
|
if mat == 0 {
|
||||||
|
let bg = [20u32, 20, 30];
|
||||||
|
let glow_r = 255u32;
|
||||||
|
let glow_g = 160u32;
|
||||||
|
let glow_b = 40u32;
|
||||||
|
let t = ((lvl * lvl) / 255).min(255);
|
||||||
|
let r = (bg[0] * (255 - t) + glow_r * t) / 255;
|
||||||
|
let g = (bg[1] * (255 - t) + glow_g * t) / 255;
|
||||||
|
let b = (bg[2] * (255 - t) + glow_b * t) / 255;
|
||||||
|
[r as u8, g as u8, b as u8, 255]
|
||||||
|
} else {
|
||||||
|
let props = &PROPS[mat as usize];
|
||||||
|
let base = props.color;
|
||||||
|
let temp = grid.temps[gi];
|
||||||
|
let t = (temp as f32 / 200.0).min(1.0);
|
||||||
|
|
||||||
|
let mut r = (base[0] as f32 + t * 60.0) as u8;
|
||||||
|
let mut g = (base[1] as f32 * (1.0 - t * 0.4)) as u8;
|
||||||
|
let mut b = (base[2] as f32 * (1.0 - t * 0.6)) as u8;
|
||||||
|
|
||||||
|
if mat == 9 || mat == 6 {
|
||||||
|
let (fr, fg, fb) = flame_variation(gx as u32, gy as u32, mat);
|
||||||
|
r = (r as i32 + fr).clamp(0, 255) as u8;
|
||||||
|
g = (g as i32 + fg).clamp(0, 255) as u8;
|
||||||
|
b = (b as i32 + fb).clamp(0, 255) as u8;
|
||||||
|
} else {
|
||||||
|
let var = texture_offset(gx as u32, gy as u32, mat);
|
||||||
|
r = (r as i32 + var).clamp(0, 255) as u8;
|
||||||
|
g = (g as i32 + var).clamp(0, 255) as u8;
|
||||||
|
b = (b as i32 + var).clamp(0, 255) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if props.light_emit == 0 {
|
||||||
|
let ambient = 150u32;
|
||||||
|
let add = lvl;
|
||||||
|
r = ((r as u32 * ambient >> 8) + add).min(255) as u8;
|
||||||
|
g = ((g as u32 * ambient >> 8) + add).min(255) as u8;
|
||||||
|
b = ((b as u32 * ambient >> 8) + add).min(255) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[r, g, b, base[3]]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
[20, 20, 30, 255]
|
||||||
|
};
|
||||||
|
|
||||||
|
let pi = ((py * rw + px) * 4) as usize;
|
||||||
|
self.pixels[pi] = color[0];
|
||||||
|
self.pixels[pi + 1] = color[1];
|
||||||
|
self.pixels[pi + 2] = color[2];
|
||||||
|
self.pixels[pi + 3] = color[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spread_light(&mut self, py: i32, px: i32, rw: i32, rh: i32) -> bool {
|
||||||
|
let idx = (py * rw + px) as usize;
|
||||||
|
let cur = self.light[idx] as u32;
|
||||||
|
let blk = (self.block[idx] as u32).min(200);
|
||||||
|
|
||||||
|
let neighbors: [(i32, i32); 8] = [
|
||||||
|
(px - 1, py - 1), (px, py - 1), (px + 1, py - 1),
|
||||||
|
(px - 1, py), (px + 1, py),
|
||||||
|
(px - 1, py + 1), (px, py + 1), (px + 1, py + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut best = cur;
|
||||||
|
for &(nx, ny) in &neighbors {
|
||||||
|
if nx >= 0 && nx < rw && ny >= 0 && ny < rh {
|
||||||
|
let ni = (ny * rw + nx) as usize;
|
||||||
|
let nlight = self.light[ni] as u32;
|
||||||
|
if nlight <= 1 { continue; }
|
||||||
|
let falloff = (if nx == px || ny == py { 2 } else { 3 }) + blk;
|
||||||
|
if nlight > falloff {
|
||||||
|
let incoming = nlight - falloff;
|
||||||
|
if incoming > best {
|
||||||
|
best = incoming;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if blk > 0 && blk < 180 && best > cur && best > 4 {
|
||||||
|
for &(nx, ny) in &neighbors {
|
||||||
|
if nx >= 0 && nx < rw && ny >= 0 && ny < rh {
|
||||||
|
let ni = (ny * rw + nx) as usize;
|
||||||
|
let nlight = self.light[ni] as u32;
|
||||||
|
if nlight >= best { continue; }
|
||||||
|
let reflect = best >> 1;
|
||||||
|
if reflect > nlight {
|
||||||
|
self.light[ni] = reflect as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if best != cur {
|
||||||
|
self.light[idx] = best as u8;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_offset(x: u32, y: u32, mat: u8) -> i32 {
|
||||||
|
match mat {
|
||||||
|
5 => {
|
||||||
|
let wobble = ((y.wrapping_mul(5) ^ y.wrapping_shr(2)) & 3) as i32 - 1;
|
||||||
|
let pos = (x as i32).wrapping_add(wobble);
|
||||||
|
let grain = pos % 5;
|
||||||
|
let line_id = (pos / 5) as u32;
|
||||||
|
let darkness = hash(line_id, 0) & 15;
|
||||||
|
if grain == 0 {
|
||||||
|
-12 - darkness as i32
|
||||||
|
} else if grain == 1 {
|
||||||
|
-5
|
||||||
|
} else {
|
||||||
|
let n = hash(x.wrapping_add(y >> 1), y);
|
||||||
|
((n as i32 - 128) * 7) >> 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let n = hash(x.wrapping_mul(3) >> 2, y.wrapping_mul(3) >> 2);
|
||||||
|
((n as i32 - 128) * 20) >> 7
|
||||||
|
}
|
||||||
|
1 | 10 => {
|
||||||
|
let n = hash(x, y);
|
||||||
|
((n as i32 - 128) * 10) >> 7
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
let n = hash(x.wrapping_add(y), y);
|
||||||
|
((n as i32 - 128) * 6) >> 7
|
||||||
|
}
|
||||||
|
11 => {
|
||||||
|
let n = hash(x, y.wrapping_mul(y));
|
||||||
|
((n as i32 - 128) * 5) >> 7
|
||||||
|
}
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash(x: u32, y: u32) -> u8 {
|
||||||
|
let h = x.wrapping_mul(374761393)
|
||||||
|
.wrapping_add(y.wrapping_mul(668265263));
|
||||||
|
(h ^ h.wrapping_shr(13)).wrapping_mul(1274126177) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flame_variation(x: u32, y: u32, mat: u8) -> (i32, i32, i32) {
|
||||||
|
let h = hash(x, y) as i32;
|
||||||
|
let r_var = ((h - 128) * 6) >> 7;
|
||||||
|
if mat == 9 {
|
||||||
|
let g_var = ((h.wrapping_sub(40) as i32 - 128) * 12) >> 7;
|
||||||
|
let b_var = ((h.wrapping_add(60) as i32 - 128) * 8) >> 7;
|
||||||
|
(-r_var, g_var, b_var)
|
||||||
|
} else {
|
||||||
|
let g_var = ((h - 128) * 8) >> 7;
|
||||||
|
let b_var = ((h - 128) * 5) >> 7;
|
||||||
|
(r_var, g_var, b_var)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pkg/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AtomicEngine</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
#panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
border-radius: 6px;
|
||||||
|
z-index: 10;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #ccc;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.mat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
min-width: 36px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
.mat.active {
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-color: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
.mat .swatch {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.mat .label {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.mat .key {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
#info {
|
||||||
|
position: fixed;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
#fps {
|
||||||
|
position: fixed;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
color: #0f0;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
<div id="info">AtomicEngine v0.2</div>
|
||||||
|
<div id="fps">60 FPS</div>
|
||||||
|
<div id="panel"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+1041
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "atomic-engine-web",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.5",
|
||||||
|
"vite": "^5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export class Camera {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
zoom = 1;
|
||||||
|
targetZoom = 1;
|
||||||
|
|
||||||
|
update(dt: number) {
|
||||||
|
const speed = 10;
|
||||||
|
this.zoom += (this.targetZoom - this.zoom) * speed * dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
setZoom(z: number) {
|
||||||
|
this.targetZoom = Math.max(0.1, Math.min(10, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import __wbg_init, {
|
||||||
|
init as wasm_init,
|
||||||
|
fill_rect,
|
||||||
|
fill_circle,
|
||||||
|
simulate,
|
||||||
|
update_render,
|
||||||
|
render_buffer_ptr,
|
||||||
|
render_buffer_len,
|
||||||
|
render_width,
|
||||||
|
render_height,
|
||||||
|
} from "../pkg/atomic_engine.js";
|
||||||
|
|
||||||
|
export const M = {
|
||||||
|
AIR: 0,
|
||||||
|
STONE: 1,
|
||||||
|
DIRT: 2,
|
||||||
|
SAND: 3,
|
||||||
|
WATER: 4,
|
||||||
|
WOOD: 5,
|
||||||
|
LAVA: 6,
|
||||||
|
PLAYER: 7,
|
||||||
|
STEAM: 8,
|
||||||
|
FIRE: 9,
|
||||||
|
GLASS: 10,
|
||||||
|
ICE: 11,
|
||||||
|
OIL: 12,
|
||||||
|
ACID: 13,
|
||||||
|
SPARK: 14,
|
||||||
|
SMOKE: 15,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const MATERIAL_NAMES: Record<number, string> = {
|
||||||
|
0: "Air",
|
||||||
|
1: "Stone",
|
||||||
|
2: "Dirt",
|
||||||
|
3: "Sand",
|
||||||
|
4: "Water",
|
||||||
|
5: "Wood",
|
||||||
|
6: "Lava",
|
||||||
|
7: "Player",
|
||||||
|
8: "Steam",
|
||||||
|
9: "Fire",
|
||||||
|
10: "Glass",
|
||||||
|
11: "Ice",
|
||||||
|
12: "Oil",
|
||||||
|
13: "Acid",
|
||||||
|
14: "Spark",
|
||||||
|
15: "Smoke",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MATERIAL_COLORS: Record<number, string> = {
|
||||||
|
0: "transparent",
|
||||||
|
1: "#787882",
|
||||||
|
2: "#654321",
|
||||||
|
3: "#c2b280",
|
||||||
|
4: "#1e3cc8",
|
||||||
|
5: "#5a3c1e",
|
||||||
|
6: "#ff6400",
|
||||||
|
7: "#32c832",
|
||||||
|
8: "#c8c8dc",
|
||||||
|
9: "#ffb41e",
|
||||||
|
10: "#b4d2dc",
|
||||||
|
11: "#b4dcff",
|
||||||
|
12: "#b48c28",
|
||||||
|
13: "#64ff3c",
|
||||||
|
14: "#ffb428",
|
||||||
|
15: "#a09b96",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Engine {
|
||||||
|
fillRect: (x: number, y: number, w: number, h: number, material: number) => void;
|
||||||
|
fillCircle: (x: number, y: number, radius: number, material: number) => void;
|
||||||
|
simulate: (camX: number, camY: number, zoom: number) => void;
|
||||||
|
updateRender: (camX: number, camY: number, zoom: number) => void;
|
||||||
|
renderBufferPtr: () => number;
|
||||||
|
renderBufferLen: () => number;
|
||||||
|
renderWidth: () => number;
|
||||||
|
renderHeight: () => number;
|
||||||
|
memory: WebAssembly.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEngine(gridWidth: number, gridHeight: number): Promise<Engine> {
|
||||||
|
const wasm = await __wbg_init();
|
||||||
|
wasm_init(gridWidth, gridHeight);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fillRect: (x, y, w, h, m) => fill_rect(x, y, w, h, m),
|
||||||
|
fillCircle: (x, y, r, m) => fill_circle(x, y, r, m),
|
||||||
|
simulate: (cx: number, cy: number, z: number) => simulate(cx, cy, z),
|
||||||
|
updateRender: (cx, cy, z) => update_render(cx, cy, z),
|
||||||
|
renderBufferPtr: () => render_buffer_ptr(),
|
||||||
|
renderBufferLen: () => render_buffer_len(),
|
||||||
|
renderWidth: () => render_width(),
|
||||||
|
renderHeight: () => render_height(),
|
||||||
|
memory: wasm.memory,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export class Input {
|
||||||
|
keys: Set<string> = new Set();
|
||||||
|
mouseX = 0;
|
||||||
|
mouseY = 0;
|
||||||
|
mouseDown = false;
|
||||||
|
wheel = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
window.addEventListener("keydown", (e) => this.keys.add(e.code));
|
||||||
|
window.addEventListener("keyup", (e) => this.keys.delete(e.code));
|
||||||
|
window.addEventListener("mousemove", (e) => {
|
||||||
|
this.mouseX = e.clientX;
|
||||||
|
this.mouseY = e.clientY;
|
||||||
|
});
|
||||||
|
window.addEventListener("mousedown", () => (this.mouseDown = true));
|
||||||
|
window.addEventListener("mouseup", () => (this.mouseDown = false));
|
||||||
|
window.addEventListener("wheel", (e) => {
|
||||||
|
this.wheel += e.deltaY > 0 ? -1 : 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isDown(key: string): boolean {
|
||||||
|
return this.keys.has(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
import { createEngine, M, MATERIAL_NAMES, MATERIAL_COLORS } from "./engine";
|
||||||
|
import { Renderer } from "./renderer";
|
||||||
|
import { Input } from "./input";
|
||||||
|
import { Camera } from "./camera";
|
||||||
|
|
||||||
|
const GRID_WIDTH = 1024;
|
||||||
|
const GRID_HEIGHT = 768;
|
||||||
|
|
||||||
|
const PAINT_MATERIALS = [M.SAND, M.WATER, M.LAVA, M.WOOD, M.FIRE, M.ICE, M.OIL, M.ACID, M.STONE, M.DIRT, M.GLASS];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
||||||
|
const renderer = new Renderer(canvas);
|
||||||
|
const input = new Input();
|
||||||
|
const camera = new Camera();
|
||||||
|
camera.x = GRID_WIDTH / 2;
|
||||||
|
camera.y = GRID_HEIGHT / 2;
|
||||||
|
camera.zoom = 3;
|
||||||
|
|
||||||
|
const engine = await createEngine(GRID_WIDTH, GRID_HEIGHT);
|
||||||
|
|
||||||
|
engine.fillRect(0, 740, 1024, 28, M.STONE);
|
||||||
|
engine.fillRect(440, 720, 144, 20, M.WOOD);
|
||||||
|
engine.fillRect(460, 700, 104, 20, M.DIRT);
|
||||||
|
engine.fillCircle(490, 560, 25, M.SAND);
|
||||||
|
engine.fillCircle(520, 550, 20, M.SAND);
|
||||||
|
engine.fillRect(430, 650, 40, 30, M.WATER);
|
||||||
|
engine.fillRect(550, 650, 40, 25, M.LAVA);
|
||||||
|
engine.fillRect(505, 640, 10, 16, M.ICE);
|
||||||
|
engine.fillCircle(620, 580, 20, M.OIL);
|
||||||
|
engine.fillRect(480, 620, 64, 30, M.WOOD);
|
||||||
|
engine.fillRect(480, 600, 64, 20, M.FIRE);
|
||||||
|
engine.fillRect(300, 720, 40, 20, M.GLASS);
|
||||||
|
engine.fillCircle(200, 680, 18, M.SAND);
|
||||||
|
engine.fillRect(100, 700, 30, 40, M.WOOD);
|
||||||
|
engine.fillRect(750, 680, 60, 60, M.STONE);
|
||||||
|
engine.fillCircle(780, 660, 15, M.LAVA);
|
||||||
|
engine.fillRect(700, 720, 30, 20, M.ICE);
|
||||||
|
|
||||||
|
const rw = engine.renderWidth();
|
||||||
|
const rh = engine.renderHeight();
|
||||||
|
|
||||||
|
camera.x = 512;
|
||||||
|
camera.y = 600;
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
renderer.resize(window.innerWidth, window.innerHeight, dpr);
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", resize);
|
||||||
|
resize();
|
||||||
|
|
||||||
|
let paintMat: number = M.SAND;
|
||||||
|
|
||||||
|
function screenToGrid(sx: number, sy: number) {
|
||||||
|
const mx = (sx / window.innerWidth - 0.5) * rw / camera.zoom + camera.x;
|
||||||
|
const my = (sy / window.innerHeight - 0.5) * rh / camera.zoom + camera.y;
|
||||||
|
return { x: Math.floor(mx), y: Math.floor(my) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = document.getElementById("panel")!;
|
||||||
|
function buildPanel() {
|
||||||
|
panel.innerHTML = "";
|
||||||
|
PAINT_MATERIALS.forEach((mat, idx) => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "mat" + (mat === paintMat ? " active" : "");
|
||||||
|
div.dataset.mat = String(mat);
|
||||||
|
div.innerHTML = `<div class="swatch" style="background:${MATERIAL_COLORS[mat]}"></div><div class="label"><span class="key">${idx}</span> ${MATERIAL_NAMES[mat]}</div>`;
|
||||||
|
div.addEventListener("click", () => selectMat(mat));
|
||||||
|
panel.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function selectMat(mat: number) {
|
||||||
|
paintMat = mat;
|
||||||
|
document.querySelectorAll(".mat").forEach((el) => {
|
||||||
|
el.classList.toggle("active", (el as HTMLElement).dataset.mat === String(mat));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
buildPanel();
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
const idx = parseInt(e.key);
|
||||||
|
if (idx >= 0 && idx < PAINT_MATERIALS.length) {
|
||||||
|
selectMat(PAINT_MATERIALS[idx]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastTime = performance.now();
|
||||||
|
let simAccum = 0;
|
||||||
|
const simStep = 1 / 60;
|
||||||
|
let fpsFrames = 0;
|
||||||
|
let fpsLast = performance.now();
|
||||||
|
const fpsEl = document.getElementById("fps")!;
|
||||||
|
|
||||||
|
function loop(now: number) {
|
||||||
|
const dt = Math.min((now - lastTime) / 1000, 0.1);
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
|
fpsFrames++;
|
||||||
|
if (now - fpsLast >= 500) {
|
||||||
|
const fps = Math.round(fpsFrames / ((now - fpsLast) / 1000));
|
||||||
|
fpsEl.textContent = `${fps} FPS`;
|
||||||
|
fpsFrames = 0;
|
||||||
|
fpsLast = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
simAccum += dt;
|
||||||
|
while (simAccum >= simStep) {
|
||||||
|
simAccum -= simStep;
|
||||||
|
engine.simulate(
|
||||||
|
Math.round(camera.x),
|
||||||
|
Math.round(camera.y),
|
||||||
|
camera.zoom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const speed = 200;
|
||||||
|
if (input.isDown("KeyA") || input.isDown("ArrowLeft")) camera.x -= speed * dt;
|
||||||
|
if (input.isDown("KeyD") || input.isDown("ArrowRight")) camera.x += speed * dt;
|
||||||
|
if (input.isDown("KeyW") || input.isDown("ArrowUp")) camera.y -= speed * dt;
|
||||||
|
if (input.isDown("KeyS") || input.isDown("ArrowDown")) camera.y += speed * dt;
|
||||||
|
if (input.isDown("Equal")) camera.setZoom(camera.zoom + 2 * dt);
|
||||||
|
if (input.isDown("Minus")) camera.setZoom(camera.zoom - 2 * dt);
|
||||||
|
if (input.wheel !== 0) {
|
||||||
|
camera.setZoom(camera.zoom + input.wheel * 0.5);
|
||||||
|
input.wheel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.mouseDown) {
|
||||||
|
const g = screenToGrid(input.mouseX, input.mouseY);
|
||||||
|
engine.fillCircle(g.x, g.y, 6, paintMat);
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.update(dt);
|
||||||
|
engine.updateRender(
|
||||||
|
Math.round(camera.x),
|
||||||
|
Math.round(camera.y),
|
||||||
|
camera.zoom,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ptr = engine.renderBufferPtr();
|
||||||
|
const len = engine.renderBufferLen();
|
||||||
|
const buf = new Uint8Array(engine.memory.buffer, ptr, len).slice(0);
|
||||||
|
renderer.uploadTexture(buf, rw, rh);
|
||||||
|
renderer.render();
|
||||||
|
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
export class Renderer {
|
||||||
|
private gl: WebGL2RenderingContext;
|
||||||
|
private texture: WebGLTexture | null = null;
|
||||||
|
private program: WebGLProgram | null = null;
|
||||||
|
private vao: WebGLVertexArrayObject | null = null;
|
||||||
|
|
||||||
|
constructor(canvas: HTMLCanvasElement) {
|
||||||
|
const gl = canvas.getContext("webgl2", {
|
||||||
|
antialias: false,
|
||||||
|
alpha: false,
|
||||||
|
premultipliedAlpha: false,
|
||||||
|
});
|
||||||
|
if (!gl) throw new Error("WebGL2 not supported");
|
||||||
|
this.gl = gl;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
const gl = this.gl;
|
||||||
|
|
||||||
|
const vs = gl.createShader(gl.VERTEX_SHADER)!;
|
||||||
|
gl.shaderSource(vs, `#version 300 es
|
||||||
|
in vec2 a_pos;
|
||||||
|
in vec2 a_uv;
|
||||||
|
out vec2 v_uv;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(a_pos, 0.0, 1.0);
|
||||||
|
v_uv = a_uv;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
gl.compileShader(vs);
|
||||||
|
|
||||||
|
const fs = gl.createShader(gl.FRAGMENT_SHADER)!;
|
||||||
|
gl.shaderSource(fs, `#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
in vec2 v_uv;
|
||||||
|
out vec4 fragColor;
|
||||||
|
uniform sampler2D u_tex;
|
||||||
|
void main() {
|
||||||
|
fragColor = texture(u_tex, v_uv);
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
gl.compileShader(fs);
|
||||||
|
|
||||||
|
this.program = gl.createProgram()!;
|
||||||
|
gl.attachShader(this.program, vs);
|
||||||
|
gl.attachShader(this.program, fs);
|
||||||
|
gl.linkProgram(this.program);
|
||||||
|
|
||||||
|
const vertices = new Float32Array([
|
||||||
|
-1, -1, 0, 1,
|
||||||
|
1, -1, 1, 1,
|
||||||
|
1, 1, 1, 0,
|
||||||
|
-1, -1, 0, 1,
|
||||||
|
1, 1, 1, 0,
|
||||||
|
-1, 1, 0, 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.vao = gl.createVertexArray();
|
||||||
|
gl.bindVertexArray(this.vao);
|
||||||
|
const buf = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
const stride = 4 * 4;
|
||||||
|
gl.enableVertexAttribArray(0);
|
||||||
|
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, stride, 0);
|
||||||
|
gl.enableVertexAttribArray(1);
|
||||||
|
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, 2 * 4);
|
||||||
|
|
||||||
|
this.texture = gl.createTexture();
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadTexture(data: Uint8Array, width: number, height: number) {
|
||||||
|
const gl = this.gl;
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const gl = this.gl;
|
||||||
|
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
gl.useProgram(this.program);
|
||||||
|
gl.bindVertexArray(this.vao);
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(cssWidth: number, cssHeight: number, dpr: number) {
|
||||||
|
const canvas = this.gl.canvas as HTMLCanvasElement;
|
||||||
|
canvas.width = cssWidth * dpr;
|
||||||
|
canvas.height = cssHeight * dpr;
|
||||||
|
canvas.style.width = cssWidth + "px";
|
||||||
|
canvas.style.height = cssHeight + "px";
|
||||||
|
this.gl.viewport(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
|
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: "es2020",
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user