250 lines
8.8 KiB
GDScript
250 lines
8.8 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 get_portrait_for_speaker(speaker_name: String) -> String:
|
|
var normalized_speaker := _normalized_speaker_name(speaker_name)
|
|
if normalized_speaker.is_empty():
|
|
return ""
|
|
for officer_id in officers.keys():
|
|
var officer := _get_dict(officers, officer_id)
|
|
if _normalized_speaker_name(str(officer.get("name", ""))) == normalized_speaker:
|
|
return str(officer.get("portrait", ""))
|
|
return ""
|
|
|
|
|
|
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 _normalized_speaker_name(value: String) -> String:
|
|
return value.strip_edges().to_lower()
|
|
|
|
|
|
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
|