Files

673 lines
23 KiB
GDScript
Raw Permalink 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.
extends Node
## Data — доступ к JSON-данным из res://data (autoload).
##
## v2: новые секции balance.json (constants/hours/grid/shop_rules/shop/
## minigames/attacks/payout/sla). Отсутствующие ключи заменяются безопасными
## дефолтами с push_warning; игра не должна падать из-за данных.
##
## Legacy-API (tr, type_def, difficulty_for, shop_item, available_shop,
## folder_name) сохранён для ещё не переписанных систем.
const DATA_DIR := "res://data"
const DEFAULT_TYPES := {
"text": {
"name_ru": "Текст", "ext": "txt", "icon": "text",
"size_kb": 64,
"cells": 1, "base_time_sec": 3, "recv_fee": 1,
"base_pay": 18, "weight": 1, "minigame_coeff": 1, "sla_loss": 2,
"shapes": ["O1"],
},
"sound": {
"name_ru": "Звук", "ext": "mp3", "icon": "sound",
"size_kb": 3072,
"cells": 2, "base_time_sec": 5, "recv_fee": 2,
"base_pay": 30, "weight": 2, "minigame_coeff": 2, "sla_loss": 4,
"shapes": ["I2H", "I2V"],
},
"photo": {
"name_ru": "Фото", "ext": "jpg", "icon": "photo",
"size_kb": 5120,
"cells": 3, "base_time_sec": 8, "recv_fee": 4,
"base_pay": 44, "weight": 4, "minigame_coeff": 4, "sla_loss": 8,
"shapes": ["I3H", "I3V", "L3a", "L3b", "L3c", "L3d"],
},
"video": {
"name_ru": "Видео", "ext": "mp4", "icon": "video",
"size_kb": 20480,
"cells": 4, "base_time_sec": 12, "recv_fee": 6,
"base_pay": 69, "weight": 8, "minigame_coeff": 8, "sla_loss": 16,
"shapes": ["I4H", "I4V", "O4", "T4a", "T4b", "T4c", "T4d",
"L4a", "L4b", "L4c", "L4d", "J4a", "J4b", "J4c", "J4d", "S4", "Z4"],
},
}
## Таблица сложности v1 — оставлена для legacy difficulty_for().
const DEFAULT_DIFFICULTY := [
{"hour": 1, "max_requests": 2, "spawn_sec": 7.0},
{"hour": 2, "max_requests": 5, "spawn_sec": 6.5},
{"hour": 3, "max_requests": 7, "spawn_sec": 6.0},
{"hour": 4, "max_requests": 9, "spawn_sec": 5.5},
{"hour": 5, "max_requests": 10, "spawn_sec": 5.0},
{"hour": 6, "max_requests": 11, "spawn_sec": 4.5},
{"hour": 7, "max_requests": 11, "spawn_sec": 4.2},
{"hour": 8, "max_requests": 12, "spawn_sec": 4.0},
]
const DEFAULT_SEND_ORDER := ["dots", "sound", "text", "video", "photo", "maze"]
const DEFAULT_ACHIEVABLE_SUMS := [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 24, 25, 26, 28, 32,
]
const DEFAULT_HOUR_FILES := [8, 8, 9, 10, 11, 11, 12, 12, 12]
const DEFAULT_ACTIVE_REQUESTS := [1, 2, 2, 3, 3, 3, 4, 4, 4]
const DEFAULT_ATTACKS := [0, 1, 1, 2, 2, 3, 3, 4, 4]
const DEFAULT_INCOME_TARGET := [90, 220, 380, 560, 760, 980, 1200, 1450, 1700]
const DEFAULT_RECV_INTERVAL := [12, 11, 10, 9, 8, 7.5, 7, 6.5, 6]
const DEFAULT_SEND_INTERVAL := [20, 24, 18, 24, 24, 24, 24, 24, 24]
## Длительность часа: 120 + 30 * (hour - 1) → 120..360.
const DEFAULT_HOUR_DURATION_BASE := 120
const DEFAULT_HOUR_DURATION_STEP := 30
## Порядок: text, sound, photo, video (§2.5). Совпадает с
## DEFAULT_REQUEST_TYPE_WEIGHTS: приём и запросы используют одни типы.
const DEFAULT_TYPE_WEIGHTS := [
[1.00, 0.00, 0.00, 0.00],
[0.80, 0.20, 0.00, 0.00],
[0.50, 0.50, 0.00, 0.00],
[0.40, 0.30, 0.30, 0.00],
[0.35, 0.35, 0.30, 0.00],
[0.30, 0.30, 0.30, 0.10],
[0.22, 0.23, 0.30, 0.25],
[0.15, 0.25, 0.35, 0.25],
[0.10, 0.15, 0.30, 0.45],
]
## Доли числа файлов в запросе: 1..4 (§2.6). Добавлен ключ "4" (4 файла).
const DEFAULT_REQUEST_MIX := {"1": 0.76, "2": 0.24, "3": 0.0, "4": 0.0}
## Веса выбора ТИПА файлов для required-слотов запроса (§2.6).
## Отдельно от type_weights: те задают типы входящих пакетов.
## Порядок: text, sound, photo, video.
const DEFAULT_REQUEST_TYPE_WEIGHTS := [
[1.00, 0.00, 0.00, 0.00],
[0.80, 0.20, 0.00, 0.00],
[0.50, 0.50, 0.00, 0.00],
[0.40, 0.30, 0.30, 0.00],
[0.35, 0.35, 0.30, 0.00],
[0.30, 0.30, 0.30, 0.10],
[0.22, 0.23, 0.30, 0.25],
[0.15, 0.25, 0.35, 0.25],
[0.10, 0.15, 0.30, 0.45],
]
const DEFAULT_LOT_IDS := [
"lot_slots", "lot_speed", "lot_sla", "lot_shield",
"lot_money", "lot_folder", "lot_channel", "lot_timer", "lot_udp",
]
const DEFAULT_LOT_NAMES := [
"Порты коммутатора", "Оптоволокно", "Резервный ИБП", "Заземление",
"Корпоративный контракт", "Складской стеллаж", "Пропускная способность",
"Регламент обработки", "UDP-повторитель",
]
const DEFAULT_LOT_EFFECTS := [
"connection_slots", "transfer_speed", "sla_cap", "shield_charge",
"money_mult", "folder_cells", "channel_capacity", "request_timer",
"udp_success",
]
const DEFAULT_LOT_VALUES := [1, 0.10, 40, 1, 0.05, 1, 6, 7, 0.10]
const DEFAULT_LOT_MAX := [4, 6, 4, 3, 8, 5, 6, 5, 4]
const DEFAULT_LOT_PRICES := [
[90, 200, 400, 700],
[55, 75, 100, 135, 180, 245],
[80, 160, 270, 430],
[100, 220, 410],
[70, 95, 130, 170, 220, 285, 365, 465],
[50, 100, 180, 290, 440],
[45, 70, 95, 130, 175, 230],
[45, 75, 115, 165, 230],
[60, 120, 210, 350],
]
const DEFAULT_LOT_WEIGHTS := [
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.8, 0.8],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.8, 0.8],
[0.8, 0.8, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
[0.3, 0.3, 0.3, 0.8, 0.8, 0.8, 1.0, 1.0, 1.0],
[0.6, 0.6, 0.6, 1.0, 1.0, 1.0, 1.2, 1.2, 1.2],
[1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.8, 0.8, 0.8],
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 0.9],
[1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.7, 0.7, 0.7],
[0.8, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
]
const DEFAULT_SHOP_RULES := {
"offers": 3,
"unlock_hour": 2,
"reroll_costs": [40, 80, 120],
"max_rerolls": 3,
"last_offer_weight": 0.5,
"pity_hours": 2,
"pity_weight": 1.5,
}
const DEFAULT_GRID_DIMS := [[5, 4], [6, 4], [7, 5], [8, 5], [8, 6], [9, 6]]
var balance: Dictionary = {}
var constants: Dictionary = {}
var economy: Dictionary = {}
var types: Dictionary = {}
var hours: Array = []
var grid: Dictionary = {}
var shop_lots: Array = []
var rules: Dictionary = {}
var minigames: Dictionary = {}
var attacks: Dictionary = {}
var payout_rules: Dictionary = {}
var sla_rules: Dictionary = {}
var difficulty: Array = []
var shop: Array = []
var names: Dictionary = {}
var strings: Dictionary = {}
var unlocks: Dictionary = {}
var meta_rewards: Dictionary = {}
var _cache: Dictionary = {}
func _ready() -> void:
load_all()
func load_all() -> void:
_cache.clear()
balance = load_json("balance.json")
constants = _as_dict(balance.get("constants", null))
economy = _as_dict(balance.get("economy", null))
types = _merge_types(balance.get("types", null))
hours = _load_hours(balance.get("hours", null))
grid = _as_dict(balance.get("grid", null))
if grid.is_empty():
push_warning("Data: balance.json → grid отсутствует, используются дефолты")
grid = {"folder_cols": 5, "folder_rows": 4, "gap": 6,
"cell_min": 56, "cell_max": 104, "table_cols": 4, "table_rows": 6,
"folder_dims": DEFAULT_GRID_DIMS.duplicate(true), "shapes": {}}
shop_lots = _load_shop_lots(balance.get("shop", null))
shop = shop_lots
rules = _load_rules(balance.get("shop_rules", null))
minigames = _as_dict(balance.get("minigames", null))
if minigames.is_empty():
push_warning("Data: balance.json → minigames отсутствует")
minigames = {"games": {}, "selection": {}}
attacks = _as_dict(balance.get("attacks", null))
payout_rules = _as_dict(balance.get("payout", null))
sla_rules = _as_dict(balance.get("sla", null))
difficulty = _load_difficulty(balance.get("difficulty", null))
names = _load_names(load_json("names_ru.json"))
strings = load_json("strings_ru.json")
var progression = balance.get("progression", null)
unlocks = _load_unlocks(progression)
meta_rewards = _load_meta_rewards(progression)
## Прочитать data/<file_name> как Dictionary. Ошибки — warning + {}.
func load_json(file_name: String) -> Dictionary:
if _cache.has(file_name):
return _cache[file_name]
var path := DATA_DIR.path_join(file_name)
if not FileAccess.file_exists(path):
push_warning("Data: файл не найден: %s — используются дефолты" % path)
_cache[file_name] = {}
return {}
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
push_warning("Data: не удалось открыть %s (код %s)" % [path, FileAccess.get_open_error()])
_cache[file_name] = {}
return {}
var text := file.get_as_text()
file.close()
var parser := JSON.new()
var err := parser.parse(text)
if err != OK:
push_warning("Data: %s — ошибка JSON (строка %d: %s), используются дефолты" % [path, parser.get_error_line(), parser.get_error_message()])
_cache[file_name] = {}
return {}
var parsed = parser.data
if not (parsed is Dictionary):
push_warning("Data: %s не является JSON-объектом — используются дефолты" % path)
_cache[file_name] = {}
return {}
_cache[file_name] = parsed
return parsed
@warning_ignore("native_method_override")
func tr(key: StringName, plural: StringName = &"") -> String:
var k := str(key)
var value = _lookup_key(strings, k)
if value == null:
push_warning("Data: нет строки '%s'" % k)
return k
return str(value)
## Есть ли строка (без warning), поддержка вложенных ключей.
func has_string(key: StringName) -> bool:
return _lookup_key(strings, str(key)) != null
## Достать значение по ключу с вложенностью ("ui.title") или плоскому ключу.
func _lookup_key(source: Dictionary, path: String):
if source.has(path):
return source[path]
if not path.contains("."):
return null
var current = source
for part in path.split(".", false):
if not (current is Dictionary) or not current.has(part):
return null
current = current[part]
return current
func type_def(type: String) -> Dictionary:
var key := str(type)
var value = types.get(key, null)
if value is Dictionary and not value.is_empty():
return value
push_warning("Data: неизвестный тип файла '%s' — дефолт text" % key)
var fallback = types.get("text", null)
if fallback is Dictionary:
return fallback
return DEFAULT_TYPES["text"].duplicate(true)
## Строка таблицы сложности; для hour > max — последняя строка (legacy).
func difficulty_for(hour: int) -> Dictionary:
var h := maxi(1, int(hour))
if difficulty.is_empty():
push_warning("Data: пустая таблица сложности — дефолт часа 1")
return DEFAULT_DIFFICULTY[0].duplicate(true)
var prev: Dictionary = DEFAULT_DIFFICULTY[0].duplicate(true)
var last: Dictionary = prev
for row in difficulty:
if not (row is Dictionary):
continue
last = row
var row_hour := int(row.get("hour", 0))
if row_hour == h:
return row.duplicate(true)
if row_hour < h:
prev = row
else:
return prev.duplicate(true) if not prev.is_empty() else row.duplicate(true)
return last.duplicate(true)
# --- v2: часы --------------------------------------------------------------
## Строка бюджета часа (duration/files/active_requests/attacks/
## income_target/type_weights/request_mix/request_type_weights).
## hour > 9 → последняя строка.
func hour_def(hour: int) -> Dictionary:
var h := maxi(1, int(hour))
if hours.is_empty():
push_warning("Data: balance.json → hours отсутствует, дефолт часа %d" % h)
return _default_hour_row(h)
var prev: Dictionary = hours[0]
for row in hours:
if not (row is Dictionary):
continue
var row_hour := int(row.get("hour", 0))
if row_hour == h:
return row.duplicate(true)
if row_hour < h:
prev = row
elif row_hour > h:
return prev.duplicate(true)
return (hours[hours.size() - 1] as Dictionary).duplicate(true)
func hour_count() -> int:
return maxi(1, hours.size())
# --- v2: магазин -----------------------------------------------------------
func lot_defs() -> Array:
return shop_lots.duplicate(true)
func lot_def(id: String) -> Dictionary:
for lot in shop_lots:
if lot is Dictionary and str(lot.get("id", "")) == str(id):
return lot.duplicate(true)
push_warning("Data: нет лота '%s'" % str(id))
return {}
func shop_rules() -> Dictionary:
return rules.duplicate(true)
# --- v2: мини-игры ---------------------------------------------------------
func minigame_def(id: String) -> Dictionary:
var games = minigames.get("games", null)
if games is Dictionary:
var def = (games as Dictionary).get(str(id), null)
if def is Dictionary:
return (def as Dictionary).duplicate(true)
push_warning("Data: нет мини-игры '%s'" % str(id))
return {}
func minigame_selection() -> Dictionary:
var out: Dictionary = {}
var raw = minigames.get("selection", null)
if raw is Dictionary:
out = (raw as Dictionary).duplicate(true)
if not (out.get("order", null) is Array) or (out.get("order", null) as Array).is_empty():
out["order"] = DEFAULT_SEND_ORDER.duplicate()
if not (out.get("achievable_sums", null) is Array) or (out.get("achievable_sums", null) as Array).is_empty():
out["achievable_sums"] = DEFAULT_ACHIEVABLE_SUMS.duplicate()
out["max_index"] = int(out.get("max_index", 26))
var coeff: Dictionary = {}
for key in ["text", "sound", "photo", "video"]:
coeff[key] = int(type_def(key).get("minigame_coeff", DEFAULT_TYPES[key].get("minigame_coeff", 1)))
out["coeff"] = coeff
return out
## Legacy-ступень для индекса (idx / 6 + 1), три ступени отправки.
func minigame_tier(index: int) -> int:
return clampi(1 + int(floor(float(maxi(0, index)) / 6.0)), 1, 3)
# --- v2: атаки -------------------------------------------------------------
func attack_def(id: String) -> Dictionary:
var kinds = attacks.get("kinds", null)
if kinds is Dictionary:
var def = (kinds as Dictionary).get(str(id), null)
if def is Dictionary:
return (def as Dictionary).duplicate(true)
push_warning("Data: нет атаки '%s'" % str(id))
return {}
func attack_tier(hour: int) -> int:
return clampi(1 + int(floor(float(maxi(1, int(hour)) - 2) / 2.0)), 1, 4)
func attack_schedule() -> Array:
var raw = attacks.get("schedule", null)
if raw is Array:
return (raw as Array).duplicate()
return DEFAULT_ATTACKS.duplicate()
# --- v2: сетка -------------------------------------------------------------
func grid_rules() -> Dictionary:
return grid.duplicate(true)
func shape_def(id: String) -> Dictionary:
var shapes = grid.get("shapes", null)
if shapes is Dictionary:
var def = (shapes as Dictionary).get(str(id), null)
if def is Dictionary:
return (def as Dictionary).duplicate(true)
push_warning("Data: нет фигуры '%s'" % str(id))
return {}
func shapes() -> Dictionary:
var raw = grid.get("shapes", null)
if raw is Dictionary:
return (raw as Dictionary).duplicate(true)
return {}
func folder_dims_table() -> Array:
var raw = grid.get("folder_dims", null)
if raw is Array and not (raw as Array).is_empty():
return (raw as Array).duplicate(true)
return DEFAULT_GRID_DIMS.duplicate(true)
# --- legacy shop API -------------------------------------------------------
func shop_item(id: String) -> Dictionary:
for item in shop:
if item is Dictionary and str(item.get("id", "")) == str(id):
return item.duplicate(true)
push_warning("Data: нет товара '%s'" % str(id))
return {}
## Ключ открытия для позиции магазина. "" — позиция доступна всегда.
func shop_unlock_key(id: String) -> String:
match str(id):
"shop_sla":
return ""
"shop_search":
return "search"
"shop_shield":
return "shield"
_:
return "shop"
## Открыта ли позиция магазина по текущему состоянию Game (run_hour/meta).
func shop_item_unlocked(item: Dictionary) -> bool:
var id := str(item.get("id", ""))
if id.begins_with("lot_"):
return true
var key := shop_unlock_key(id)
if key.is_empty():
return true
var game = _game_node()
if game != null and game.has_method("feature_unlocked"):
return bool(game.feature_unlocked(key))
return true
## Каталог магазина без закрытых позиций.
func available_shop() -> Array:
var out: Array = []
for item in shop:
if item is Dictionary and not str(item.get("id", "")).is_empty() and shop_item_unlocked(item):
out.append(item.duplicate(true))
return out
## Случайное имя папки из names_ru.json → folders (массив или {default: []}).
func folder_name() -> String:
var pool = names.get("folders", null)
var values: Array = []
if pool is Array:
values = pool
elif pool is Dictionary:
var default_pool = pool.get("default", [])
if default_pool is Array:
values = default_pool
if values.is_empty():
return "Разное"
return str(values[randi_range(0, values.size() - 1)])
## Значение из constants с fallback (для других систем).
func const_value(key: String, fallback_value = null):
if constants.has(key):
return constants[key]
return fallback_value
# --- служебное -------------------------------------------------------------
func _game_node():
var loop := Engine.get_main_loop()
if loop is SceneTree:
return (loop as SceneTree).root.get_node_or_null("Game")
return null
func _as_dict(raw) -> Dictionary:
if raw is Dictionary:
return (raw as Dictionary).duplicate(true)
return {}
func _load_unlocks(progression) -> Dictionary:
if progression is Dictionary:
var raw = progression.get("unlocks", null)
if raw is Dictionary and not raw.is_empty():
return raw.duplicate(true)
push_warning("Data: balance.json → progression.unlocks отсутствует, используются дефолты")
return {
"shop": {"run_hour": 2, "label": "Магазин"},
"search": {"meta": "survive_hour_3", "label": "Поиск"},
"shield": {"meta": "survive_hour_5", "label": "Щит"},
"minigame_text": {"run_hour": 1},
"minigame_sound": {"run_hour": 2},
"minigame_photo": {"run_hour": 3},
"minigame_video": {"run_hour": 4},
"attack_junk": {"run_hour": 2},
"attack_ddos": {"run_hour": 2},
"attack_phishing": {"run_hour": 3},
"attack_ransomware": {"run_hour": 4},
}
func _load_meta_rewards(progression) -> Dictionary:
if progression is Dictionary:
var raw = progression.get("meta_rewards", null)
if raw is Dictionary and not raw.is_empty():
return raw.duplicate(true)
push_warning("Data: balance.json → progression.meta_rewards отсутствует, используются дефолты")
return {
"survive_hour_3": {"min_hours": 3, "unlocks": ["search"]},
"survive_hour_5": {"min_hours": 5, "unlocks": ["shield"]},
"complete_run": {"min_hours": 9, "unlocks": ["minigame_advanced"]},
}
func _merge_types(raw) -> Dictionary:
var out: Dictionary = DEFAULT_TYPES.duplicate(true)
if not (raw is Dictionary) or raw.is_empty():
push_warning("Data: balance.json → types отсутствует, используются дефолты")
return out
for key in raw.keys():
var src = raw[key]
if not (src is Dictionary):
continue
var base: Dictionary = {}
var existing = out.get(str(key), null)
if existing is Dictionary:
base = existing.duplicate(true)
for field in src.keys():
base[field] = src[field]
out[str(key)] = base
return out
func _load_difficulty(raw) -> Array:
if raw is Array and not raw.is_empty():
var out: Array = []
for row in raw:
if row is Dictionary and row.has("hour"):
out.append(row.duplicate(true))
if not out.is_empty():
out.sort_custom(func(a, b): return int(a.get("hour", 0)) < int(b.get("hour", 0)))
return out
push_warning("Data: balance.json → difficulty отсутствует, используется дефолт")
return DEFAULT_DIFFICULTY.duplicate(true)
func _load_hours(raw) -> Array:
if raw is Array and not (raw as Array).is_empty():
var out: Array = []
for row in raw:
if row is Dictionary:
out.append(row.duplicate(true))
if not out.is_empty():
out.sort_custom(func(a, b): return int(a.get("hour", 0)) < int(b.get("hour", 0)))
return out
push_warning("Data: balance.json → hours отсутствует, используются дефолты")
var fallback: Array = []
for i in range(9):
fallback.append(_default_hour_row(i + 1))
return fallback
func _default_hour_row(hour: int) -> Dictionary:
var idx := clampi(hour - 1, 0, 8)
var type_keys := ["text", "sound", "photo", "video"]
var weights: Dictionary = {}
var req_weights: Dictionary = {}
for i in range(type_keys.size()):
weights[type_keys[i]] = DEFAULT_TYPE_WEIGHTS[idx][i]
req_weights[type_keys[i]] = DEFAULT_REQUEST_TYPE_WEIGHTS[idx][i]
return {
"hour": maxi(1, hour),
"duration": DEFAULT_HOUR_DURATION_BASE + DEFAULT_HOUR_DURATION_STEP * idx,
"files": DEFAULT_HOUR_FILES[idx],
"active_requests": DEFAULT_ACTIVE_REQUESTS[idx],
"attacks": DEFAULT_ATTACKS[idx],
"income_target": DEFAULT_INCOME_TARGET[idx],
"send_interval": DEFAULT_SEND_INTERVAL[idx],
"type_weights": weights,
"request_mix": DEFAULT_REQUEST_MIX.duplicate(),
"request_type_weights": req_weights,
}
func _load_shop_lots(raw) -> Array:
if raw is Array and not (raw as Array).is_empty():
var out: Array = []
for item in raw:
if item is Dictionary and not str(item.get("id", "")).is_empty():
out.append(item.duplicate(true))
if not out.is_empty():
return out
push_warning("Data: balance.json → shop отсутствует, используются %d дефолтных лотов" % DEFAULT_LOT_IDS.size())
var fallback: Array = []
for i in range(DEFAULT_LOT_IDS.size()):
fallback.append({
"id": DEFAULT_LOT_IDS[i],
"name_ru": DEFAULT_LOT_NAMES[i],
"desc_ru": "",
"effect": DEFAULT_LOT_EFFECTS[i],
"value": DEFAULT_LOT_VALUES[i],
"max_level": DEFAULT_LOT_MAX[i],
"prices": DEFAULT_LOT_PRICES[i].duplicate(),
"level_names": [],
"weight_by_hour": DEFAULT_LOT_WEIGHTS[i].duplicate(),
})
return fallback
func _load_rules(raw) -> Dictionary:
var out: Dictionary = DEFAULT_SHOP_RULES.duplicate(true)
if raw is Dictionary and not raw.is_empty():
for key in raw.keys():
out[str(key)] = raw[key]
return out
push_warning("Data: balance.json → shop_rules отсутствует, используются дефолты")
return out
func _load_names(raw) -> Dictionary:
if raw is Dictionary and not raw.is_empty():
return raw.duplicate(true)
push_warning("Data: names_ru.json пуст, используются пустые пулы имён")
return {
"adjectives": [],
"nouns": [],
"organizations": [],
"avatar_captions": [],
"first_names": [],
"last_names": [],
"folders": {"default": ["Разное"], "section_page_prefixes": []},
}