3 Commits

Author SHA1 Message Date
Kirill a7c05e3796 Add report generation functionality to the UI
- Introduced a new script `report.gd` that extends `VBoxContainer` to manage the display of reports.
- Implemented dynamic localization support for report headers and body content based on the global locale.
- Created a `refresh` method to update the report content and header image when visibility changes or locale updates.
- Added a static `build` method to format and compile report data from the world state, including calculations for star totals and effects.
- Integrated presentation logic to render report templates based on the current state of the game world.
2026-10-18 21:48:15 +03:00
Kirill 2aced0c61a Add localization support for achievements: implement language-specific titles and update UI components 2026-10-18 20:32:20 +03:00
Kirill e1782c7b10 Implement achievement system: add achievement controller, UI components, and tests 2026-10-18 20:14:27 +03:00
19 changed files with 533 additions and 48 deletions
+6 -6
View File
@@ -1008,7 +1008,7 @@ Trends.Report.Header|[b]Влияние развитых цивилизаций
Int.Fed.Report.Max|[Интеграция] Федерация: [color=#cccc88]+%value0%[/color]. Увеличено число цивилизаций, подверженных воздействию дипломатии.|max||||
Int.Enl.Report.Max|[Интеграция] Просвещение: [color=#cccc88]+%value0%[/color]. Ускорен прогресс на примитивных стадиях жизни.|max||||
Int.Ord.Report.Max|[Интеграция] Новый порядок: [color=#cccc88]+%value0%[/color]. Оказывается помощь и поддержка цивилизациям, попавшим в кризисные обстоятельства.|max||||
Emerged.Report.Header|[b]Максимальное число возникших сущностей[/b]|||||
Emerged.Report.Header|[b]Всего возникло сущностей[/b]|||||
Ctm.Report.Max|Трансцендентная цивилизация мистиков: [color=#cccc88]%value0%[/color]|max||||
Ctb.Report.Max|Трансцендентная цивилизация строителей: [color=#cccc88]%value0%[/color]|max||||
Ctd.Report.Max|Трансцендентная цивилизация дипломатов: [color=#cccc88]%value0%[/color]|max||||
@@ -1075,7 +1075,7 @@ HP.Report.Max|Обитаемая планета: [color=#cccc88]%value0%[/color]
RP.Report.Max|Каменистая планета: [color=#cccc88]%value0%[/color]|max||||
AB.Report.Max|Астероиды: [color=#cccc88]%value0%[/color]|max||||
GG.Report.Max|Газовый гигант: [color=#cccc88]%value0%[/color]|max||||
Produced.Report.Header|[b]Максимальное число созданных объектов[/b]|||||
Produced.Report.Header|[b]Всего создано объектов[/b]|||||
LUB.Report.Max|Вселенский пузырь: [color=#cccc88]%value0%[/color]|max||||
LGW.Report.Max|Великая стена: [color=#cccc88]%value0%[/color]|max||||
LGF.Report.Max|Галактическая нить: [color=#cccc88]%value0%[/color]|max||||
@@ -1116,7 +1116,7 @@ YD.Report.Max|Жёлтый карлик: [color=#cccc88]%value0%[/color]|max||||
OD.Report.Max|Оранжевый карлик: [color=#cccc88]%value0%[/color]|max||||
RD.Report.Max|Красный карлик: [color=#cccc88]%value0%[/color]|max||||
BD.Report.Max|Бурый карлик: [color=#cccc88]%value0%[/color]|max||||
Obtained.Report.Header|[b]Максимум за время существования вселенной[/b]|||||
Obtained.Report.Header|[b]Всего получено ресурсов[/b]|||||
S.Report.Max|Пространство: [color=#cccc88]%value0%[/color]|max||||
H.Report.Max|Водород: [color=#cccc88]%value0%[/color]|max||||
He.Report.Max|Гелий: [color=#cccc88]%value0%[/color]|max||||
@@ -2199,7 +2199,7 @@ Trends.Header|[b]Influence of advanced civilizations on the universe[/b]|||||
Int.Fed.Max|[Integration] Federation: [color=#cccc88]+%value0%[/color]. The number of civilizations affected by diplomacy is increased.|max||||
Int.Enl.Max|[Integration] Enlightenment: [color=#cccc88]+%value0%[/color]. The progress during primitive stages of life development is accelerated.|max||||
Int.Ord.Max|[Integration] New Order: [color=#cccc88]+%value0%[/color]. Civilizations in crisis obtain aid and support.|max||||
Emerged.Header|[b]Maximum amount of emerged entities[/b]|||||
Emerged.Header|[b]Total entities emerged[/b]|||||
Ctm.Max|Transcendent mystics civilization: [color=#cccc88]%value0%[/color]|max||||
Ctb.Max|Transcendent builders civilization: [color=#cccc88]%value0%[/color]|max||||
Ctd.Max|Transcendent diplomats civilization: [color=#cccc88]%value0%[/color]|max||||
@@ -2266,7 +2266,7 @@ HP.Max|Habitable Planet: [color=#cccc88]%value0%[/color]|max||||
RP.Max|Rocky Planet: [color=#cccc88]%value0%[/color]|max||||
AB.Max|Asteroids: [color=#cccc88]%value0%[/color]|max||||
GG.Max|Gas Giant: [color=#cccc88]%value0%[/color]|max||||
Produced.Header|[b]Maximum amount of created objects[/b]|||||
Produced.Header|[b]Total objects created[/b]|||||
LUB.Max|Universe Bubble: [color=#cccc88]%value0%[/color]|max||||
LGW.Max|Great Wall: [color=#cccc88]%value0%[/color]|max||||
LGF.Max|Galactic Filament: [color=#cccc88]%value0%[/color]|max||||
@@ -2307,7 +2307,7 @@ YD.Max|Yellow Dwarf: [color=#cccc88]%value0%[/color]|max||||
OD.Max|Orange Dwarf: [color=#cccc88]%value0%[/color]|max||||
RD.Max|Red Dwarf: [color=#cccc88]%value0%[/color]|max||||
BD.Max|Brown Dwarf: [color=#cccc88]%value0%[/color]|max||||
Obtained.Header|[b]Maximum for all the existence of the universe[/b]|||||
Obtained.Header|[b]Total resources obtained[/b]|||||
S.Max|Space: [color=#cccc88]%value0%[/color]|max||||
H.Max|Hydrogen: [color=#cccc88]%value0%[/color]|max||||
He.Max|Helium: [color=#cccc88]%value0%[/color]|max||||
+1 -1
View File
@@ -12,7 +12,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../../Documents/Owad/Owniverse.I/Builds/owniverse.0.7.0-146.console.exe"
export_path="../../../Documents/Owad/Owniverse.I/Builds/owniverse.0.7.0-146.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
+43
View File
@@ -249,3 +249,46 @@ H списывает только оставшуюся после замещен
`tests/test_extra_params.gd` проверяет загрузку точных чисел и изменение энергии,
плотности, объёма пустот, порогов индикатора и бонуса горючести.
## Достижения и уведомления
`OwniverseAchievementLoader` задаёт локализованные достижения. Строки с
`Ignore=1` исключены из проверок, списка и общего количества. Для включённых
строк поддерживаются `age::порог` и `total:ID:порог`. `total` — накопленные
поступления `stats.total_amount`, включая автоматическое производство и
эволюцию; расход и гибель объектов не уменьшают этот счётчик. Возраст берётся
из завершённого состояния симуляции. Пороги разбираются точной арифметикой.
`ui/achievements/achievement_controller.gd` проверяет условия при смене
ревизии состояния и после создания новой вселенной. Пустое OpenCondition
делает достижение видимым без уведомления; непустое открывает его с серой
иконкой `g`. Выполнение AchieveCondition выдаёт цветную иконку `c`. Если оба
условия выполнены одновременно, показывается только цветное уведомление.
Уже полученные достижения повторно не выдаются.
Состояния видимости и получения сохраняются отдельно в
`user://achievements.cfg` и сохраняются между вселенными. Очередь уведомлений
не сохраняется. Единая панель поверх индикаторов следует за выбранной
страницей: показ 3 секунды, затем затухание 0,3 секунды. Меню приостанавливает
таймер. Клик или касание закрывает текущее уведомление; очередь сохраняется. После показа индикаторы возвращаются. Тексты списка и уведомления
обновляются при смене языка; список прокручивается.
`tests/test_achievements.gd` проверяет все включённые пороги, фильтр Ignore,
открытие и получение, отсутствие повторных уведомлений, паузу в меню,
возврат индикаторов, сохранение прогресса и локализацию. Файл прогресса теста
создаётся отдельно в `/tmp`.
## Отчёт о вселенной
`ui/report/report.gd` форматирует готовое состояние при открытии
экрана отчёта и смене языка. Результат хранится в `Report/ReportBody`.
Периодического пересчёта отчёта и новых счётчиков симуляции нет.
Доли восьми типов звёзд учитывают звёзды в структурах. Строки количеств
используют накопленные `stats.total_amount` с существующими алиасами;
заголовки обозначают суммарное получение, а не исторические максимумы.
Эффекты интеграции показывают прирост готовых параметров Federation,
Enlightenment и Order относительно начального значения 1.
Нулевые строки и пустые разделы скрыты. Количества выводятся полностью,
с пробелами между группами по три цифры. Тексты и заголовок поддерживают RU/EN.
+6
View File
@@ -7,6 +7,7 @@ const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
const ItemDescription = preload("res://world/owniverse/item_description.gd")
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
var achievements: Node
var world_indicators: RefCounted
var indicator_elapsed: float = 0.0
var indicator_revision: int = -1
@@ -339,6 +340,10 @@ func _ready() -> void:
create_new_universe()
load_data()
_refresh_universe_ui()
achievements = preload("res://ui/achievements/achievement_controller.gd").new()
add_child(achievements)
achievements.initialize(self)
achievements.update(0.0)
g_counters.swipe.sec = 0.2
@@ -406,6 +411,7 @@ func _process(delta: float) -> void:
owniverse.evolute_frame(delta, get_tree())
_process_apocalypse(delta)
if achievements != null: achievements.update(delta)
clock_display_elapsed += delta
if clock_display_elapsed >= 0.1:
clock_display_elapsed = fmod(clock_display_elapsed, 0.1)
+129
View File
@@ -0,0 +1,129 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Controller = preload("res://ui/achievements/achievement_controller.gd")
var failures := 0
class MockWorld extends RefCounted:
var state := {"time": {"year": 0}, "stats": {"total_amount": {}}}
var presentation := {"revision": 0}
class MockStage extends Control:
const PAGE_WIDTH = 1080
var owniverse = MockWorld.new()
var screens: Dictionary
var stage_page_scroller: ScrollContainer
func _initialize() -> void:
call_deferred("run")
func check(value: bool, message: String) -> void:
if not value:
failures += 1
push_error(message)
func node(parent: Node, child: Node, label: String) -> Node:
child.name = label
parent.add_child(child)
return child
func run() -> void:
check(load("res://stage/stage.gd") != null, "Stage integration compiles")
var scene: PackedScene = load("res://world/world.tscn")
var scene_state: SceneState = scene.get_state()
for language in ["English", "Russian"]:
var connected := false
for i in scene_state.get_connection_count():
if str(scene_state.get_connection_source(i)) == "Stage/MainMenu/Language/" + language:
connected = scene_state.get_connection_signal(i) == &"pressed" and str(scene_state.get_connection_target(i)) == "Stage/MainMenu" and scene_state.get_connection_method(i) == &"_on_change_locale_pressed" and scene_state.get_connection_binds(i) == ["en" if language == "English" else "ru"]
check(connected, language + " button connects to locale handler")
var stage := MockStage.new()
root.add_child(stage)
var world = node(stage, Control.new(), "World")
stage.screens = {"World": world}
stage.stage_page_scroller = node(world, ScrollContainer.new(), "PageScroller")
var pages = node(stage.stage_page_scroller, HBoxContainer.new(), "HBoxContainer")
for names in [["ForcePage", "ForceHeader"], ["ResourcePage", "ResourceHeader"], ["StarPage", "StarHeader"], ["StructurePage", "StructureHeader"], ["LifePage", "LifeHeader"]]:
node(node(pages, VBoxContainer.new(), names[0]), HBoxContainer.new(), names[1])
var screen = node(stage, Control.new(), "Achievements")
var title = node(node(screen, Control.new(), "Header"), TextureRect.new(), "Title")
node(node(screen, ScrollContainer.new(), "AchievementList"), VBoxContainer.new(), "AchievementItems")
var controller = Controller.new()
controller.progress_path = "/tmp/owniverse-achievements-test-%s.cfg" % OS.get_process_id()
stage.add_child(controller)
controller.initialize(stage)
check(controller.definitions.size() == 72, "All 72 enabled achievements loaded")
check(not controller.definitions.has("a95"), "Ignored achievement excluded")
controller.update(0.0)
check(controller.queue.is_empty() and controller.current.is_empty(), "Initially visible achievements do not notify")
stage.owniverse.state.stats.total_amount.H = E.of("5E4")
stage.owniverse.presentation.revision += 1
controller.update(0.0)
check(controller.progress.a13.unlocked, "Cumulative hydrogen threshold unlocks")
check(controller.progress.a14.visible and not controller.progress.a14.unlocked, "Opening threshold reveals next achievement")
controller.queue.clear()
controller._enqueue("a14", "g")
controller._enqueue("a14", "c")
check(controller.queue.size() == 1 and controller.queue[0].type == "c", "Pending reveal upgrades to completion")
controller.queue.clear()
controller.current = {}
stage.owniverse.state.time.year = E.of("1E12")
stage.owniverse.state.stats.total_amount.H = E.of("1E24")
stage.owniverse.state.stats.total_amount.Star = E.of("9E16")
stage.owniverse.presentation.revision += 1
controller.update(0.0)
check(controller.progress.a4.unlocked and controller.progress.a20.unlocked and controller.progress.a71.unlocked, "Exact large thresholds unlock")
check(controller.current.type == "c", "Immediate completion emits only completed notification")
var count: int = controller.queue.size()
controller.update(0.0)
check(controller.queue.size() == count, "Repeated update does not duplicate events")
world.hide()
var time: float = controller.elapsed
controller.update(5.0)
check(controller.elapsed == time, "Menu pauses notification")
world.show()
controller.update(3.4)
check(controller.current.is_empty() and not controller.panel.visible, "Notification expires")
for header in controller.headers: check(header.modulate.a == 1.0, "Indicators restored")
controller.update(0.0)
var remaining: int = controller.queue.size()
var click := InputEventMouseButton.new()
click.button_index = MOUSE_BUTTON_LEFT
click.pressed = true
controller._on_notification_input(click)
check(controller.current.is_empty() and not controller.panel.visible, "Click dismisses notification")
check(controller.queue.size() == remaining, "Click preserves pending notifications")
for header in controller.headers: check(header.modulate.a == 1.0, "Click restores indicators")
stage.owniverse = MockWorld.new()
controller.update(0.0)
check(controller.progress.a71.unlocked, "New universe preserves unlocked achievements")
controller.progress.clear()
controller._load_progress()
check(controller.progress.a71.unlocked, "Progress survives reload")
# Every enabled definition must be reachable through its actual compiled rule.
for id in controller.definitions:
var rule: Dictionary = controller.conditions[id].achieve
var source := {"time": {"year": 0}, "stats": {"total_amount": {}}}
if rule.type == "age": source.time.year = E.subtract(rule.amount, 1)
else: source.stats.total_amount[rule.id] = E.subtract(rule.amount, 1)
check(not controller._matches(rule, source), id + " stays locked below threshold")
if rule.type == "age": source.time.year = rule.amount
else: source.stats.total_amount[rule.id] = rule.amount
check(controller._matches(rule, source), id + " completes at exact threshold")
var global: Node = root.get_node("Global")
var previous_locale: String = global.Locale
global.Locale = "ru"
controller.update(0.0)
check(controller.status.text.begins_with("Открыто"), "Status localizes")
check(title.texture == Controller.TITLES.ru, "Russian title selected")
check(controller.items.a71.event_label.text.contains("90 квадриллионов"), "Russian achievement text selected")
global.Locale = "en"
controller.update(0.0)
check(controller.status.text.begins_with("Unlocked"), "English status selected")
check(title.texture == Controller.TITLES.en, "English title selected")
check(controller.items.a71.event_label.text.contains("90 quadrillion"), "English achievement text selected")
controller.current = {"id": "a71", "type": "c"}
controller._display_event(controller.current)
check(controller.notification_item.event_label.text.contains("90 quadrillion"), "English notification text selected")
global.Locale = "ru"
controller.update(0.0)
check(controller.notification_item.event_label.text.contains("90 квадриллионов"), "Active notification changes language")
global.Locale = previous_locale
var path: String = controller.progress_path
stage.free()
DirAccess.remove_absolute(path)
print("Achievement checks: ", "PASS" if failures == 0 else "FAIL")
quit(1 if failures else 0)
+1
View File
@@ -0,0 +1 @@
uid://uslcyxm8vpcn
+36
View File
@@ -0,0 +1,36 @@
extends SceneTree
const E = preload("res://world/owniverse/math/exact.gd")
const Report = preload("res://ui/report/report.gd")
class Ledger extends RefCounted:
func physical_amounts(_source: Dictionary) -> Dictionary:
return {"RD": 3, "HG": 1}
class Catalog extends RefCounted:
var entities: Dictionary = {}
func is_group(_id: String) -> bool: return false
class World extends RefCounted:
var catalog = Catalog.new()
var ledger = Ledger.new()
var state = {"amount": {}, "stats": {"total_amount": {"H": 1234}}, "world_parameters": {"Federation": 1, "Enlightenment": 1, "Order": 1}}
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var global: Node = root.get_node("Global")
var world = World.new()
world.state.stats.total_amount.H = E.of("1234567890123456789012345")
var before: Dictionary = E.encode(world.state)
global.Locale = "en"
var text: String = Report.build(world, global)
assert(text.contains("75.00%") and text.contains("25.00%"), text)
assert(text.contains("1 234 567 890 123 456 789 012 345"), text)
assert(text.contains("Total resources obtained") and not text.contains("Total entities emerged"), text)
world.state.world_parameters.Federation = E.of("1.3")
text = Report.build(world, global)
assert(text.contains("Federation:") and text.contains("0.30"), text)
world.state.world_parameters.Federation = 1
global.Locale = "ru"
text = Report.build(world, global)
assert(text.contains("Всего получено ресурсов") and text.contains("Красные карлики"), text)
assert(E.encode(world.state) == before, "Report mutated simulation")
assert(load("res://stage/stage.gd") != null)
print("Universe report checks: PASS")
quit()
+1
View File
@@ -0,0 +1 @@
uid://brb04x12elvqt
+206
View File
@@ -0,0 +1,206 @@
extends Node
## Player-wide achievement progress and the shared in-game notification queue.
const E = preload("res://world/owniverse/math/exact.gd")
const ITEM = preload("res://ui/achievements/achievement_item.tscn")
const TITLES = {
"en": preload("res://ui/achievements/achievements.en.png"),
"ru": preload("res://ui/achievements/achievements.ru.png"),
}
var progress_path: String = "user://achievements.cfg"
const HOLD_SECONDS = 3.0
const FADE_SECONDS = 0.3
var global: Node
var stage: Control
var definitions: Dictionary = {}
var conditions: Dictionary = {}
var progress: Dictionary = {}
var items: Dictionary = {}
var queue: Array[Dictionary] = []
var current: Dictionary = {}
var elapsed: float = 0.0
var last_revision: int = -1
var last_world: Object
var locale: String = ""
var panel: PanelContainer
var notification_item: Control
var headers: Array[Control] = []
var status: Label
var list: VBoxContainer
var storage_available: bool = true
func initialize(owner_stage: Control) -> void:
stage = owner_stage
global = get_tree().root.get_node("Global")
locale = global.Locale
for id in global.Achievements.en:
var row: Dictionary = global.Achievements.en[id]
if str(row.Ignore) == "1": continue
definitions[id] = row
conditions[id] = {
"open": _parse_condition(str(row.OpenCondition)),
"achieve": _parse_condition(str(row.AchieveCondition)),
}
_load_progress()
list = stage.get_node("Achievements/AchievementList/AchievementItems")
for child in list.get_children():
list.remove_child(child)
child.queue_free()
status = Label.new()
status.add_theme_font_size_override("font_size", 32)
list.add_child(status)
var pages: Node = stage.get_node("World/PageScroller/HBoxContainer")
for path in ["ForcePage/ForceHeader", "ResourcePage/ResourceHeader", "StarPage/StarHeader", "StructurePage/StructureHeader", "LifePage/LifeHeader"]:
headers.append(pages.get_node(path))
panel = PanelContainer.new()
panel.name = "AchievementNotification"
panel.set_as_top_level(true)
panel.mouse_filter = Control.MOUSE_FILTER_STOP
panel.gui_input.connect(_on_notification_input)
panel.z_index = 20
stage.get_node("World").add_child(panel)
notification_item = ITEM.instantiate()
panel.add_child(notification_item)
_ignore_notification_children(notification_item)
panel.hide()
_rebuild_list()
func _parse_condition(expression: String) -> Dictionary:
if expression.is_empty(): return {}
var parts := expression.split(":")
assert(parts.size() == 3 and parts[0] in ["age", "total"], "Unsupported achievement condition: " + expression)
return {"type": parts[0], "id": parts[1], "amount": E.of(parts[2])}
func _matches(condition: Dictionary, source: Dictionary) -> bool:
if condition.is_empty(): return true
var amount: Variant = source.time.year if condition.type == "age" else source.stats.total_amount.get(condition.id, 0)
return E.ge(amount, condition.amount)
func update(delta: float) -> void:
if global.Locale != locale:
locale = global.Locale
_rebuild_list()
if not current.is_empty(): _display_event(current)
var world = stage.owniverse
if world != last_world or world.presentation.revision != last_revision:
last_world = world
last_revision = world.presentation.revision
_check(world.state)
# Menu time does not consume a notification's display time.
if not stage.screens.World.visible: return
if current.is_empty() and not queue.is_empty():
current = queue.pop_front()
elapsed = 0.0
_display_event(current)
if current.is_empty(): return
elapsed += delta
var page: int = clampi(roundi(float(stage.stage_page_scroller.scroll_horizontal) / stage.PAGE_WIDTH), 0, headers.size() - 1)
for i in headers.size():
headers[i].modulate.a = 0.0 if i == page else 1.0
panel.global_position = headers[page].global_position
panel.size = Vector2(headers[page].size.x, maxf(128.0, headers[page].size.y))
panel.modulate.a = clampf(1.0 - (elapsed - HOLD_SECONDS) / FADE_SECONDS, 0.0, 1.0)
if elapsed >= HOLD_SECONDS + FADE_SECONDS:
dismiss_notification()
func _ignore_notification_children(node: Node) -> void:
if node is Control: node.mouse_filter = Control.MOUSE_FILTER_IGNORE
for child in node.get_children(): _ignore_notification_children(child)
func _on_notification_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
panel.accept_event()
dismiss_notification()
elif event is InputEventScreenTouch and event.pressed:
panel.accept_event()
dismiss_notification()
func dismiss_notification() -> void:
panel.hide()
current = {}
elapsed = 0.0
for header in headers: header.modulate.a = 1.0
func _check(source: Dictionary) -> void:
var changed := false
for id in definitions:
var saved: Dictionary = progress.get(id, {"visible": false, "unlocked": false})
if bool(saved.unlocked): continue
var rule: Dictionary = conditions[id]
if _matches(rule.achieve, source):
saved.visible = true
saved.unlocked = true
_enqueue(id, "c")
changed = true
elif not bool(saved.visible) and _matches(rule.open, source):
saved.visible = true
# An empty opening condition makes an item visible without an alert.
if not rule.open.is_empty(): _enqueue(id, "g")
changed = true
progress[id] = saved
if changed:
_save_progress()
_rebuild_list()
func _enqueue(id: String, event_type: String) -> void:
for event in queue:
if event.id == id:
event.type = event_type
return
queue.append({"id": id, "type": event_type})
func _text(id: String) -> String:
var row: Dictionary = global.Achievements[locale][id]
var prefix: String = "Ru" if locale == "ru" else "En"
var title: String = str(row[prefix + "1"])
var description: String = str(row[prefix + "2"])
return "[b]" + title + "[/b]" + ("\n" + description if not description.is_empty() else "")
func _display_event(event: Dictionary) -> void:
notification_item.open(int(definitions[event.id].Points), event.type, _text(event.id))
panel.modulate.a = 1.0
panel.show()
func _rebuild_list() -> void:
var title: TextureRect = stage.get_node_or_null("Achievements/Header/Title")
if title != null: title.texture = TITLES[locale]
var unlocked := 0
var position := 1
for id in definitions:
var saved: Dictionary = progress.get(id, {})
if not bool(saved.get("visible", false)) and not bool(saved.get("unlocked", false)): continue
if not items.has(id):
var item: Control = ITEM.instantiate()
list.add_child(item)
items[id] = item
list.move_child(items[id], position)
position += 1
var completed: bool = bool(saved.get("unlocked", false))
if completed: unlocked += 1
items[id].open(int(definitions[id].Points), "c" if completed else "g", _text(id))
var row: Dictionary = global.get_template("Achievement-Status-Template", "Interface", "Header")
status.text = global.render_template(row, {"count": unlocked, "total": definitions.size()})
func _load_progress() -> void:
var config := ConfigFile.new()
var error := config.load(progress_path)
if error == ERR_FILE_NOT_FOUND: return
if error != OK:
storage_available = false
push_error("Cannot load achievement progress: " + error_string(error))
return
for id in definitions:
progress[id] = {"visible": bool(config.get_value(id, "visible", false)), "unlocked": bool(config.get_value(id, "unlocked", false))}
func _save_progress() -> void:
if not storage_available: return
var config := ConfigFile.new()
# Preserve entries for achievements temporarily disabled in the catalog.
config.load(progress_path)
for id in progress:
config.set_value(id, "visible", bool(progress[id].visible))
config.set_value(id, "unlocked", bool(progress[id].unlocked))
var error := config.save(progress_path + ".tmp")
if error == OK:
error = DirAccess.rename_absolute(progress_path + ".tmp", progress_path)
if error != OK: push_error("Cannot save achievement progress: " + error_string(error))
@@ -0,0 +1 @@
uid://c1x220s0rloxc
+11 -2
View File
@@ -14,12 +14,14 @@
custom_minimum_size = Vector2(1080, 128)
layout_mode = 3
anchors_preset = 0
size_flags_horizontal = 3
script = ExtResource("1_w8f20")
[node name="Row" type="HBoxContainer" parent="." unique_id=2025386077]
layout_mode = 0
offset_right = 1080.0
offset_bottom = 128.0
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_constants/separation = 16
[node name="Icon1c" type="TextureRect" parent="Row" unique_id=2146478339]
visible = false
@@ -72,3 +74,10 @@ texture = ExtResource("9_3xyrb")
[node name="Label" type="RichTextLabel" parent="Row" unique_id=230945261]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/normal_font_size = 30
theme_override_font_sizes/bold_font_size = 30
bbcode_enabled = true
text = "123"
fit_content = true
scroll_active = false
vertical_alignment = 1
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+63
View File
@@ -0,0 +1,63 @@
extends VBoxContainer
const E = preload("res://world/owniverse/math/exact.gd")
const Presentation = preload("res://world/owniverse/item_presentation.gd")
const STARS = ["RD", "OD", "YD", "WYD", "WS", "BG", "SG", "HG"]
const EFFECTS = {"Int.Fed": "Federation", "Int.Enl": "Enlightenment", "Int.Ord": "Order"}
var report_locale: String = ""
@onready var report_body: RichTextLabel = $ReportBody
@onready var report_header: TextureRect = $Header/Header
func _ready() -> void:
visibility_changed.connect(_on_visibility_changed)
func _process(_delta: float) -> void:
if is_visible_in_tree() and report_locale != get_node("/root/Global").Locale:
refresh()
func _on_visibility_changed() -> void:
if is_node_ready() and is_visible_in_tree():
refresh()
report_body.scroll_to_line(0)
func refresh() -> void:
var global: Node = get_node("/root/Global")
report_locale = global.Locale
report_body.text = build(get_parent().owniverse, global)
report_header.texture = load("res://ui/report/" + report_locale + "/report.png")
## Formats existing committed state only. No historical counters or simulation work.
static func build(world, global: Node) -> String:
var source: Dictionary = world.state
var totals: Dictionary = Presentation.amounts(source, world.catalog, true)
var physical: Dictionary = world.ledger.physical_amounts(source)
var star_total: Variant = 0
for id in STARS: star_total = E.add(star_total, physical.get(id, 0))
var blocks: PackedStringArray = []
var lines: PackedStringArray = []
var header: String = ""
# Russian keys provide the common order; get_template resolves both key formats.
for key in global.Templates.ru:
if not str(key).contains(".Report."): continue
var parts: PackedStringArray = str(key).rsplit(".Report.", true, 1)
var id: String = parts[0]
var kind: String = parts[1]
var row: Dictionary = global.get_template(id, "Report", kind)
if kind == "Header":
if not lines.is_empty(): blocks.append(header + "\n\n" + "\n".join(lines))
lines.clear()
header = global.render_template(row, {})
elif kind == "Fraction":
var amount: Variant = physical.get(id, 0)
if E.gt(amount, 0) and E.gt(star_total, 0):
var percent: String = E.grouped(E.mul_div(amount, 100, star_total), 2, false) + "%"
lines.append(global.render_template(row, {"proportion": percent}))
elif kind == "Max":
var amount: Variant = totals.get(id, 0)
if EFFECTS.has(id):
amount = E.subtract(source.world_parameters.get(EFFECTS[id], 1), 1)
if E.gt(amount, 0):
lines.append(global.render_template(row, {"max": E.grouped(amount, 2 if EFFECTS.has(id) else 0, false)}))
if not lines.is_empty(): blocks.append(header + "\n\n" + "\n".join(lines))
return "\n\n".join(blocks)
+1
View File
@@ -0,0 +1 @@
uid://v8h4ov33opyf
BIN
View File
Binary file not shown.
+20 -31
View File
@@ -71,6 +71,8 @@
[ext_resource type="Script" uid="uid://btsrt6rw3nr04" path="res://ui/audio/owniverse_music_player.gd" id="67_music"]
[ext_resource type="Script" uid="uid://botb0k4qr0hqo" path="res://ui/audio/owniverse_effects_player.gd" id="68_effects"]
[ext_resource type="Script" path="res://ui/report/report.gd" id="69_report"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ifjjt"]
[sub_resource type="ButtonGroup" id="ButtonGroup_58wjt"]
@@ -255,10 +257,6 @@ texture = ExtResource("13_dp8ul")
custom_minimum_size = Vector2(316, 64)
layout_mode = 2
[node name="ForceAchievementItem" parent="Stage/World/PageScroller/HBoxContainer/ForcePage" unique_id=1273881744 instance=ExtResource("64_0drd1")]
visible = false
layout_mode = 2
[node name="PageHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/ForcePage" unique_id=1411786120 instance=ExtResource("12_58wjt")]
layout_mode = 2
@@ -402,10 +400,6 @@ grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("23_bcq5b")
[node name="ResourceAchievementItem" parent="Stage/World/PageScroller/HBoxContainer/ResourcePage" unique_id=746617076 instance=ExtResource("64_0drd1")]
visible = false
layout_mode = 2
[node name="ResourceHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/ResourcePage" unique_id=2672876 instance=ExtResource("12_58wjt")]
layout_mode = 2
@@ -534,10 +528,6 @@ grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("28_upe80")
[node name="StarAchievementItem" parent="Stage/World/PageScroller/HBoxContainer/StarPage" unique_id=1287836220 instance=ExtResource("64_0drd1")]
visible = false
layout_mode = 2
[node name="StarHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/StarPage" unique_id=2057421818 instance=ExtResource("12_58wjt")]
layout_mode = 2
@@ -679,10 +669,6 @@ grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("23_bcq5b")
[node name="StructureAchievementItem" parent="Stage/World/PageScroller/HBoxContainer/StructurePage" unique_id=1834701519 instance=ExtResource("64_0drd1")]
visible = false
layout_mode = 2
[node name="StructureHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/StructurePage" unique_id=522613609 instance=ExtResource("12_58wjt")]
layout_mode = 2
@@ -831,10 +817,6 @@ texture = ExtResource("40_50rdj")
layout_mode = 2
texture = ExtResource("41_mi6gc")
[node name="LifeAchievementItem" parent="Stage/World/PageScroller/HBoxContainer/LifePage" unique_id=1647265705 instance=ExtResource("64_0drd1")]
visible = false
layout_mode = 2
[node name="LifeHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/LifePage" unique_id=100084430 instance=ExtResource("12_58wjt")]
layout_mode = 2
@@ -1032,7 +1014,8 @@ layout_mode = 2
texture_normal = ExtResource("52_uvuuk")
[node name="Report" type="VBoxContainer" parent="Stage" unique_id=1422399839]
visible = false
script = ExtResource("69_report")
custom_minimum_size = Vector2(1080, 1920)
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
@@ -1053,11 +1036,19 @@ custom_minimum_size = Vector2(1080, 0)
layout_mode = 2
[node name="Header" type="TextureRect" parent="Stage/Report/Header" unique_id=1321780840]
custom_minimum_size = Vector2(824, 256)
layout_mode = 2
texture = ExtResource("60_an6vj")
[node name="ReportBody" type="RichTextLabel" parent="Stage/Report" unique_id=1952288631]
layout_mode = 2
size_flags_vertical = 3
bbcode_enabled = true
scroll_active = true
theme_override_font_sizes/normal_font_size = 30
theme_override_font_sizes/bold_font_size = 30
[node name="Achievements" type="VBoxContainer" parent="Stage" unique_id=1847732432]
visible = false
custom_minimum_size = Vector2(1080, 1920)
layout_mode = 0
offset_right = 40.0
@@ -1076,28 +1067,24 @@ texture_normal = ExtResource("44_uvuuk")
texture_pressed = ExtResource("45_an6vj")
[node name="Header" type="CenterContainer" parent="Stage/Achievements" unique_id=51521239]
custom_minimum_size = Vector2(1080, 256)
layout_mode = 2
[node name="Title" type="TextureRect" parent="Stage/Achievements/Header" unique_id=1195638572]
custom_minimum_size = Vector2(824, 256)
custom_minimum_size = Vector2(824, 128)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
texture = ExtResource("61_d36xh")
stretch_mode = 3
[node name="AchievementList" type="Control" parent="Stage/Achievements" unique_id=1872434750]
[node name="AchievementList" type="ScrollContainer" parent="Stage/Achievements" unique_id=1872434750]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="AchievementItems" type="VBoxContainer" parent="Stage/Achievements/AchievementList" unique_id=1878761996]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
layout_mode = 2
size_flags_horizontal = 3
[node name="AchievementItem" parent="Stage/Achievements/AchievementList/AchievementItems" unique_id=1952626354 instance=ExtResource("64_0drd1")]
visible = false
@@ -1180,6 +1167,8 @@ script = ExtResource("67_music")
[connection signal="pressed" from="Stage/MainMenu/TopMenu/BackButton" to="Stage" method="_on_topmenu_button_pressed" binds= ["World"]]
[connection signal="pressed" from="Stage/MainMenu/CreateOwniverse" to="Stage" method="create_new_universe"]
[connection signal="pressed" from="Stage/MainMenu/Achievements" to="Stage" method="_on_topmenu_button_pressed" binds= ["Achievements"]]
[connection signal="pressed" from="Stage/MainMenu/Language/English" to="Stage/MainMenu" method="_on_change_locale_pressed" binds= ["en"]]
[connection signal="pressed" from="Stage/MainMenu/Language/Russian" to="Stage/MainMenu" method="_on_change_locale_pressed" binds= ["ru"]]
[connection signal="pressed" from="Stage/MainMenu/Media/Sound" to="Stage/MainMenu" method="_on_sound_pressed"]
[connection signal="pressed" from="Stage/MainMenu/Media/Music" to="Stage/MainMenu" method="_on_music_pressed"]
[connection signal="value_changed" from="Stage/MainMenu/Volume/SoundVolume" to="Stage/MainMenu" method="_on_sound_volume_value_changed"]