Redraw campfire and torch so they don't look like colonists

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 10:20 UTC
commit
5a1800529809b9356790b012c8d3f55ad9d1f34e
parent
46b5569b4f
tree
browse at this commit

2 files changed +61 -27

CLAUDE.md +1 -1
src/light.rs +60 -26

CLAUDE.md +1 -1

@@ -26,7 +26,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che
26 26 - `src/items.rs` `Item{kind: ItemKind::{Wood, Stone, Iron, Gold}, count}` + `TilePos` entities (Mesh2d rect, z=0.7), one stack per tile, `STACK_LIMIT`=50; `ItemQuery` alias; `drop_items` stacks, `take_items`, `room_for(tile, kind)` (0 if other kind present), `stock()`. `Carrying(Option<Item>)` on colonists, shown via hidden/visible `CarriedVisual` child. Items cleared on WorldGenerated.
27 27 - `src/zones.rs` `Zones` resource (stockpile tile set; only walkable dry land), cleared on WorldGenerated — worlds start without stockpiles, colonists found one when needed. `plan_expansion` grows one tile at a time next to stockpile/planned tiles, preferring tree-free tiles near the stockpile centroid (deterministic tiebreak). `find_stockpile_site` = BFS for nearest fully open (zonable, tree-free) square, fallback any zonable tile. `ZoneTool` (Z/button toggles, Cancel exits): left-drag adds, shift-drag removes (`zone_tool` input + `draw_zone_preview` gizmo, PostUpdate after `update_cursor`). Rendered as tilemap `Layer::Zones` (z=0.3), refreshed on `resource_changed::<Zones>`.
28 28 - `src/sim.rs` `SimPlugin`: `SimSystems::{Decide, Act, Move}` chained sets in FixedUpdate; fixed timestep `Time<Fixed>` 60 Hz = sim ticks; all sim logic in `FixedUpdate` (Res<Time> there = fixed delta). `SimTick` counter (FixedFirst), `format_clock` (600 ticks/hour = 10 s real at 1x, starts Day 1 06:00). `SimSpeed` Normal/Fast/Fastest = 1x/3x/6x via `Time<Virtual>::set_relative_speed`; keys 1/2/3 set speed + unpause, Space toggles pause. Render interpolation: sim entities hold `SimPos` (+ `PrevSimPos`, copied in FixedFirst); `interpolate_transforms` lerps Transform by `overstep_fraction` in `RunFixedMainLoop`/AfterFixedMainLoop. Spawn with `sim_pos(v)` so prev==cur. Never write Transform of sim entities directly.
29 -- `src/light.rs` day/night + fires. `daylight(hour)` (dawn 5–7, dusk 18–20), `ambient_color` (night = blue-ish), `dark_soon(tick)` (now or within LIGHT_LOOKAHEAD_TICKS). `Structure{kind: StructureKind::{Campfire, Torch}, fuel}` + TilePos entities (Mesh2d children, z=0.8); `burn_fuel` (SimSystems::Act) despawns at 0. Kind stats: wood_cost 10/2, fuel 12h/8h, radius 7/4, `light_at` falloff. `LightMap` resource = ambient + per-tile source light, recomputed in `update_light_map` (SimSystems::Decide) only when the ambient step (16 levels) changes or structures are added/removed; default (empty) = fully lit. `is_lit` = brightness ≥ LIT_BRIGHTNESS. Tilemap tints terrain/plant tiles by `LightMap::color` and refreshes all chunks on `LightMap` change (zones overlay unlit, colonists/items/fires not darkened).
29 +- `src/light.rs` day/night + fires. `daylight(hour)` (dawn 5–7, dusk 18–20), `ambient_color` (night = blue-ish), `dark_soon(tick)` (now or within LIGHT_LOOKAHEAD_TICKS). `Structure{kind: StructureKind::{Campfire, Torch}, fuel}` + TilePos entities (Mesh2d children: campfire = stone ring + crossed logs + triangle flame, torch = stick + small flame; avoid plain circles, those read as colonists; z=0.8); `burn_fuel` (SimSystems::Act) despawns at 0. Kind stats: wood_cost 10/2, fuel 12h/8h, radius 7/4, `light_at` falloff. `LightMap` resource = ambient + per-tile source light, recomputed in `update_light_map` (SimSystems::Decide) only when the ambient step (16 levels) changes or structures are added/removed; default (empty) = fully lit. `is_lit` = brightness ≥ LIT_BRIGHTNESS. Tilemap tints terrain/plant tiles by `LightMap::color` and refreshes all chunks on `LightMap` change (zones overlay unlit, colonists/items/fires not darkened).
30 30 - `src/rng.rs` `SimRng` SplitMix64 resource, seeded (deterministic sim; no `rand` dep).
31 31
32 32 ## Gotchas

src/light.rs +60 -26

