wire buff consumers, sick penalty, multi-count cremation
A: Storyteller.multiply_drops() stochastic-rounding helper drives crop_growth, harvest_yield, chop, mine consumption sites. sleep_decay multiplied in Pawn sleep tick. B: Pawn._sick_speed_penalty() (0% healthy → 75% severity 3, clamped to 25% min speed). JobRunner._work_speed_mult coin-flips per-tick progress on INTERACT/BUILD/CRAFT toils. Sleep/eat/treat unaffected. C: CraftingProvider builds N deposit trips for ingredient2_count > 1. JobRunner._tick_craft validates+consumes the full count from buffer. Cremation now actually requires and consumes 5 wood. crop._stage_accum round-trips through save/load to preserve buff- accumulated fractional growth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d9638a4ea4
commit
2afca16299
9 changed files with 156 additions and 46 deletions
|
|
@ -519,6 +519,7 @@ func _spawn_crop(world_scene: Node, d: Dictionary) -> void:
|
|||
int(spec.get("stage", 0))
|
||||
)
|
||||
ent.stage_progress = int(spec.get("stage_progress", 0))
|
||||
ent._stage_accum = float(spec.get("stage_accum", 0.0))
|
||||
|
||||
|
||||
func _spawn_bed(world_scene: Node, d: Dictionary) -> void:
|
||||
|
|
|
|||
|
|
@ -403,6 +403,18 @@ func _on_sim_tick_buffs(_tick: int) -> void:
|
|||
_prune_expired_buffs()
|
||||
|
||||
|
||||
## Stochastic rounding for fractional drop counts.
|
||||
## Converts a float drop amount to an int: floor(amt) drops are guaranteed;
|
||||
## the fractional remainder is a probability of one extra drop. This way a
|
||||
## multiplier of 1.25 on a base of 4 yields 5 drops 25% of the time and 4
|
||||
## drops 75% of the time — expected value exactly matches the multiplier.
|
||||
static func multiply_drops(base: int, mult: float) -> int:
|
||||
var raw: float = float(base) * mult
|
||||
var guaranteed: int = int(floor(raw))
|
||||
var frac: float = raw - float(guaranteed)
|
||||
return guaranteed + (1 if randf() < frac else 0)
|
||||
|
||||
|
||||
## Convert a Category enum value to the snake-case StringName used in
|
||||
## CATEGORY_COOLDOWN_DAYS and _category_last_fired.
|
||||
func _category_to_str(cat: EventDef.Category) -> StringName:
|
||||
|
|
|
|||
|
|
@ -44,10 +44,14 @@ func _init() -> void:
|
|||
## Single-ingredient recipes produce a 4-toil job:
|
||||
## walk_to(ing1) → pickup → walk_to(wb) → craft_at(wb, bill_index)
|
||||
##
|
||||
## Two-ingredient recipes (ingredient2_type != &"") produce a 7-toil job:
|
||||
## walk_to(ing1) → pickup → walk_to(wb) → deposit_at_wb(wb)
|
||||
## → walk_to(ing2) → pickup → craft_at(wb, bill_index)
|
||||
## The first ingredient is stashed in wb.deposited_inputs; _tick_craft consumes both.
|
||||
## Two-ingredient recipes (ingredient2_type != &"") build the toil sequence:
|
||||
## For each item needed to satisfy ingredient2_count (one trip per item):
|
||||
## walk_to(ing2_item) → pickup → walk_to(wb) → deposit_at_wb(wb)
|
||||
## Then: walk_to(ing1) → pickup → craft_at(wb, bill_index)
|
||||
## Stack-aware: if a single wood item has stack_size >= ingredient2_count, only
|
||||
## one deposit trip is needed. Multiple smaller stacks each get their own trip.
|
||||
## The primary ingredient (ingredient_type / corpse) is always the last pickup
|
||||
## before crafting; ingredient2 is deposited in the buffer beforehand.
|
||||
##
|
||||
## No-ingredient recipes (ingredient_type == &"") produce a 2-toil job:
|
||||
## walk_to(wb) → craft_at(wb, bill_index)
|
||||
|
|
@ -92,12 +96,15 @@ func find_best_for(pawn) -> Job:
|
|||
_emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb)
|
||||
continue
|
||||
|
||||
# Two-ingredient check: secondary ingredient must also be on the floor.
|
||||
# Two-ingredient check: enough secondary ingredient must be on the floor.
|
||||
# ingredient2_count items (or fewer stacks totalling that count) required.
|
||||
if b.recipe.ingredient2_type != &"":
|
||||
src2 = _find_ingredient_item(b.recipe.ingredient2_type)
|
||||
if src2 == null:
|
||||
var ing2_items := _find_ingredient_items_for_count(b.recipe.ingredient2_type, b.recipe.ingredient2_count)
|
||||
if ing2_items.is_empty():
|
||||
_emit_bill_blocked(b.recipe.label, &"missing_ingredient2", wb)
|
||||
continue
|
||||
# Use the first item as src2 for the scoring distance heuristic.
|
||||
src2 = ing2_items[0]
|
||||
|
||||
# Score: total Manhattan travel distance including both ingredient trips.
|
||||
# No-ingredient: pawn → wb.
|
||||
|
|
@ -125,23 +132,29 @@ func find_best_for(pawn) -> Job:
|
|||
best_src1 = _find_ingredient_item(best_bill.recipe.ingredient_type)
|
||||
if best_src1 == null:
|
||||
return null
|
||||
var ing2_items: Array = []
|
||||
if best_bill.recipe.ingredient2_type != &"":
|
||||
best_src2 = _find_ingredient_item(best_bill.recipe.ingredient2_type)
|
||||
if best_src2 == null:
|
||||
ing2_items = _find_ingredient_items_for_count(best_bill.recipe.ingredient2_type, best_bill.recipe.ingredient2_count)
|
||||
if ing2_items.is_empty():
|
||||
return null
|
||||
|
||||
var j := Job.new()
|
||||
j.label = "Craft %s at %s" % [best_bill.recipe.label, best_wb.get("label_text") if best_wb.get("label_text") != null else "workbench"]
|
||||
j.target_node = best_wb
|
||||
|
||||
if best_src1 != null and best_src2 != null:
|
||||
# Two-ingredient path: deposit ing1 at wb, then fetch ing2 and craft.
|
||||
j.toils.append(Toil.walk_to(best_src1.tile))
|
||||
j.toils.append(Toil.pickup())
|
||||
j.toils.append(Toil.walk_to(best_wb.tile))
|
||||
j.toils.append(Toil.deposit_at_wb(best_wb.get_path()))
|
||||
j.toils.append(Toil.walk_to(best_src2.tile))
|
||||
j.toils.append(Toil.pickup())
|
||||
if not ing2_items.is_empty():
|
||||
# Two-ingredient path: deposit ingredient2 item(s) at workbench buffer,
|
||||
# then fetch the primary ingredient and craft.
|
||||
# One deposit trip per item in ing2_items (stack-aware: fewer trips when
|
||||
# item.stack_size covers multiple units, e.g. a 5-stack of wood = 1 trip).
|
||||
for ing2 in ing2_items:
|
||||
j.toils.append(Toil.walk_to(ing2.tile))
|
||||
j.toils.append(Toil.pickup())
|
||||
j.toils.append(Toil.walk_to(best_wb.tile))
|
||||
j.toils.append(Toil.deposit_at_wb(best_wb.get_path()))
|
||||
if best_src1 != null:
|
||||
j.toils.append(Toil.walk_to(best_src1.tile))
|
||||
j.toils.append(Toil.pickup())
|
||||
elif best_src1 != null:
|
||||
# Single-ingredient path: carry ing1 directly to craft.
|
||||
j.toils.append(Toil.walk_to(best_src1.tile))
|
||||
|
|
@ -166,6 +179,27 @@ func _find_ingredient_item(item_type: StringName):
|
|||
return null
|
||||
|
||||
|
||||
## Returns a list of on-floor Items of `item_type` whose combined stack_size
|
||||
## totals at least `needed_count`. Returns an empty array if not enough exist.
|
||||
## Stack-aware: a single item with stack_size >= needed_count yields one entry.
|
||||
## Multiple smaller stacks are collected until the total is met.
|
||||
## This powers the multi-trip deposit loop for ingredient2_count > 1 recipes.
|
||||
func _find_ingredient_items_for_count(item_type: StringName, needed_count: int) -> Array:
|
||||
var result: Array = []
|
||||
var accumulated: int = 0
|
||||
for it in World.items:
|
||||
if it.being_carried:
|
||||
continue
|
||||
if it.item_type != item_type:
|
||||
continue
|
||||
result.append(it)
|
||||
accumulated += int(it.get("stack_size") if it.get("stack_size") != null else 1)
|
||||
if accumulated >= needed_count:
|
||||
return result
|
||||
# Not enough items found.
|
||||
return []
|
||||
|
||||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -237,7 +237,10 @@ func _tick_interact(t) -> void:
|
|||
t.done = true
|
||||
return
|
||||
|
||||
target.call(StringName(t.data.get("tick_method", "")))
|
||||
# Sick pawn coin-flip: skip this tick's work at a rate equal to the penalty.
|
||||
# Expected-value progress is unchanged when healthy (mult == 1.0).
|
||||
if randf() < _work_speed_mult():
|
||||
target.call(StringName(t.data.get("tick_method", "")))
|
||||
|
||||
# Re-check validity after the call (the call may have freed the node).
|
||||
if target == null or not is_instance_valid(target):
|
||||
|
|
@ -292,7 +295,9 @@ func _tick_build(t) -> void:
|
|||
t.done = true
|
||||
return
|
||||
|
||||
target.on_build_tick()
|
||||
# Sick pawn coin-flip: same slow-work mechanic as _tick_interact.
|
||||
if randf() < _work_speed_mult():
|
||||
target.on_build_tick()
|
||||
|
||||
# Re-check after the call (on_build_tick may complete + free the ghost state).
|
||||
if target == null or not is_instance_valid(target):
|
||||
|
|
@ -415,7 +420,7 @@ func _tick_deposit_at_wb(t) -> void:
|
|||
## true when bill.recipe.work_ticks is reached.
|
||||
## - On completion:
|
||||
## * Consumes pawn.carried_item (ingredient or ingredient2); queue_free + clear.
|
||||
## * For two-ingredient recipes, also calls wb.consume_deposited_input for ing1.
|
||||
## * For two-ingredient recipes, also calls wb.consume_deposited_input for ing2.
|
||||
## * Rolls quality via QualityCalc.roll() using the pawn's skill level.
|
||||
## * Spawns an Item scene child of wb.get_parent() at wb.tile.
|
||||
## * Calls wb.on_craft_complete() (records bill completion + resets state).
|
||||
|
|
@ -459,18 +464,20 @@ func _tick_craft(t) -> void:
|
|||
var has_secondary: bool = bill.recipe.ingredient2_type != &""
|
||||
if has_primary:
|
||||
if has_secondary:
|
||||
# Two-ingredient path: check buffer (ing1) + carry (ing2).
|
||||
if not wb.has_deposited_input(bill.recipe.ingredient_type, 1):
|
||||
# Two-ingredient path: check buffer (ing2) + carry (ing1 / primary).
|
||||
# Validate that the full ingredient2_count has been deposited.
|
||||
var ing2_count: int = int(bill.recipe.ingredient2_count) if int(bill.recipe.ingredient2_count) > 0 else 1
|
||||
if not wb.has_deposited_input(bill.recipe.ingredient2_type, ing2_count):
|
||||
Audit.log(
|
||||
"job_runner",
|
||||
"%s craft: ingredient1 not in workbench buffer — skipping" % pawn.pawn_name
|
||||
"%s craft: ingredient2 buffer has %d/%d — skipping" % [pawn.pawn_name, wb.deposited_inputs.get(bill.recipe.ingredient2_type, 0), ing2_count]
|
||||
)
|
||||
t.done = true
|
||||
return
|
||||
if pawn.carried_item == null or pawn.carried_item.item_type != bill.recipe.ingredient2_type:
|
||||
if pawn.carried_item == null or pawn.carried_item.item_type != bill.recipe.ingredient_type:
|
||||
Audit.log(
|
||||
"job_runner",
|
||||
"%s craft: wrong or missing ingredient2 — skipping" % pawn.pawn_name
|
||||
"%s craft: wrong or missing primary ingredient (expect %s) — skipping" % [pawn.pawn_name, bill.recipe.ingredient_type]
|
||||
)
|
||||
t.done = true
|
||||
return
|
||||
|
|
@ -497,7 +504,10 @@ func _tick_craft(t) -> void:
|
|||
return
|
||||
|
||||
# tick_craft() advances the progress counter and returns true when done.
|
||||
var craft_done: bool = wb.tick_craft()
|
||||
# Sick pawn coin-flip: skip this tick's progress at a rate equal to the penalty.
|
||||
var craft_done: bool = false
|
||||
if randf() < _work_speed_mult():
|
||||
craft_done = wb.tick_craft()
|
||||
if not craft_done:
|
||||
return # Still working — toil remains active.
|
||||
|
||||
|
|
@ -522,8 +532,11 @@ func _tick_craft(t) -> void:
|
|||
if ingredient != null and is_instance_valid(ingredient):
|
||||
ingredient.queue_free()
|
||||
if bill.recipe.ingredient2_type != &"":
|
||||
# ingredient2 was the carried item (consumed above); ingredient1 is in the buffer.
|
||||
wb.consume_deposited_input(bill.recipe.ingredient_type, 1)
|
||||
# New two-ingredient convention: ingredient2 was deposited in the buffer
|
||||
# (potentially multiple stacks for ingredient2_count > 1), ingredient1
|
||||
# (primary) was the carried item (consumed above via queue_free).
|
||||
var ing2_count: int = int(bill.recipe.ingredient2_count) if int(bill.recipe.ingredient2_count) > 0 else 1
|
||||
wb.consume_deposited_input(bill.recipe.ingredient2_type, ing2_count)
|
||||
|
||||
# Roll quality based on pawn skill for this recipe.
|
||||
var skill_level: int = pawn.get_skill(bill.recipe.required_skill)
|
||||
|
|
@ -974,6 +987,19 @@ func _resolve_patient(t):
|
|||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
## Returns the effective work-speed multiplier for the current pawn.
|
||||
## Currently only the SICK status reduces it. 1.0 = full speed; < 1.0 = slow.
|
||||
## Used as a coin-flip gate: skip the per-tick work call at a rate proportional
|
||||
## to the penalty so expected-value progress matches the multiplier exactly
|
||||
## (no progress accumulator needed — the tick stream is already stochastic).
|
||||
func _work_speed_mult() -> float:
|
||||
if pawn == null:
|
||||
return 1.0
|
||||
if not pawn.has_method("_sick_speed_penalty"):
|
||||
return 1.0
|
||||
return clampf(1.0 - pawn._sick_speed_penalty(), 0.25, 1.0)
|
||||
|
||||
|
||||
## Emit job_completed, log, and clear the job reference.
|
||||
func _emit_complete() -> void:
|
||||
var completed = job
|
||||
|
|
|
|||
|
|
@ -142,11 +142,17 @@ func on_mine_tick() -> void:
|
|||
## Drop four stone Items (one per footprint tile) and free this node. Called
|
||||
## by on_mine_tick() automatically; can also be called for scripted removal.
|
||||
func mined() -> void:
|
||||
for ft in footprint_tiles():
|
||||
# Apply mine buff (veins_of_iron): multiply total stone drops with stochastic rounding,
|
||||
# then distribute one item per footprint tile (first N tiles get 1 each; extras stack
|
||||
# on the origin tile for simple overflow handling).
|
||||
var total_drops: int = Storyteller.multiply_drops(STONE_DROPS_ON_MINE, Storyteller.get_buff_multiplier(&"mine"))
|
||||
var tiles := footprint_tiles()
|
||||
for i in total_drops:
|
||||
var drop_tile: Vector2i = tiles[mini(i, tiles.size() - 1)]
|
||||
var item: Item = ITEM_SCENE.instantiate()
|
||||
get_parent().add_child(item)
|
||||
item.setup(Item.TYPE_STONE, 1, ft)
|
||||
Audit.log("big_rock", "mined 2×2 at %s; %d stone drops" % [origin_tile, STONE_DROPS_ON_MINE])
|
||||
item.setup(Item.TYPE_STONE, 1, drop_tile)
|
||||
Audit.log("big_rock", "mined 2×2 at %s; %d stone drops (buff mult=%.2f)" % [origin_tile, total_drops, Storyteller.get_buff_multiplier(&"mine")])
|
||||
if Audio != null:
|
||||
Audio.play_sfx(&"mine_tick")
|
||||
# BigRocks span 2×2 — clear the designation stamp from every footprint tile.
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ const _STAGE_H: int = 32
|
|||
var stage: Stage = Stage.SOWN
|
||||
## Progress within the current growth stage; 0..STAGE_TICKS.
|
||||
var stage_progress: int = 0
|
||||
## Float accumulator for sub-tick growth when crop_growth buff is active.
|
||||
## Carries fractional progress between ticks so the buff has the correct
|
||||
## expected-value effect even though stage_progress is stored as int.
|
||||
var _stage_accum: float = 0.0
|
||||
|
||||
# Phase 13 — "no growth indoors" rule. True once we've logged the first
|
||||
# indoor detection for this crop instance so we don't flood the audit log.
|
||||
|
|
@ -106,9 +110,11 @@ func on_harvest_tick() -> void:
|
|||
if not is_harvestable():
|
||||
return
|
||||
var item_type := _harvest_output_for(crop_kind)
|
||||
var base_qty: int = 1
|
||||
var drop_qty: int = Storyteller.multiply_drops(base_qty, Storyteller.get_buff_multiplier(&"harvest_yield"))
|
||||
var it: Item = ITEM_SCENE.instantiate()
|
||||
get_parent().add_child(it)
|
||||
it.setup(item_type, 1, tile)
|
||||
it.setup(item_type, drop_qty, tile)
|
||||
# Carry the crop_kind through as a visual subtype so wheat / corn /
|
||||
# potato / strawberry each render distinctly, while item_type stays
|
||||
# generic (TYPE_GRAIN / TYPE_VEGETABLE) for stockpile filter purposes.
|
||||
|
|
@ -149,7 +155,13 @@ func _on_sim_tick(_n: int) -> void:
|
|||
# Crop has moved outdoors or was never indoors — reset the log flag so a
|
||||
# future re-roofing produces another audit line.
|
||||
_logged_indoor = false
|
||||
stage_progress += 1
|
||||
# Apply crop_growth buff: accumulate fractional progress each tick so the
|
||||
# expected-value growth rate exactly matches the multiplier.
|
||||
var growth_mult: float = Storyteller.get_buff_multiplier(&"crop_growth")
|
||||
_stage_accum += growth_mult
|
||||
var whole: int = int(floor(_stage_accum))
|
||||
_stage_accum -= float(whole)
|
||||
stage_progress += whole
|
||||
if stage_progress >= STAGE_TICKS:
|
||||
stage_progress = 0
|
||||
stage = (int(stage) + 1) as Stage
|
||||
|
|
@ -169,6 +181,7 @@ func to_dict() -> Dictionary:
|
|||
"crop_kind": String(crop_kind),
|
||||
"stage": int(stage),
|
||||
"stage_progress": stage_progress,
|
||||
"stage_accum": _stage_accum,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -182,6 +195,7 @@ static func from_dict(d: Dictionary) -> Dictionary:
|
|||
"crop_kind": StringName(d.get("crop_kind", "wheat")),
|
||||
"stage": int(d.get("stage", Stage.SOWN)),
|
||||
"stage_progress": int(d.get("stage_progress", 0)),
|
||||
"stage_accum": float(d.get("stage_accum", 0.0)),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -117,11 +117,12 @@ func on_mine_tick() -> void:
|
|||
## Drop stone Item(s) and free this node. Called automatically by on_mine_tick()
|
||||
## but also accessible for scripted removal (debug, storyteller events).
|
||||
func mined() -> void:
|
||||
# Single drop lands on the rock's own tile.
|
||||
# Apply mine buff (veins_of_iron): multiply stone drops with stochastic rounding.
|
||||
var drop_count: int = Storyteller.multiply_drops(STONE_DROPS_ON_MINE, Storyteller.get_buff_multiplier(&"mine"))
|
||||
var item: Item = ITEM_SCENE.instantiate()
|
||||
get_parent().add_child(item)
|
||||
item.setup(Item.TYPE_STONE, 1, tile)
|
||||
Audit.log("rock", "mined at %s; %d stone drop" % [tile, STONE_DROPS_ON_MINE])
|
||||
item.setup(Item.TYPE_STONE, drop_count, tile)
|
||||
Audit.log("rock", "mined at %s; %d stone drop(s) (buff mult=%.2f)" % [tile, drop_count, Storyteller.get_buff_multiplier(&"mine")])
|
||||
if Audio != null:
|
||||
Audio.play_sfx(&"mine_tick")
|
||||
World.clear_designation_at(tile)
|
||||
|
|
|
|||
|
|
@ -241,14 +241,16 @@ func on_chop_tick() -> void:
|
|||
## Drop wood Items and free this node. Called by on_chop_tick() automatically,
|
||||
## but also accessible for scripted felling (debug, storyteller events).
|
||||
func fell() -> void:
|
||||
var drop_tiles := _pick_drop_tiles()
|
||||
# Apply chop buff (lumberjacks_luck): multiply total wood drops with stochastic rounding.
|
||||
var total_drops: int = Storyteller.multiply_drops(WOOD_DROPS_ON_FELL, Storyteller.get_buff_multiplier(&"chop"))
|
||||
var drop_tiles := _pick_drop_tiles_count(total_drops)
|
||||
var drops_count := 0
|
||||
for drop_tile in drop_tiles:
|
||||
var item: Item = ITEM_SCENE.instantiate()
|
||||
get_parent().add_child(item)
|
||||
item.setup(Item.TYPE_WOOD, STACK_SIZE_PER_DROP, drop_tile)
|
||||
drops_count += 1
|
||||
Audit.log("tree", "felled at %s; %d wood drops" % [tile, drops_count])
|
||||
Audit.log("tree", "felled at %s; %d wood drops (buff mult=%.2f)" % [tile, drops_count, Storyteller.get_buff_multiplier(&"chop")])
|
||||
if Audio != null:
|
||||
Audio.play_sfx(&"tree_fell")
|
||||
World.clear_designation_at(tile)
|
||||
|
|
@ -310,10 +312,12 @@ func _draw() -> void:
|
|||
|
||||
# ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
## Returns up to WOOD_DROPS_ON_FELL tile positions for wood drops.
|
||||
## Prefers the tree's own tile then walkable 4-neighbours; falls back to the
|
||||
## tree tile for any remaining drops when neighbours are scarce.
|
||||
func _pick_drop_tiles() -> Array[Vector2i]:
|
||||
## Returns `count` tile positions for wood drops.
|
||||
## Prefers the tree's own tile then walkable 4-neighbours; fills any remaining
|
||||
## slots with the tree tile if neighbours are scarce.
|
||||
## Previously took an implicit count of WOOD_DROPS_ON_FELL; now accepts an
|
||||
## explicit count so the chop buff can request more drops.
|
||||
func _pick_drop_tiles_count(count: int) -> Array[Vector2i]:
|
||||
var chosen: Array[Vector2i] = []
|
||||
|
||||
# First drop always goes on the tree's tile itself.
|
||||
|
|
@ -322,14 +326,14 @@ func _pick_drop_tiles() -> Array[Vector2i]:
|
|||
# Remaining drops prefer walkable neighbours.
|
||||
var offsets: Array[Vector2i] = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]
|
||||
for offset in offsets:
|
||||
if chosen.size() >= WOOD_DROPS_ON_FELL:
|
||||
if chosen.size() >= count:
|
||||
break
|
||||
var candidate: Vector2i = tile + offset
|
||||
if World.pathfinder != null and World.pathfinder.is_walkable(candidate):
|
||||
chosen.append(candidate)
|
||||
|
||||
# Fill any remaining slots with the tree tile (all 3 land there if boxed in).
|
||||
while chosen.size() < WOOD_DROPS_ON_FELL:
|
||||
# Fill any remaining slots with the tree tile (all land there if boxed in).
|
||||
while chosen.size() < count:
|
||||
chosen.append(tile)
|
||||
|
||||
return chosen
|
||||
|
|
|
|||
|
|
@ -467,6 +467,17 @@ func _check_death() -> void:
|
|||
queue_free()
|
||||
|
||||
|
||||
## Returns the work-speed penalty fraction from the SICK status (0.0 if not sick).
|
||||
## severity 1 → 0.25 penalty (75% speed), severity 2 → 0.50, severity 3 → 0.75.
|
||||
## The resulting multiplier is clamped to at least 0.25 so pawns never stop
|
||||
## working entirely from illness alone (design.md — sick pawns work slowly, not stop).
|
||||
func _sick_speed_penalty() -> float:
|
||||
for s in statuses:
|
||||
if s.kind == Status.Kind.SICK:
|
||||
return clampf(0.25 * float(s.severity), 0.0, 0.75)
|
||||
return 0.0
|
||||
|
||||
|
||||
## Returns the pawn's current level (0–10) for the given skill.
|
||||
## Returns 0 for unknown skills so callers need no nil-guard.
|
||||
func get_skill(skill: StringName) -> int:
|
||||
|
|
@ -1069,7 +1080,8 @@ func _on_sim_tick(_tick_number: int) -> void:
|
|||
hunger = maxf(0.0, hunger - HUNGER_DECAY_PER_TICK)
|
||||
# Phase 8 — decay sleep before orchestration so the AI sees the updated value
|
||||
# this tick and can immediately seek a bed once sleep < 30.
|
||||
sleep = maxf(0.0, sleep - SLEEP_DECAY_PER_TICK)
|
||||
# sleep_decay buff (summer): multiplier > 1.0 → faster depletion.
|
||||
sleep = maxf(0.0, sleep - SLEEP_DECAY_PER_TICK * Storyteller.get_buff_multiplier(&"sleep_decay"))
|
||||
# Phase 8 — process thoughts AFTER hunger/sleep decay so is_hungry() / is_tired()
|
||||
# reflect the freshly-decayed values when _sync_persistent_thought fires.
|
||||
_process_thoughts()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue