349 lines
12 KiB
GDScript
349 lines
12 KiB
GDScript
extends Node
|
|
## Data — доступ к JSON-данным из res://data (autoload).
|
|
##
|
|
## Файлы данных сейчас заготовки, поэтому отсутствующие ключи заменяются
|
|
## безопасными дефолтами с push_warning; игра не должна падать из-за данных.
|
|
|
|
const DATA_DIR := "res://data"
|
|
|
|
const DEFAULT_TYPES := {
|
|
"text": {
|
|
"name_ru": "Текст", "ext": "txt", "icon": "text",
|
|
"recv_time": 2.0, "send_time": 1.5, "recv_fee": 1,
|
|
"base_pay": 80, "weight": 8, "minigame_coeff": 1.0, "size_kb": 64,
|
|
"sla_loss": 2,
|
|
},
|
|
"sound": {
|
|
"name_ru": "Звук", "ext": "mp3", "icon": "sound",
|
|
"recv_time": 3.5, "send_time": 2.5, "recv_fee": 2,
|
|
"base_pay": 140, "weight": 12, "minigame_coeff": 2.0, "size_kb": 3072,
|
|
"sla_loss": 4,
|
|
},
|
|
"photo": {
|
|
"name_ru": "Фото", "ext": "jpg", "icon": "photo",
|
|
"recv_time": 5.0, "send_time": 3.5, "recv_fee": 3,
|
|
"base_pay": 220, "weight": 18, "minigame_coeff": 3.0, "size_kb": 5120,
|
|
"sla_loss": 8,
|
|
},
|
|
"video": {
|
|
"name_ru": "Видео", "ext": "mp4", "icon": "video",
|
|
"recv_time": 8.0, "send_time": 6.0, "recv_fee": 4,
|
|
"base_pay": 380, "weight": 22, "minigame_coeff": 4.0, "size_kb": 20480,
|
|
"sla_loss": 16,
|
|
},
|
|
}
|
|
|
|
## Таблица сложности — до часа 8 (D12); дальше используется последняя строка.
|
|
const DEFAULT_DIFFICULTY := [
|
|
{"hour": 1, "max_requests": 2, "spawn_sec": 7.0},
|
|
{"hour": 2, "max_requests": 3, "spawn_sec": 6.5},
|
|
{"hour": 3, "max_requests": 3, "spawn_sec": 6.0},
|
|
{"hour": 4, "max_requests": 4, "spawn_sec": 5.5},
|
|
{"hour": 5, "max_requests": 4, "spawn_sec": 5.0},
|
|
{"hour": 6, "max_requests": 5, "spawn_sec": 4.5},
|
|
{"hour": 7, "max_requests": 5, "spawn_sec": 4.2},
|
|
{"hour": 8, "max_requests": 6, "spawn_sec": 4.0},
|
|
]
|
|
|
|
const DEFAULT_SHOP := [
|
|
{"id": "slot_3", "name": "3-й слот", "cost": 250, "max_level": 1},
|
|
{"id": "slot_4", "name": "4-й слот", "cost": 450, "max_level": 1},
|
|
{"id": "buffer", "name": "Буфер +2", "cost": 200, "max_level": 2},
|
|
{"id": "packet_timer", "name": "Таймер пакетов +4 с", "cost": 350, "max_level": 3},
|
|
{"id": "zone", "name": "Зона +5 %", "cost": 300, "max_level": 3},
|
|
{"id": "channel", "name": "Канал +25", "cost": 250, "max_level": 4},
|
|
{"id": "section", "name": "Секция", "cost": 300, "max_level": 5},
|
|
{"id": "search", "name": "Поиск", "cost": 250, "max_level": 1},
|
|
{"id": "shield", "name": "Щит", "cost": 400, "max_level": 1},
|
|
{"id": "shop_sla", "name": "SLA +24", "effect": "sla_add", "value": 24, "cost": 300, "max_level": 3},
|
|
]
|
|
|
|
## Прогрессия/открытия (дублирует data/balance.json → progression).
|
|
const DEFAULT_PROGRESSION := {
|
|
"unlocks": {
|
|
"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},
|
|
"minigame_advanced": {"meta": "complete_run", "label": "Продвинутые мини-игры"},
|
|
"attack_junk": {"run_hour": 2},
|
|
"attack_ddos": {"run_hour": 2},
|
|
"attack_phishing": {"run_hour": 3},
|
|
"attack_ransomware": {"run_hour": 4},
|
|
},
|
|
"meta_rewards": {
|
|
"survive_hour_3": {"min_hours": 3, "unlocks": ["search"]},
|
|
"survive_hour_5": {"min_hours": 5, "unlocks": ["shield"]},
|
|
"complete_run": {"min_hours": 9, "unlocks": ["minigame_advanced"]},
|
|
},
|
|
}
|
|
|
|
var balance: Dictionary = {}
|
|
var types: 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")
|
|
types = _merge_types(balance.get("types", null))
|
|
difficulty = _load_difficulty(balance.get("difficulty", null))
|
|
shop = _load_shop(balance.get("shop", 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 — последняя строка.
|
|
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)
|
|
|
|
|
|
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 {}
|
|
|
|
|
|
## Ключ открытия для позиции магазина. "" — позиция доступна всегда.
|
|
## shop_sla продаётся вне прогрессии; поиск/щит открываются мета-наградами.
|
|
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 key := shop_unlock_key(str(item.get("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
|
|
|
|
|
|
## Каталог магазина без закрытых позиций (shop_sla виден всегда).
|
|
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)])
|
|
|
|
|
|
# --- служебное -------------------------------------------------------------
|
|
|
|
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 _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 DEFAULT_PROGRESSION["unlocks"].duplicate(true)
|
|
|
|
|
|
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 DEFAULT_PROGRESSION["meta_rewards"].duplicate(true)
|
|
|
|
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_shop(raw) -> Array:
|
|
if raw is Array and not raw.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 отсутствует, используется дефолт")
|
|
return DEFAULT_SHOP.duplicate(true)
|
|
|
|
|
|
func _load_names(raw) -> Dictionary:
|
|
if raw is Dictionary and not raw.is_empty():
|
|
# Сохраняем все ключи файла (adjectives/nouns/organizations/folders/...),
|
|
# а не только жёстко заданный набор.
|
|
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": []},
|
|
}
|