Crop sprites — atlas art + four growable kinds

Replaces the procedural stem-and-circle draw with an ElvGames atlas-backed
Sprite2D. Crops now pick a per-kind 64×32 (or 80×32) sheet from
art/sprites/crops/ and slice cols 0..3 across the SOWN..READY stage range
(TILLED keeps the bare dirt rect). The plant sprite is anchored so its
bottom edge sits at the tile bottom, matching the tree convention.

Four kinds wired in: wheat, potato, corn, strawberry. The boot demo's
crop patch now plants one column per kind so all four show up in the
spring start state. Harvest outputs map: wheat/corn → grain,
potato/strawberry → vegetable.

Tooltip already capitalises crop_kind so 'Corn' / 'Strawberry' show
through with no UI change.
This commit is contained in:
megaproxy 2026-05-15 19:39:57 +01:00
parent f67c12c51f
commit c93f889ff1
10 changed files with 261 additions and 42 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

View file

@ -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

View file

@ -15,9 +15,12 @@ class_name Crop extends Node2D
const TILE_SIZE_PX: int = 16 const TILE_SIZE_PX: int = 16
## Phase 7 ships wheat and potato. Phase 17 expands (berry, hop) per design.md. ## 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_WHEAT: StringName = &"wheat"
const KIND_POTATO: StringName = &"potato" 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. ## 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. ## 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 } 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 crop_kind: StringName = KIND_WHEAT
@export var tile: Vector2i = Vector2i.ZERO @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. # indoor detection for this crop instance so we don't flood the audit log.
var _logged_indoor: bool = false 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") 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: func _ready() -> void:
position = _tile_to_world(tile) position = _tile_to_world(tile)
_build_sprite()
World.register_crop(self) World.register_crop(self)
EventBus.sim_tick.connect(_on_sim_tick) EventBus.sim_tick.connect(_on_sim_tick)
queue_redraw() queue_redraw()
@ -62,6 +83,9 @@ func setup(p_tile: Vector2i, p_kind: StringName, p_stage: Stage = Stage.SOWN) ->
stage = p_stage stage = p_stage
stage_progress = 0 stage_progress = 0
position = _tile_to_world(tile) position = _tile_to_world(tile)
if _sprite != null:
_sprite.texture = _texture_for(crop_kind)
_refresh_sprite_region()
queue_redraw() queue_redraw()
Audit.log("crop", "spawned %s at %s (stage=%s)" % [crop_kind, tile, Stage.keys()[stage]]) 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) it.setup(item_type, 1, tile)
stage = Stage.TILLED stage = Stage.TILLED
stage_progress = 0 stage_progress = 0
_refresh_sprite_region()
Audit.log("crop", "harvested %s at %s%s" % [crop_kind, tile, item_type]) Audit.log("crop", "harvested %s at %s%s" % [crop_kind, tile, item_type])
queue_redraw() queue_redraw()
@ -98,6 +123,7 @@ func on_sow_tick() -> void:
return return
stage = Stage.SOWN stage = Stage.SOWN
stage_progress = 0 stage_progress = 0
_refresh_sprite_region()
Audit.log("crop", "sown %s at %s" % [crop_kind, tile]) Audit.log("crop", "sown %s at %s" % [crop_kind, tile])
queue_redraw() queue_redraw()
@ -122,6 +148,7 @@ func _on_sim_tick(_n: int) -> void:
if stage_progress >= STAGE_TICKS: if stage_progress >= STAGE_TICKS:
stage_progress = 0 stage_progress = 0
stage = (int(stage) + 1) as Stage stage = (int(stage) + 1) as Stage
_refresh_sprite_region()
queue_redraw() queue_redraw()
if stage == Stage.READY: if stage == Stage.READY:
Audit.log("crop", "%s ready at %s" % [crop_kind, tile]) Audit.log("crop", "%s ready at %s" % [crop_kind, tile])
@ -156,53 +183,70 @@ static func from_dict(d: Dictionary) -> Dictionary:
# ── render ──────────────────────────────────────────────────────────────────── # ── render ────────────────────────────────────────────────────────────────────
func _draw() -> void: 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_color := Color(0.32, 0.20, 0.10)
var soil_dark := Color(0.22, 0.14, 0.06) 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_color)
draw_rect(Rect2(Vector2(-7.0, -7.0), Vector2(14.0, 14.0)), soil_dark, false, 1.0) 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 # ── sprite helpers ────────────────────────────────────────────────────────────
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)
# Stem func _build_sprite() -> void:
draw_rect(Rect2(Vector2(-2.0, 5.0 - height), Vector2(4.0, height)), plant_color) _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 func _refresh_sprite_region() -> void:
if stage == Stage.READY: if _sprite == null:
draw_circle(Vector2(0.0, 5.0 - height), 2.0, _ready_accent_for(crop_kind)) 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 ─────────────────────────────────────────────────────────────────── # ── helpers ───────────────────────────────────────────────────────────────────
func _texture_for(kind: StringName) -> Texture2D:
return _CROP_TEXTURES.get(kind, _CROP_TEXTURES[KIND_WHEAT])
func _harvest_output_for(kind: StringName) -> StringName: func _harvest_output_for(kind: StringName) -> StringName:
match kind: match kind:
KIND_WHEAT: return Item.TYPE_GRAIN KIND_WHEAT: return Item.TYPE_GRAIN
KIND_POTATO: return Item.TYPE_VEGETABLE KIND_POTATO: return Item.TYPE_VEGETABLE
_: return Item.TYPE_VEGETABLE # fallback KIND_CORN: return Item.TYPE_GRAIN
KIND_STRAWBERRY: return Item.TYPE_VEGETABLE
_: return Item.TYPE_VEGETABLE
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)
func _tile_to_world(t: Vector2i) -> Vector2: func _tile_to_world(t: Vector2i) -> Vector2:

View file

@ -561,15 +561,30 @@ func _seed_phase5_demo_buildings() -> void:
meal_bill.mode = Bill.Mode.FOREVER meal_bill.mode = Bill.Mode.FOREVER
hearth.add_bill(meal_bill) hearth.add_bill(meal_bill)
# Wheat crops east of the cabin, near the trees. # Mixed crops east of the cabin, near the trees. One column per kind so the
var crop_tiles: Array[Vector2i] = [ # player sees the four atlas variants side-by-side from the boot demo.
Vector2i(54, 24), Vector2i(54, 25), Vector2i(54, 26), var crop_plan: Array = [
Vector2i(55, 24), Vector2i(55, 25), Vector2i(55, 26), [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() var c: Crop = CROP_SCENE.instantiate()
add_child(c) 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 # Pre-baked breads + a vegetable meal so pawns can eat before the
# full cooking chain finishes. Phase 17 may remove these as cooking # 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.setup(Item.TYPE_BREAD, 1, st)
bread_item.quality = Item.Quality.NORMAL 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() crop_tiles.size(), snack_tiles.size()
]) ])