Tree growth: dedicated stage atlas + tuned WildGrowth rate

Sub-mature stages (0/1/2) now use FG_Tree_Stages.png (a 128×32 atlas
with 8 progressively-larger tree cells from the bundle's "Crops with
Stages 03" pack). Stage 0 = tiny sprout (col 0), Stage 1 = small
leaf (col 1), Stage 2 = small tree (col 3). Stage 3 (Mature) keeps
the existing 64×80 seasonal canopy atlases.

Visually distinct progression replaces the previous scale-down-the-
mature-texture placeholder + procedural sapling dots.

WildGrowth pacing tuned: INTERVAL 1200 → 3000, PROBABILITY 0.30 →
0.12, LIMIT 60 → 80. Previous values flooded the map with saplings
within ~30 seconds of 12× play. New rate gives a slow but visible
regrowth over a season at default speed.

_draw simplified: removed procedural sapling fallback (atlas handles
all stages now). Pending-plant ghosts get the alpha tint via
sprite.modulate.
This commit is contained in:
megaproxy 2026-05-16 16:42:38 +01:00
parent d98d2c2425
commit 5a6ec53b12
4 changed files with 78 additions and 37 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bws0uhalpfbln"
path="res://.godot/imported/FG_Tree_Stages.png-620ce94d31b96e88794dc081cb63ea2e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/sprites/FG_Tree_Stages.png"
dest_files=["res://.godot/imported/FG_Tree_Stages.png-620ce94d31b96e88794dc081cb63ea2e.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

@ -81,6 +81,17 @@ const _TREE_VARIANT_W: int = 64
const _TREE_VARIANT_H: int = 80
const _TREE_SILHOUETTES: int = 4 # silhouettes per atlas (columns)
## Growth-stage atlas — 128×32, 8 columns × 1 row of 16×32 cells, left-to-right
## progressively larger trees. Used for sub-mature stages so the player sees
## a proper sapling → small tree silhouette change instead of a scaled-down
## mature canopy. Stage MATURE keeps using _TREE_TEXES above (the full 64×80
## canopy).
const _STAGE_TEX: Texture2D = preload("res://art/sprites/FG_Tree_Stages.png")
const _STAGE_CELL_W: int = 16
const _STAGE_CELL_H: int = 32
## Atlas column to use per growth_stage. Stage 3 (MATURE) is unused here.
const _STAGE_COLS: Array[int] = [0, 1, 3, -1]
# ── lifecycle ─────────────────────────────────────────────────────────────────
@ -94,36 +105,33 @@ func _ready() -> void:
## Rebuild the Sprite2D child to match the current growth_stage.
## Sapling (stage 0): no Sprite2D — rendered procedurally in _draw().
## Young (1) → scale 0.35, Growing (2) → 0.65, Mature (3) → 1.0.
## Any existing "Sprite" child is removed first so re-calls don't stack.
## Stages 0-2 use _STAGE_TEX (a dedicated growth-stage atlas with progressively
## larger trees per cell). Stage 3 (Mature) uses the seasonal full-canopy
## atlases. Any existing "Sprite" child is removed first so re-calls don't stack.
func _refresh_sprite() -> void:
var old := get_node_or_null("Sprite")
if old != null:
old.queue_free()
if growth_stage == STAGE_SAPLING:
# No Sprite2D for saplings — all rendering done in _draw().
queue_redraw()
return
var scale_map: Array[float] = [1.0, 0.35, 0.65, 1.0] # indexed by stage
var sprite_scale: float = scale_map[growth_stage]
var sprite := Sprite2D.new()
sprite.name = "Sprite"
var hash_seed: int = tile.x * 31 + tile.y * 17
var silhouette: int = hash_seed % _TREE_SILHOUETTES
# Independent hash mix for season so neighbouring tiles don't all match.
var season: int = ((hash_seed / _TREE_SILHOUETTES) + tile.x * 7 + tile.y * 11) % _TREE_TEXES.size()
sprite.texture = _TREE_TEXES[season]
sprite.region_enabled = true
sprite.region_rect = Rect2(silhouette * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H)
sprite.centered = true
# Lift the sprite up so its bottom edge sits at the tile's bottom row.
# Sprite center is at offset.y; sprite half-height is _TREE_VARIANT_H/2 = 40.
# We want bottom edge at +8 (tile bottom) → center at 8 - 40 = -32.
sprite.offset = Vector2(0, -32)
sprite.scale = Vector2(sprite_scale, sprite_scale)
# Render behind pawns/items that are at higher z_index; trees live at z=0.
sprite.z_index = 0
if growth_stage < STAGE_MATURE:
# Sub-mature: 16×32 cell from _STAGE_TEX. Bottom edge at tile bottom
# (+8); sprite half-height 16 → centre offset y = 8 - 16 = -8.
var col: int = _STAGE_COLS[growth_stage]
sprite.texture = _STAGE_TEX
sprite.region_rect = Rect2(col * _STAGE_CELL_W, 0, _STAGE_CELL_W, _STAGE_CELL_H)
sprite.offset = Vector2(0, -8)
else:
# Mature: full 64×80 seasonal canopy. Bottom at +8 → centre at -32.
var hash_seed: int = tile.x * 31 + tile.y * 17
var silhouette: int = hash_seed % _TREE_SILHOUETTES
var season: int = ((hash_seed / _TREE_SILHOUETTES) + tile.x * 7 + tile.y * 11) % _TREE_TEXES.size()
sprite.texture = _TREE_TEXES[season]
sprite.region_rect = Rect2(silhouette * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H)
sprite.offset = Vector2(0, -32)
add_child(sprite)
queue_redraw()
@ -280,19 +288,12 @@ static func from_dict(d: Dictionary) -> Dictionary:
# ── render ────────────────────────────────────────────────────────────────────
func _draw() -> void:
# Sapling stage: draw a procedural sprout — no atlas sprite available.
# Three small green leaf-dots clustered above a thin brown stem.
if growth_stage == STAGE_SAPLING:
# Ghost tint for pending-plant saplings so the player can tell it's
# waiting for a pawn to build it.
var alpha := 0.55 if pending_plant else 1.0
# Stem
draw_line(Vector2(0.0, 6.0), Vector2(0.0, 0.0), Color(0.35, 0.22, 0.10, alpha), 1.5)
# Three leaf dots
draw_circle(Vector2(0.0, -2.0), 2.5, Color(0.30, 0.65, 0.20, alpha))
draw_circle(Vector2(-3.0, 1.0), 1.8, Color(0.25, 0.58, 0.18, alpha))
draw_circle(Vector2(3.0, 0.5), 1.8, Color(0.28, 0.62, 0.19, alpha))
return
# Ghost tint for pending-plant saplings — apply via modulate on the sprite
# child instead of drawing extra overlay shapes here.
if pending_plant and growth_stage == STAGE_SAPLING:
var s := get_node_or_null("Sprite")
if s != null:
s.modulate = Color(1, 1, 1, 0.55)
# Mature / growing stages: canopy + trunk come from the Sprite2D child.
# This _draw renders only the chop-progress notch overlaid on the trunk.

View file

@ -98,9 +98,9 @@ const HAUL_SWEEP_INTERVAL_TICKS: int = 100
# WildGrowth — spontaneous sapling spawning on eligible grass tiles.
# 1200 ticks = 1 in-game hour at 20 Hz (20 ticks/s × 60 s/min = 1200 ticks/min,
# but 1 in-game minute = 20 ticks at 1× so 1 hour = 1200 ticks at 1×).
const WILD_GROWTH_INTERVAL: int = 1200
const WILD_GROWTH_SPAWN_PROBABILITY: float = 0.30
const MAP_TREE_LIMIT: int = 60
const WILD_GROWTH_INTERVAL: int = 3000 # ~2.5 in-game hours between attempts
const WILD_GROWTH_SPAWN_PROBABILITY: float = 0.12 # 12% chance per attempt
const MAP_TREE_LIMIT: int = 80
# Rejection-sample attempts before giving up for this tick.
const WILD_GROWTH_MAX_ATTEMPTS: int = 10