Inspect tooltip: add crops, bed sprite-canopy, layer audit

Player report: hovering trees works after the canopy fix, but crops show
nothing and beds still report "Wood floor" sometimes.

* Crops are added to the lookup, showing kind + growth stage + percent
  ("Wheat | sown · 98%", "Wheat | ready to harvest", "tilled — not sown").

* Bed sprite is 16×32 (two tiles tall, anchor at the foot). Hovering on
  the headboard (one tile above the anchor) used to miss. Added a sprite-
  canopy pass for beds mirroring the existing 4-tile tree canopy logic.

* Full layer audit and reordering. Final priority top-down:
    1. Pawn / Wolf / Corpse / Grave marker — entities the player cares
       about first.
    2. Crop — small sprite, exact tile only.
    3. Tree — trunk tile + 4-tile canopy.
    4. Big rock (2×2 footprint) / Rock — exact tile / footprint.
    5. Furniture at exact tile (wall / door / bed / crate / workbench /
       torch).
    6. Furniture sprite-canopy (currently only bed; future tall furniture
       slots in here).
    7. Item on the ground (loose stack).
    8. Stockpile / zone region overlay.
    9. Floor — only when nothing physical is on the tile.

Confirmed at runtime: bed foot → Bed; bed headboard → Bed (via canopy);
two-tiles-above-bed → empty; crop at growing stage shows percent; bare
floor still says Wood floor; furniture on wood floor wins over floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-15 19:10:20 +01:00
parent 00f38ffd95
commit 531b907012

View file

