712 lines
25 KiB
GDScript
712 lines
25 KiB
GDScript
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. Routed through semantic design tokens
|
|
## (good → warning → diplomacy → bad); populated in _ready() because token lookups
|
|
## are autoload calls and cannot appear in a const initializer.
|
|
var _rank_colors: Array[Color] = []
|
|
|
|
## 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:
|
|
_rank_colors = [
|
|
ThemeAssets.color("semantic.positive"),
|
|
ThemeAssets.color("semantic.warning"),
|
|
ThemeAssets.color("semantic.diplomacy"),
|
|
ThemeAssets.color("semantic.negative"),
|
|
]
|
|
_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 = ThemeAssets.color("background.overlay")
|
|
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 = ThemeAssets.color("background.panel")
|
|
style.border_color = ThemeAssets.color("border.panel")
|
|
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", ThemeAssets.color("text.title"))
|
|
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", ThemeAssets.color("text.secondary"))
|
|
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", ThemeAssets.color("text.secondary"))
|
|
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", ThemeAssets.color("text.muted"))
|
|
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", ThemeAssets.color("text.secondary"))
|
|
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", ThemeAssets.color("text.disabled"))
|
|
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", ThemeAssets.color("text.muted"))
|
|
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", ThemeAssets.color("text.disabled"))
|
|
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", ThemeAssets.color("text.disabled"))
|
|
_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", ThemeAssets.color("text.disabled"))
|
|
_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", ThemeAssets.color("accent.goldResource"))
|
|
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", ThemeAssets.color("semantic.positive"))
|
|
-1:
|
|
trend_lbl.text = ThemeVocabulary.lookup("trend_down")
|
|
trend_lbl.add_theme_color_override("font_color", ThemeAssets.color("semantic.negative"))
|
|
_:
|
|
trend_lbl.text = ThemeVocabulary.lookup("trend_flat")
|
|
trend_lbl.add_theme_color_override("font_color", ThemeAssets.color("text.muted"))
|
|
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", ThemeAssets.color("text.disabled"))
|
|
_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", ThemeAssets.color("text.muted"))
|
|
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()
|