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
|
#+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/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)
- [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)
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 per-shot RECOIL on the firing structure (we apply knockback
to targets only).
* 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
- [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 -- =data/mods/enemy-fort/= builds a
triangulated fort at x=25..29 with the enemy reactor inside. Wood only;
no bg-brace or ropes in it yet.
- [~] Map layout: metal deposits on both sides exist (=data/map.lua=), but the
terrain is still a flat half-plane, not a real heightmap.
- [~] Player starting area: player reactor + nearby deposits on the left; there
is no pre-built starter shell.
- [~] 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 / 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
|