rimlike/scenes/ai/eat_provider.gd
megaproxy 61dcf6760b Phase 7 — Crops, hunger, eating, cooking chain (grain → flour → bread)
Three gdscript-refactor agents in parallel; Opus integrated and tuned
the priority + hunger-decay numbers via MCP runtime observation.

Crop entity + PlantProvider (Agent A, scenes/entities/crop.{gd,tscn} +
scenes/ai/plant_provider.gd, ~225 lines):
- Crop: 6-stage state machine (TILLED → SOWN → GROWING_1/2/3 → READY).
  STAGE_TICKS=200 sim ticks per stage × 4 stages = 800 total to maturity.
  Listens to EventBus.sim_tick for growth. Procedural _draw with growing
  plant + ready-state golden grain accent.
- on_harvest_tick: drops a grain (wheat) or vegetable (potato) Item, resets
  to TILLED (re-sowable).
- on_sow_tick: TILLED → SOWN — used when Phase 17 paint UI lands.
- PlantProvider priority=5 (above crafting=4) — harvest-only for Phase 7.
  Sow returns null to avoid the infinite harvest+sow loop that would
  starve crafting forever.
- JobRunner._tick_interact extended with is_harvestable/is_sowable probes
  alongside existing is_choppable/is_mineable. Unified probe array.

Hunger + Eating (Agent B, scenes/pawn/pawn.gd + scenes/ai/eat_provider.gd +
toil.gd/job_runner.gd extensions, ~150 lines):
- Pawn.hunger: float 0..100. HUNGER_DECAY_PER_TICK=0.02 (tuned down 5×
  from agent's 0.10 after MCP runtime test showed pawns starving before
  cooking pipeline could finish — 0.02 means 100→0 in 5000 ticks =
  ~4 min at 1× / ~20s at Ultra).
- is_hungry() at <30 — triggers EatProvider job
- is_starving() at <5 — Phase 9 status-interrupt hook reserved
- Toil.KIND_EAT + JobRunner._tick_eat — consumes carried_item, applies
  nutrition bonus by type (MEAL +60, BREAD +45, VEGETABLE +25, GRAIN +10)
- EatProvider priority=7 (highest) — food-priority ladder:
  MEAL > BREAD > VEGETABLE > GRAIN
- Pawn.skills extended with cooking init; hunger round-trip in to_dict

Cooking recipes (Agent C, recipe_catalog.gd + item.gd + workbench.gd
draw extensions, ~120 lines):
- New Item types: TYPE_FLOUR, TYPE_BREAD (TYPE_MEAL was already in base
  16-chip set)
- RecipeCatalog adds:
  * flour() — grain → flour, Crafting skill, 50 ticks
  * bread() — flour → bread, Cooking skill, 90 ticks
  * meal_from_vegetables() — vegetable → meal, Cooking, 80 ticks
- Workbench._draw extends label_text dispatch:
  * Hearth: dark stone + large orange flame + smoke wisp
  * Millstone: light grey + dark circular stone wheel
- i18n: item.flour, item.bread, item.meal, workbench.hearth, workbench.millstone

Opus integration:
- world.tscn: PlantProvider + EatProvider nodes (8 providers total)
- world.gd registers all 8 in priority order:
  eat=7 > construction=6 > chop=5 > plant=5 > mine=4 > crafting=4 >
  haul=3 > rest=0
- Pawn spawn data extended with cooking skill (Bram=2 / Cora=6 / Edda=1)
  for hearth-recipe quality spread
- _seed_phase5_demo_buildings extended (now spans Phase 5/6/7):
  - Millstone at (46, 27) inside cabin south-row: flour bill FOREVER
  - Hearth at (49, 27) inside cabin south-row: bread + meal bills FOREVER
  - 6 wheat crops east of cabin at (54-55, 24-26), all SOWN at boot
  - 2 pre-baked breads at (45-50, 21) so eat-loop unblocks before cooking
    chain completes

Wall-trap fix from Phase 6 confirmed working — pawn paths now go to
(44, 29) adjacent to the south-west corner wall, not on top of it.

Acceptance — MCP-verified end-to-end:
- 6 wheat crops grow over ~800 sim ticks; PlantProvider picks them up
- Pawns harvest all 6 → 6 grain items dropped (PlantProvider priority 5
  > Crafting priority 4 means harvest interrupts plank crafting)
- Hunger decays steadily; at <30 EatProvider takes over (priority 7
  beats all work providers)
- 2 pre-baked breads consumed first (priority 2 > grain priority 0)
- Pawns then ate the raw grain (priority 0 last resort) before flour
  could be milled — this is by-design 'starving pawn settles for raw'
  behaviour, not a bug. Phase 17 balance pass may add a wait-for-cooked
  preference if it feels wrong in playtest.