@ -120,21 +120,54 @@ func _position_near(mouse_screen: Vector2) -> void:
# ── tile → description ──────────────────────────────────────────────────────
## Priority order matches what the player visually expects on top of a tile:
## pawn > wolf > corpse > grave > crop > tree (4-tile canopy) > big rock /
## rock > furniture (bed/wall/door/crate/workbench/torch with 1-tile sprite
## canopy for tall bottom-anchored entities) > item > stockpile region > floor.
##
## Floor + terrain are the bottom layer — only shown when nothing else covers
## the tile. "Wood floor" tooltip means the tile is genuinely empty.
##
## "Sprite canopy" handles entities whose visual extends above their anchor
## tile (trees: trunk + 4 tiles up; beds: 16×32 sprite covering tile.y - 1
## as well as tile.y). Walls / torches / crates / doors are 16×16 — anchor
## tile only, no canopy.
func _describe_tile(tile: Vector2i) -> String:
# Priority order: pawn > corpse > wolf > big_rock > tree > rock > furniture
# (wall/door/bed/crate/workbench/torch/floor) > item > stockpile zone > terrain.
# 1. Pawn
var p = World.pawn_at_tile(tile) if World.has_method("pawn_at_tile") else null
if p != null and is_instance_valid(p):
return _describe_pawn(p)
for c in World.corpses:
if is_instance_valid(c) and c.get("tile") == tile:
return _describe_corpse(c)
# 2. Wolf
for w in World.wolves:
if is_instance_valid(w) and w.get("tile") == tile:
return _describe_wolf(w)
# 3. Corpse
for c in World.corpses:
if is_instance_valid(c) and c.get("tile") == tile:
return _describe_corpse(c)
# 4. Grave marker (permanent burial, dedicated structure)
for gm in World.grave_markers:
if is_instance_valid(gm) and gm.get("tile") == tile:
return _describe_grave(gm)
# 5. Crop (farm plant — small sprite, exact tile only)
for cp in World.crops:
if is_instance_valid(cp) and cp.get("tile") == tile:
return _describe_crop(cp)
# 6. Tree — trunk tile + 4 tiles of canopy above (sprite is 64×80 px,
# anchored at trunk bottom; covers tile.y - 4..tile.y vertically).
for t in World.trees:
if not is_instance_valid(t):
continue
var tt: Vector2i = t.get("tile")
if tile.x == tt.x and tile.y >= tt.y - 4 and tile.y <= tt.y:
return _describe_tree(t)
# 7. Rock (big rock = 2×2 footprint match; single rock = exact tile).
for r in World.rocks:
if not is_instance_valid(r):
continue
@ -144,46 +177,43 @@ func _describe_tile(tile: Vector2i) -> String:
elif r.get("tile") == tile:
return _describe_rock(r)
# Trees anchor to the trunk tile (bottom) but visually rise up to 4 tiles
# above (canopy). Treat any hover within that vertical band as the tree.
for t in World.trees:
if not is_instance_valid(t):
continue
var tt: Vector2i = t.get("tile")
if tile.x == tt.x and tile.y >= tt.y - 4 and tile.y <= tt.y:
return _describe_tree(t)
# Furniture / build sites at this tile (Wall, Floor, Door, Bed, Crate,
# Workbench, Torch, GraveSlot all live in World.build_queue via _ready).
# Two-pass: above-ground furniture wins over floor, so hovering on a bed
# placed on a wood floor reports the bed, not the floor underneath.
# 8. Furniture (build_queue) — three-pass priority:
# (a) exact tile, non-floor: walls/doors/beds/crates/workbenches/torches.
# (b) canopy match for tall sprites (bed = 16×32 covers tile.y - 1 too).
# (c) floor + zone overlays + items below (lower layers).
var floor_hit = null
for s in World.build_queue:
if not is_instance_valid(s):
continue
if s.get("tile") != tile:
continue
var s_tile: Vector2i = s.get("tile")
var path: String = s.get_script().resource_path if s.get_script() else ""
if "floor.gd" in path:
floor_hit = s
continue # remember it, but keep scanning for furniture on top
var d := _describe_build_site(s)
if d != "":
return d
# Items take priority over the bare floor too — a wood stack should show
# as "wood ×N", not as the floor it sits on.
if s_tile == tile:
if "floor.gd" in path:
floor_hit = s
continue
var d := _describe_build_site(s)
if d != "":
return d
# Sprite canopy pass: beds (16×32) cover the tile above their anchor.
# Any other furniture taller than 16 px should be added here.
for s in World.build_queue:
if not is_instance_valid(s):
continue
var path2: String = s.get_script().resource_path if s.get_script() else ""
if "bed.gd" not in path2:
continue
var s_tile2: Vector2i = s.get("tile")
if tile.x == s_tile2.x and tile.y == s_tile2.y - 1:
return _describe_build_site(s)
# 9. Loose items on the ground (wood stacks, food, ingots).
for it in World.items:
if is_instance_valid(it) and it.get("tile") == tile:
return _describe_item(it)
if floor_hit != null:
return _describe_build_site(floor_hit)
# Grave markers (permanent, not in build_queue once converted).
for gm in World.grave_markers:
if is_instance_valid(gm) and gm.get("tile") == tile:
return _describe_grave(gm)
# Stockpile / graveyard / cremation zones (region-based, may be multi-cell).
# 10. Stockpile / graveyard / cremation zones — region overlay shown
# only when nothing physical occupies the tile (items above already
# win, which is what the player expects).
for sp in World.stockpiles:
if not is_instance_valid(sp):
continue
@ -191,6 +221,10 @@ func _describe_tile(tile: Vector2i) -> String:
if region != null and region.has_point(tile):
return _describe_stockpile(sp)
# 11. Floor fallback (the genuinely-bare-floor case).
if floor_hit != null:
return _describe_build_site(floor_hit)
return "" # nothing to show
@ -344,6 +378,28 @@ func _describe_build_site(s) -> String:
return "[b]%s[/b]" % label
func _describe_crop(cp) -> String:
var kind: String = String(cp.crop_kind).capitalize() if "crop_kind" in cp else "Crop"
var stage_name: String = "?"
if "stage" in cp and "Stage" in cp:
var keys: Array = cp.Stage.keys()
var idx: int = int(cp.stage)
if idx >= 0 and idx < keys.size():
stage_name = String(keys[idx]).to_lower().replace("_", " ")
var pct: int = -1
if "stage_progress" in cp and "STAGE_TICKS" in cp:
var st: int = int(cp.STAGE_TICKS) if cp.STAGE_TICKS != 0 else 1
pct = int(100.0 * float(cp.stage_progress) / float(st))
var head := "[b]%s[/b]" % kind
if stage_name == "ready":
return "%s\n[color=#fc6]ready to harvest[/color]" % head
if stage_name == "tilled":
return "%s\n[color=#888]tilled (not sown)[/color]" % head
if pct >= 0:
return "%s\n%s · %d%%" % [head, stage_name, pct]
return "%s\n%s" % [head, stage_name]
func _describe_item(it) -> String:
return "[b]%s[/b] ×%d" % [String(it.item_type).capitalize(), int(it.stack_size)]