class_name Crop extends Node2D ## Crop entity — a farm plant tile that grows through stages and is harvested by a pawn. ## ## Growth model (docs/implementation.md Phase 7): ## SOWN → GROWING_1 → GROWING_2 → GROWING_3 → READY, each stage taking STAGE_TICKS sim ticks. ## At 20 Hz, 200 ticks ≈ 10 sim seconds ≈ 2 in-game minutes at Fast speed. ## "No growth indoors" rule (docs/design.md) lands in Phase 13 when the Roof flag ## system is fully wired; for now crops always grow. ## ## A PlantProvider creates a Job whose INTERACT toil calls on_harvest_tick() or ## on_sow_tick() once per sim tick via JobRunner. Both are single-tick completions. ## The INTERACT toil finishes when is_harvestable() / is_sowable() returns false. ## ## World registration (World.register_crop / World.unregister_crop) is called here. const TILE_SIZE_PX: int = 16 ## Available crop kinds. Each maps to an ElvGames 64×32 sprite sheet ## (4 stages × 16w × 32h, plant anchored to bottom row). const KIND_WHEAT: StringName = &"wheat" const KIND_POTATO: StringName = &"potato" const KIND_CORN: StringName = &"corn" const KIND_STRAWBERRY: StringName = &"strawberry" ## Sim ticks per growth stage. 200 ticks × 4 stages = 800 total. ## At 20 Hz × 5× speed = 100 ticks/sec → 8 real seconds per stage, 32 seconds full grow. const STAGE_TICKS: int = 200 const STAGE_COUNT: int = 4 enum Stage { TILLED, SOWN, GROWING_1, GROWING_2, GROWING_3, READY } ## Per-kind sprite atlas. Strawberry / Corn are 80×32 (5 stages) — we still slice ## the first 4 cols, which gives the same progression (the 5th col is a ## post-harvest regrow frame we don't use). const _CROP_TEXTURES: Dictionary = { KIND_WHEAT: preload("res://art/sprites/crops/FG_Crops_Wheat.png"), KIND_POTATO: preload("res://art/sprites/crops/FG_Crops_Potato.png"), KIND_CORN: preload("res://art/sprites/crops/FG_Crops_Corns.png"), KIND_STRAWBERRY: preload("res://art/sprites/crops/FG_Crops_Strawberry.png"), } ## Width / height of one stage cell in pixels. const _STAGE_W: int = 16 const _STAGE_H: int = 32 @export var crop_kind: StringName = KIND_WHEAT @export var tile: Vector2i = Vector2i.ZERO var stage: Stage = Stage.SOWN ## Progress within the current growth stage; 0..STAGE_TICKS. var stage_progress: int = 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. var _logged_indoor: bool = false # Child Sprite2D — created in _ready, region_rect updated whenever stage flips. var _sprite: Sprite2D = null const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") # ── lifecycle ───────────────────────────────────────────────────────────────── func _ready() -> void: position = _tile_to_world(tile) _build_sprite() World.register_crop(self) EventBus.sim_tick.connect(_on_sim_tick) queue_redraw() func _exit_tree() -> void: World.unregister_crop(self) # ── public API ──────────────────────────────────────────────────────────────── ## One-shot initialiser. Call after add_child() so _ready() has already fired. func setup(p_tile: Vector2i, p_kind: StringName, p_stage: Stage = Stage.SOWN) -> void: tile = p_tile crop_kind = p_kind stage = p_stage stage_progress = 0 position = _tile_to_world(tile) if _sprite != null: _sprite.texture = _texture_for(crop_kind) _refresh_sprite_region() queue_redraw() Audit.log("crop", "spawned %s at %s (stage=%s)" % [crop_kind, tile, Stage.keys()[stage]]) ## True when this crop can be harvested by a pawn. func is_harvestable() -> bool: return stage == Stage.READY ## True when this crop can be sown by a pawn (bare tilled soil, no plant yet). func is_sowable() -> bool: return stage == Stage.TILLED ## Called by the INTERACT toil in JobRunner once per sim tick while a pawn harvests. ## Single-tick harvest: drops an output Item and resets to TILLED (re-sowable). func on_harvest_tick() -> void: if not is_harvestable(): return var item_type := _harvest_output_for(crop_kind) var it: Item = ITEM_SCENE.instantiate() get_parent().add_child(it) it.setup(item_type, 1, 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. it.subtype = crop_kind it.queue_redraw() stage = Stage.TILLED stage_progress = 0 _refresh_sprite_region() Audit.log("crop", "harvested %s at %s → %s" % [crop_kind, tile, item_type]) queue_redraw() ## Called by the INTERACT toil in JobRunner once per sim tick while a pawn sows. ## Single-tick sow: transitions TILLED → SOWN so growth begins on the next sim tick. func on_sow_tick() -> void: if not is_sowable(): return stage = Stage.SOWN stage_progress = 0 _refresh_sprite_region() Audit.log("crop", "sown %s at %s" % [crop_kind, tile]) queue_redraw() # ── growth ──────────────────────────────────────────────────────────────────── func _on_sim_tick(_n: int) -> void: if stage == Stage.READY or stage == Stage.TILLED: return # Phase 13 — crops don't grow indoors (no sunlight under a roof). # World.is_indoor() returns false while RoomDetector has not yet fired, so # outdoor crops planted during boot are unaffected. if World.is_indoor(tile): if not _logged_indoor: Audit.log("crop", "%s at %s won't grow (indoor)" % [crop_kind, tile]) _logged_indoor = true return # 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 if stage_progress >= STAGE_TICKS: stage_progress = 0 stage = (int(stage) + 1) as Stage _refresh_sprite_region() queue_redraw() if stage == Stage.READY: Audit.log("crop", "%s ready at %s" % [crop_kind, tile]) # ── save / load ─────────────────────────────────────────────────────────────── func to_dict() -> Dictionary: return { "class_id": &"crop", "tile_x": tile.x, "tile_y": tile.y, "crop_kind": String(crop_kind), "stage": int(stage), "stage_progress": stage_progress, } ## Returns a plain Dictionary spec for World to instantiate from. ## Crops cannot reconstruct themselves standalone — they need a parent in the ## scene tree. World adds the node, then calls setup() from the returned dict. static func from_dict(d: Dictionary) -> Dictionary: return { "tile_x": int(d.get("tile_x", 0)), "tile_y": int(d.get("tile_y", 0)), "crop_kind": StringName(d.get("crop_kind", "wheat")), "stage": int(d.get("stage", Stage.SOWN)), "stage_progress": int(d.get("stage_progress", 0)), } # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: # Tilled-soil base draws under the plant sprite. Stays visible at every stage # so the player sees the patch as cultivated. var soil_color := Color(0.32, 0.20, 0.10) var soil_dark := Color(0.22, 0.14, 0.06) draw_rect(Rect2(Vector2(-7.0, -7.0), Vector2(14.0, 14.0)), soil_color) draw_rect(Rect2(Vector2(-7.0, -7.0), Vector2(14.0, 14.0)), soil_dark, false, 1.0) # ── sprite helpers ──────────────────────────────────────────────────────────── func _build_sprite() -> void: _sprite = Sprite2D.new() _sprite.name = "Sprite" _sprite.texture = _texture_for(crop_kind) _sprite.region_enabled = true _sprite.centered = true # 32-tall sprite anchored so its bottom edge sits at the tile's bottom row # (local +8 from tile centre). Sprite half-height = 16 → offset.y = 8 - 16 = -8. _sprite.offset = Vector2(0, -8) # Draw the plant above the soil rect but below pawns/items. _sprite.z_index = 0 add_child(_sprite) _refresh_sprite_region() func _refresh_sprite_region() -> void: if _sprite == null: return var idx := _sprite_stage_index(stage) if idx < 0: _sprite.visible = false return _sprite.visible = true _sprite.region_rect = Rect2(idx * _STAGE_W, 0, _STAGE_W, _STAGE_H) ## Map game-stage to one of the 4 sprite columns. TILLED has no plant frame. ## SOWN..GROWING_2 step through cols 0..2; GROWING_3 and READY both land on ## col 3 (mature). The harvest designation overlay is what cues the player ## that READY is ready — sprite alone doesn't need a fifth frame. func _sprite_stage_index(s: Stage) -> int: match s: Stage.TILLED: return -1 Stage.SOWN: return 0 Stage.GROWING_1: return 1 Stage.GROWING_2: return 2 Stage.GROWING_3: return 3 Stage.READY: return 3 _: return 0 # ── helpers ─────────────────────────────────────────────────────────────────── func _texture_for(kind: StringName) -> Texture2D: return _CROP_TEXTURES.get(kind, _CROP_TEXTURES[KIND_WHEAT]) func _harvest_output_for(kind: StringName) -> StringName: match kind: KIND_WHEAT: return Item.TYPE_GRAIN KIND_POTATO: return Item.TYPE_VEGETABLE KIND_CORN: return Item.TYPE_GRAIN KIND_STRAWBERRY: return Item.TYPE_VEGETABLE _: return Item.TYPE_VEGETABLE func _tile_to_world(t: Vector2i) -> Vector2: return Vector2( t.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, t.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0 )