rimlike/scenes/world/dirtiness_system.gd
megaproxy fd6f958344 sprint A cleanup: accessibility, signals, race, debris
G: large_text scales global theme font (14→20 at 1.4×) via new
GameState.get_font_scale + EventBus.settings_changed. reduce_motion
gates ResumeToast fade (HintOverlay already gated).

I: InspectTooltip long-press wired (500ms hold, 12px drift cancel,
tap-to-clear pin). Stale Phase 19 TODO replaced with accurate doc.

H: Pawn.arrived_at_destination now also emitted on
EventBus.pawn_arrived_at_destination; DirtinessSystem subscribes and
bumps indoor traffic dirt (BUMP_INDOOR_TRAFFIC = 0.2). Outdoor-tracked
bump needs Pawn.prev_tile — flagged for Phase 20.

P: CraftingProvider caches ingredient item ref on Job.ingredient_item;
JobRunner._tick_pickup validates is_instance_valid + not being_carried
before the tile scan, cancels cleanly if another pawn grabbed it.

J: rest_provider.gd deleted. Removed @onready + register call from
world.gd, ext_resource + node from world.tscn. Provider count comment
updated to 9.

M: DIRTY_THRESHOLD extracted — cleaning_provider and job_runner now
reference DirtinessSystem.DIRT_DIRTY_THRESHOLD.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:38:14 +01:00

