aboutsummaryrefslogtreecommitdiffstats
path: root/requirements/milestones.org
blob: 7cc2e898c93d114474f84492ac07350f5563a2cf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#+TITLE: Forts Clone v0.1 -- Milestones
#+AUTHOR: Forts Clone Project
#+DATE: 2025-06-19
#+OPTIONS: toc:3 num:t

* Overview

v0.1 is split into 7 milestones. Each milestone delivers something you can
run and test. Milestones are sequential -- each builds on the previous.

* M0: Scaffolding & Build Environment [DONE]
SCHEDULED: <2025-06-19>
CLOSED: <2025-06-19>

Goal: Container builds, SDL3 window opens, clears to a color.

- [X] Containerfile with all dependencies pre-built
- [X] meson.build at project root, compiles and links SDL3 + Box2D + all deps
- [X] =src/main.cpp= -- SDL3 creates a window, clears to dark blue-gray
- [X] Camera: 2D orthographic, pan (arrows or WASD/middle-drag), zoom (scroll)
- [X] Input system: polling keyboard/mouse state
- [X] Podman build + run workflow

Notes:
- Pivoted from bgfx to SDL3 renderer for M0. bgfx init fails silently on this
  system (EGL-only Linux backend, GLX removed upstream). SDL3 OpenGL renderer
  works perfectly. Revisit bgfx later in the project.
- Follow-up: no source file =#include=s bgfx, SoLoud, ENet or EnTT, so
  =meson.build= now treats them as OPTIONAL (linked only if a source build has
  put them in /usr/local) and falls back to system packages otherwise. That is
  what makes a native (non-container) build possible -- see README. Promote each
  back to a required dependency when it is actually wired up.
- Header collision: -I/usr/local/include/bx caused sys/types.h to find
  bx/endian.h instead of glibc endian.h. Fixed by using single
  -I/usr/local/include with qualified <bx/...> includes.

* M1: Rendering & Physics Foundations [DONE]
SCHEDULED: <2025-06-19>
CLOSED: <2025-06-19>

Goal: See physics bodies rendered as colored rectangles against terrain.

- [X] Box2D v3 integration -- world, gravity, fixed timestep at 60Hz
- [X] Static terrain body (ground plane + platform)
- [X] Dynamic bodies falling and landing (proof of physics->render)
- [X] SDL3 sprite rendering (colored quads, world-to-screen transform)
- [X] Fixed-timestep accumulator pattern (physics at 60Hz, render at display rate)
- [X] Camera pan/zoom with proper defaults (zoom 10, centered on action)

Notes:
- Box2D v3.0.0 has broken CMake (FetchContent pulls GLFW unconditionally).
  Built from v3.1.1 directly (gcc -c src/*.c + ar rcs).
- b2ShapeDef.friction moved to b2ShapeDef.material.friction in v3.
- b2Rot is { float c, s; } (cos/sin), angle via std::atan2(rot.s, rot.c).

* ARCHITECTURE NOTE: Building physics is a mass-spring system (the canon Forts model)

The locked stack specified Box2D (weld/distance joints, b2DestructionListener)
for the building structure. Instead the building system is a custom *stiff
damped mass-spring solver* (=src/game/build_graph.*=) — the same model the
shipped game uses (see =research/forts-data-reference.org=). It maps directly
onto Forts' node/edge graph and onto its data.

- Node = point mass with velocity; foundation nodes are pinned at the ground.
- Strut = a damped Hookean spring (F = k.dx + c.v), per-material stiffness.
  Integrated semi-implicit with OVERSAMPLES (14) substeps for stiff-spring
  stability (Forts uses the same oversampling).
- Rigidity is GEOMETRIC (triangulation); a lone strut is a free-hinging pin
  joint. It SNAPS on (a) axial deformation past per-material MaxCompression/
  MaxExpansion, or (b) the 30-degree angle-stress rule for un-triangulated
  struts — both taken from the shipped game's constants.
- Box2D is retained (=src/engine/physics.*=) but idle; foundation for M4
  projectiles / terrain collision.

Consequence: multiplayer determinism now rests on our own solver, not Box2D's
cross-platform guarantees (revisit before netcode work).

* LUA / MODDING: materials are now data-driven (sol2)  [DONE for materials]

Lua (sol2 3.5) is up. Materials are no longer hard-coded — they load at startup
from =data/materials.lua= via =load_materials()= (=src/game/data.*=), into a
runtime =BuildGraph::materials= vector. The layered loader works:

1. =ScriptEngine= (=src/engine/script.*=) wraps a sol2 state, runs files with
   errors caught (a bad mod can't crash the game).
2. Base =data/materials.lua= defines the global =Materials= array + helpers
   (=FindMaterial=, =IndexOfMaterial=).
3. Every mod under =data/mods/<name>/= with a =materials.lua= is applied on top
   in (Priority, name) order; mods mutate the shared =Materials= table.
   (=data/mods/example-recolour-wood/= ships as a live demo.)

Runtime data path is relative to CWD (run from the repo root). NEXT for Lua:
extend the same loader to weapons/devices/projectiles tables (lands with M4),
and a mod-selection step instead of "apply everything in data/mods/".

* M2: Building System [MOSTLY DONE]

Goal: Click to place struts on the map. They have physics, connect into a
graph, and form structures.

- [X] Build mode: number keys 1-3 select material (wood / bg-brace / rope)
- [X] Strut placed as a mass-spring node/edge + colored quad, stress-tinted
      (red under compression, blue under tension)
- [X] Anchored building only: a drag must START from an existing node or the
      ground (foundation) — no growing structure out of thin air
- [X] Click-drag single strut; endpoints snap onto existing nodes
- [X] Drag-build (extrude): sweep an edge into a box with node-merging and an
      auto diagonal brace; free (non-perpendicular) drag angle
- [X] Build-grace radial timer on un-triangulated struts (temporary rigid hold)
- [X] Building costs: placing a piece deducts metal + energy
- [X] Simple HUD: resource bars + current-material indicator
- [X] Resource system: Metal (mines on deposits) + Energy (wind turbines)
      -- was deferred to M5 and DELIVERED there (generation, storage caps,
      build + firing costs).

* M3: Structural Destruction  [MOSTLY DONE]
  (swapped with weapons -- destruction goes hand-in-hand with M2 building)

Goal: Overloaded structures snap and collapse; debris that falls is destroyed.
This is the purely-structural half of Forts destruction (no weapons yet).

- [X] Axial failure: deformation past per-material MaxCompression/MaxExpansion
      (wood +/-10%, asymmetric per material) — canon, read straight off the
      spring length (not a reconstructed force)
- [X] Angle-stress failure: an un-triangulated strut snaps once it rotates past
      ~30 deg from its built angle (Forts' rule; replaces the old dangle timer)
- [X] Cascading collapse: a snapped support redistributes load and overloads
      neighbours, which snap in turn (emergent, not a scripted event)
- [X] Ground destroys falling debris: any non-foundation node that sinks below
      the ground surface is removed (+ explosion puff), taking its edges with it
- [X] Disconnection -> fall -> destroy: a chunk that loses its path to a
      foundation simply falls under gravity and dies on the ground
- [X] Materials: wood, background bracing, ropes (tension-only), armour
      (HP 400, 25% stiffer, 2x mass, never burns -- build mode 4)
- [X] Per-strut HP pool (separate from stress) -- delivered in M4
- [X] Fire (damage-over-time, strut-to-strut spread) -- delivered in M4

* M4: Weapons & Damage  [MOSTLY DONE]
  (swapped with destruction)

Goal: Place and fire all three weapons. They feed the M3 graph via two damage
models (per-strut HP and radius splash) plus fire.

- [X] Per-strut HP pool, depleted by weapon hits (independent of stress)
- [X] Cannon: ballistic projectile, splash damage with linear radius falloff,
      knockback impulse, ~25% fire chance, per-shot energy cost.
      NOTE: the UX is Forts' select-a-weapon-and-aim-within-its-arc, not the
      click-drag angle/power originally sketched here. The projectile uses our
      own integrator; Box2D is still idle.
- [X] Laser: beam; stops at wood, passes bg-brace/ropes; 100% ignite
- [ ] Flak: auto-target incoming shells within an arc; hitscan/fast projectile
- [X] Direct vs splash delivery: foreground struts block direct fire;
      background bracing is passed through by direct hits but takes splash + fire
- [X] Fire system: ignition flag, damage-over-time, spread along the graph,
      burns background bracing faster (the counter to the bg-brace trick)

- [X] Firing recoil: each shot shoves the FIRING structure back along -aim
      (=BuildGraph::apply_recoil=, per-weapon =recoil= in data/weapons.lua;
      the beam laser has none). Tunable live in the Dev panel.

Still missing from the vanilla roster / mechanics (see gameplay-gaps.org #5, #8):
mortar, flak + point-defence, machinegun/sniper (hitscan), guided swarm
missiles, EMP, and the per-shot METAL cost (only energy is charged).

* M5: Reactor, Resources & Game Loop  [MOSTLY DONE]

Goal: There's a reactor to destroy, and a real economy to build it with.

- [X] Reactor entity -- large block, HP bar (a device with =is_core=)
- [X] Player reactor on left, enemy reactor on right (via the enemy-fort mod)
- [~] Reactor takes damage from direct hits, fire, falling/collision
      -- splash and beam damage hit devices; FIRE does not damage devices, and
      devices are not attached to the strut graph, so they never fall.
- [~] Win condition: destroy reactor HP OR disconnect it from its foundation
      (collapse) -- only the HP path exists. The round ends when a team has no
      surviving =is_core= device. The collapse path needs devices mounted on
      the graph (see M6 / gameplay-gaps.org #3).
- [X] Resource system (deferred from M2): mines on metal deposits, wind
      turbines for energy, generation rates, storage caps, build/fire costs.
      Build costs are now PER-MATERIAL (=cost_metal= / =cost_energy= in
      data/materials.lua) and enforced -- you cannot place a strut you cannot
      afford, which is what makes armour's 18x metal price a real choice.
- [X] Win/lose condition + restart (R key)

* M6: Dummy Fort & Map  [PARTIAL]

Goal: The dummy fort on the right is a hand-crafted target. The map has metal
deposits and a complete layout.

- [X] Dummy fort pre-built structure -- every map in =data/maps.lua= ships a
      triangulated enemy fort, now using armour faces and (Iron Bastion)
      bg-brace interior bracing.
- [X] Selectable maps: three scenarios chosen live from the Dev panel's "Map"
      section, which rebuilds the world via =build_map()=. Maps carry their own
      ground height, deposits, devices, weapon spawns and structures.
      Proving Ground (36 struts) is the simple default; Iron Bastion (62: walled
      keep, armoured outer wall, curtain bridge, bg-brace interior) and The
      Narrows (40: two three-storey redoubts) are the larger ones.
- [X] Headless map soak test (=tools/sim_check.cpp=, =make sim-check=): settles
      every map with the game's tick order and fails if any fort loses struts.
      Added after a stale-topology bug shipped forts that collapsed on load.
- [~] Map layout: metal deposits on both sides of every map, but the terrain is
      still a flat half-plane. =ground_level= moves the plane up and down (The
      Narrows sits at 2.0) yet cannot change its SHAPE -- that needs a real
      heightmap and is the remaining M6 work.
- [X] Player starting area: each map places the player reactor, nearby deposits,
      a small triangulated starter deck and the weapon spawns on top of it (the
      deck also gives firing recoil something to shove against).
- [~] Visual polish: materials are visually distinct and can load real Forts art
      (see CLAUDE.md section 5); no sky backdrop is drawn yet.

* M7: Polish & Controls

Goal: Everything feels good. Controls are responsive. Visuals are clear.

- [ ] Repair tool: right-click damaged piece, pay resources to restore HP
- [ ] Recycle: shift+click to remove piece, recover partial resources
- [X] Camera feel: pan (arrows or WASD / middle-drag), scroll zoom
      (basic; edge-scroll TBD)
- [~] Visual feedback: fire glow done (burning struts flicker orange); no
      per-damage-state art yet. Forts ships damage-state textures
      (=materials/wood_panel_damaged_1..3.tga=, =bracing-d1/d2=, =armor-d1/d2=)
      that a =texture_forts=-style field could pick up.
- [X] Build UX: ghost/extrusion previews, hover highlights, build-grace timers
- [ ] Edge cases: zero resources enforcement, etc.

* Dependency Graph

#+begin_src text
M0 -- M1 -- M2 -- M3 -- M4 -- M5 -- M6 -- M7
#+end_src