From 531b907012222fca4bf24645c6ebd6cf5ee28d0d Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 15 May 2026 19:10:20 +0100 Subject: [PATCH] Inspect tooltip: add crops, bed sprite-canopy, layer audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scenes/ui/inspect_tooltip.gd | 130 +++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 37 deletions(-) diff --git a/scenes/ui/inspect_tooltip.gd b/scenes/ui/inspect_tooltip.gd index 3a09e99..c3a716d 100644 --- a/scenes/ui/inspect_tooltip.gd +++ b/scenes/ui/inspect_tooltip.gd @@ -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)]