Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aced0c61a | |||
| e1782c7b10 |
@@ -249,3 +249,32 @@ H списывает только оставшуюся после замещен
|
|||||||
|
|
||||||
`tests/test_extra_params.gd` проверяет загрузку точных чисел и изменение энергии,
|
`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`.
|
||||||
|
|||||||
@@ -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 ItemDescription = preload("res://world/owniverse/item_description.gd")
|
||||||
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
|
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
|
||||||
|
var achievements: Node
|
||||||
var world_indicators: RefCounted
|
var world_indicators: RefCounted
|
||||||
var indicator_elapsed: float = 0.0
|
var indicator_elapsed: float = 0.0
|
||||||
var indicator_revision: int = -1
|
var indicator_revision: int = -1
|
||||||
@@ -339,6 +340,10 @@ func _ready() -> void:
|
|||||||
create_new_universe()
|
create_new_universe()
|
||||||
load_data()
|
load_data()
|
||||||
_refresh_universe_ui()
|
_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
|
g_counters.swipe.sec = 0.2
|
||||||
|
|
||||||
@@ -406,6 +411,7 @@ func _process(delta: float) -> void:
|
|||||||
|
|
||||||
owniverse.evolute_frame(delta, get_tree())
|
owniverse.evolute_frame(delta, get_tree())
|
||||||
_process_apocalypse(delta)
|
_process_apocalypse(delta)
|
||||||
|
if achievements != null: achievements.update(delta)
|
||||||
clock_display_elapsed += delta
|
clock_display_elapsed += delta
|
||||||
if clock_display_elapsed >= 0.1:
|
if clock_display_elapsed >= 0.1:
|
||||||
clock_display_elapsed = fmod(clock_display_elapsed, 0.1)
|
clock_display_elapsed = fmod(clock_display_elapsed, 0.1)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://uslcyxm8vpcn
|
||||||
@@ -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
|
||||||
@@ -14,12 +14,14 @@
|
|||||||
custom_minimum_size = Vector2(1080, 128)
|
custom_minimum_size = Vector2(1080, 128)
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
anchors_preset = 0
|
anchors_preset = 0
|
||||||
|
size_flags_horizontal = 3
|
||||||
script = ExtResource("1_w8f20")
|
script = ExtResource("1_w8f20")
|
||||||
|
|
||||||
[node name="Row" type="HBoxContainer" parent="." unique_id=2025386077]
|
[node name="Row" type="HBoxContainer" parent="." unique_id=2025386077]
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_right = 1080.0
|
anchor_right = 1.0
|
||||||
offset_bottom = 128.0
|
anchor_bottom = 1.0
|
||||||
|
theme_override_constants/separation = 16
|
||||||
|
|
||||||
[node name="Icon1c" type="TextureRect" parent="Row" unique_id=2146478339]
|
[node name="Icon1c" type="TextureRect" parent="Row" unique_id=2146478339]
|
||||||
visible = false
|
visible = false
|
||||||
@@ -72,3 +74,10 @@ texture = ExtResource("9_3xyrb")
|
|||||||
[node name="Label" type="RichTextLabel" parent="Row" unique_id=230945261]
|
[node name="Label" type="RichTextLabel" parent="Row" unique_id=230945261]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
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.
+7
-29
@@ -255,10 +255,6 @@ texture = ExtResource("13_dp8ul")
|
|||||||
custom_minimum_size = Vector2(316, 64)
|
custom_minimum_size = Vector2(316, 64)
|
||||||
layout_mode = 2
|
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")]
|
[node name="PageHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/ForcePage" unique_id=1411786120 instance=ExtResource("12_58wjt")]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
@@ -402,10 +398,6 @@ grow_horizontal = 2
|
|||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
texture = ExtResource("23_bcq5b")
|
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")]
|
[node name="ResourceHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/ResourcePage" unique_id=2672876 instance=ExtResource("12_58wjt")]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
@@ -534,10 +526,6 @@ grow_horizontal = 2
|
|||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
texture = ExtResource("28_upe80")
|
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")]
|
[node name="StarHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/StarPage" unique_id=2057421818 instance=ExtResource("12_58wjt")]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
@@ -679,10 +667,6 @@ grow_horizontal = 2
|
|||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
texture = ExtResource("23_bcq5b")
|
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")]
|
[node name="StructureHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/StructurePage" unique_id=522613609 instance=ExtResource("12_58wjt")]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
@@ -831,10 +815,6 @@ texture = ExtResource("40_50rdj")
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
texture = ExtResource("41_mi6gc")
|
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")]
|
[node name="LifeHeaderBorder" parent="Stage/World/PageScroller/HBoxContainer/LifePage" unique_id=100084430 instance=ExtResource("12_58wjt")]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
@@ -1076,28 +1056,24 @@ texture_normal = ExtResource("44_uvuuk")
|
|||||||
texture_pressed = ExtResource("45_an6vj")
|
texture_pressed = ExtResource("45_an6vj")
|
||||||
|
|
||||||
[node name="Header" type="CenterContainer" parent="Stage/Achievements" unique_id=51521239]
|
[node name="Header" type="CenterContainer" parent="Stage/Achievements" unique_id=51521239]
|
||||||
custom_minimum_size = Vector2(1080, 256)
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
[node name="Title" type="TextureRect" parent="Stage/Achievements/Header" unique_id=1195638572]
|
[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
|
layout_mode = 2
|
||||||
size_flags_horizontal = 4
|
size_flags_horizontal = 4
|
||||||
size_flags_vertical = 4
|
size_flags_vertical = 4
|
||||||
texture = ExtResource("61_d36xh")
|
texture = ExtResource("61_d36xh")
|
||||||
stretch_mode = 3
|
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
|
layout_mode = 2
|
||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
|
horizontal_scroll_mode = 0
|
||||||
|
|
||||||
[node name="AchievementItems" type="VBoxContainer" parent="Stage/Achievements/AchievementList" unique_id=1878761996]
|
[node name="AchievementItems" type="VBoxContainer" parent="Stage/Achievements/AchievementList" unique_id=1878761996]
|
||||||
layout_mode = 1
|
layout_mode = 2
|
||||||
anchors_preset = 15
|
size_flags_horizontal = 3
|
||||||
anchor_right = 1.0
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 2
|
|
||||||
|
|
||||||
[node name="AchievementItem" parent="Stage/Achievements/AchievementList/AchievementItems" unique_id=1952626354 instance=ExtResource("64_0drd1")]
|
[node name="AchievementItem" parent="Stage/Achievements/AchievementList/AchievementItems" unique_id=1952626354 instance=ExtResource("64_0drd1")]
|
||||||
visible = false
|
visible = false
|
||||||
@@ -1180,6 +1156,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/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/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/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/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="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"]
|
[connection signal="value_changed" from="Stage/MainMenu/Volume/SoundVolume" to="Stage/MainMenu" method="_on_sound_volume_value_changed"]
|
||||||
|
|||||||
Reference in New Issue
Block a user