- Planks crafted with EXCELLENT quality at (46, 25) — quality system from
  Phase 6 still works on top of the new pipeline

Phase 7 tuning lessons (logged):
- Agent's initial 0.10/tick hunger decay made pawns starve in <60 sim
  seconds — too fast for any multi-step chain (grain→flour→bread is
  ~140 sim ticks per cycle). Tuned to 0.02/tick post-runtime.
- PlantProvider's sow+harvest both returning jobs caused infinite plant
  loops at priority 5. Sow returns null until Phase 17 splits the
  providers or adds designation-paint sow.
- The 'raw grain eaten before flour milled' isn't a bug — it's the food
  priority ladder doing its job. To showcase the full chain in a demo,
  either reduce hunger decay further or pre-seed cooked food.

Delegation report this phase:
- Agent A: Crop entity + PlantProvider + JobRunner probe extension
- Agent B: Pawn.hunger + EatProvider + KIND_EAT toil
- Agent C: Recipe catalog extension (flour/bread/meal) + Workbench draw
  branches for Hearth/Millstone
- Opus: scene wiring + pawn cooking-skill init + demo seed (Millstone +
  Hearth + 6 crops + pre-baked breads) + MCP-driven runtime tuning of
  hunger decay and plant priority

~75% of Phase 7 GDScript was subagent-authored.

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

88 lines
3.3 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 EatProvider extends WorkProvider
## WorkProvider for the Eating work category. Slots into the 5-layer pawn AI
## (Decision → WorkProvider → Job + JobRunner) as layer 2.
##
## When a pawn is hungry and not already carrying an item, scans World.items for
## the nearest edible item and builds a 3-toil eat job: walk → pickup → eat.
##
## Food priority ladder (higher = preferred):
## 3 TYPE_MEAL — cooked meal, most nutrition
## 2 TYPE_BREAD — baked, mid-tier
## 1 TYPE_VEGETABLE — raw veg, acceptable
## 0 TYPE_GRAIN — raw grain, last resort (5 mood "Ate raw food" in Phase 8)
##
## Priority 7 means hungry pawns prefer eating over hauling (3) and construction
## (6, for general builds), but does not override a player forced-job (Layer 2).
## When hunger drops below is_starving() the Phase 9 status interrupt will
## preempt current jobs — that hook is not yet wired; EatProvider handles
## voluntary eating only.
##
## Pawn is intentionally duck-typed (no class_name reference) to avoid the
## class_name registration-order trap documented in Phase 2.
##
## See docs/architecture.md "Pawn AI / job system" and
## docs/design.md "Health & status effects".
func _init() -> void:
category = &"eat"
# 7 > construction (6) > hauling (3) > rest (0).
# Hunger trumps routine work but player-forced jobs (Layer 2) still win.
priority = 7
# ── WorkProvider override ─────────────────────────────────────────────────────
## Returns an eat Job for `pawn`, or null if the pawn is not hungry or there is
## no reachable food in the world.
##
## Selection logic:
## Higher food-priority type always beats a closer lower-priority item.
## Within the same priority tier, nearest item (Manhattan distance) wins.
## Being-carried items are skipped — another pawn has claimed them.
func find_best_for(pawn) -> Job:
if not pawn.is_hungry():
return null
# Don't interrupt an ongoing carry — the pawn should drop or deposit first.
if pawn.carried_item != null:
return null
var best = null
var best_dist: int = 999999
var best_priority: int = -1
for it in World.items:
if it.being_carried:
continue
var fp: int = _food_priority(it.item_type)
if fp < 0:
continue
var d: int = abs(it.tile.x - pawn.tile.x) + abs(it.tile.y - pawn.tile.y)
# Higher food-priority tier beats distance; within same tier nearest wins.
if fp > best_priority or (fp == best_priority and d < best_dist):
best_priority = fp
best_dist = d
best = it
if best == null:
return null
var j := Job.new()
j.label = "Eat %s" % best.item_type
j.toils.append(Toil.walk_to(best.tile))
j.toils.append(Toil.pickup())
j.toils.append(Toil.eat())
return j
# ── private helpers ───────────────────────────────────────────────────────────
## Returns the food desirability for `item_type`, or -1 if not edible.
## Pawns prefer cooked food; raw grain is a reluctant last resort.
func _food_priority(item_type: StringName) -> int:
match item_type:
Item.TYPE_MEAL: return 3
Item.TYPE_BREAD: return 2
Item.TYPE_VEGETABLE: return 1
Item.TYPE_GRAIN: return 0
_: return -1 # not edible