aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.clangd10
-rw-r--r--.gitignore3
-rw-r--r--CLAUDE.md11
-rw-r--r--data/devices.lua14
-rw-r--r--data/materials.lua6
-rw-r--r--data/weapons.lua3
-rw-r--r--src/engine/app.cpp32
-rw-r--r--src/engine/renderer.cpp8
-rw-r--r--src/engine/renderer.hpp7
-rw-r--r--src/game/build_graph.hpp4
-rw-r--r--src/game/data.cpp4
-rw-r--r--src/game/data.hpp9
12 files changed, 87 insertions, 24 deletions
diff --git a/.clangd b/.clangd
new file mode 100644
index 0000000..030d130
--- /dev/null
+++ b/.clangd
@@ -0,0 +1,10 @@
+# Point clangd at Meson's compile database (generated in builddir/ by
+# `meson setup`/`meson compile`). This gives it -Isrc, the vendored third_party
+# include dirs, and the /usr/local + system include paths the build uses, so
+# project headers, SDL3, ImGui and stb resolve.
+#
+# glm, sol2 and Box2D headers come from /usr/local/include in the container
+# build. For clangd on the host to resolve them too, install their dev headers
+# on the host (see README, "Local (Arch Linux)"): pacman glm box2d, AUR sol2.
+CompileFlags:
+ CompilationDatabase: builddir
diff --git a/.gitignore b/.gitignore
index bfd896f..cf6685d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,9 @@ subprojects/enet/
*.swo
*~
+# clangd index cache (.clangd config itself is committed)
+.cache/
+
# Object files
*.o
*.obj
diff --git a/CLAUDE.md b/CLAUDE.md
index 5b8be60..e94eeb5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -134,8 +134,15 @@ Games' copyrighted work.
MIT `third_party/imgui`).
- To debug with the *real* game art, we use **bring-your-own game files** (like a
Doom source port + WAD): a texture path `forts:REL` resolves at runtime to
- `$FORTS_DATA/REL` (auto-detected Steam path if unset). Missing game → graceful
- fallback to CC0 / colored boxes. Nothing copyrighted ever enters the repo.
+ `<root>/REL`, where `<root>` is the first that exists of some known dev install
+ paths then `$FORTS_DATA` (see `forts_data_root()` in `renderer.cpp`). Nothing
+ copyrighted ever enters the repo.
+- **Two-field fallback:** material/weapon/device defs carry both `texture` (a CC0
+ placeholder committed to the repo) and `texture_forts` (a `forts:` path to the
+ real art). `Renderer::load_texture_or` prefers the Forts art, falls back to the
+ CC0 placeholder, then to the flat `color`. So a user without Forts always gets
+ the placeholder; a user with it gets the real sprite. **stb_image only decodes
+ TGA/PNG/JPG, not DDS** (most Forts art), so only TGA/PNG/JPG paths will load.
- Keep personal/BYO override mods under `data/mods/local-*/` (git-ignored).
---
diff --git a/data/devices.lua b/data/devices.lua
index e5309dc..dd1b3a9 100644
--- a/data/devices.lua
+++ b/data/devices.lua
@@ -1,7 +1,7 @@
-- Device catalog (reactor / mine / turbine / battery). Values scaled from Forts'
--- devices/*.lua. `texture` may use a "forts:REL" path to load real art from your
--- own Forts install at runtime (nothing copyrighted is stored in the repo); if
--- the game isn't present it falls back to the flat `color`.
+-- devices/*.lua. `texture_forts` is a "forts:REL" path that loads real art from
+-- your own Forts install at runtime (nothing copyrighted is stored in the repo);
+-- it falls back to `texture` (a CC0 placeholder), then to the flat `color`.
DeviceDefs =
{
@@ -10,7 +10,7 @@ DeviceDefs =
hp = 100, -- Forts reactor HP 100 (fragile)
energy_rate = 100, metal_rate = 5, -- also the base generator
half_w = 0.7, half_h = 0.9,
- texture = "forts:devices/reactor/reactor_core01.tga",
+ texture_forts = "forts:devices/reactor/reactor_core01.tga",
color = { 0.3, 1.0, 0.5 },
},
{
@@ -18,7 +18,7 @@ DeviceDefs =
hp = 60, wind_max_rate = 16, wind_max_height = 22,
half_w = 0.5, half_h = 1.3,
cost_metal = 30, cost_energy = 50,
- texture = "forts:devices/windturbine/base.tga",
+ texture_forts = "forts:devices/windturbine/base.tga",
color = { 0.6, 0.8, 1.0 },
},
{
@@ -26,7 +26,7 @@ DeviceDefs =
hp = 80, energy_rate = -5, metal_rate = 4, -- Forts mine: -5 e, +4 metal
half_w = 0.6, half_h = 0.5,
cost_metal = 0, cost_energy = 100,
- texture = "",
+ texture_forts = "forts:devices/mine/base.png",
color = { 0.75, 0.55, 0.2 },
},
{
@@ -34,7 +34,7 @@ DeviceDefs =
hp = 100, energy_storage = 2000, -- Forts battery cap 2000
half_w = 0.6, half_h = 0.6,
cost_metal = 20, cost_energy = 100,
- texture = "",
+ texture_forts = "forts:devices/battery/base.tga",
color = { 0.9, 0.85, 0.3 },
},
}
diff --git a/data/materials.lua b/data/materials.lua
index a2f876c..3725aa9 100644
--- a/data/materials.lua
+++ b/data/materials.lua
@@ -19,6 +19,7 @@ Materials =
half_width = 0.15,
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",
},
{
name = "bgbrace", -- background bracing
@@ -32,6 +33,7 @@ Materials =
half_width = 0.15,
color = { 1.0, 1.0, 1.0, 0.45 }, -- translucent
texture = "data/textures/bgbrace.png",
+ texture_forts = "forts:materials/bracing-bg.tga",
},
{
name = "rope",
@@ -43,7 +45,9 @@ 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,
- color = { 0.15, 0.12, 0.10, 1.0 },
+ 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",
},
}
diff --git a/data/weapons.lua b/data/weapons.lua
index 8fd2a6b..a6e23d5 100644
--- a/data/weapons.lua
+++ b/data/weapons.lua
@@ -17,6 +17,8 @@ Weapons =
energy_cost = 120, -- energy per shot (Forts EnergyFireCost)
texture = "data/textures/weapon_turret.png",
projectile_texture = "data/textures/projectile_laser.png",
+ texture_forts = "forts:weapons/cannon/base.tga",
+ projectile_texture_forts = "forts:weapons/media/CannonShell.tga",
},
{
name = "laser", -- line-of-sight beam; ignites what it hits
@@ -30,6 +32,7 @@ Weapons =
max_fire_angle = 45, -- Forts laser MaxFireAngle 45
energy_cost = 200, -- beam is energy-hungry
texture = "data/textures/weapon_turret.png",
+ texture_forts = "forts:weapons/beamlaser/base.tga",
},
}
diff --git a/src/engine/app.cpp b/src/engine/app.cpp
index 64b9f01..d5001c1 100644
--- a/src/engine/app.cpp
+++ b/src/engine/app.cpp
@@ -89,12 +89,14 @@ void App::run() {
// above is sky. Anything that falls into the ground is destroyed.
graph.ground_level = 1.0f;
- // Resolve each material's texture once (nullptr = draw flat colour).
+ // Resolve each material's texture once: prefer BYO Forts art, fall back to
+ // the CC0 placeholder, then to a flat colour (nullptr).
std::vector<SDL_Texture*> mat_tex(graph.materials.size(), nullptr);
for (size_t i = 0; i < graph.materials.size(); i++)
- if (!graph.materials[i].texture.empty())
- mat_tex[i] = renderer.load_texture(graph.materials[i].texture);
- SDL_Texture* ground_tex = renderer.load_texture("data/textures/ground.png");
+ mat_tex[i] = renderer.load_texture_or(graph.materials[i].texture_forts,
+ graph.materials[i].texture);
+ SDL_Texture* ground_tex = renderer.load_texture_or(
+ "forts:environment/midwest/ground/ground1.tga", "data/textures/ground.png");
// Short-lived explosion puffs spawned when debris hits the ground.
struct Explosion { float x, y, life; };
@@ -108,8 +110,16 @@ void App::run() {
if (weapons[i].name == "cannon") cannon_type = (int)i;
if (weapons[i].name == "laser") laser_type = (int)i;
}
- SDL_Texture* turret_tex = renderer.load_texture("data/textures/weapon_turret.png");
- SDL_Texture* shell_tex = renderer.load_texture("data/textures/projectile_laser.png");
+ // Per-weapon turret sprite (BYO Forts art, else CC0 placeholder).
+ std::vector<SDL_Texture*> weapon_tex(weapons.size(), nullptr);
+ for (size_t i = 0; i < weapons.size(); i++)
+ weapon_tex[i] = renderer.load_texture_or(weapons[i].texture_forts,
+ weapons[i].texture);
+ SDL_Texture* shell_tex = nullptr;
+ if (!weapons.empty())
+ shell_tex = renderer.load_texture_or(
+ weapons[cannon_type].projectile_texture_forts,
+ weapons[cannon_type].projectile_texture);
struct Cannon { float x, y, mount, aim, cooldown; int type; };
std::vector<Cannon> cannons;
@@ -127,8 +137,8 @@ void App::run() {
MapData map = load_map(script, "data");
std::vector<SDL_Texture*> dev_tex(device_defs.size(), nullptr);
for (size_t i = 0; i < device_defs.size(); i++)
- if (!device_defs[i].texture.empty())
- dev_tex[i] = renderer.load_texture(device_defs[i].texture);
+ dev_tex[i] = renderer.load_texture_or(device_defs[i].texture_forts,
+ device_defs[i].texture);
SDL_Texture* deposit_tex = renderer.load_texture("data/textures/deposit.png");
struct Device { int def, team; float x, y, hp, max_hp; bool on_deposit; };
@@ -673,8 +683,10 @@ void App::run() {
float bx = c.x + std::cos(c.aim) * bl * 0.5f;
float by = c.y + std::sin(c.aim) * bl * 0.5f;
renderer.draw_box(bx, by, bl * 0.5f, 0.12f, c.aim, 0.30f, 0.30f, 0.34f, 1.0f);
- if (turret_tex) renderer.draw_sprite(c.x, c.y, 0.5f, 0.5f, 0.0f, turret_tex);
- else renderer.draw_box(c.x, c.y, 0.5f, 0.5f, 0.0f, 0.4f, 0.4f, 0.45f, 1.0f);
+ SDL_Texture* wt = (c.type >= 0 && c.type < (int)weapon_tex.size())
+ ? weapon_tex[c.type] : nullptr;
+ if (wt) renderer.draw_sprite(c.x, c.y, 0.5f, 0.5f, 0.0f, wt);
+ else renderer.draw_box(c.x, c.y, 0.5f, 0.5f, 0.0f, 0.4f, 0.4f, 0.45f, 1.0f);
}
// Firing arc of the selected cannon (the cone of allowed directions).
diff --git a/src/engine/renderer.cpp b/src/engine/renderer.cpp
index 0ec6371..bc9bbf7 100644
--- a/src/engine/renderer.cpp
+++ b/src/engine/renderer.cpp
@@ -192,6 +192,14 @@ SDL_Texture* Renderer::load_texture(const std::string& path) {
return tex;
}
+SDL_Texture* Renderer::load_texture_or(const std::string& primary,
+ const std::string& fallback) {
+ if (!primary.empty()) {
+ if (SDL_Texture* t = load_texture(primary)) return t;
+ }
+ return fallback.empty() ? nullptr : load_texture(fallback);
+}
+
void Renderer::draw_sprite(float cx, float cy, float half_w, float half_h,
float angle, SDL_Texture* tex,
float r, float g, float b, float a) {
diff --git a/src/engine/renderer.hpp b/src/engine/renderer.hpp
index ddc8055..f5a087f 100644
--- a/src/engine/renderer.hpp
+++ b/src/engine/renderer.hpp
@@ -25,6 +25,13 @@ public:
// Load a texture (cached by path). Returns nullptr on failure.
SDL_Texture* load_texture(const std::string& path);
+ // Load `primary` if set, falling back to `fallback` if it is empty or fails
+ // to load. Used for BYO Forts art ("forts:...") that degrades to the CC0
+ // placeholder when the game is not installed. Returns nullptr if neither
+ // loads.
+ SDL_Texture* load_texture_or(const std::string& primary,
+ const std::string& fallback);
+
// Draw a textured quad in world coordinates, colour-modulated by (r,g,b,a).
void draw_sprite(float cx, float cy, float half_w, float half_h,
float angle, SDL_Texture* tex,
diff --git a/src/game/build_graph.hpp b/src/game/build_graph.hpp
index f92dbd3..bf83a18 100644
--- a/src/game/build_graph.hpp
+++ b/src/game/build_graph.hpp
@@ -36,7 +36,9 @@ struct MaterialDef {
float half_width; // visual thickness
float r, g, b, a; // visual colour (also tints the texture)
std::string name;
- std::string texture; // optional texture path ("" = flat colour)
+ std::string texture; // CC0 placeholder path ("" = flat colour)
+ std::string texture_forts; // preferred BYO Forts art ("forts:..."), falls
+ // back to `texture` then colour when absent
};
struct BuildNode {
diff --git a/src/game/data.cpp b/src/game/data.cpp
index 54f7fdb..654cac6 100644
--- a/src/game/data.cpp
+++ b/src/game/data.cpp
@@ -37,6 +37,7 @@ MaterialDef read_material(const sol::table& m) {
d.tension_only = m.get_or("tension_only", false);
d.half_width = m.get_or("half_width", 0.15f);
d.texture = m.get_or("texture", std::string(""));
+ d.texture_forts = m.get_or("texture_forts", std::string(""));
sol::optional<sol::table> col = m["color"];
d.r = col ? col->get_or(1, 0.8f) : 0.8f;
@@ -138,6 +139,8 @@ WeaponDef read_weapon(const sol::table& w) {
d.energy_cost = w.get_or("energy_cost", 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(""));
+ d.projectile_texture_forts = w.get_or("projectile_texture_forts", std::string(""));
return d;
}
} // namespace
@@ -213,6 +216,7 @@ DeviceDef read_device(const sol::table& d) {
v.cost_metal = d.get_or("cost_metal", 0.0f);
v.cost_energy = d.get_or("cost_energy", 0.0f);
v.texture = d.get_or("texture", std::string(""));
+ v.texture_forts = d.get_or("texture_forts", std::string(""));
sol::optional<sol::table> col = d["color"];
v.r = col ? col->get_or(1, 0.8f) : 0.8f;
v.g = col ? col->get_or(2, 0.8f) : 0.8f;
diff --git a/src/game/data.hpp b/src/game/data.hpp
index 3a7f912..fdf9e08 100644
--- a/src/game/data.hpp
+++ b/src/game/data.hpp
@@ -24,8 +24,10 @@ 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)
- std::string texture; // turret/base sprite
- std::string projectile_texture; // shell sprite
+ 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
+ std::string projectile_texture_forts; // preferred BYO Forts shell art
};
// Loads the material table from <data_dir>/materials.lua, then applies every
@@ -57,7 +59,8 @@ struct DeviceDef {
bool is_core; // reactor: destroying it decides the game
float half_w, half_h;
float cost_metal, cost_energy; // build cost
- std::string texture;
+ std::string texture; // CC0 placeholder ("" = flat colour)
+ std::string texture_forts; // preferred BYO Forts art, else texture
float r, g, b; // fallback colour when no texture
};