@@ -203,12 +203,26 @@ fn clear_structures(mut commands: Commands, structures: Query<Entity, With<Struc
203 203 }
204 204 }
205 205
206 +type Part = (Handle<Mesh>, Handle<ColorMaterial>);
207 +
206 208 #[derive(Resource, Default)]
207 209 pub struct StructureAssets {
208 - ring: (Handle<Mesh>, Handle<ColorMaterial>),
209 - flame: (Handle<Mesh>, Handle<ColorMaterial>),
210 - stick: (Handle<Mesh>, Handle<ColorMaterial>),
211 - torch_flame: (Handle<Mesh>, Handle<ColorMaterial>),
210 + stone: Part,
211 + log: Part,
212 + flame: Part,
213 + flame_core: Part,
214 + stick: Part,
215 +}
216 +
217 +const CAMPFIRE_STONES: usize = 7;
218 +
219 +fn flame_mesh(width: f32, height: f32) -> Mesh {
220 + Triangle2d::new(
221 + Vec2::new(-width / 2.0, -height / 2.0),
222 + Vec2::new(width / 2.0, -height / 2.0),
223 + Vec2::new(0.0, height / 2.0),
224 + )
225 + .into()
212 226 }
213 227
214 228 fn create_structure_assets(
@@ -218,38 +232,43 @@ fn create_structure_assets(
218 232 ) {
219 233 let mut part = |mesh: Mesh, color: Color| (meshes.add(mesh), materials.add(color));
220 234 commands.insert_resource(StructureAssets {
221 - ring: part(
222 - Circle::new(TILE_SIZE * 0.45).into(),
223 - Color::srgb_u8(90, 86, 80),
235 + stone: part(
236 + Circle::new(TILE_SIZE * 0.09).into(),
237 + Color::srgb_u8(120, 116, 110),
238 + ),
239 + log: part(
240 + Rectangle::new(TILE_SIZE * 0.7, TILE_SIZE * 0.14).into(),
241 + Color::srgb_u8(110, 74, 42),
224 242 ),
225 243 flame: part(
226 - Circle::new(TILE_SIZE * 0.28).into(),
227 - Color::srgb_u8(255, 150, 40),
244 + flame_mesh(TILE_SIZE * 0.36, TILE_SIZE * 0.5),
245 + Color::srgb_u8(240, 110, 30),
246 + ),
247 + flame_core: part(
248 + flame_mesh(TILE_SIZE * 0.18, TILE_SIZE * 0.28),
249 + Color::srgb_u8(255, 220, 90),
228 250 ),
229 251 stick: part(
230 252 Rectangle::new(TILE_SIZE * 0.12, TILE_SIZE * 0.5).into(),
231 253 Color::srgb_u8(100, 70, 40),
232 254 ),
233 - torch_flame: part(
234 - Circle::new(TILE_SIZE * 0.16).into(),
235 - Color::srgb_u8(255, 200, 70),
236 - ),
237 255 });
238 256 }
239 257
258 +fn part_bundle((mesh, material): &Part, transform: Transform) -> impl Bundle {
259 + (
260 + Mesh2d(mesh.clone()),
261 + MeshMaterial2d(material.clone()),
262 + transform,
263 + )
264 +}
265 +
240 266 pub fn spawn_structure(
241 267 commands: &mut Commands,
242 268 assets: &StructureAssets,
243 269 tile: UVec2,
244 270 kind: StructureKind,
245 271 ) {
246 - let part = |(mesh, material): &(Handle<Mesh>, Handle<ColorMaterial>), offset: Vec3| {
247 - (
248 - Mesh2d(mesh.clone()),
249 - MeshMaterial2d(material.clone()),
250 - Transform::from_translation(offset),
251 - )
252 - };
253 272 let mut entity = commands.spawn((
254 273 Name::new(format!("{kind:?}")),
255 274 Structure {
@@ -259,16 +278,31 @@ pub fn spawn_structure(
259 278 TilePos(tile),
260 279 Transform::from_translation(WorldMap::tile_center(tile).extend(0.8)),
261 280 ));
281 + let at = |x: f32, y: f32, z: f32| Transform::from_xyz(x * TILE_SIZE, y * TILE_SIZE, z);
262 282 match kind {
263 283 StructureKind::Campfire => entity.with_children(|c| {
264 - c.spawn(part(&assets.ring, Vec3::ZERO));
265 - c.spawn(part(&assets.flame, Vec3::Z * 0.01));
284 + for i in 0..CAMPFIRE_STONES {
285 + let angle = i as f32 / CAMPFIRE_STONES as f32 * std::f32::consts::TAU;
286 + let pos = Vec2::from_angle(angle) * 0.38;
287 + c.spawn(part_bundle(&assets.stone, at(pos.x, pos.y, 0.0)));
288 + }
289 + for angle in [0.6_f32, -0.6] {
290 + let log = at(0.0, -0.08, 0.01).with_rotation(Quat::from_rotation_z(angle));
291 + c.spawn(part_bundle(&assets.log, log));
292 + }
293 + c.spawn(part_bundle(&assets.flame, at(0.0, 0.08, 0.02)));
294 + c.spawn(part_bundle(&assets.flame_core, at(0.0, 0.03, 0.03)));
266 295 }),
267 296 StructureKind::Torch => entity.with_children(|c| {
268 - c.spawn(part(&assets.stick, Vec3::new(0.0, -TILE_SIZE * 0.1, 0.0)));
269 - c.spawn(part(
270 - &assets.torch_flame,
271 - Vec3::new(0.0, TILE_SIZE * 0.2, 0.01),
297 + c.spawn(part_bundle(&assets.stick, at(0.0, -0.15, 0.0)));
298 + let small = Vec3::splat(0.6);
299 + c.spawn(part_bundle(
300 + &assets.flame,
301 + at(0.0, 0.18, 0.01).with_scale(small),
302 + ));
303 + c.spawn(part_bundle(
304 + &assets.flame_core,
305 + at(0.0, 0.15, 0.02).with_scale(small),
272 306 ));
273 307 }),
274 308 };