feat(world-map): ✨ Implement biome details and collectibles display in the tile info panel with new UI elements and unit tests
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
bf06b1a8d6
commit
53135542b5
3 changed files with 210 additions and 39 deletions
|
|
@ -1,12 +1,13 @@
|
|||
extends PanelContainer
|
||||
## Bottom-of-screen tooltip showing terrain info when hovering a tile.
|
||||
## Displays terrain name, movement cost, defense bonus, and yields.
|
||||
## Row 1: biome name, movement cost, defense, food/production/trade yields.
|
||||
## Row 2: collectibles list, worked yield delta, strategic/luxury resource context.
|
||||
|
||||
const TileScript: GDScript = preload("res://engine/src/map/tile.gd")
|
||||
|
||||
var _current_axial: Vector2i = Vector2i(-9999, -9999)
|
||||
|
||||
@onready var _terrain_name: Label = %TerrainName
|
||||
@onready var _biome_name: Label = %BiomeName
|
||||
@onready var _move_cost: Label = %MoveCost
|
||||
@onready var _defense_bonus: Label = %DefenseBonus
|
||||
@onready var _food_yield: Label = %FoodYield
|
||||
|
|
@ -14,6 +15,8 @@ var _current_axial: Vector2i = Vector2i(-9999, -9999)
|
|||
@onready var _trade_yield: Label = %TradeYield
|
||||
@onready var _resource_label: Label = %ResourceLabel
|
||||
@onready var _position_label: Label = %PositionLabel
|
||||
@onready var _collectibles_label: Label = %CollectiblesLabel
|
||||
@onready var _worked_yield_label: Label = %WorkedYieldLabel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -36,13 +39,13 @@ func show_tile(tile: RefCounted, axial: Vector2i) -> void:
|
|||
return
|
||||
_current_axial = axial
|
||||
|
||||
var terrain_id: String = str(tile.get("terrain_id"))
|
||||
var terrain_data: Dictionary = DataLoader.get_terrain(terrain_id)
|
||||
var biome_id: String = str(tile.get("biome_id"))
|
||||
var terrain_data: Dictionary = DataLoader.get_terrain(biome_id)
|
||||
if terrain_data.is_empty():
|
||||
hide_panel()
|
||||
return
|
||||
|
||||
_terrain_name.text = ThemeVocabulary.lookup(terrain_id)
|
||||
_biome_name.text = ThemeVocabulary.lookup(biome_id)
|
||||
_move_cost.text = "%s: %d" % [
|
||||
ThemeVocabulary.lookup("movement_cost"),
|
||||
terrain_data.get("movement_cost", 1) as int,
|
||||
|
|
@ -64,33 +67,113 @@ func show_tile(tile: RefCounted, axial: Vector2i) -> void:
|
|||
terrain_data.get("trade", 0) as int,
|
||||
]
|
||||
|
||||
var resource_id: String = str(tile.get("resource_id"))
|
||||
if not resource_id.is_empty() and resource_id != "null":
|
||||
var resource_data: Dictionary = DataLoader.get_resource(resource_id)
|
||||
var resource_name: String = ThemeVocabulary.lookup(resource_id)
|
||||
if not resource_data.is_empty():
|
||||
var bonus_parts: PackedStringArray = PackedStringArray()
|
||||
var bonus_food: int = resource_data.get("bonus_food", 0) as int
|
||||
var bonus_prod: int = resource_data.get("bonus_production", 0) as int
|
||||
var bonus_trade: int = resource_data.get("bonus_trade", 0) as int
|
||||
if bonus_food > 0:
|
||||
bonus_parts.append("+%d %s" % [bonus_food, ThemeVocabulary.lookup("food")])
|
||||
if bonus_prod > 0:
|
||||
bonus_parts.append("+%d %s" % [bonus_prod, ThemeVocabulary.lookup("production")])
|
||||
if bonus_trade > 0:
|
||||
bonus_parts.append("+%d %s" % [bonus_trade, ThemeVocabulary.lookup("trade")])
|
||||
_resource_label.text = "%s (%s)" % [resource_name, ", ".join(bonus_parts)]
|
||||
else:
|
||||
_resource_label.text = resource_name
|
||||
_resource_label.visible = true
|
||||
else:
|
||||
_resource_label.text = ""
|
||||
_resource_label.visible = false
|
||||
_populate_resource(tile)
|
||||
_populate_collectibles(biome_id)
|
||||
_populate_worked_yield(tile, terrain_data)
|
||||
|
||||
_position_label.text = "(%d, %d)" % [axial.x, axial.y]
|
||||
visible = true
|
||||
|
||||
|
||||
func _populate_resource(tile: RefCounted) -> void:
|
||||
var resource_id: String = str(tile.get("resource_id"))
|
||||
if not resource_id.is_empty() and resource_id != "null":
|
||||
var resource_data: Dictionary = DataLoader.get_resource(resource_id)
|
||||
var resource_name: String = ThemeVocabulary.lookup(resource_id)
|
||||
var category: String = resource_data.get("category", "bonus")
|
||||
var bonus_parts: PackedStringArray = PackedStringArray()
|
||||
var bonus_food: int = resource_data.get("bonus_food", 0) as int
|
||||
var bonus_prod: int = resource_data.get("bonus_production", 0) as int
|
||||
var bonus_trade: int = resource_data.get("bonus_trade", 0) as int
|
||||
if bonus_food > 0:
|
||||
bonus_parts.append("+%d %s" % [bonus_food, ThemeVocabulary.lookup("food")])
|
||||
if bonus_prod > 0:
|
||||
bonus_parts.append("+%d %s" % [bonus_prod, ThemeVocabulary.lookup("production")])
|
||||
if bonus_trade > 0:
|
||||
bonus_parts.append("+%d %s" % [bonus_trade, ThemeVocabulary.lookup("trade")])
|
||||
|
||||
var context_tag: String = ""
|
||||
if category == "luxury":
|
||||
var happiness: int = resource_data.get("happiness_per_copy", 0) as int
|
||||
context_tag = " [%s +%d]" % [ThemeVocabulary.lookup("happiness"), happiness]
|
||||
elif category == "strategic":
|
||||
var gates: Array = resource_data.get("gates_units", [])
|
||||
if not gates.is_empty():
|
||||
context_tag = " [%s: %s]" % [
|
||||
ThemeVocabulary.lookup("gates_units"),
|
||||
ThemeVocabulary.lookup(gates[0] as String),
|
||||
]
|
||||
var reveal_tech: String = resource_data.get("revealed_by_tech", "")
|
||||
if not reveal_tech.is_empty():
|
||||
context_tag += " (%s)" % ThemeVocabulary.lookup(reveal_tech)
|
||||
|
||||
if bonus_parts.is_empty():
|
||||
_resource_label.text = resource_name + context_tag
|
||||
else:
|
||||
_resource_label.text = "%s (%s)%s" % [resource_name, ", ".join(bonus_parts), context_tag]
|
||||
_resource_label.visible = true
|
||||
else:
|
||||
_resource_label.text = ""
|
||||
_resource_label.visible = false
|
||||
|
||||
|
||||
func _populate_collectibles(biome_id: String) -> void:
|
||||
var entries: Array = DataLoader.get_biome_collectibles(biome_id)
|
||||
if entries.is_empty():
|
||||
_collectibles_label.text = ""
|
||||
_collectibles_label.visible = false
|
||||
return
|
||||
var parts: PackedStringArray = PackedStringArray()
|
||||
for entry: Variant in entries:
|
||||
if entry is Dictionary:
|
||||
var res_id: String = entry.get("resource", "") as String
|
||||
if not res_id.is_empty():
|
||||
parts.append(ThemeVocabulary.lookup(res_id))
|
||||
if parts.is_empty():
|
||||
_collectibles_label.visible = false
|
||||
return
|
||||
_collectibles_label.text = "%s: %s" % [
|
||||
ThemeVocabulary.lookup("collectibles"),
|
||||
", ".join(parts),
|
||||
]
|
||||
_collectibles_label.visible = true
|
||||
|
||||
|
||||
func _populate_worked_yield(tile: RefCounted, terrain_data: Dictionary) -> void:
|
||||
var improvement: String = str(tile.get("improvement"))
|
||||
if improvement.is_empty() or improvement == "null":
|
||||
_worked_yield_label.text = ""
|
||||
_worked_yield_label.visible = false
|
||||
return
|
||||
var yields: Dictionary = tile.call("get_quality_yields", -1) as Dictionary
|
||||
var f: int = yields.get("food", 0) as int
|
||||
var p: int = yields.get("production", 0) as int
|
||||
var t: int = yields.get("trade", 0) as int
|
||||
_worked_yield_label.text = "%s: %s %d / %s %d / %s %d" % [
|
||||
ThemeVocabulary.lookup("worked_yield"),
|
||||
ThemeVocabulary.lookup("food"), f,
|
||||
ThemeVocabulary.lookup("production"), p,
|
||||
ThemeVocabulary.lookup("trade"), t,
|
||||
]
|
||||
_worked_yield_label.visible = true
|
||||
|
||||
|
||||
func hide_panel() -> void:
|
||||
visible = false
|
||||
_current_axial = Vector2i(-9999, -9999)
|
||||
|
||||
|
||||
## Pure helper — builds the collectibles display string from a raw entry array.
|
||||
## Used by GUT tests without needing a scene tree.
|
||||
static func build_collectibles_text(entries: Array) -> String:
|
||||
if entries.is_empty():
|
||||
return ""
|
||||
var parts: PackedStringArray = PackedStringArray()
|
||||
for entry: Variant in entries:
|
||||
if entry is Dictionary:
|
||||
var res_id: String = entry.get("resource", "") as String
|
||||
if not res_id.is_empty():
|
||||
parts.append(ThemeVocabulary.lookup(res_id))
|
||||
if parts.is_empty():
|
||||
return ""
|
||||
return "%s: %s" % [ThemeVocabulary.lookup("collectibles"), ", ".join(parts)]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ anchor_left = 0.0
|
|||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -64.0
|
||||
offset_top = -84.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
script = ExtResource("1")
|
||||
|
|
@ -16,59 +16,81 @@ script = ExtResource("1")
|
|||
[node name="MarginContainer" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 12
|
||||
theme_override_constants/margin_top = 6
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 12
|
||||
theme_override_constants/margin_bottom = 6
|
||||
theme_override_constants/margin_bottom = 5
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 3
|
||||
|
||||
[node name="Row1" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="TerrainName" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="BiomeName" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(120, 0)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
|
||||
[node name="MoveCost" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="MoveCost" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1)
|
||||
|
||||
[node name="DefenseBonus" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="DefenseBonus" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.5, 0.8, 0.5, 1)
|
||||
|
||||
[node name="FoodYield" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="FoodYield" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.5, 0.85, 0.4, 1)
|
||||
|
||||
[node name="ProductionYield" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="ProductionYield" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.85, 0.5, 0.2, 1)
|
||||
|
||||
[node name="TradeYield" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="TradeYield" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.9, 0.82, 0.2, 1)
|
||||
|
||||
[node name="ResourceLabel" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="ResourceLabel" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 13
|
||||
theme_override_colors/font_color = Color(0.8, 0.65, 0.9, 1)
|
||||
visible = false
|
||||
|
||||
[node name="PositionLabel" type="Label" parent="MarginContainer/HBoxContainer"]
|
||||
[node name="PositionLabel" type="Label" parent="MarginContainer/VBoxContainer/Row1"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 11
|
||||
theme_override_colors/font_color = Color(0.4, 0.4, 0.4, 1)
|
||||
|
||||
[node name="Row2" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="CollectiblesLabel" type="Label" parent="MarginContainer/VBoxContainer/Row2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 12
|
||||
theme_override_colors/font_color = Color(0.75, 0.82, 0.6, 1)
|
||||
visible = false
|
||||
|
||||
[node name="WorkedYieldLabel" type="Label" parent="MarginContainer/VBoxContainer/Row2"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 12
|
||||
theme_override_colors/font_color = Color(0.6, 0.78, 0.9, 1)
|
||||
visible = false
|
||||
|
|
|
|||
66
src/game/engine/tests/unit/test_tile_tooltip.gd
Normal file
66
src/game/engine/tests/unit/test_tile_tooltip.gd
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
extends GutTest
|
||||
## Tests for tile_info_panel collectibles display logic.
|
||||
## Uses the static build_collectibles_text helper — no scene tree required.
|
||||
|
||||
const TileInfoPanelScript: GDScript = preload(
|
||||
"res://engine/scenes/world_map/tile_info_panel.gd"
|
||||
)
|
||||
|
||||
|
||||
func before_all() -> void:
|
||||
DataLoader.load_theme("age-of-dwarves")
|
||||
DataLoader.load_world("earth")
|
||||
ThemeVocabulary.load_vocabulary("age-of-dwarves")
|
||||
|
||||
|
||||
func test_empty_entries_returns_empty_string() -> void:
|
||||
var result: String = TileInfoPanelScript.build_collectibles_text([])
|
||||
assert_eq(result, "", "empty entry array must produce empty string")
|
||||
|
||||
|
||||
func test_single_collectible_appears_in_text() -> void:
|
||||
var entries: Array = [{"resource": "wild_game", "base_quantity": 3, "quality_range": [1, 3]}]
|
||||
var result: String = TileInfoPanelScript.build_collectibles_text(entries)
|
||||
assert_true(
|
||||
"Wild Game" in result or "wild_game" in result,
|
||||
"collectibles text must contain the resource name"
|
||||
)
|
||||
|
||||
|
||||
func test_multiple_collectibles_all_appear() -> void:
|
||||
var entries: Array = [
|
||||
{"resource": "iron_ore", "base_quantity": 2, "quality_range": [1, 4]},
|
||||
{"resource": "bog_mushrooms", "base_quantity": 3, "quality_range": [1, 3]},
|
||||
]
|
||||
var result: String = TileInfoPanelScript.build_collectibles_text(entries)
|
||||
assert_true(
|
||||
"Iron Ore" in result or "iron_ore" in result,
|
||||
"iron_ore must appear in collectibles text"
|
||||
)
|
||||
assert_true(
|
||||
"Bog Mushrooms" in result or "bog_mushrooms" in result,
|
||||
"bog_mushrooms must appear in collectibles text"
|
||||
)
|
||||
|
||||
|
||||
func test_entry_missing_resource_key_is_skipped() -> void:
|
||||
var entries: Array = [
|
||||
{"base_quantity": 3},
|
||||
{"resource": "grain_fields", "base_quantity": 4},
|
||||
]
|
||||
var result: String = TileInfoPanelScript.build_collectibles_text(entries)
|
||||
assert_true(
|
||||
"Grain Fields" in result or "grain_fields" in result,
|
||||
"valid entry must appear despite malformed sibling"
|
||||
)
|
||||
|
||||
|
||||
func test_dataloader_get_biome_collectibles_returns_array() -> void:
|
||||
var entries: Array = DataLoader.get_biome_collectibles("temperate_forest")
|
||||
assert_true(entries is Array, "get_biome_collectibles must return an Array")
|
||||
assert_gt(entries.size(), 0, "temperate_forest must have collectibles loaded")
|
||||
|
||||
|
||||
func test_unknown_biome_returns_empty_array() -> void:
|
||||
var entries: Array = DataLoader.get_biome_collectibles("void_realm_does_not_exist")
|
||||
assert_eq(entries.size(), 0, "unknown biome must return empty array")
|
||||
Loading…
Add table
Reference in a new issue