Files
heros/scripts/core/data_catalog.gd

235 lines
8.3 KiB
GDScript

extends RefCounted
class_name DataCatalog
const OFFICERS_PATH := "res://data/defs/officers.json"
const CLASSES_PATH := "res://data/defs/classes.json"
const TERRAIN_PATH := "res://data/defs/terrain.json"
const ITEMS_PATH := "res://data/defs/items.json"
const SKILLS_PATH := "res://data/defs/skills.json"
var officers: Dictionary = {}
var classes: Dictionary = {}
var terrain: Dictionary = {}
var items: Dictionary = {}
var skills: Dictionary = {}
func load_defaults() -> bool:
officers = _load_json_object(OFFICERS_PATH)
classes = _load_json_object(CLASSES_PATH)
terrain = _load_json_object(TERRAIN_PATH)
items = _load_json_object(ITEMS_PATH)
skills = _load_json_object(SKILLS_PATH)
return not officers.is_empty() and not classes.is_empty() and not terrain.is_empty()
func hydrate_deployment(deployment: Dictionary) -> Dictionary:
var officer := _get_dict(officers, deployment.get("officer_id", ""))
var class_id := str(deployment.get("class_id", officer.get("class_id", "infantry")))
var class_def := _get_dict(classes, class_id)
var equipment := _merged_equipment(officer.get("equipment", {}), deployment.get("equipment", {}))
var range_pair := _resolve_attack_range(class_def, equipment)
var stats := {}
_overlay_stats(stats, officer.get("base", {}))
_overlay_stats(stats, deployment.get("base", {}))
_add_stats(stats, class_def.get("base_bonus", {}))
_add_equipment_bonuses(stats, equipment)
var unit := {}
unit["id"] = str(deployment.get("unit_id", deployment.get("id", deployment.get("officer_id", ""))))
unit["officer_id"] = str(deployment.get("officer_id", ""))
unit["name"] = str(deployment.get("name", officer.get("name", unit["id"])))
unit["class_id"] = class_id
unit["class"] = str(class_def.get("name", class_id.capitalize()))
unit["team"] = str(deployment.get("team", "enemy"))
unit["pos"] = deployment.get("pos", [0, 0])
unit["requires_joined"] = bool(deployment.get("requires_joined", false))
unit["controllable"] = bool(deployment.get("controllable", true))
unit["persist_progression"] = bool(deployment.get("persist_progression", true))
unit["ai_target_priority"] = int(deployment.get("ai_target_priority", 0))
unit["level"] = int(deployment.get("level", officer.get("level", 1)))
unit["exp"] = int(deployment.get("exp", officer.get("exp", 0)))
unit["max_hp"] = int(stats.get("hp", stats.get("max_hp", 1)))
unit["hp"] = int(deployment.get("hp", unit["max_hp"]))
unit["max_mp"] = int(stats.get("mp", 0))
unit["mp"] = int(deployment.get("mp", unit["max_mp"]))
unit["atk"] = int(stats.get("atk", 1))
unit["def"] = int(stats.get("def", 0))
unit["int"] = int(stats.get("int", 0))
unit["agi"] = int(stats.get("agi", 0))
unit["move"] = int(deployment.get("move", class_def.get("move", 3)))
unit["move_type"] = str(deployment.get("move_type", class_def.get("move_type", "foot")))
if deployment.has("range"):
range_pair = _range_pair_from_value(deployment["range"])
unit["min_range"] = int(deployment.get("min_range", range_pair["min"]))
unit["range"] = int(range_pair["max"])
unit["growth"] = class_def.get("growth", {}).duplicate(true)
unit["growth_bonus"] = officer.get("growth_bonus", {}).duplicate(true)
if deployment.has("growth_bonus") and typeof(deployment["growth_bonus"]) == TYPE_DICTIONARY:
for stat in deployment["growth_bonus"].keys():
unit["growth_bonus"][stat] = deployment["growth_bonus"][stat]
unit["equipment"] = equipment
unit["skills"] = _merged_skill_list(class_def.get("skills", []), officer.get("skills", []), deployment.get("skills", []))
return unit
func get_runtime_terrain_defs(default_defs: Dictionary) -> Dictionary:
var result := default_defs.duplicate(true)
for key in terrain.keys():
var source := _get_dict(terrain, key)
var fallback: Dictionary = result.get(key, {})
var runtime := fallback.duplicate(true)
runtime["id"] = str(source.get("id", runtime.get("id", key)))
runtime["name"] = str(source.get("name", runtime.get("name", key)))
runtime["defense"] = int(source.get("defense", runtime.get("defense", 0)))
runtime["avoid"] = int(source.get("avoid", runtime.get("avoid", 0)))
runtime["move_cost"] = source.get("move_cost", runtime.get("move_cost", 1))
runtime["color"] = _parse_color(source.get("color", runtime.get("color", Color.WHITE)), runtime.get("color", Color.WHITE))
result[key] = runtime
return result
func get_skill(skill_id: String) -> Dictionary:
return _get_dict(skills, skill_id).duplicate(true)
func get_item(item_id: String) -> Dictionary:
return _get_dict(items, item_id).duplicate(true)
func get_item_ids() -> Array:
var result := []
for item_id in items.keys():
result.append(str(item_id))
result.sort()
return result
func get_class_def(class_id: String) -> Dictionary:
return _get_dict(classes, class_id).duplicate(true)
func _load_json_object(path: String) -> Dictionary:
if not FileAccess.file_exists(path):
push_error("Missing data file: %s" % path)
return {}
var text := FileAccess.get_file_as_string(path)
var parsed = JSON.parse_string(text)
if typeof(parsed) != TYPE_DICTIONARY:
push_error("Data file is not a JSON object: %s" % path)
return {}
return parsed
func _get_dict(source: Dictionary, key) -> Dictionary:
if key == null:
return {}
var value = source.get(str(key), {})
if typeof(value) == TYPE_DICTIONARY:
return value
return {}
func _merged_equipment(base_equipment, override_equipment) -> Dictionary:
var result := {}
if typeof(base_equipment) == TYPE_DICTIONARY:
result = base_equipment.duplicate(true)
if typeof(override_equipment) == TYPE_DICTIONARY:
for slot in override_equipment.keys():
result[slot] = override_equipment[slot]
return result
func _overlay_stats(target: Dictionary, source) -> void:
if typeof(source) != TYPE_DICTIONARY:
return
for key in source.keys():
target[key] = int(source[key])
func _add_stats(target: Dictionary, source) -> void:
if typeof(source) != TYPE_DICTIONARY:
return
for key in source.keys():
target[key] = int(target.get(key, 0)) + int(source[key])
func _add_equipment_bonuses(target: Dictionary, equipment: Dictionary) -> void:
for slot in equipment.keys():
var item_id = equipment[slot]
if item_id == null:
continue
var item := _get_dict(items, item_id)
_add_stats(target, item.get("bonuses", {}))
func _merged_skill_list(class_skills, officer_skills, deployment_skills) -> Array:
var result := []
_append_unique_skills(result, class_skills)
_append_unique_skills(result, officer_skills)
_append_unique_skills(result, deployment_skills)
return result
func _append_unique_skills(target: Array, source) -> void:
if typeof(source) != TYPE_ARRAY:
return
for skill_id in source:
var normalized := str(skill_id)
if normalized.is_empty() or target.has(normalized):
continue
target.append(normalized)
func _resolve_attack_range(class_def: Dictionary, equipment: Dictionary) -> Dictionary:
var attack_range := _range_pair_from_value(class_def.get("attack_range", 1))
for slot in equipment.keys():
var item_id = equipment[slot]
if item_id == null:
continue
var item := _get_dict(items, item_id)
if item.has("range"):
var item_range := _range_pair_from_value(item["range"])
attack_range["min"] = min(int(attack_range["min"]), int(item_range["min"]))
attack_range["max"] = max(int(attack_range["max"]), int(item_range["max"]))
return attack_range
func _range_pair_from_value(value) -> Dictionary:
if typeof(value) == TYPE_ARRAY:
if value.size() >= 2:
return _normalized_range_pair(int(value[0]), int(value[1]))
if value.size() == 1:
return _normalized_range_pair(int(value[0]), int(value[0]))
return _normalized_range_pair(1, 1)
if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
return _normalized_range_pair(1, 1)
var scalar := max(1, int(value))
return _normalized_range_pair(scalar, scalar)
func _normalized_range_pair(min_range: int, max_range: int) -> Dictionary:
var safe_min := max(1, min_range)
var safe_max := max(safe_min, max_range)
return {
"min": safe_min,
"max": safe_max
}
func _parse_color(value, fallback: Color) -> Color:
if typeof(value) == TYPE_COLOR:
return value
if typeof(value) == TYPE_ARRAY and value.size() >= 3:
var alpha := 1.0
if value.size() >= 4:
alpha = float(value[3])
return Color(float(value[0]), float(value[1]), float(value[2]), alpha)
var text := str(value)
if Color.html_is_valid(text):
return Color.html(text)
return fallback