aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md5
-rw-r--r--data/materials.lua27
-rw-r--r--data/weapons.lua3
-rw-r--r--requirements/milestones.org15
-rw-r--r--research/gameplay-gaps.org47
-rw-r--r--src/engine/app.cpp36
-rw-r--r--src/game/build_graph.cpp15
-rw-r--r--src/game/build_graph.hpp9
-rw-r--r--src/game/data.cpp4
-rw-r--r--src/game/data.hpp2
10 files changed, 128 insertions, 35 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index a2453f7..23aec69 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -155,8 +155,9 @@ See `requirements/milestones.org` for the live checklist. Roughly:
destruction (stress/collapse/fire).
- **M4 mostly done:** per-strut HP, cannon (ballistic) + laser (beam/fire) with
the Forts select-a-weapon-and-aim-in-its-arc UX, direct-vs-splash delivery,
- fire + spread. Missing: mortar, flak/point-defence, machinegun/sniper
- (hitscan), guided missiles, EMP, per-shot recoil, metal-per-shot.
+ fire + spread, firing recoil, and the armour material (build mode 4).
+ Missing: mortar, flak/point-defence, machinegun/sniper (hitscan), guided
+ missiles, EMP, metal-per-shot.
- **M5 mostly done:** reactors + win/loss + restart (R), real resource economy
(mines on deposits, turbines by height, storage caps, firing costs energy),
device framework + placement. Two gaps: fire does **not** damage devices, and
diff --git a/data/materials.lua b/data/materials.lua
index 3725aa9..b88a1da 100644
--- a/data/materials.lua
+++ b/data/materials.lua
@@ -1,7 +1,7 @@
-- Base building materials for the clone.
--
--- The engine reads the global `Materials` array (in order). Build modes 1/2/3
--- map to entries 1/2/3. Mods under data/mods/ run AFTER this file and may use
+-- The engine reads the global `Materials` array (in order). Build modes 1-4
+-- map to entries 1-4. Mods under data/mods/ run AFTER this file and may use
-- FindMaterial(name) to tweak these, or table.insert(Materials, ...) to add new
-- ones. (angle_threshold is in DEGREES; the engine converts to radians.)
@@ -17,6 +17,7 @@ Materials =
flammable = true, burn_rate = 15, spread_time = 2.0, -- wood: slow burn/spread
blocks_projectiles = true, blocks_beam = true, -- solid: stops shells & beams
half_width = 0.15,
+ cost_metal = 5, cost_energy = 25, -- Forts wood 0.1 / 0.5
color = { 1.0, 1.0, 1.0, 1.0 }, -- white = show texture untinted
texture = "data/textures/wood.png",
texture_forts = "forts:materials/wood_panel.tga",
@@ -31,6 +32,7 @@ Materials =
flammable = true, burn_rate = 22, spread_time = 0.7, -- burns ~3x faster than wood
blocks_projectiles = false, blocks_beam = false, -- shells AND beams pass through (beam still burns it)
half_width = 0.15,
+ cost_metal = 5, cost_energy = 25,
color = { 1.0, 1.0, 1.0, 0.45 }, -- translucent
texture = "data/textures/bgbrace.png",
texture_forts = "forts:materials/bracing-bg.tga",
@@ -45,10 +47,31 @@ Materials =
flammable = true, burn_rate = 25, spread_time = 1.0, -- burns through fast
blocks_projectiles = false, blocks_beam = false, -- shells AND beams pass through
half_width = 0.04,
+ cost_metal = 5, cost_energy = 25,
color = { 0.72, 0.66, 0.55, 1.0 }, -- tan: tints the cable art, and is
-- the visible rope when it is absent
texture_forts = "forts:materials/cable.tga",
},
+ -- Appended LAST on purpose: build modes map to this array by index, so a new
+ -- material goes on the end (key 4) rather than shifting bg-brace and rope.
+ {
+ name = "armour", -- the kinetic tank
+ -- Forts armor: 25% stiffer than wood, 2x the mass, HP 400, and it does
+ -- NOT catch fire (its Fire deg/s is n/a). 3 wood (450 HP) still beats
+ -- 1 armour (400 HP), so armour buys durability per strut, not per metal.
+ stiffness = 750, damping = 12, mass = 0.5,
+ max_compression = 0.90, max_expansion = 1.10, -- same +/-10% as wood
+ angle_threshold = 30, tension_only = false,
+ min_length = 0.4, max_length = 3.0, max_link_length = 6.0,
+ hit_points = 400,
+ flammable = false, burn_rate = 0, spread_time = 0, -- metal: never burns
+ blocks_projectiles = true, blocks_beam = true, -- solid
+ half_width = 0.18, -- reads chunkier than wood
+ cost_metal = 90, cost_energy = 75, -- Forts 1.8 / 1.5, i.e.
+ -- 18x wood metal, 3x energy
+ color = { 0.62, 0.66, 0.70, 1.0 }, -- steel (also the fallback)
+ texture_forts = "forts:materials/armor.tga",
+ },
}
-- Helpers mods can use (mirrors the Forts modding API shape).
diff --git a/data/weapons.lua b/data/weapons.lua
index a6e23d5..98b7464 100644
--- a/data/weapons.lua
+++ b/data/weapons.lua
@@ -15,6 +15,8 @@ Weapons =
min_fire_angle = -20, -- firing arc (degrees, rel. to mount) — Forts MinFireAngle
max_fire_angle = 30, -- Forts MaxFireAngle
energy_cost = 120, -- energy per shot (Forts EnergyFireCost)
+ recoil = 12, -- kick shoved back into the firing fort
+ -- (Forts Recoil). Tune in the Dev panel.
texture = "data/textures/weapon_turret.png",
projectile_texture = "data/textures/projectile_laser.png",
texture_forts = "forts:weapons/cannon/base.tga",
@@ -31,6 +33,7 @@ Weapons =
min_fire_angle = -20, -- Forts laser MinFireAngle -20
max_fire_angle = 45, -- Forts laser MaxFireAngle 45
energy_cost = 200, -- beam is energy-hungry
+ recoil = 0, -- a beam has no kickback
texture = "data/textures/weapon_turret.png",
texture_forts = "forts:weapons/beamlaser/base.tga",
},
diff --git a/requirements/milestones.org b/requirements/milestones.org
index 3aeb4a9..a2cabe8 100644
--- a/requirements/milestones.org
+++ b/requirements/milestones.org
@@ -130,7 +130,8 @@ This is the purely-structural half of Forts destruction (no weapons yet).
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] 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
@@ -153,10 +154,13 @@ models (per-strut HP and radius splash) plus 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 per-shot RECOIL on the firing structure (we apply knockback
-to targets only).
+missiles, EMP, and the per-shot METAL cost (only energy is charged).
* M5: Reactor, Resources & Game Loop [MOSTLY DONE]
@@ -172,7 +176,10 @@ Goal: There's a reactor to destroy, and a real economy to build it with.
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
+ 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]
diff --git a/research/gameplay-gaps.org b/research/gameplay-gaps.org
index c6d1b17..fb02b4f 100644
--- a/research/gameplay-gaps.org
+++ b/research/gameplay-gaps.org
@@ -35,6 +35,9 @@ Since the first draft of this document we also have:
restart on R.
- *Bring-your-own Forts art*: =texture_forts= paths load real sprites from the
player's own install, falling back to the CC0 placeholders.
+- *Armour* (build mode 4): HP 400, 25% stiffer, 2x mass, never burns.
+- *Per-material build costs*, enforced: you cannot place what you cannot afford.
+- *Firing recoil*: a shot shoves the firing structure back, not just the target.
* MISSING — grouped by system, each with the in-game reference
@@ -100,9 +103,12 @@ swarm missiles (guided), missile launcher, 20mm cannon, cannon, flak
(beam). Notable *mechanics* we lack: point-defence (shoot down incoming shells),
guided/tracking projectiles, hitscan (machinegun/sniper), EMP disable.
-** 6. Building materials we lack
-=materials/building_materials.lua=: *armour* (HP 400, +25% stiffness, 2x mass —
-kinetic tank), *door* (openable/closable, blocks then opens for your own fire),
+** 6. Building materials we lack [PARTIAL — armour landed]
+DONE: *armour* (HP 400, +25% stiffness, 2x mass, does not burn, 18x wood's metal
+cost) is build mode 4. The rest of this list still stands.
+
+=materials/building_materials.lua=: *door* (openable/closable, blocks then opens
+for your own fire),
*energy shield* (reflects beams/plasma, warmup), *metal beam*, *camo*, *solar*
(energy on the strut), *fuse* (relays fire slowly). We have wood/bg/rope only.
Doors and shields are real tactical mechanics (open to shoot, shield vs lasers).
@@ -117,8 +123,10 @@ Doors and shields are real tactical mechanics (open to shoot, shield vs lasers).
- Cannon/weapons cost energy+metal PER SHOT (EnergyFireCost) — firing is
resource-limited, not just reload-limited. DONE for energy (=energy_cost= per
weapon); the per-shot METAL cost is still missing.
-- Recoil/kickback pushes the fort (we apply knockback to targets only, not
- recoil to the firing structure — Forts Recoil 600000, KickbackMean 40).
+- Recoil/kickback pushes the fort — DONE: each shot applies a velocity kick to
+ the firing structure along -aim (=BuildGraph::apply_recoil=), separate from the
+ knockback dealt to targets. Per-weapon =recoil= in data, tunable in the Dev
+ panel; the beam laser has none.
- Spotters: sniper reveals targets and guides swarm missiles.
- Fire-clearance: a weapon won't fire if its own structure blocks the muzzle
(MinFireClearance) — prevents shooting through your own fort.
@@ -148,8 +156,9 @@ Mapping the above onto the (already-reordered) milestones:
| Resource economy (mine/turbine/store) | M5 | DONE (no mine2/turbine2/store) |
| Per-shot energy cost | M4 | DONE (metal-per-shot missing) |
| Devices framework (mount on struts) | M5/M6 | not mounted on strut graph |
-| Armour + Door + Shield materials | M4/M7 | data-driven, small each |
-| Firing recoil on the shooter | M4 | small, faithful, not started |
+| Armour material | M4/M7 | DONE (build mode 4) |
+| Firing recoil on the shooter | M4 | DONE (per-weapon, in data) |
+| Door + Shield materials | M7 | need mechanics, not just data |
| More weapons (mortar/flak/mg/missile) | M4 | point-defence + guided are new |
| Repair + Recycle | M7 | resource-cost actions |
| Tech tree + Upgrades | post-v0.1 | big; build-order strategy |
@@ -159,21 +168,19 @@ Mapping the above onto the (already-reordered) milestones:
* Smallest high-value next steps (concrete)
-Items 1 and 2 of the original list (reactor + win/loss, real resources) are DONE;
-what follows is the refreshed queue.
+Reactor + win/loss, real resources, armour and firing recoil are all DONE; what
+follows is the refreshed queue.
-1. *Armour material*: data-only addition to =data/materials.lua= (Forts armour is
- HP 400, +25% stiffness, 2x mass). The material system already has every field
- it needs, and =materials/armor.tga= is a TGA, so =texture_forts= picks up the
- real sprite for free. Cheapest real gameplay depth available.
-2. *Firing recoil*: apply an impulse to the FIRING structure, not just the target
- (Forts Recoil 600000, KickbackMean 40). The knockback machinery already
- exists; this is a few lines and makes combat feel like Forts.
-3. *Mount devices on the strut graph*: attach devices to nodes so cutting the
+1. *Mount devices on the strut graph*: attach devices to nodes so cutting the
supports drops them. Unlocks the second victory path in #1 and makes reactors
- killable by collapse, which is a signature Forts moment.
-4. *Door material*: toggles =blocks_projectiles= so you can open to shoot and
+ killable by collapse, a signature Forts moment. This one root cause blocks
+ three separate gaps, so it buys the most per unit of work.
+2. *Fire damages devices*: Forts burns devices at 2.5 hp/s, 5x the 0.5 hp/s it
+ does to struts. We do not damage devices with fire at all.
+3. *Door material*: toggles =blocks_projectiles= so you can open to shoot and
close to tank. Needs a small mechanic, not just data.
-5. *Mortar or flak*: the next weapons. Flak brings a genuinely new mechanic
+4. *Mortar or flak*: the next weapons. Flak brings a genuinely new mechanic
(point-defence intercepting incoming shells); mortar is a high-arc,
high-building-damage counterpart to the cannon.
+5. *Per-shot metal cost*: firing charges energy but not metal (Forts charges
+ both). A one-line data + deduction change.
diff --git a/src/engine/app.cpp b/src/engine/app.cpp
index d5001c1..2108316 100644
--- a/src/engine/app.cpp
+++ b/src/engine/app.cpp
@@ -226,6 +226,7 @@ void App::run() {
if (input.key_pressed(SDLK_1)) { cur_mat = 0; selected_cannon = -1; place_dev = -1; }
if (input.key_pressed(SDLK_2)) { cur_mat = 1; selected_cannon = -1; place_dev = -1; }
if (input.key_pressed(SDLK_3)) { cur_mat = 2; selected_cannon = -1; place_dev = -1; }
+ if (input.key_pressed(SDLK_4)) { cur_mat = 3; selected_cannon = -1; place_dev = -1; }
if (input.key_pressed(SDLK_5)) { place_dev = turbine_i; selected_cannon = -1; }
if (input.key_pressed(SDLK_6)) { place_dev = mine_i; selected_cannon = -1; }
if (input.key_pressed(SDLK_7)) { place_dev = battery_i; selected_cannon = -1; }
@@ -279,6 +280,10 @@ void App::run() {
wpn.splash_damage, wpn.splash_radius, wpn.knockback,
wpn.projectile_radius, 6.0f, wpn.fire_chance });
}
+ // Recoil: shove the firing structure back along -aim. Radius
+ // picks up the nodes the weapon is sitting on.
+ graph.apply_recoil(c.x, c.y, 1.5f,
+ -std::cos(c.aim), -std::sin(c.aim), wpn.recoil);
c.cooldown = wpn.reload;
energy -= wpn.energy_cost;
}
@@ -333,22 +338,29 @@ void App::run() {
// ---- Finish edge placement ---- (add_link enforces the length rules
// and subdivides a long drag into segments)
if (placing && !input.mouse_down(SDL_BUTTON_LEFT)) {
+ const auto& m = graph.materials[cur_mat];
+ // Per-material cost, and you must be able to afford it. Without the
+ // affordability check an expensive material (armour) would be free
+ // whenever you are broke, since resources are clamped back to 0.
+ bool can_pay = metal >= m.cost_metal && energy >= m.cost_energy;
int target = graph.find_nearest_node(wx, wy, 0.5f);
if (target >= 0 && target != drag_node) {
- if (graph.add_link(drag_node, target, cur_mat) > 0) {
- metal -= 5.0f; energy -= 25.0f;
+ if (can_pay && graph.add_link(drag_node, target, cur_mat) > 0) {
+ metal -= m.cost_metal; energy -= m.cost_energy;
}
} else if (drag_node >= 0 && drag_node < (int)graph.nodes.size()) {
bool foundation = graph.is_on_ground(wy);
float ny = foundation ? graph.ground_level : wy;
// Length guard first, so a rejected link leaves no orphan node.
- const auto& m = graph.materials[cur_mat];
float dlen = std::hypot(wx - graph.nodes[drag_node].x,
ny - graph.nodes[drag_node].y);
- if (dlen >= m.min_length && dlen <= m.max_link_length) {
+ // A foundation node costs extra on top of the strut.
+ bool pay_all = metal >= m.cost_metal + (foundation ? 50.0f : 0.0f) &&
+ energy >= m.cost_energy + (foundation ? 400.0f : 0.0f);
+ if (pay_all && dlen >= m.min_length && dlen <= m.max_link_length) {
int new_id = graph.add_node(wx, ny, foundation);
if (graph.add_link(drag_node, new_id, cur_mat) > 0) {
- metal -= 5.0f; energy -= 25.0f;
+ metal -= m.cost_metal; energy -= m.cost_energy;
if (foundation) { metal -= 50.0f; energy -= 400.0f; }
}
}
@@ -361,9 +373,14 @@ void App::run() {
if (extruding_edge >= 0 && !input.mouse_down(SDL_BUTTON_RIGHT)) {
float offx = wx - extrude_start_x;
float offy = wy - extrude_start_y;
- if (std::sqrt(offx*offx + offy*offy) > 0.1f) {
+ // An extrusion sweeps out several struts, so it costs 2x the
+ // material's metal (for wood that is the flat 10/25 it used before).
+ const auto& em = graph.materials[cur_mat];
+ float ex_metal = em.cost_metal * 2.0f, ex_energy = em.cost_energy;
+ if (std::sqrt(offx*offx + offy*offy) > 0.1f &&
+ metal >= ex_metal && energy >= ex_energy) {
graph.extrude_edge(extruding_edge, offx, offy);
- metal -= 10.0f; energy -= 25.0f;
+ metal -= ex_metal; energy -= ex_energy;
}
extruding_edge = -1;
}
@@ -759,6 +776,10 @@ void App::run() {
if (ImGui::Button("wood")) { cur_mat = 0; place_dev = -1; selected_cannon = -1; }
ImGui::SameLine(); if (ImGui::Button("bg")) { cur_mat = 1; place_dev = -1; selected_cannon = -1; }
ImGui::SameLine(); if (ImGui::Button("rope")) { cur_mat = 2; place_dev = -1; selected_cannon = -1; }
+ if ((int)graph.materials.size() > 3) {
+ ImGui::SameLine();
+ if (ImGui::Button("armour")) { cur_mat = 3; place_dev = -1; selected_cannon = -1; }
+ }
if (turbine_i >= 0 && ImGui::Button("turbine")) { place_dev = turbine_i; selected_cannon = -1; }
if (mine_i >= 0) { ImGui::SameLine(); if (ImGui::Button("mine")) { place_dev = mine_i; selected_cannon = -1; } }
if (battery_i >= 0) { ImGui::SameLine(); if (ImGui::Button("battery")) { place_dev = battery_i; selected_cannon = -1; } }
@@ -787,6 +808,7 @@ void App::run() {
ImGui::PushID(&wpn);
if (ImGui::TreeNode(wpn.name.c_str())) {
ImGui::SliderFloat("reload", &wpn.reload, 0.1f, 6.0f);
+ ImGui::SliderFloat("recoil", &wpn.recoil, 0.0f, 60.0f);
if (wpn.is_beam) {
ImGui::SliderFloat("beam_dps", &wpn.beam_dps, 10.0f, 300.0f);
ImGui::SliderFloat("beam_duration", &wpn.beam_duration, 0.2f, 4.0f);
diff --git a/src/game/build_graph.cpp b/src/game/build_graph.cpp
index 032bbf5..a1195af 100644
--- a/src/game/build_graph.cpp
+++ b/src/game/build_graph.cpp
@@ -291,6 +291,21 @@ int BuildGraph::kill_grounded() {
// Destruction
// =============================================================================
+void BuildGraph::apply_recoil(float x, float y, float radius,
+ float dx, float dy, float strength) {
+ if (radius <= 0.0f || strength <= 0.0f) return;
+
+ for (auto& n : nodes) {
+ if (n.is_foundation) continue; // pinned: the ground eats the recoil
+ float ox = n.x - x, oy = n.y - y;
+ float d = std::sqrt(ox*ox + oy*oy);
+ if (d >= radius) continue;
+ float f = 1.0f - d / radius; // linear falloff, as in apply_splash
+ n.vx += dx * strength * f;
+ n.vy += dy * strength * f;
+ }
+}
+
void BuildGraph::apply_splash(float x, float y, float radius, float damage,
float knockback) {
if (radius <= 0.0f) return;
diff --git a/src/game/build_graph.hpp b/src/game/build_graph.hpp
index bf83a18..4452139 100644
--- a/src/game/build_graph.hpp
+++ b/src/game/build_graph.hpp
@@ -34,6 +34,8 @@ struct MaterialDef {
bool blocks_projectiles; // stops cannon shells (wood yes; bg-brace/rope no)
bool blocks_beam; // stops laser beams (all yes in Forts)
float half_width; // visual thickness
+ float cost_metal; // metal spent placing one strut of this material
+ float cost_energy; // energy spent placing one strut of this material
float r, g, b, a; // visual colour (also tints the texture)
std::string name;
std::string texture; // CC0 placeholder path ("" = flat colour)
@@ -104,6 +106,13 @@ public:
// not block splash (pure radius falloff, like Forts AoE).
void apply_splash(float x, float y, float radius, float damage, float knockback);
+ // Directional velocity kick to free nodes within `radius` of (x,y), along the
+ // unit vector (dx,dy), with linear falloff. Weapon recoil uses this to shove
+ // the FIRING structure back (Forts' Recoil), unlike apply_splash's radial
+ // knockback on the target. No damage: recoil only moves the fort.
+ void apply_recoil(float x, float y, float radius,
+ float dx, float dy, float strength);
+
// Trace a laser beam from (ox,oy) along (dx,dy) up to `range`: damage +
// optionally ignite EVERY strut it crosses, passing through beam-transparent
// materials (bg-brace, ropes) and STOPPING at the first blocking one (wood).
diff --git a/src/game/data.cpp b/src/game/data.cpp
index 654cac6..3d9f6c0 100644
--- a/src/game/data.cpp
+++ b/src/game/data.cpp
@@ -36,6 +36,9 @@ MaterialDef read_material(const sol::table& m) {
d.blocks_beam = m.get_or("blocks_beam", true);
d.tension_only = m.get_or("tension_only", false);
d.half_width = m.get_or("half_width", 0.15f);
+ // Defaults match the flat costs struts used before they became per-material.
+ d.cost_metal = m.get_or("cost_metal", 5.0f);
+ d.cost_energy = m.get_or("cost_energy", 25.0f);
d.texture = m.get_or("texture", std::string(""));
d.texture_forts = m.get_or("texture_forts", std::string(""));
@@ -137,6 +140,7 @@ WeaponDef read_weapon(const sol::table& w) {
d.beam_dps = w.get_or("beam_dps", 120.0f);
d.beam_duration = w.get_or("beam_duration", 1.5f);
d.energy_cost = w.get_or("energy_cost", 0.0f);
+ d.recoil = w.get_or("recoil", 0.0f);
d.texture = w.get_or("texture", std::string(""));
d.projectile_texture = w.get_or("projectile_texture", std::string(""));
d.texture_forts = w.get_or("texture_forts", std::string(""));
diff --git a/src/game/data.hpp b/src/game/data.hpp
index fdf9e08..9b9aec1 100644
--- a/src/game/data.hpp
+++ b/src/game/data.hpp
@@ -24,6 +24,8 @@ struct WeaponDef {
float beam_dps; // HP damage per second to the struck strut
float beam_duration; // seconds the beam stays on
float energy_cost; // energy spent per shot (Forts EnergyFireCost)
+ float recoil; // velocity kick shoved back into the FIRING
+ // structure on each shot (Forts Recoil); 0 = none
std::string texture; // turret/base sprite (CC0 placeholder)
std::string projectile_texture; // shell sprite (CC0 placeholder)
std::string texture_forts; // preferred BYO Forts turret art