Alerts: wire room_too_large, no_stockpile_accepts, bill_blocked

Three alert signals had no UI subscribers — gameplay failures vanished
silently. Now all three feed AlertsLog via translator handlers that
forward to the generic alert_added sink.

- EventBus: new no_stockpile_accepts(item_type, tile) and
  bill_blocked(recipe_label, reason, focus_tile) signals.
- HaulingProvider: per-item-type 30s cooldown; emits when find_best_for
  scan finishes with viable items but no destinations.
- CraftingProvider: per-(workbench, reason) 60s cooldown; emits at the
  skill_too_low and missing_ingredient continue sites. no_workbench
  reason declared for future use but not emitted (the iteration shape
  has no natural site for it).
- AlertsLog: connect + disconnect for all three signals using the same
  has_signal-guarded pattern; translator handlers convert to localized
  alert_added(severity, text, focus_tile).
- AlertsLog catch-up: room_too_large emits during World init, before
  this CanvasLayer mounts. _catch_up_room_too_large() in _ready scans
  World.rooms for rooms > ROOM_AUTOROOF_CAP and replays them, so the
  pre-built cabin's 24-tile-too-large warning lands in the log on every
  boot. Hauling/bill signals fire at runtime so they need no catch-up.

Verified runtime: cabin warning shows up in AlertsLog with severity
'warn' and focus_tile (45, 24) — the cabin top-left.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-12 13:16:25 +01:00
parent 335ccf52b2
commit 708080a022
4 changed files with 115 additions and 0 deletions

View file

@ -11,11 +11,22 @@ class_name HaulingProvider extends WorkProvider
## higher-priority destination has space — enabling the "items flow upward"
## priority cascade described in design.md.
##
## When an item needs hauling but no stockpile accepts it, the provider emits
## EventBus.no_stockpile_accepts once per item_type per ALERT_COOLDOWN_TICKS
## ticks (30 in-game seconds at 20 Hz).
##
## Pawn is intentionally duck-typed (no class_name reference) to match the
## WorkProvider convention and avoid init-order issues.
##
## See docs/architecture.md "HaulingProvider".
## Rate-limit for no_stockpile_accepts alerts: one emit per item_type per
## 30 in-game seconds (20 Hz × 30 s = 600 ticks).
const ALERT_COOLDOWN_TICKS: int = 600
## Per-item-type cooldown map: StringName → tick at which the next emit is allowed.
var _alert_cooldown: Dictionary = {}
func _init() -> void:
category = &"haul"
@ -41,6 +52,11 @@ func find_best_for(pawn) -> Job:
var best_dist: int = 999999
var best_is_corpse: bool = false
# Tracks the first item_type that needed hauling but had no valid destination.
# Used to emit no_stockpile_accepts once per cooldown window.
var first_orphan_type: StringName = &""
var first_orphan_tile: Vector2i = Vector2i(-1, -1)
# ── regular items ─────────────────────────────────────────────────────────
for item in World.items_needing_haul.keys():
# Skip items another pawn is already carrying.
@ -50,6 +66,10 @@ func find_best_for(pawn) -> Job:
# Find the best destination for this item type + priority.
var dest = _find_best_destination_for(item)
if dest == null:
# Item needs hauling but no stockpile accepts it — record first occurrence.
if first_orphan_type == &"":
first_orphan_type = item.item_type
first_orphan_tile = item.tile
continue
var drop: Vector2i = dest.find_drop_position(item)
@ -72,6 +92,12 @@ func find_best_for(pawn) -> Job:
best_drop_cell = drop
best_is_corpse = false
# Emit alert for the first orphaned item_type, rate-limited per type.
if first_orphan_type != &"":
if _alert_cooldown.get(first_orphan_type, 0) <= Sim.tick:
EventBus.no_stockpile_accepts.emit(first_orphan_type, first_orphan_tile)
_alert_cooldown[first_orphan_type] = Sim.tick + ALERT_COOLDOWN_TICKS
# ── Phase 14: corpses ─────────────────────────────────────────────────────
# Corpses route to GraveSlot StorageDestinations exactly like items, but
# use PICKUP_CORPSE / DEPOSIT_CORPSE toils (since Corpse is not an Item).