diff --git a/art/sprites/crops/FG_Crops_Corns.png b/art/sprites/crops/FG_Crops_Corns.png new file mode 100644 index 0000000..2144b82 Binary files /dev/null and b/art/sprites/crops/FG_Crops_Corns.png differ diff --git a/art/sprites/crops/FG_Crops_Corns.png.import b/art/sprites/crops/FG_Crops_Corns.png.import new file mode 100644 index 0000000..5dbf366 --- /dev/null +++ b/art/sprites/crops/FG_Crops_Corns.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dnojyvpvkbgp" +path="res://.godot/imported/FG_Crops_Corns.png-52c1e938a7347a7a7cc4f2d86fb0cf6a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/crops/FG_Crops_Corns.png" +dest_files=["res://.godot/imported/FG_Crops_Corns.png-52c1e938a7347a7a7cc4f2d86fb0cf6a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/art/sprites/crops/FG_Crops_Potato.png b/art/sprites/crops/FG_Crops_Potato.png new file mode 100644 index 0000000..a8a9bcb Binary files /dev/null and b/art/sprites/crops/FG_Crops_Potato.png differ diff --git a/art/sprites/crops/FG_Crops_Potato.png.import b/art/sprites/crops/FG_Crops_Potato.png.import new file mode 100644 index 0000000..def2935 --- /dev/null +++ b/art/sprites/crops/FG_Crops_Potato.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bog4kf8oxem3c" +path="res://.godot/imported/FG_Crops_Potato.png-88c3e45c5fd7242fdfc215cb59085e31.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/crops/FG_Crops_Potato.png" +dest_files=["res://.godot/imported/FG_Crops_Potato.png-88c3e45c5fd7242fdfc215cb59085e31.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/art/sprites/crops/FG_Crops_Strawberry.png b/art/sprites/crops/FG_Crops_Strawberry.png new file mode 100644 index 0000000..f02de34 Binary files /dev/null and b/art/sprites/crops/FG_Crops_Strawberry.png differ diff --git a/art/sprites/crops/FG_Crops_Strawberry.png.import b/art/sprites/crops/FG_Crops_Strawberry.png.import new file mode 100644 index 0000000..d5a7897 --- /dev/null +++ b/art/sprites/crops/FG_Crops_Strawberry.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c3wjj1t2ye2wr" +path="res://.godot/imported/FG_Crops_Strawberry.png-76d99d39b51e1c404f00c643db378da4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/crops/FG_Crops_Strawberry.png" +dest_files=["res://.godot/imported/FG_Crops_Strawberry.png-76d99d39b51e1c404f00c643db378da4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/art/sprites/crops/FG_Crops_Wheat.png b/art/sprites/crops/FG_Crops_Wheat.png new file mode 100644 index 0000000..9334247 Binary files /dev/null and b/art/sprites/crops/FG_Crops_Wheat.png differ diff --git a/art/sprites/crops/FG_Crops_Wheat.png.import b/art/sprites/crops/FG_Crops_Wheat.png.import new file mode 100644 index 0000000..2810c12 --- /dev/null +++ b/art/sprites/crops/FG_Crops_Wheat.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://hbjgdtt0tkpe" +path="res://.godot/imported/FG_Crops_Wheat.png-e1d42717b3eaa01ef1c1aae3ae9445e7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/crops/FG_Crops_Wheat.png" +dest_files=["res://.godot/imported/FG_Crops_Wheat.png-e1d42717b3eaa01ef1c1aae3ae9445e7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scenes/entities/crop.gd b/scenes/entities/crop.gd index 7e914d3..f47ed74 100644 --- a/scenes/entities/crop.gd +++ b/scenes/entities/crop.gd @@ -15,9 +15,12 @@ class_name Crop extends Node2D const TILE_SIZE_PX: int = 16 -## Phase 7 ships wheat and potato. Phase 17 expands (berry, hop) per design.md. -const KIND_WHEAT: StringName = &"wheat" -const KIND_POTATO: StringName = &"potato" +## 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. @@ -26,6 +29,20 @@ 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 @@ -37,6 +54,9 @@ var stage_progress: int = 0 # 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") @@ -44,6 +64,7 @@ const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") func _ready() -> void: position = _tile_to_world(tile) + _build_sprite() World.register_crop(self) EventBus.sim_tick.connect(_on_sim_tick) queue_redraw() @@ -62,6 +83,9 @@ func setup(p_tile: Vector2i, p_kind: StringName, p_stage: Stage = Stage.SOWN) -> 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]]) @@ -87,6 +111,7 @@ func on_harvest_tick() -> void: it.setup(item_type, 1, tile) stage = Stage.TILLED stage_progress = 0 + _refresh_sprite_region() Audit.log("crop", "harvested %s at %s → %s" % [crop_kind, tile, item_type]) queue_redraw() @@ -98,6 +123,7 @@ func on_sow_tick() -> void: return stage = Stage.SOWN stage_progress = 0 + _refresh_sprite_region() Audit.log("crop", "sown %s at %s" % [crop_kind, tile]) queue_redraw() @@ -122,6 +148,7 @@ func _on_sim_tick(_n: int) -> void: 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]) @@ -156,53 +183,70 @@ static func from_dict(d: Dictionary) -> Dictionary: # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: - # Tilled-soil base: a small dark-earth square. + # 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) - if stage == Stage.TILLED: - return # Bare soil — no plant drawn. - # stage_idx: 0 = SOWN, 4 = READY - var stage_idx := int(stage) - int(Stage.SOWN) - var height: float = lerp(2.0, 12.0, float(stage_idx) / float(STAGE_COUNT)) - var plant_color := _plant_color_for(crop_kind) +# ── sprite helpers ──────────────────────────────────────────────────────────── - # Stem - draw_rect(Rect2(Vector2(-2.0, 5.0 - height), Vector2(4.0, height)), plant_color) +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() - # Foliage circle grows in from GROWING_2 onward - if stage_idx >= 2: - draw_circle(Vector2(0.0, 5.0 - height), 3.0 + float(stage_idx), plant_color) - # Ready accent — grain head or potato cap - if stage == Stage.READY: - draw_circle(Vector2(0.0, 5.0 - height), 2.0, _ready_accent_for(crop_kind)) +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 - _: return Item.TYPE_VEGETABLE # fallback - - -func _plant_color_for(kind: StringName) -> Color: - match kind: - KIND_WHEAT: return Color(0.50, 0.65, 0.20) # bright green sprout - KIND_POTATO: return Color(0.30, 0.55, 0.20) # darker green - _: return Color(0.40, 0.60, 0.20) - - -func _ready_accent_for(kind: StringName) -> Color: - match kind: - KIND_WHEAT: return Color(0.95, 0.85, 0.20) # golden grain head - KIND_POTATO: return Color(0.95, 0.60, 0.30) # orange potato cap - _: return Color(1.0, 0.4, 0.4) + 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: diff --git a/scenes/world/world.gd b/scenes/world/world.gd index 4d0137f..734cc1c 100644 --- a/scenes/world/world.gd +++ b/scenes/world/world.gd @@ -561,15 +561,30 @@ func _seed_phase5_demo_buildings() -> void: meal_bill.mode = Bill.Mode.FOREVER hearth.add_bill(meal_bill) - # Wheat crops east of the cabin, near the trees. - var crop_tiles: Array[Vector2i] = [ - Vector2i(54, 24), Vector2i(54, 25), Vector2i(54, 26), - Vector2i(55, 24), Vector2i(55, 25), Vector2i(55, 26), + # Mixed crops east of the cabin, near the trees. One column per kind so the + # player sees the four atlas variants side-by-side from the boot demo. + var crop_plan: Array = [ + [Vector2i(54, 24), Crop.KIND_WHEAT], + [Vector2i(54, 25), Crop.KIND_WHEAT], + [Vector2i(54, 26), Crop.KIND_WHEAT], + [Vector2i(55, 24), Crop.KIND_POTATO], + [Vector2i(55, 25), Crop.KIND_POTATO], + [Vector2i(55, 26), Crop.KIND_POTATO], + [Vector2i(56, 24), Crop.KIND_CORN], + [Vector2i(56, 25), Crop.KIND_CORN], + [Vector2i(56, 26), Crop.KIND_CORN], + [Vector2i(57, 24), Crop.KIND_STRAWBERRY], + [Vector2i(57, 25), Crop.KIND_STRAWBERRY], + [Vector2i(57, 26), Crop.KIND_STRAWBERRY], ] - for ct in crop_tiles: + var crop_tiles: Array[Vector2i] = [] + for entry in crop_plan: + var ct: Vector2i = entry[0] + var kind: StringName = entry[1] + crop_tiles.append(ct) var c: Crop = CROP_SCENE.instantiate() add_child(c) - c.setup(ct, Crop.KIND_WHEAT, Crop.Stage.SOWN) + c.setup(ct, kind, Crop.Stage.SOWN) # Pre-baked breads + a vegetable meal so pawns can eat before the # full cooking chain finishes. Phase 17 may remove these as cooking @@ -581,7 +596,7 @@ func _seed_phase5_demo_buildings() -> void: bread_item.setup(Item.TYPE_BREAD, 1, st) bread_item.quality = Item.Quality.NORMAL - Audit.log("world", "phase 7 demo: Millstone+Hearth built, %d wheat crops sown, %d pre-baked breads placed" % [ + Audit.log("world", "phase 7 demo: Millstone+Hearth built, %d crops sown (mixed kinds), %d pre-baked breads placed" % [ crop_tiles.size(), snack_tiles.size() ])