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

@ -12,12 +12,22 @@ class_name CraftingProvider extends WorkProvider
## search is global (no per-bench radius restriction — Phase 17 polish item per
## docs/architecture.md "Ingredient acquisition radius").
##
## When a bill cannot proceed it emits EventBus.bill_blocked once per
## (workbench × reason) per BILL_BLOCKED_COOLDOWN_TICKS (60 in-game seconds).
##
## Workbench and Pawn are intentionally duck-typed (no class_name reference) to match
## WorkProvider convention and avoid init-order issues. Only Item.Quality and
## QualityCalc are referenced by class_name (in job_runner.gd, not here).
##
## See docs/architecture.md "CraftingProvider" and docs/design.md "Bills".
## Rate-limit for bill_blocked alerts: one emit per (workbench × reason) per
## 60 in-game seconds (20 Hz × 60 s = 1200 ticks).
const BILL_BLOCKED_COOLDOWN_TICKS: int = 1200
## Per-(workbench_id|reason) cooldown map: String → tick at which next emit is allowed.
var _bill_blocked_cooldown: Dictionary = {}
func _init() -> void:
category = &"crafting"
@ -53,11 +63,13 @@ func find_best_for(pawn) -> Job:
# Skill threshold check — pawn must meet the bill's minimum.
if pawn.get_skill(b.recipe.required_skill) < b.recipe.skill_threshold:
_emit_bill_blocked(b.recipe.label, &"skill_too_low", wb)
continue
# Confirm a qualifying ingredient exists on the floor.
var src = _find_ingredient_item(b.recipe.ingredient_type)
if src == null:
_emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb)
continue
# Score: total Manhattan travel distance pawn → ingredient → workbench.
@ -103,3 +115,14 @@ func _find_ingredient_item(item_type: StringName):
## Manhattan distance between two Vector2i tile coordinates.
func _manhattan(a: Vector2i, b: Vector2i) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
## Emits EventBus.bill_blocked for the given workbench and reason, rate-limited
## to once per (workbench × reason) per BILL_BLOCKED_COOLDOWN_TICKS.
func _emit_bill_blocked(recipe_label: String, reason: StringName, wb) -> void:
var key: String = "%s|%s" % [wb.get_instance_id(), reason]
if _bill_blocked_cooldown.get(key, 0) > Sim.tick:
return
_bill_blocked_cooldown[key] = Sim.tick + BILL_BLOCKED_COOLDOWN_TICKS
var focus: Vector2i = wb.get("tile") if wb.get("tile") != null else Vector2i(-1, -1)
EventBus.bill_blocked.emit(recipe_label, reason, focus)