feat(@projects/@magic-civilization): ✨ add new city-building actions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7a51e3b159
commit
26d14d79bc
10 changed files with 770 additions and 1 deletions
23
public/resources/unit_actions/build_improvement.json
Normal file
23
public/resources/unit_actions/build_improvement.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"id": "build_improvement",
|
||||
"name": "Build Improvement",
|
||||
"description": "Construct a tile improvement on owned territory. AP cost varies by improvement type.",
|
||||
"ap_cost": "per_improvement",
|
||||
"ap_cost_value": null,
|
||||
"eligible_units": ["engineer", "dwarf_engineer", "dwarf_high_engineer", "dwarf_grand_engineer", "dwarf_ascendant_engineer"],
|
||||
"terrain_restrictions": {
|
||||
"requires_owned": true,
|
||||
"blocked_by": ["water", "deep_ocean"]
|
||||
},
|
||||
"outcome": "improvement_built",
|
||||
"ap_cost_table": {
|
||||
"farm": 3,
|
||||
"mine": 4,
|
||||
"road": 2,
|
||||
"field_fortification": 2,
|
||||
"clear_forest": 4,
|
||||
"clear_rubble": 5,
|
||||
"repair": 3
|
||||
},
|
||||
"notes": "ap_cost_table entries are canonical AP costs per improvement. Rust resolver reads this table via cost_for(unit, action). fast_build flag halves cost (rounded up)."
|
||||
}
|
||||
15
public/resources/unit_actions/found_city.json
Normal file
15
public/resources/unit_actions/found_city.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "found_city",
|
||||
"name": "Found City",
|
||||
"description": "Establish a new hold on this tile. Consumes the Pioneer's entire AP pool — a one-time, irreversible act.",
|
||||
"ap_cost": "all",
|
||||
"ap_cost_value": null,
|
||||
"eligible_units": ["pioneer", "dwarf_founder"],
|
||||
"terrain_restrictions": {
|
||||
"requires_unclaimed": true,
|
||||
"blocked_by": ["water", "mountain", "deep_ocean"]
|
||||
},
|
||||
"outcome": "city_founded",
|
||||
"consumes_unit": false,
|
||||
"notes": "ap_cost 'all' means drain() — current AP is consumed, capacity is retained. Pioneer survives as a citizen."
|
||||
}
|
||||
17
public/resources/unit_actions/prepare_land.json
Normal file
17
public/resources/unit_actions/prepare_land.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "prepare_land",
|
||||
"name": "Prepare Land",
|
||||
"description": "Reduce terrain cultural resistance on the current tile, making it a higher-priority expansion target. Effect decays if the Pioneer leaves (unless Boundary Markers tech is researched).",
|
||||
"ap_cost": 2,
|
||||
"ap_cost_value": 2,
|
||||
"eligible_units": ["pioneer"],
|
||||
"terrain_restrictions": {
|
||||
"requires_unclaimed": true,
|
||||
"blocked_by": ["water", "deep_ocean"]
|
||||
},
|
||||
"outcome": "terrain_resistance_reduced",
|
||||
"resistance_reduction_per_turn": 2,
|
||||
"decay_on_departure": true,
|
||||
"decay_override_tech": "boundary_markers",
|
||||
"notes": "Stackable per-tile. Multiple Prepare Land uses accumulate resistance reduction on the same tile."
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
]
|
||||
},
|
||||
"tier": 8,
|
||||
"action_point_capacity": 16,
|
||||
"great_person_class": "great_engineer",
|
||||
"great_person_gpp_type": "engineering",
|
||||
"great_person_produces": "wonder_hurry",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
]
|
||||
},
|
||||
"tier": 1,
|
||||
"action_point_capacity": 6,
|
||||
"great_person_class": "great_engineer",
|
||||
"great_person_gpp_type": "engineering",
|
||||
"great_person_produces": "wonder_hurry",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
]
|
||||
},
|
||||
"tier": 2,
|
||||
"action_point_capacity": 10,
|
||||
"capturable": true,
|
||||
"ransom_multiplier": 2.0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
]
|
||||
},
|
||||
"tier": 3,
|
||||
"action_point_capacity": 14,
|
||||
"great_person_class": "great_engineer",
|
||||
"great_person_gpp_type": "engineering",
|
||||
"great_person_produces": "wonder_hurry",
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
]
|
||||
},
|
||||
"tier": 2,
|
||||
"action_point_capacity": 10,
|
||||
"great_person_class": "great_engineer",
|
||||
"great_person_gpp_type": "engineering",
|
||||
"great_person_produces": "wonder_hurry",
|
||||
|
|
|
|||
709
src/game/engine/scenes/statistics/statistics.gd
Normal file
709
src/game/engine/scenes/statistics/statistics.gd
Normal file
|
|
@ -0,0 +1,709 @@
|
|||
class_name StatisticsModal
|
||||
extends Control
|
||||
## Five-tab statistics modal (p2-47).
|
||||
##
|
||||
## Tab 0 — Demographics: sortable single-turn rankings table (extracted from
|
||||
## the former standalone demographics.gd widget).
|
||||
## Tab 1 — Graphs: multi-line chart with Y-axis metric selector.
|
||||
## Tab 2 — Rankings: leaderboard for the selected metric + trend arrows.
|
||||
## Tab 3 — Replay: embeds replay_viewer.tscn scoped to the current game.
|
||||
## Tab 4 — Histories: per-clan chronicle; requires GdGameHistory bridge
|
||||
## (mc-replay/api-gdext, p3-05). Shows a pending-bridge notice until wired.
|
||||
##
|
||||
## Entry points: F9 key OR an "info" HUD button → world_map.gd →
|
||||
## main.push_overlay("res://engine/scenes/statistics/statistics.tscn").
|
||||
##
|
||||
## Last-viewed tab persisted in SettingsManager section "ui", key
|
||||
## "last_statistics_tab" so the modal reopens where the player left off.
|
||||
##
|
||||
## Rail-1: no simulation logic here. Data comes from StatsTracker autoload
|
||||
## (existing Demographics/Graphs data path) and, once wired, GdGameHistory.
|
||||
## Rail-2: metric label strings come from ThemeVocabulary.
|
||||
|
||||
const ReplayViewerScene: PackedScene = preload(
|
||||
"res://engine/scenes/menus/replay_viewer.tscn"
|
||||
)
|
||||
|
||||
const TAB_DEMOGRAPHICS: int = 0
|
||||
const TAB_GRAPHS: int = 1
|
||||
const TAB_RANKINGS: int = 2
|
||||
const TAB_REPLAY: int = 3
|
||||
const TAB_HISTORIES: int = 4
|
||||
|
||||
## Rank-badge colours, highest → lowest.
|
||||
const RANK_COLORS: Array[Color] = [
|
||||
Color(0.3, 0.9, 0.4),
|
||||
Color(0.9, 0.85, 0.3),
|
||||
Color(0.9, 0.6, 0.2),
|
||||
Color(0.9, 0.3, 0.3),
|
||||
]
|
||||
|
||||
## Active tab index.
|
||||
var _current_tab: int = 0
|
||||
## Active metric index (shared by Graphs + Rankings tabs).
|
||||
var _metric_idx: int = 0
|
||||
|
||||
## Replay viewer child (instantiated once, kept hidden when not on Replay tab).
|
||||
var _replay_viewer: Control = null
|
||||
|
||||
## Container nodes for each tab content area (set in _build_*).
|
||||
var _tab_bar: TabBar = null
|
||||
var _tab_panels: Array[Control] = []
|
||||
|
||||
## Demographics tab widgets.
|
||||
var _demo_list: VBoxContainer = null
|
||||
|
||||
## Graphs tab widgets.
|
||||
var _graph_area: Control = null
|
||||
var _graph_metric_label: Label = null
|
||||
|
||||
## Rankings tab widgets.
|
||||
var _rank_list: VBoxContainer = null
|
||||
var _rank_metric_label: Label = null
|
||||
|
||||
## Histories tab widgets.
|
||||
var _hist_notice: Label = null
|
||||
|
||||
## Close button.
|
||||
var _close_button: Button = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build_modal()
|
||||
_restore_last_tab()
|
||||
_refresh_current_tab()
|
||||
|
||||
|
||||
## ── Layout ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_modal() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
## Darkened backdrop.
|
||||
var backdrop: ColorRect = ColorRect.new()
|
||||
backdrop.name = "Backdrop"
|
||||
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
backdrop.color = Color(0.0, 0.0, 0.0, 0.72)
|
||||
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(backdrop)
|
||||
|
||||
## Centred panel.
|
||||
var panel: PanelContainer = PanelContainer.new()
|
||||
panel.name = "Panel"
|
||||
panel.anchor_left = 0.05
|
||||
panel.anchor_right = 0.95
|
||||
panel.anchor_top = 0.04
|
||||
panel.anchor_bottom = 0.96
|
||||
var style: StyleBoxFlat = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.09, 0.08, 0.07, 0.97)
|
||||
style.border_color = Color(0.55, 0.48, 0.32, 0.9)
|
||||
style.set_border_width_all(2)
|
||||
style.set_corner_radius_all(6)
|
||||
style.content_margin_left = 12.0
|
||||
style.content_margin_right = 12.0
|
||||
style.content_margin_top = 10.0
|
||||
style.content_margin_bottom = 10.0
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
add_child(panel)
|
||||
|
||||
var vbox: VBoxContainer = VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 8)
|
||||
panel.add_child(vbox)
|
||||
|
||||
## Title row.
|
||||
var title_row: HBoxContainer = HBoxContainer.new()
|
||||
title_row.add_theme_constant_override("separation", 8)
|
||||
vbox.add_child(title_row)
|
||||
|
||||
var title_lbl: Label = Label.new()
|
||||
title_lbl.name = "TitleLabel"
|
||||
title_lbl.text = ThemeVocabulary.lookup("statistics_title")
|
||||
title_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
title_lbl.add_theme_font_size_override("font_size", 20)
|
||||
title_lbl.add_theme_color_override("font_color", Color(0.9, 0.84, 0.65))
|
||||
title_row.add_child(title_lbl)
|
||||
|
||||
_close_button = Button.new()
|
||||
_close_button.name = "CloseButton"
|
||||
_close_button.text = ThemeVocabulary.lookup("close")
|
||||
_close_button.custom_minimum_size = Vector2(80, 32)
|
||||
_close_button.pressed.connect(_on_close)
|
||||
title_row.add_child(_close_button)
|
||||
|
||||
## Tab bar.
|
||||
_tab_bar = TabBar.new()
|
||||
_tab_bar.name = "TabBar"
|
||||
_tab_bar.add_tab(ThemeVocabulary.lookup("statistics_tab_demographics"))
|
||||
_tab_bar.add_tab(ThemeVocabulary.lookup("statistics_tab_graphs"))
|
||||
_tab_bar.add_tab(ThemeVocabulary.lookup("statistics_tab_rankings"))
|
||||
_tab_bar.add_tab(ThemeVocabulary.lookup("statistics_tab_replay"))
|
||||
_tab_bar.add_tab(ThemeVocabulary.lookup("statistics_tab_histories"))
|
||||
_tab_bar.tab_changed.connect(_on_tab_changed)
|
||||
vbox.add_child(_tab_bar)
|
||||
|
||||
## Content area — one Control per tab, swapped visible.
|
||||
var content_area: Control = Control.new()
|
||||
content_area.name = "ContentArea"
|
||||
content_area.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
content_area.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_child(content_area)
|
||||
|
||||
_build_demographics_tab(content_area)
|
||||
_build_graphs_tab(content_area)
|
||||
_build_rankings_tab(content_area)
|
||||
_build_replay_tab(content_area)
|
||||
_build_histories_tab(content_area)
|
||||
|
||||
|
||||
func _make_tab_panel(parent: Control, tab_index: int) -> Control:
|
||||
var panel: Control = Control.new()
|
||||
panel.name = "TabPanel%d" % tab_index
|
||||
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
panel.visible = false
|
||||
parent.add_child(panel)
|
||||
_tab_panels.append(panel)
|
||||
return panel
|
||||
|
||||
|
||||
func _build_demographics_tab(parent: Control) -> void:
|
||||
var panel: Control = _make_tab_panel(parent, TAB_DEMOGRAPHICS)
|
||||
|
||||
var scroll: ScrollContainer = ScrollContainer.new()
|
||||
scroll.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
panel.add_child(scroll)
|
||||
|
||||
var vbox: VBoxContainer = VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 4)
|
||||
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.add_child(vbox)
|
||||
|
||||
## Column headers.
|
||||
var header_row: HBoxContainer = HBoxContainer.new()
|
||||
vbox.add_child(header_row)
|
||||
|
||||
_add_header_label(header_row, ThemeVocabulary.lookup("demographics_col_category"), 160)
|
||||
_add_header_label(header_row, ThemeVocabulary.lookup("demographics_col_you"), 100, true)
|
||||
_add_header_label(header_row, ThemeVocabulary.lookup("demographics_col_rank"), 60, true)
|
||||
_add_header_label(header_row, ThemeVocabulary.lookup("demographics_col_best"), 100, true)
|
||||
_add_header_label(header_row, ThemeVocabulary.lookup("demographics_col_average"), 100, true)
|
||||
|
||||
var sep: HSeparator = HSeparator.new()
|
||||
vbox.add_child(sep)
|
||||
|
||||
_demo_list = VBoxContainer.new()
|
||||
_demo_list.name = "DemoList"
|
||||
_demo_list.add_theme_constant_override("separation", 2)
|
||||
vbox.add_child(_demo_list)
|
||||
|
||||
|
||||
func _build_graphs_tab(parent: Control) -> void:
|
||||
var panel: Control = _make_tab_panel(parent, TAB_GRAPHS)
|
||||
|
||||
var vbox: VBoxContainer = VBoxContainer.new()
|
||||
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.add_theme_constant_override("separation", 8)
|
||||
panel.add_child(vbox)
|
||||
|
||||
var ctrl_row: HBoxContainer = HBoxContainer.new()
|
||||
ctrl_row.add_theme_constant_override("separation", 8)
|
||||
vbox.add_child(ctrl_row)
|
||||
|
||||
var prev_btn: Button = Button.new()
|
||||
prev_btn.text = ThemeVocabulary.lookup("arrow_prev")
|
||||
prev_btn.pressed.connect(_on_metric_prev)
|
||||
ctrl_row.add_child(prev_btn)
|
||||
|
||||
_graph_metric_label = Label.new()
|
||||
_graph_metric_label.name = "GraphMetricLabel"
|
||||
_graph_metric_label.custom_minimum_size.x = 200
|
||||
_graph_metric_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_graph_metric_label.add_theme_font_size_override("font_size", 15)
|
||||
_graph_metric_label.add_theme_color_override("font_color", Color(0.85, 0.78, 0.58))
|
||||
ctrl_row.add_child(_graph_metric_label)
|
||||
|
||||
var next_btn: Button = Button.new()
|
||||
next_btn.text = ThemeVocabulary.lookup("arrow_next")
|
||||
next_btn.pressed.connect(_on_metric_next)
|
||||
ctrl_row.add_child(next_btn)
|
||||
|
||||
_graph_area = Control.new()
|
||||
_graph_area.name = "GraphArea"
|
||||
_graph_area.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_graph_area.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_graph_area.custom_minimum_size = Vector2(0, 200)
|
||||
vbox.add_child(_graph_area)
|
||||
|
||||
|
||||
func _build_rankings_tab(parent: Control) -> void:
|
||||
var panel: Control = _make_tab_panel(parent, TAB_RANKINGS)
|
||||
|
||||
var vbox: VBoxContainer = VBoxContainer.new()
|
||||
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.add_theme_constant_override("separation", 8)
|
||||
panel.add_child(vbox)
|
||||
|
||||
var ctrl_row: HBoxContainer = HBoxContainer.new()
|
||||
ctrl_row.add_theme_constant_override("separation", 8)
|
||||
vbox.add_child(ctrl_row)
|
||||
|
||||
var prev_btn: Button = Button.new()
|
||||
prev_btn.text = ThemeVocabulary.lookup("arrow_prev")
|
||||
prev_btn.pressed.connect(_on_metric_prev)
|
||||
ctrl_row.add_child(prev_btn)
|
||||
|
||||
_rank_metric_label = Label.new()
|
||||
_rank_metric_label.name = "RankMetricLabel"
|
||||
_rank_metric_label.custom_minimum_size.x = 200
|
||||
_rank_metric_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_rank_metric_label.add_theme_font_size_override("font_size", 15)
|
||||
_rank_metric_label.add_theme_color_override("font_color", Color(0.85, 0.78, 0.58))
|
||||
ctrl_row.add_child(_rank_metric_label)
|
||||
|
||||
var next_btn: Button = Button.new()
|
||||
next_btn.text = ThemeVocabulary.lookup("arrow_next")
|
||||
next_btn.pressed.connect(_on_metric_next)
|
||||
ctrl_row.add_child(next_btn)
|
||||
|
||||
var scroll: ScrollContainer = ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_child(scroll)
|
||||
|
||||
_rank_list = VBoxContainer.new()
|
||||
_rank_list.name = "RankList"
|
||||
_rank_list.add_theme_constant_override("separation", 4)
|
||||
_rank_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.add_child(_rank_list)
|
||||
|
||||
|
||||
func _build_replay_tab(parent: Control) -> void:
|
||||
var panel: Control = _make_tab_panel(parent, TAB_REPLAY)
|
||||
|
||||
## Embed the existing replay_viewer.tscn. Its Back button is suppressed
|
||||
## when embedded inside this modal — close goes through _on_close instead.
|
||||
_replay_viewer = ReplayViewerScene.instantiate() as Control
|
||||
_replay_viewer.name = "ReplayViewer"
|
||||
_replay_viewer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
panel.add_child(_replay_viewer)
|
||||
|
||||
|
||||
func _build_histories_tab(parent: Control) -> void:
|
||||
var panel: Control = _make_tab_panel(parent, TAB_HISTORIES)
|
||||
|
||||
_hist_notice = Label.new()
|
||||
_hist_notice.name = "HistoriesNotice"
|
||||
_hist_notice.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_hist_notice.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_hist_notice.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_hist_notice.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_hist_notice.text = ThemeVocabulary.lookup("statistics_histories_pending")
|
||||
_hist_notice.add_theme_font_size_override("font_size", 15)
|
||||
_hist_notice.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
|
||||
panel.add_child(_hist_notice)
|
||||
|
||||
|
||||
## ── Tab switching ─────────────────────────────────────────────────────────────
|
||||
|
||||
func _on_tab_changed(tab: int) -> void:
|
||||
_current_tab = tab
|
||||
_persist_tab(tab)
|
||||
_show_tab(tab)
|
||||
_refresh_current_tab()
|
||||
|
||||
|
||||
func _show_tab(tab: int) -> void:
|
||||
for i: int in _tab_panels.size():
|
||||
_tab_panels[i].visible = (i == tab)
|
||||
|
||||
|
||||
func _restore_last_tab() -> void:
|
||||
var saved: int = 0
|
||||
if Engine.has_singleton("SettingsManager"):
|
||||
saved = int(SettingsManager.get_setting("ui", "last_statistics_tab"))
|
||||
elif _has_settings_manager_node():
|
||||
saved = int(SettingsManager.get_setting("ui", "last_statistics_tab"))
|
||||
_current_tab = clampi(saved, 0, 4)
|
||||
_tab_bar.current_tab = _current_tab
|
||||
_show_tab(_current_tab)
|
||||
|
||||
|
||||
func _persist_tab(tab: int) -> void:
|
||||
if Engine.has_singleton("SettingsManager") or _has_settings_manager_node():
|
||||
SettingsManager.set_setting("ui", "last_statistics_tab", tab)
|
||||
|
||||
|
||||
func _has_settings_manager_node() -> bool:
|
||||
var tree: SceneTree = Engine.get_main_loop() as SceneTree
|
||||
if tree == null or tree.root == null:
|
||||
return false
|
||||
return tree.root.has_node("SettingsManager")
|
||||
|
||||
|
||||
## ── Refresh routing ──────────────────────────────────────────────────────────
|
||||
|
||||
func _refresh_current_tab() -> void:
|
||||
match _current_tab:
|
||||
TAB_DEMOGRAPHICS:
|
||||
_refresh_demographics()
|
||||
TAB_GRAPHS:
|
||||
_refresh_graph()
|
||||
TAB_RANKINGS:
|
||||
_refresh_rankings()
|
||||
TAB_REPLAY, TAB_HISTORIES:
|
||||
pass ## Static content — no dynamic refresh needed.
|
||||
|
||||
|
||||
## ── Demographics tab ─────────────────────────────────────────────────────────
|
||||
|
||||
func _refresh_demographics() -> void:
|
||||
for child: Node in _demo_list.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var current_idx: int = GameState.current_player_index
|
||||
|
||||
for cat: String in StatsTracker.CATEGORIES:
|
||||
var label_text: String = StatsTracker.category_labels.get(cat, cat) as String
|
||||
var rankings: Array = StatsTracker.get_rankings(cat)
|
||||
if rankings.is_empty():
|
||||
_add_demo_row(label_text, "--", 0, "--", "--")
|
||||
continue
|
||||
|
||||
var my_val: int = 0
|
||||
var my_rank: int = 0
|
||||
var best_val: int = int(rankings[0]["value"])
|
||||
var total: int = 0
|
||||
for entry: Dictionary in rankings:
|
||||
total += int(entry["value"])
|
||||
if int(entry["index"]) == current_idx:
|
||||
my_val = int(entry["value"])
|
||||
my_rank = int(entry["rank"])
|
||||
|
||||
var avg: float = float(total) / maxf(float(rankings.size()), 1.0)
|
||||
_add_demo_row(
|
||||
label_text,
|
||||
_format_val(my_val),
|
||||
my_rank,
|
||||
_format_val(best_val),
|
||||
_format_val(int(avg)),
|
||||
)
|
||||
|
||||
|
||||
func _add_demo_row(
|
||||
category: String, value: String, rank: int,
|
||||
best: String, average: String
|
||||
) -> void:
|
||||
var row: HBoxContainer = HBoxContainer.new()
|
||||
row.custom_minimum_size.y = 32
|
||||
|
||||
var cat_lbl: Label = Label.new()
|
||||
cat_lbl.text = category
|
||||
cat_lbl.custom_minimum_size.x = 160
|
||||
cat_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
cat_lbl.add_theme_font_size_override("font_size", 13)
|
||||
cat_lbl.add_theme_color_override("font_color", Color(0.7, 0.65, 0.5))
|
||||
row.add_child(cat_lbl)
|
||||
|
||||
var val_lbl: Label = Label.new()
|
||||
val_lbl.text = value
|
||||
val_lbl.custom_minimum_size.x = 100
|
||||
val_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
val_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
val_lbl.add_theme_font_size_override("font_size", 14)
|
||||
row.add_child(val_lbl)
|
||||
|
||||
var rank_lbl: Label = Label.new()
|
||||
rank_lbl.custom_minimum_size.x = 60
|
||||
rank_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
rank_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
rank_lbl.add_theme_font_size_override("font_size", 14)
|
||||
if rank > 0:
|
||||
rank_lbl.text = "#%d" % rank
|
||||
var ci: int = mini(rank - 1, RANK_COLORS.size() - 1)
|
||||
rank_lbl.add_theme_color_override("font_color", RANK_COLORS[ci])
|
||||
else:
|
||||
rank_lbl.text = "--"
|
||||
rank_lbl.add_theme_color_override("font_color", Color(0.4, 0.4, 0.4))
|
||||
row.add_child(rank_lbl)
|
||||
|
||||
var best_lbl: Label = Label.new()
|
||||
best_lbl.text = best
|
||||
best_lbl.custom_minimum_size.x = 100
|
||||
best_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
best_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
best_lbl.add_theme_font_size_override("font_size", 13)
|
||||
best_lbl.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
|
||||
row.add_child(best_lbl)
|
||||
|
||||
var avg_lbl: Label = Label.new()
|
||||
avg_lbl.text = average
|
||||
avg_lbl.custom_minimum_size.x = 100
|
||||
avg_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
avg_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
avg_lbl.add_theme_font_size_override("font_size", 13)
|
||||
avg_lbl.add_theme_color_override("font_color", Color(0.4, 0.4, 0.4))
|
||||
row.add_child(avg_lbl)
|
||||
|
||||
_demo_list.add_child(row)
|
||||
|
||||
|
||||
## ── Graphs tab ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _refresh_graph() -> void:
|
||||
var cat: String = StatsTracker.CATEGORIES[_metric_idx]
|
||||
_graph_metric_label.text = StatsTracker.category_labels.get(cat, cat) as String
|
||||
|
||||
for child: Node in _graph_area.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var hist: Array = StatsTracker.get_history()
|
||||
if hist.is_empty():
|
||||
_add_graph_placeholder(ThemeVocabulary.lookup("no_history"))
|
||||
return
|
||||
|
||||
var min_val: float = INF
|
||||
var max_val: float = -INF
|
||||
var max_turn: int = 1
|
||||
for snap: Dictionary in hist:
|
||||
var turn: int = snap.get("turn", 1) as int
|
||||
if turn > max_turn:
|
||||
max_turn = turn
|
||||
for pd: Dictionary in snap.get("players", []):
|
||||
var v: float = float(pd.get(cat, 0))
|
||||
min_val = minf(min_val, v)
|
||||
max_val = maxf(max_val, v)
|
||||
|
||||
if min_val == INF:
|
||||
min_val = 0.0
|
||||
if max_val <= min_val:
|
||||
max_val = min_val + 1.0
|
||||
|
||||
var area_size: Vector2 = _graph_area.size
|
||||
if area_size.x < 10.0 or area_size.y < 10.0:
|
||||
area_size = Vector2(600.0, 280.0)
|
||||
|
||||
var pad: float = 24.0
|
||||
var plot_w: float = area_size.x - pad * 2.0
|
||||
var plot_h: float = area_size.y - pad * 2.0
|
||||
|
||||
for p: Variant in GameState.players:
|
||||
if p == null:
|
||||
continue
|
||||
var series: Array = StatsTracker.get_player_series(int(p.get("index")), cat)
|
||||
if series.size() < 2:
|
||||
continue
|
||||
var line: Line2D = Line2D.new()
|
||||
line.width = 2.0
|
||||
line.default_color = p.get("color") as Color if p.get("color") is Color else Color.WHITE
|
||||
line.antialiased = true
|
||||
for pt: Dictionary in series:
|
||||
var t: float = float(pt.get("turn", 1))
|
||||
var v: float = float(pt.get("value", 0))
|
||||
var x: float = pad + (t / maxf(float(max_turn), 1.0)) * plot_w
|
||||
var y: float = pad + (1.0 - (v - min_val) / (max_val - min_val)) * plot_h
|
||||
line.add_point(Vector2(x, y))
|
||||
_graph_area.add_child(line)
|
||||
|
||||
_add_axis_label(Vector2(pad, pad - 14.0), _format_val(int(max_val)))
|
||||
_add_axis_label(
|
||||
Vector2(pad, area_size.y - pad + 2.0),
|
||||
_format_val(int(min_val))
|
||||
)
|
||||
_add_axis_label(
|
||||
Vector2(area_size.x - pad - 36.0, area_size.y - pad + 2.0),
|
||||
"T%d" % max_turn
|
||||
)
|
||||
|
||||
|
||||
func _add_axis_label(pos: Vector2, text: String) -> void:
|
||||
var lbl: Label = Label.new()
|
||||
lbl.text = text
|
||||
lbl.position = pos
|
||||
lbl.add_theme_font_size_override("font_size", 10)
|
||||
lbl.add_theme_color_override("font_color", Color(0.4, 0.4, 0.4))
|
||||
_graph_area.add_child(lbl)
|
||||
|
||||
|
||||
func _add_graph_placeholder(text: String) -> void:
|
||||
var lbl: Label = Label.new()
|
||||
lbl.text = text
|
||||
lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
lbl.add_theme_font_size_override("font_size", 16)
|
||||
lbl.add_theme_color_override("font_color", Color(0.4, 0.4, 0.4))
|
||||
_graph_area.add_child(lbl)
|
||||
|
||||
|
||||
## ── Rankings tab ─────────────────────────────────────────────────────────────
|
||||
|
||||
func _refresh_rankings() -> void:
|
||||
var cat: String = StatsTracker.CATEGORIES[_metric_idx]
|
||||
_rank_metric_label.text = StatsTracker.category_labels.get(cat, cat) as String
|
||||
|
||||
for child: Node in _rank_list.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var rankings: Array = StatsTracker.get_rankings(cat)
|
||||
if rankings.is_empty():
|
||||
_add_rank_placeholder(ThemeVocabulary.lookup("no_data"))
|
||||
return
|
||||
|
||||
## Previous-turn data for trend arrows.
|
||||
var hist: Array = StatsTracker.get_history()
|
||||
var prev_snap: Dictionary = {}
|
||||
if hist.size() >= 2:
|
||||
prev_snap = hist[hist.size() - 2]
|
||||
|
||||
for entry: Dictionary in rankings:
|
||||
var idx: int = entry.get("index", -1) as int
|
||||
var rank: int = entry.get("rank", 0) as int
|
||||
var value: int = int(entry.get("value", 0))
|
||||
var player: RefCounted = GameState.get_player(idx) as RefCounted
|
||||
var player_name: String = "Player %d" % idx
|
||||
if player != null:
|
||||
player_name = str(player.get("player_name"))
|
||||
|
||||
## Trend: compare to previous turn snapshot value for the same player/cat.
|
||||
var trend: int = 0
|
||||
for pd: Dictionary in prev_snap.get("players", []):
|
||||
if int(pd.get("index", -1)) == idx:
|
||||
var prev_val: int = int(pd.get(cat, 0))
|
||||
if value > prev_val:
|
||||
trend = 1
|
||||
elif value < prev_val:
|
||||
trend = -1
|
||||
break
|
||||
|
||||
var clan_color: Color = Color.GRAY
|
||||
if player != null and player.get("color") is Color:
|
||||
clan_color = player.get("color") as Color
|
||||
|
||||
_add_rank_row(rank, player_name, clan_color, _format_val(value), trend)
|
||||
|
||||
|
||||
func _add_rank_row(
|
||||
rank: int, name: String, clan_color: Color,
|
||||
value: String, trend: int
|
||||
) -> void:
|
||||
var row: HBoxContainer = HBoxContainer.new()
|
||||
row.custom_minimum_size.y = 36
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
|
||||
var rank_lbl: Label = Label.new()
|
||||
rank_lbl.text = "#%d" % rank
|
||||
rank_lbl.custom_minimum_size.x = 44
|
||||
rank_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
rank_lbl.add_theme_font_size_override("font_size", 16)
|
||||
var ci: int = mini(rank - 1, RANK_COLORS.size() - 1)
|
||||
rank_lbl.add_theme_color_override("font_color", RANK_COLORS[ci])
|
||||
row.add_child(rank_lbl)
|
||||
|
||||
var dot: ColorRect = ColorRect.new()
|
||||
dot.custom_minimum_size = Vector2(14, 14)
|
||||
dot.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
dot.color = clan_color
|
||||
row.add_child(dot)
|
||||
|
||||
var name_lbl: Label = Label.new()
|
||||
name_lbl.text = name
|
||||
name_lbl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
name_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
name_lbl.add_theme_font_size_override("font_size", 15)
|
||||
row.add_child(name_lbl)
|
||||
|
||||
var val_lbl: Label = Label.new()
|
||||
val_lbl.text = value
|
||||
val_lbl.custom_minimum_size.x = 80
|
||||
val_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
val_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
val_lbl.add_theme_font_size_override("font_size", 15)
|
||||
val_lbl.add_theme_color_override("font_color", Color(0.95, 0.82, 0.3))
|
||||
row.add_child(val_lbl)
|
||||
|
||||
var trend_lbl: Label = Label.new()
|
||||
trend_lbl.custom_minimum_size.x = 24
|
||||
trend_lbl.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
trend_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
trend_lbl.add_theme_font_size_override("font_size", 15)
|
||||
match trend:
|
||||
1:
|
||||
trend_lbl.text = ThemeVocabulary.lookup("trend_up")
|
||||
trend_lbl.add_theme_color_override("font_color", Color(0.3, 0.9, 0.4))
|
||||
-1:
|
||||
trend_lbl.text = ThemeVocabulary.lookup("trend_down")
|
||||
trend_lbl.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3))
|
||||
_:
|
||||
trend_lbl.text = ThemeVocabulary.lookup("trend_flat")
|
||||
trend_lbl.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
|
||||
row.add_child(trend_lbl)
|
||||
|
||||
_rank_list.add_child(row)
|
||||
|
||||
|
||||
func _add_rank_placeholder(text: String) -> void:
|
||||
var lbl: Label = Label.new()
|
||||
lbl.text = text
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
lbl.add_theme_font_size_override("font_size", 15)
|
||||
lbl.add_theme_color_override("font_color", Color(0.4, 0.4, 0.4))
|
||||
_rank_list.add_child(lbl)
|
||||
|
||||
|
||||
## ── Metric cycling (Graphs + Rankings share _metric_idx) ─────────────────────
|
||||
|
||||
func _on_metric_prev() -> void:
|
||||
var count: int = StatsTracker.CATEGORIES.size()
|
||||
_metric_idx = (_metric_idx - 1 + count) % count
|
||||
if _current_tab == TAB_GRAPHS:
|
||||
_refresh_graph()
|
||||
elif _current_tab == TAB_RANKINGS:
|
||||
_refresh_rankings()
|
||||
|
||||
|
||||
func _on_metric_next() -> void:
|
||||
var count: int = StatsTracker.CATEGORIES.size()
|
||||
_metric_idx = (_metric_idx + 1) % count
|
||||
if _current_tab == TAB_GRAPHS:
|
||||
_refresh_graph()
|
||||
elif _current_tab == TAB_RANKINGS:
|
||||
_refresh_rankings()
|
||||
|
||||
|
||||
## ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func _add_header_label(
|
||||
parent: HBoxContainer, text: String, min_w: int,
|
||||
right_align: bool = false
|
||||
) -> void:
|
||||
var lbl: Label = Label.new()
|
||||
lbl.text = text
|
||||
lbl.custom_minimum_size.x = min_w
|
||||
lbl.add_theme_font_size_override("font_size", 12)
|
||||
lbl.add_theme_color_override("font_color", Color(0.55, 0.5, 0.38))
|
||||
if right_align:
|
||||
lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
parent.add_child(lbl)
|
||||
|
||||
|
||||
func _format_val(value: int) -> String:
|
||||
if value >= 10000:
|
||||
return "%.1fK" % (float(value) / 1000.0)
|
||||
return str(value)
|
||||
|
||||
|
||||
## ── Input / close ────────────────────────────────────────────────────────────
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not event is InputEventKey:
|
||||
return
|
||||
var key: InputEventKey = event as InputEventKey
|
||||
if key.pressed and (key.keycode == KEY_ESCAPE or key.keycode == KEY_F9):
|
||||
get_viewport().set_input_as_handled()
|
||||
_on_close()
|
||||
|
||||
|
||||
func _on_close() -> void:
|
||||
var main: Node = get_tree().root.get_node_or_null("Main")
|
||||
if main != null and main.has_method("pop_overlay"):
|
||||
main.pop_overlay()
|
||||
else:
|
||||
queue_free()
|
||||
|
|
@ -77,7 +77,7 @@ func _on_stats() -> void:
|
|||
_close()
|
||||
var main: Node = get_tree().root.get_node_or_null("Main")
|
||||
if main != null and main.has_method("push_overlay"):
|
||||
main.push_overlay("res://engine/scenes/overviews/demographics.tscn")
|
||||
main.push_overlay("res://engine/scenes/statistics/statistics.tscn")
|
||||
|
||||
|
||||
func _on_options() -> void:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue