class_name LedgerBackedResourceState extends RefCounted ## Construction/operating adapter that keeps Gold authoritative in the ledger ## while preserving the generic resource-state contract used by services. const PLAYER_CASH := &"PLAYER_CASH" var _state: Variant var _economy: EconomyService var _counter_account: StringName var _expense_category: StringName var _refund_category: StringName var _source_entity_id: StringName var _minute_provider: Callable func _init( state: Variant, economy: EconomyService, counter_account: StringName, expense_category: StringName, refund_category: StringName, source_entity_id: StringName, minute_provider: Callable = Callable() ) -> void: _state = state _economy = economy _counter_account = counter_account _expense_category = expense_category _refund_category = refund_category _source_entity_id = source_entity_id _minute_provider = minute_provider func get_resource(resource_name: StringName) -> int: if resource_name == &"gold": return _economy.get_balance(PLAYER_CASH) return int(_state.call(&"get_resource", resource_name)) func can_afford(cost: Dictionary) -> bool: for raw_name: Variant in cost: var resource_name := StringName(raw_name) var amount := int(cost[raw_name]) if amount < 0 or get_resource(resource_name) < amount: return false return true func spend(cost: Dictionary) -> bool: if not can_afford(cost): return false var deltas: Dictionary = {} for raw_name: Variant in cost: deltas[StringName(raw_name)] = -int(cost[raw_name]) return apply_resource_deltas(deltas) func apply_resource_deltas(deltas: Dictionary) -> bool: if not _validate_deltas(deltas): return false var before := create_snapshot() var non_gold: Dictionary = {} var gold_delta := 0 for raw_name: Variant in deltas: var resource_name := StringName(raw_name) if resource_name == &"gold": gold_delta += int(deltas[raw_name]) else: non_gold[resource_name] = int(deltas[raw_name]) if not non_gold.is_empty() and not bool(_state.call(&"apply_resource_deltas", non_gold)): return false if gold_delta != 0: var result: CommandResult if gold_delta < 0: result = _economy.transfer( PLAYER_CASH, _counter_account, -gold_delta, _current_minute(), _expense_category, _source_entity_id ) else: result = _economy.transfer( _counter_account, PLAYER_CASH, gold_delta, _current_minute(), _refund_category, _source_entity_id ) if not result.is_success(): restore_snapshot(before) return false _sync_gold_mirror() return true func create_snapshot() -> Dictionary: return { &"state": _state.call(&"create_snapshot"), &"economy": _economy.create_snapshot(), } func restore_snapshot(snapshot: Dictionary) -> bool: if not snapshot.get(&"state", null) is Dictionary \ or not snapshot.get(&"economy", null) is Dictionary: return false var previous_state: Dictionary = _state.call(&"create_snapshot") var previous_economy: Dictionary = _economy.create_snapshot() if not _economy.restore_snapshot(snapshot[&"economy"]): return false if not bool(_state.call(&"restore_snapshot", snapshot[&"state"])): _economy.restore_snapshot(previous_economy) _state.call(&"restore_snapshot", previous_state) return false if int(_state.call(&"get_resource", &"gold")) != _economy.get_balance(PLAYER_CASH): _economy.restore_snapshot(previous_economy) _state.call(&"restore_snapshot", previous_state) return false return true func _validate_deltas(deltas: Dictionary) -> bool: var next_values: Dictionary = {} for raw_name: Variant in deltas: var resource_name := StringName(raw_name) var next_value := int(next_values.get(resource_name, get_resource(resource_name))) + int(deltas[raw_name]) if next_value < 0: return false next_values[resource_name] = next_value return true func _sync_gold_mirror() -> void: _state.call(&"set_resource", &"gold", _economy.get_balance(PLAYER_CASH)) func _current_minute() -> int: if _minute_provider.is_valid(): return maxi(int(_minute_provider.call()), 0) return _economy.current_minute