201 lines
8.1 KiB
GDScript
201 lines
8.1 KiB
GDScript
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")
|
|
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 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))
|