Files
spellforge/scripts/autoloads/settings_manager.gd
T

183 lines
7.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## SettingsManager — 全局设置 + 难度(Autoload: SettingsManager
## 权威来源:development_plan.md S6 P1(难度三档持久化)、certification_checklist.md ST-03(清除本地数据)
## 持久化 user://save_data.json{ difficulty:int, locale:String, master_volume:float }
extends Node
const SAVE_PATH: String = "user://save_data.json"
enum Difficulty { BEGINNER = 0, STANDARD = 1, CHALLENGE = 2 }
var difficulty: int = Difficulty.STANDARD
var locale: String = "zh_CN"
var master_volume: float = 1.0
var ftue_done: bool = false # 首次启动难度引导是否已完成(FTUE)
func _ready() -> void:
_load()
_load_balance()
_apply_audio()
if Locale:
Locale.set_locale(locale)
## 是否需要首启难度引导(FTUE
func needs_ftue() -> bool:
return not ftue_done
## 完成 FTUE:设定难度并持久化标志
func complete_ftue(chosen_difficulty: int) -> void:
difficulty = clampi(chosen_difficulty, 0, 2)
ftue_done = true
_save()
## 难度乘子 + Boss 血量:纯数据驱动,唯一来源 data/balance.json(游戏设计器「平衡」面板维护)
## get() 的默认仅为防崩溃,非内容副本
var _MULTS: Dictionary = {} # key → [初学者, 标准, 挑战]
var _BOSS_HP: Dictionary = {} # 原型→基础血量
var _AETHER_THRESHOLDS: Array = [] # [[到达波次, 碎片], ...] 升序
var _MANA_HEAT: Dictionary = {"per_cast": 0.01, "decay_per_sec": 0.1, "max": 1.0}
var _INFINITE: Dictionary = {"hp_per_shot": 1.0, "hp_cost_cap_ratio": 0.15}
var _IFRAME_SEC: float = 0.5 # 玩家受击无敌窗口(秒);balance.json player_iframe_sec
const BALANCE_JSON: String = "res://data/balance.json"
func _mult(key: String) -> float:
var arr: Array = _MULTS.get(key, [1.0, 1.0, 1.0])
return float(arr[clampi(difficulty, 0, arr.size() - 1)])
func enemy_hp_mult() -> float: return _mult("enemy_hp")
func player_dmg_taken_mult() -> float: return _mult("player_dmg")
func wave_count_mult() -> float: return _mult("wave_count")
func boss_hp_mult() -> float: return _mult("boss_hp")
## Boss 基础血量(WaveManager._spawn_boss 查询;balance.json 可调)
func get_boss_base_hp(boss_type: int) -> float:
return float(_BOSS_HP.get(str(boss_type), 500.0))
## 到达波次 → 以太碎片(阶梯查表:取"波次阈值 ≤ wave"的最高阶梯值;升序表)
func aether_for_wave(wave: int) -> int:
var table: Array = _AETHER_THRESHOLDS
if table.is_empty():
table = [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]]
var result: int = 0
for entry in table:
if entry is Array and entry.size() >= 2 and int(entry[0]) <= wave:
result = maxi(result, int(entry[1])) # 取 ≤wave 阶梯中的最大值,容忍无序表(设计师手填不必排序)
return result
func mana_heat_per_cast() -> float: return float(_MANA_HEAT.get("per_cast", 0.01))
func mana_heat_decay() -> float: return float(_MANA_HEAT.get("decay_per_sec", 0.1))
func mana_heat_max() -> float: return float(_MANA_HEAT.get("max", 1.0))
func infinite_hp_per_shot() -> float: return float(_INFINITE.get("hp_per_shot", 1.0))
func hp_cost_cap_ratio() -> float: return float(_INFINITE.get("hp_cost_cap_ratio", 0.15))
func player_iframe_sec() -> float: return _IFRAME_SEC
## 数据驱动:加载 balance.json(难度乘子 + Boss 血量)
func _load_balance() -> void:
if not FileAccess.file_exists(BALANCE_JSON):
push_error("SettingsManager: 缺失 res://data/balance.json(难度乘子/Boss血量)")
return
var data = JSON.parse_string(FileAccess.get_file_as_string(BALANCE_JSON))
if not (data is Dictionary):
push_error("SettingsManager: balance.json 格式错误")
return
if data.has("difficulty_mults") and data["difficulty_mults"] is Dictionary:
for k in data["difficulty_mults"]:
_MULTS[k] = data["difficulty_mults"][k]
if data.has("boss_hp") and data["boss_hp"] is Dictionary:
_BOSS_HP = data["boss_hp"]
if data.has("aether_thresholds") and data["aether_thresholds"] is Array:
_AETHER_THRESHOLDS = data["aether_thresholds"]
if data.has("mana_heat") and data["mana_heat"] is Dictionary:
for k in data["mana_heat"]:
_MANA_HEAT[k] = data["mana_heat"][k]
if data.has("infinite_spells") and data["infinite_spells"] is Dictionary:
for k in data["infinite_spells"]:
_INFINITE[k] = data["infinite_spells"][k]
if data.has("player_iframe_sec"):
_IFRAME_SEC = float(data["player_iframe_sec"])
func difficulty_name() -> String:
return ["DIFF_BEGINNER", "DIFF_STANDARD", "DIFF_CHALLENGE"][difficulty]
# ── 设置变更(即时保存)──────────────────────────────────────
func set_difficulty(d: int) -> void:
difficulty = clampi(d, 0, 2)
_save()
func cycle_difficulty() -> int:
set_difficulty((difficulty + 1) % 3)
return difficulty
func set_locale(loc: String) -> void:
locale = loc
if Locale:
Locale.set_locale(loc)
_save()
func set_master_volume(v: float) -> void:
master_volume = clampf(v, 0.0, 1.0)
_apply_audio()
_save()
func _apply_audio() -> void:
var bus: int = AudioServer.get_bus_index("Master")
if bus >= 0:
AudioServer.set_bus_volume_db(bus, linear_to_db(maxf(0.0001, master_volume)))
# ── ST-03:清除所有本地数据 ─────────────────────────────────
func clear_all_local_data() -> void:
var files: Array = [
"user://crash_log.txt", "user://crash_log_prev.txt",
"user://run_a.json", "user://run_b.json",
"user://endless_records.json", "user://save_data.json",
]
for path in files:
if FileAccess.file_exists(path):
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
_remove_dir_recursive("user://runs")
# 重置内存态
difficulty = Difficulty.STANDARD
master_volume = 1.0
ftue_done = false # 清数据后重新触发首启难度引导
# 用路径引用避免编译时 autoload 依赖(打破 SettingsManager→ProfileManager→WaveManager→SettingsManager 循环)
var pm: Node = get_node_or_null("/root/ProfileManager")
if pm and pm.has_method("clear_run"):
pm.clear_run()
MetaProgress.clear()
_apply_audio()
func _remove_dir_recursive(path: String) -> void:
var d := DirAccess.open(path)
if d == null:
return
d.list_dir_begin()
var fn := d.get_next()
while fn != "":
if not d.current_is_dir():
DirAccess.remove_absolute(ProjectSettings.globalize_path(path + "/" + fn))
fn = d.get_next()
d.list_dir_end()
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
# ── 持久化 ──────────────────────────────────────────────────
func _save() -> void:
var data: Dictionary = {
"difficulty": difficulty, "locale": locale,
"master_volume": master_volume, "ftue_done": ftue_done,
}
var f := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if f:
f.store_string(JSON.stringify(data))
f.close()
func _load() -> void:
if not FileAccess.file_exists(SAVE_PATH):
return
var parsed = JSON.parse_string(FileAccess.get_file_as_string(SAVE_PATH))
if not (parsed is Dictionary):
return
difficulty = clampi(int(parsed.get("difficulty", 1)), 0, 2)
locale = String(parsed.get("locale", "zh_CN"))
master_volume = clampf(float(parsed.get("master_volume", 1.0)), 0.0, 1.0)
ftue_done = bool(parsed.get("ftue_done", false))