145 lines
5.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name DirtinessSystem extends Node
## Phase 13 — per-tile dirtiness accumulation and tier tracking.
##
## dirt_map: Dictionary[Vector2i, float], 0..100 scale. Only tiles with non-zero
## dirt are keyed (sparse).
##
## Public API:
## bump(tile, amount) — add dirtiness to a tile. Positive adds dirt; use
## negative values to reduce (cleaning does this via
## bump_clean). Phase 14 calls bump(tile, +5) per hour
## for corpse decay and bump(tile, +20) for combat blood.
## bump_clean(tile, amount) — specialised helper for the CleaningProvider;
## reduces dirt and removes the tile from the map at 0.
## dirt_at(tile) — returns the current dirtiness for a tile (0.0 default).
##
## Tier thresholds (per design.md):
## Clean < 25
## Dirty 25..60
## Filthy >= 60
##
## EventBus.tile_dirtiness_changed fires on TIER crossings only, not every bump.
## This keeps signal volume low since bumps fire 20×/s per pawn crossing.
##
## Pawn tile-change hook:
## _ready() connects to EventBus.pawn_arrived_at_destination. Indoor arrivals bump
## BUMP_INDOOR_TRAFFIC (0.2); outdoor arrivals are skipped. Phase 20 can upgrade to
## BUMP_OUTDOOR_TRACKED (0.5) on outdoor→indoor transitions once Pawn.prev_tile lands.
##
## Wire as child of the World scene (after Pathfinder). world.gd exposes the
## instance on the World autoload as World.dirtiness_system for entity code.
## Sparse map: only tiles with non-zero dirt are stored.
## Keys = Vector2i tile coords; Values = float in [0, 100].
var dirt_map: Dictionary = {}
func _ready() -> void:
EventBus.pawn_arrived_at_destination.connect(_on_pawn_arrived_at_destination)
## Tier boundaries (thresholds match design.md; tune Phase 20).
const DIRT_DIRTY_THRESHOLD: float = 25.0
const DIRT_FILTHY_THRESHOLD: float = 60.0
## Traffic bump amounts per pawn step.
const BUMP_OUTDOOR_TRACKED: float = 0.5 # boots bringing dirt in from outside
const BUMP_INDOOR_TRAFFIC: float = 0.2 # indoor walking
# ── public API ────────────────────────────────────────────────────────────────
## Returns the dirtiness score for `tile` (0.0 if clean / not in map).
func dirt_at(tile: Vector2i) -> float:
return float(dirt_map.get(tile, 0.0))
## Add `amount` of dirt to `tile`. Clamped to [0, 100].
## Emits EventBus.tile_dirtiness_changed when a tier boundary is crossed.
## Phase 14 calls this for blood (+20) and corpse decay (+5/h).
func bump(tile: Vector2i, amount: float) -> void:
var old_val: float = float(dirt_map.get(tile, 0.0))
var new_val: float = clampf(old_val + amount, 0.0, 100.0)
if is_equal_approx(new_val, old_val):
return
_set_dirt(tile, old_val, new_val)
## Reduce dirt by `amount`, flooring at 0. Removes tile from map when clean.
## Used by CleaningProvider's KIND_CLEAN toil each tick.
func bump_clean(tile: Vector2i, amount: float) -> void:
var old_val: float = float(dirt_map.get(tile, 0.0))
if old_val <= 0.0:
return
var new_val: float = maxf(0.0, old_val - amount)
_set_dirt(tile, old_val, new_val)
## Traffic-driven dirt bump. Called internally by _on_pawn_arrived_at_destination.
## `indoor` = true when the destination tile is inside a roofed room (World.is_indoor check).
func bump_pawn_traffic(tile: Vector2i, indoor: bool) -> void:
var amount := BUMP_INDOOR_TRAFFIC if indoor else BUMP_OUTDOOR_TRACKED
bump(tile, amount)
## Pawn arrival handler — wired to EventBus.pawn_arrived_at_destination in _ready().
## Skips outdoor arrivals (no floor to dirty); bumps indoor tiles by BUMP_INDOOR_TRAFFIC.
## Phase 20 follow-up: add Pawn.prev_tile tracking so an outdoor→indoor transition can
## instead use BUMP_OUTDOOR_TRACKED (0.5) to model boots tracking mud in.
func _on_pawn_arrived_at_destination(_pawn, dest_tile: Vector2i) -> void:
if not World.is_indoor(dest_tile):
return # Only dirtiness indoor floor tiles.
bump_pawn_traffic(dest_tile, true)
# ── save / load ───────────────────────────────────────────────────────────────
## Serialise the sparse dirt map as an array of {x, y, v} dicts.
## Only non-zero tiles are stored (map is already sparse).
func save_dict() -> Dictionary:
var entries: Array = []
for t in dirt_map:
entries.append({"x": t.x, "y": t.y, "v": float(dirt_map[t])})
return {"dirtiness": entries}
## Restore the dirt map from a dict produced by save_dict().
## Replaces the current map in full.
func apply_dict(d: Dictionary) -> void:
dirt_map.clear()
for entry in d.get("dirtiness", []):
if entry is Dictionary:
var t := Vector2i(int(entry.get("x", 0)), int(entry.get("y", 0)))
var v: float = clampf(float(entry.get("v", 0.0)), 0.0, 100.0)
if v > 0.0:
dirt_map[t] = v
# ── internal ──────────────────────────────────────────────────────────────────
## Apply a dirt change from old_val to new_val for `tile`.
## Updates the map and emits the tier-crossing signal when the tier changes.
func _set_dirt(tile: Vector2i, old_val: float, new_val: float) -> void:
var old_tier: int = _tier_for(old_val)
var new_tier: int = _tier_for(new_val)
if new_val <= 0.0:
dirt_map.erase(tile)
else:
dirt_map[tile] = new_val
if old_tier != new_tier:
EventBus.tile_dirtiness_changed.emit(tile, new_val)
Audit.log(
"dirtiness",
"tile %s tier %d%d (dirt=%.1f)" % [tile, old_tier, new_tier, new_val]
)
## Returns the dirtiness tier for a given value.
## 0 = clean, 1 = dirty, 2 = filthy.
func _tier_for(val: float) -> int:
if val >= DIRT_FILTHY_THRESHOLD:
return 2
if val >= DIRT_DIRTY_THRESHOLD:
return 1
return 0