G: large_text scales global theme font (14→20 at 1.4×) via new GameState.get_font_scale + EventBus.settings_changed. reduce_motion gates ResumeToast fade (HintOverlay already gated). I: InspectTooltip long-press wired (500ms hold, 12px drift cancel, tap-to-clear pin). Stale Phase 19 TODO replaced with accurate doc. H: Pawn.arrived_at_destination now also emitted on EventBus.pawn_arrived_at_destination; DirtinessSystem subscribes and bumps indoor traffic dirt (BUMP_INDOOR_TRAFFIC = 0.2). Outdoor-tracked bump needs Pawn.prev_tile — flagged for Phase 20. P: CraftingProvider caches ingredient item ref on Job.ingredient_item; JobRunner._tick_pickup validates is_instance_valid + not being_carried before the tile scan, cancels cleanly if another pawn grabbed it. J: rest_provider.gd deleted. Removed @onready + register call from world.gd, ext_resource + node from world.tscn. Provider count comment updated to 9. M: DIRTY_THRESHOLD extracted — cleaning_provider and job_runner now reference DirtinessSystem.DIRT_DIRTY_THRESHOLD. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
516 lines
18 KiB
GDScript
516 lines
18 KiB
GDScript
extends CanvasLayer
|
||
## InspectTooltip — hover (mouse) or long-press (touch) on a world tile to see
|
||
## what's there.
|
||
##
|
||
## Mouse path: samples position every frame, maps to a tile, builds a short
|
||
## description, renders a small panel offset from the cursor.
|
||
## Empty tile or off-map → tooltip hides.
|
||
##
|
||
## Touch path: finger held for LONG_PRESS_MS without drifting more than
|
||
## LONG_PRESS_DRIFT_PX pins the tooltip at the held tile until the next tap
|
||
## anywhere. Touch-down cancels a previous pin. This self-contained path does
|
||
## not depend on Selection; it reads the same tile + description helpers.
|
||
|
||
const TILE_SIZE_PX: int = 16
|
||
const CURSOR_OFFSET: Vector2 = Vector2(14, 14)
|
||
const EDGE_MARGIN_PX: int = 8
|
||
const LONG_PRESS_MS: int = 500 # ms finger must be held to trigger inspect
|
||
const LONG_PRESS_DRIFT_PX: float = 12.0 # cancel long-press if finger moves more than this
|
||
|
||
var _panel: PanelContainer = null
|
||
var _label: RichTextLabel = null
|
||
var _last_tile: Vector2i = Vector2i(-9999, -9999)
|
||
var _last_text: String = ""
|
||
|
||
# Touch long-press state.
|
||
var _touch_pressing: bool = false
|
||
var _touch_screen_pos: Vector2 = Vector2.ZERO
|
||
var _touch_start_ms: int = 0
|
||
var _touch_pinned: bool = false # true while a long-press pin is held open
|
||
var _touch_pin_tile: Vector2i = Vector2i(-9999, -9999)
|
||
|
||
|
||
func _ready() -> void:
|
||
layer = 50 # below modals (which are 100+), above world
|
||
# Process while paused so hover-inspect works during storyteller modals.
|
||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||
_panel = PanelContainer.new()
|
||
_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_panel.visible = false
|
||
# Dark translucent background so text stays readable on any biome.
|
||
var sb := StyleBoxFlat.new()
|
||
sb.bg_color = Color(0.06, 0.08, 0.12, 0.92)
|
||
sb.border_color = Color(0.55, 0.55, 0.55, 0.9)
|
||
sb.border_width_left = 1
|
||
sb.border_width_top = 1
|
||
sb.border_width_right = 1
|
||
sb.border_width_bottom = 1
|
||
sb.content_margin_left = 8
|
||
sb.content_margin_right = 8
|
||
sb.content_margin_top = 6
|
||
sb.content_margin_bottom = 6
|
||
sb.corner_radius_top_left = 3
|
||
sb.corner_radius_top_right = 3
|
||
sb.corner_radius_bottom_left = 3
|
||
sb.corner_radius_bottom_right = 3
|
||
_panel.add_theme_stylebox_override("panel", sb)
|
||
add_child(_panel)
|
||
|
||
_label = RichTextLabel.new()
|
||
_label.bbcode_enabled = true
|
||
_label.fit_content = true
|
||
_label.scroll_active = false
|
||
_label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_label.custom_minimum_size = Vector2(140, 0)
|
||
_label.add_theme_constant_override("line_separation", 2)
|
||
_label.add_theme_color_override("default_color", Color(0.95, 0.95, 0.95, 1.0))
|
||
_panel.add_child(_label)
|
||
|
||
|
||
## Touch-input handler for long-press inspect.
|
||
## InputEventScreenTouch press starts the long-press clock; release or excess
|
||
## drift cancels it. A subsequent tap anywhere while pinned dismisses the panel.
|
||
func _input(event: InputEvent) -> void:
|
||
if event is InputEventScreenTouch:
|
||
if event.pressed:
|
||
if _touch_pinned:
|
||
# A new tap clears the pin.
|
||
_touch_pinned = false
|
||
_panel.visible = false
|
||
_last_text = ""
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
_touch_pressing = true
|
||
_touch_screen_pos = event.position
|
||
_touch_start_ms = Time.get_ticks_msec()
|
||
else:
|
||
# Finger lifted — cancel the pending long-press (pin only fires from _process).
|
||
_touch_pressing = false
|
||
elif event is InputEventScreenDrag and _touch_pressing:
|
||
var drift: float = event.position.distance_to(_touch_screen_pos)
|
||
if drift > LONG_PRESS_DRIFT_PX:
|
||
_touch_pressing = false
|
||
|
||
|
||
func _process(_dt: float) -> void:
|
||
var vp := get_viewport()
|
||
if vp == null:
|
||
return
|
||
|
||
# ── Touch long-press poll ─────────────────────────────────────────────────
|
||
if _touch_pressing and not _touch_pinned:
|
||
var held_ms: int = Time.get_ticks_msec() - _touch_start_ms
|
||
if held_ms >= LONG_PRESS_MS:
|
||
_touch_pressing = false
|
||
_touch_pinned = true
|
||
# Describe tile under held finger.
|
||
var world_touch: Vector2 = vp.get_canvas_transform().affine_inverse() * _touch_screen_pos
|
||
_touch_pin_tile = Vector2i(
|
||
floori(world_touch.x / float(TILE_SIZE_PX)),
|
||
floori(world_touch.y / float(TILE_SIZE_PX)),
|
||
)
|
||
if _tile_in_map(_touch_pin_tile):
|
||
var pin_text: String = _describe_tile(_touch_pin_tile)
|
||
if pin_text != "":
|
||
_label.text = pin_text
|
||
_last_text = pin_text
|
||
_panel.reset_size()
|
||
_panel.visible = true
|
||
_position_near(_touch_screen_pos)
|
||
return
|
||
# Nothing to show — cancel pin.
|
||
_touch_pinned = false
|
||
|
||
# Pinned: keep the panel up; skip the mouse-hover update.
|
||
if _touch_pinned:
|
||
return
|
||
|
||
# ── Mouse hover path ──────────────────────────────────────────────────────
|
||
var mouse_screen: Vector2 = vp.get_mouse_position()
|
||
# Convert screen → world via canvas transform (selection.gd does the same).
|
||
var world_pos: Vector2 = vp.get_canvas_transform().affine_inverse() * mouse_screen
|
||
var tile: Vector2i = Vector2i(
|
||
floori(world_pos.x / float(TILE_SIZE_PX)),
|
||
floori(world_pos.y / float(TILE_SIZE_PX)),
|
||
)
|
||
# Off-map → hide.
|
||
if not _tile_in_map(tile):
|
||
_panel.visible = false
|
||
_last_tile = Vector2i(-9999, -9999)
|
||
return
|
||
|
||
var text: String = _describe_tile(tile)
|
||
if text == "":
|
||
_panel.visible = false
|
||
_last_tile = tile
|
||
_last_text = ""
|
||
return
|
||
|
||
if text != _last_text:
|
||
_label.text = text
|
||
_last_text = text
|
||
# Force a layout pass so size is fresh before positioning.
|
||
_panel.reset_size()
|
||
_last_tile = tile
|
||
_panel.visible = true
|
||
_position_near(mouse_screen)
|
||
|
||
|
||
func _tile_in_map(tile: Vector2i) -> bool:
|
||
if World.pathfinder == null:
|
||
return false
|
||
# Pathfinder.is_walkable checks bounds; cheap and reused.
|
||
# We don't actually care about walkability — bounds-check via direct access.
|
||
# Map size is exposed on the autoload after World scene _ready.
|
||
var ms: Vector2i = Vector2i(80, 80) # default; refined below if available
|
||
if "MAP_SIZE_TILES" in World:
|
||
ms = World.MAP_SIZE_TILES
|
||
return tile.x >= 0 and tile.y >= 0 and tile.x < ms.x and tile.y < ms.y
|
||
|
||
|
||
func _position_near(mouse_screen: Vector2) -> void:
|
||
var vp_size: Vector2 = Vector2(get_viewport().get_visible_rect().size)
|
||
var sz: Vector2 = _panel.size
|
||
var pos: Vector2 = mouse_screen + CURSOR_OFFSET
|
||
# Flip to the other side of cursor when overflowing right / bottom edges.
|
||
if pos.x + sz.x + EDGE_MARGIN_PX > vp_size.x:
|
||
pos.x = mouse_screen.x - sz.x - CURSOR_OFFSET.x
|
||
if pos.y + sz.y + EDGE_MARGIN_PX > vp_size.y:
|
||
pos.y = mouse_screen.y - sz.y - CURSOR_OFFSET.y
|
||
pos.x = clampf(pos.x, EDGE_MARGIN_PX, vp_size.x - sz.x - EDGE_MARGIN_PX)
|
||
pos.y = clampf(pos.y, EDGE_MARGIN_PX, vp_size.y - sz.y - EDGE_MARGIN_PX)
|
||
_panel.position = pos
|
||
|
||
|
||
# ── 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:
|
||
# 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)
|
||
|
||
# 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
|
||
if r.has_method("footprint_tiles"):
|
||
if tile in r.footprint_tiles():
|
||
return _describe_big_rock(r)
|
||
elif r.get("tile") == tile:
|
||
return _describe_rock(r)
|
||
|
||
# 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
|
||
var s_tile: Vector2i = s.get("tile")
|
||
var path: String = s.get_script().resource_path if s.get_script() else ""
|
||
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)
|
||
|
||
# 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
|
||
var region = sp.get("region")
|
||
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
|
||
|
||
|
||
# ── per-entity describers ───────────────────────────────────────────────────
|
||
|
||
func _describe_pawn(p) -> String:
|
||
var name_s: String = p.pawn_name
|
||
var hp_s: String = "%d/100" % int(p.hp)
|
||
var mood_s: String = "%d" % int(p.mood)
|
||
var lines: Array[String] = []
|
||
lines.append("[b]%s[/b]" % name_s)
|
||
lines.append("HP %s · Mood %s" % [hp_s, mood_s])
|
||
if p.has_method("is_downed") and p.is_downed():
|
||
lines.append("[color=red]Downed[/color]")
|
||
if p.job_runner != null and p.job_runner.job != null:
|
||
lines.append("[color=#aaa]%s[/color]" % p.job_runner.job.label)
|
||
return "\n".join(lines)
|
||
|
||
|
||
func _describe_corpse(c) -> String:
|
||
var name_s: String = str(c.get("deceased_name"))
|
||
var decay: float = float(c.get("decay") if "decay" in c else 0.0)
|
||
var state := "fresh"
|
||
if decay >= 100.0:
|
||
state = "rotted"
|
||
elif decay >= 50.0:
|
||
state = "rotting"
|
||
return "[b]Corpse of %s[/b]\n%s (%d%%)" % [name_s, state, int(decay)]
|
||
|
||
|
||
func _describe_wolf(w) -> String:
|
||
var state_s := "?"
|
||
if w.has_method("get_state_name"):
|
||
state_s = w.get_state_name()
|
||
var hp_s := str(int(w.hp)) if "hp" in w else "?"
|
||
return "[b]Wolf[/b]\nHP %s · %s" % [hp_s, state_s]
|
||
|
||
|
||
func _describe_tree(t) -> String:
|
||
# Growth stage label.
|
||
var stage: int = int(t.get("growth_stage")) if "growth_stage" in t else 3
|
||
var stage_key_map: Array[StringName] = [
|
||
&"tree.stage.sapling",
|
||
&"tree.stage.young",
|
||
&"tree.stage.growing",
|
||
&"tree.stage.mature",
|
||
]
|
||
var stage_label: String = Strings.t(stage_key_map[clamp(stage, 0, 3)])
|
||
|
||
# Pending-plant ghost indicator.
|
||
var pending: bool = bool(t.get("pending_plant")) if "pending_plant" in t else false
|
||
if pending:
|
||
return "[b]%s[/b]\n[color=#aaa]awaiting pawn[/color]" % stage_label
|
||
|
||
# Growth progress for sub-mature trees.
|
||
var is_mature: bool = (stage >= 3)
|
||
if not is_mature:
|
||
var progress: int = int(t.get("growth_progress")) if "growth_progress" in t else 0
|
||
var stage_ticks: int = int(t.get("STAGE_TICKS")) if "STAGE_TICKS" in t else 1
|
||
var pct_grow: int = int(100.0 * float(progress) / float(max(stage_ticks, 1)))
|
||
return "[b]%s[/b]\nGrowing %d%%" % [stage_label, pct_grow]
|
||
|
||
# Mature tree — show chop progress if any.
|
||
var pct: int = int(100.0 * float(t.chop_progress) / float(t.CHOP_TICKS))
|
||
var designated: bool = bool(t.get("chop_designated"))
|
||
var tag := " · [color=#fc6]marked[/color]" if designated else ""
|
||
if pct > 0:
|
||
return "[b]%s[/b]\nChop %d%%%s" % [stage_label, pct, tag]
|
||
return "[b]%s[/b]%s" % [stage_label, tag]
|
||
|
||
|
||
func _describe_rock(r) -> String:
|
||
var pct: int = int(100.0 * float(r.mine_progress) / float(r.MINE_TICKS))
|
||
var designated: bool = bool(r.get("mine_designated"))
|
||
var tag := " · [color=#fc6]marked[/color]" if designated else ""
|
||
if pct > 0:
|
||
return "[b]Rock[/b]\nMine %d%%%s" % [pct, tag]
|
||
return "[b]Rock[/b]%s" % tag
|
||
|
||
|
||
func _describe_big_rock(r) -> String:
|
||
var pct: int = int(100.0 * float(r.mine_progress) / float(r.MINE_TICKS))
|
||
var designated: bool = bool(r.get("mine_designated"))
|
||
var tag := " · [color=#fc6]marked[/color]" if designated else ""
|
||
return "[b]Boulder (2×2)[/b]\nMine %d%%%s" % [pct, tag]
|
||
|
||
|
||
func _describe_build_site(s) -> String:
|
||
var path: String = ""
|
||
if s.get_script() != null:
|
||
path = s.get_script().resource_path
|
||
var label: String = s.label() if s.has_method("label") else "Build site"
|
||
var completed: bool = false
|
||
if s.has_method("is_completed"):
|
||
completed = s.is_completed()
|
||
elif s.has_method("is_buildable"):
|
||
completed = not s.is_buildable()
|
||
|
||
if "wall.gd" in path:
|
||
var mat: String = str(s.get("wall_material"))
|
||
var head := "[b]%s wall[/b]" % mat.capitalize()
|
||
if not completed:
|
||
var prog: int = int(100.0 * float(s.build_progress) / float(s.BUILD_TICKS))
|
||
return "%s\n[color=#aaa]ghost · %d%%[/color]" % [head, prog]
|
||
return head
|
||
|
||
if "floor.gd" in path:
|
||
var mat2: String = str(s.get("floor_material"))
|
||
var head2 := "[b]%s floor[/b]" % mat2.capitalize()
|
||
if not completed:
|
||
return "%s\n[color=#aaa]ghost[/color]" % head2
|
||
return head2
|
||
|
||
if "door.gd" in path:
|
||
var head3 := "[b]Door[/b]"
|
||
if not completed:
|
||
return "%s\n[color=#aaa]ghost[/color]" % head3
|
||
return head3
|
||
|
||
if "bed.gd" in path:
|
||
var lines: Array[String] = []
|
||
var head4 := "Medical bed" if bool(s.get("is_medical")) else "Bed"
|
||
lines.append("[b]%s[/b]" % head4)
|
||
if not completed:
|
||
lines.append("[color=#aaa]ghost[/color]")
|
||
else:
|
||
var occ = s.get("_occupant_pawn")
|
||
if occ != null and is_instance_valid(occ):
|
||
lines.append("Occupied by %s" % occ.pawn_name)
|
||
else:
|
||
lines.append("[color=#888]available[/color]")
|
||
return "\n".join(lines)
|
||
|
||
if "crate.gd" in path:
|
||
var lines2: Array[String] = []
|
||
lines2.append("[b]Crate[/b]")
|
||
if not completed:
|
||
lines2.append("[color=#aaa]ghost[/color]")
|
||
var contents: Array = s._contents if "_contents" in s else []
|
||
if contents.is_empty():
|
||
lines2.append("[color=#888]empty[/color]")
|
||
else:
|
||
var counts: Dictionary = {}
|
||
for it in contents:
|
||
if not is_instance_valid(it):
|
||
continue
|
||
var k: String = str(it.item_type)
|
||
counts[k] = int(counts.get(k, 0)) + int(it.stack_size)
|
||
for k in counts.keys():
|
||
lines2.append("· %s ×%d" % [k, counts[k]])
|
||
return "\n".join(lines2)
|
||
|
||
if "workbench.gd" in path or "cremation_pyre.gd" in path:
|
||
var lines3: Array[String] = []
|
||
lines3.append("[b]%s[/b]" % s.label_text)
|
||
if not completed:
|
||
lines3.append("[color=#aaa]ghost[/color]")
|
||
var bills: Array = s.get("bills") if "bills" in s else []
|
||
if not bills.is_empty():
|
||
lines3.append("Bills: %d" % bills.size())
|
||
return "\n".join(lines3)
|
||
|
||
if "torch.gd" in path:
|
||
var head5 := "[b]Torch[/b]"
|
||
if not completed:
|
||
var prog2: int = int(100.0 * float(s.build_progress) / float(s.BUILD_TICKS))
|
||
return "%s\n[color=#aaa]ghost · %d%%[/color]" % [head5, prog2]
|
||
var lit: bool = bool(s.is_on()) if s.has_method("is_on") else true
|
||
return "%s\n%s" % [head5, ("[color=#fc6]lit[/color]" if lit else "[color=#888]unlit[/color]")]
|
||
|
||
if "grave_slot.gd" in path:
|
||
return "[b]Grave slot[/b]\n[color=#aaa]ghost[/color]"
|
||
|
||
# Fallback for any other build_queue entity.
|
||
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)]
|
||
|
||
|
||
func _describe_grave(gm) -> String:
|
||
var name_s: String = str(gm.get("deceased_name"))
|
||
return "[b]Grave of %s[/b]" % name_s
|
||
|
||
|
||
func _describe_stockpile(sp) -> String:
|
||
var label: String = str(sp.get("label"))
|
||
var prio = sp.get("priority")
|
||
var accepts: Array = sp.get("accepted_types") if "accepted_types" in sp else []
|
||
var lines: Array[String] = []
|
||
lines.append("[b]%s[/b]" % (label if label != "" else "Stockpile"))
|
||
if prio != null:
|
||
lines.append("Priority %s" % str(prio))
|
||
if accepts.is_empty():
|
||
lines.append("[color=#888]accepts: any[/color]")
|
||
else:
|
||
var s := ""
|
||
for a in accepts:
|
||
s += ("· " + String(a) + "\n")
|
||
lines.append(s.strip_edges())
|
||
return "\n".join(lines)
|