246 lines
7.8 KiB
GDScript
246 lines
7.8 KiB
GDScript
class_name Shop
|
|
extends RefCounted
|
|
## Магазин: 10 позиций из data/balance.json → shop (D13 + SLA +24, п.13).
|
|
##
|
|
## Уровни покупок хранятся в Game.upgrades. Для совместимости с ядром
|
|
## дублируются агрегированные ключи эффектов: slot_level, buffer_level,
|
|
## timer_level, zone_level, channel_level, sections_unlocked,
|
|
## search_unlocked, shield_unlocked.
|
|
|
|
signal purchased(id: String)
|
|
|
|
const EFFECT_TARGETS := {
|
|
"connection_slots": "slot_level",
|
|
"buffer_slots": "buffer_level",
|
|
"packet_timer": "timer_level",
|
|
"zone_width": "zone_level",
|
|
"channel_capacity": "channel_level",
|
|
"sections": "sections_unlocked",
|
|
"search_highlight": "search_unlocked",
|
|
"shield": "shield_unlocked",
|
|
"sla_add": "sla_level",
|
|
}
|
|
|
|
const FALLBACK_ITEMS := [
|
|
{"id": "shop_slot3", "name_ru": "Третий слот", "effect": "connection_slots", "value": 1, "price": 250, "max_level": 1},
|
|
{"id": "shop_slot4", "name_ru": "Четвёртый слот", "effect": "connection_slots", "value": 1, "price": 450, "max_level": 1},
|
|
{"id": "shop_buffer", "name_ru": "Буфер +2", "effect": "buffer_slots", "value": 2, "price": 200, "max_level": 2},
|
|
{"id": "shop_timer", "name_ru": "Таймер пакетов +4 с", "effect": "packet_timer", "value": 4, "price": 350, "max_level": 3},
|
|
{"id": "shop_zone", "name_ru": "Зона +5%", "effect": "zone_width", "value": 0.05, "price": 300, "max_level": 3},
|
|
{"id": "shop_channel", "name_ru": "Канал +25", "effect": "channel_capacity", "value": 25, "price": 250, "max_level": 4},
|
|
{"id": "shop_section", "name_ru": "Новая секция", "effect": "sections", "value": 1, "price": 300, "max_level": 5},
|
|
{"id": "shop_search", "name_ru": "Поиск", "effect": "search_highlight", "value": 1, "price": 250, "max_level": 1},
|
|
{"id": "shop_shield", "name_ru": "Щит", "effect": "shield", "value": 1, "price": 400, "max_level": 1},
|
|
{"id": "shop_sla", "name_ru": "SLA +24", "effect": "sla_add", "value": 24, "price": 300, "max_level": 3},
|
|
]
|
|
|
|
var _items: Array = []
|
|
|
|
|
|
func _init() -> void:
|
|
refresh()
|
|
|
|
|
|
func refresh() -> void:
|
|
_items = []
|
|
var data = _data_node()
|
|
var raw = null
|
|
if data != null and data.get("balance") is Dictionary:
|
|
raw = data.balance.get("shop", null)
|
|
if raw is Array and not raw.is_empty():
|
|
for item in raw:
|
|
if item is Dictionary and not str(item.get("id", "")).is_empty():
|
|
_items.append(item.duplicate(true))
|
|
if _items.is_empty():
|
|
for item in FALLBACK_ITEMS:
|
|
_items.append(item.duplicate(true))
|
|
|
|
|
|
func items() -> Array:
|
|
return _items.duplicate(true)
|
|
|
|
|
|
## Каталог без закрытых прогрессией позиций. shop_sla доступен всегда.
|
|
func available_items() -> Array:
|
|
var out: Array = []
|
|
for item in _items:
|
|
if item is Dictionary and item_unlocked(item):
|
|
out.append(item.duplicate(true))
|
|
return out
|
|
|
|
|
|
## Открыта ли позиция: shop_sla всегда; shop_search/shield — по мета-награде;
|
|
## остальные — по открытию магазина (progression.unlocks.shop).
|
|
func item_unlocked(item: Dictionary) -> bool:
|
|
var key := _unlock_key(str(item.get("id", "")))
|
|
if key.is_empty():
|
|
return true
|
|
var game = _game()
|
|
if game != null and game.has_method("feature_unlocked"):
|
|
return bool(game.feature_unlocked(key))
|
|
return true
|
|
|
|
|
|
func item_count() -> int:
|
|
return _items.size()
|
|
|
|
|
|
func get_item(id: String) -> Dictionary:
|
|
for item in _items:
|
|
if str(item.get("id", "")) == str(id):
|
|
return item.duplicate(true)
|
|
return {}
|
|
|
|
|
|
func level(id: String) -> int:
|
|
var game = _game()
|
|
if game == null:
|
|
return 0
|
|
var upgrades: Dictionary = game.upgrades if game.upgrades is Dictionary else {}
|
|
if upgrades.has(str(id)):
|
|
return int(upgrades[str(id)])
|
|
var item := get_item(id)
|
|
if item.is_empty():
|
|
return 0
|
|
var effect := str(item.get("effect", ""))
|
|
var target := str(EFFECT_TARGETS.get(effect, ""))
|
|
var aggregate := 0
|
|
if not target.is_empty():
|
|
aggregate = int(upgrades.get(target, 0))
|
|
# Для двух слотовых товаров агрегат slot_level раскладывается по порядку
|
|
# с учётом уже купленного второго товара.
|
|
if effect == "connection_slots":
|
|
var other_id := "shop_slot4" if id == "shop_slot3" else "shop_slot3"
|
|
var other_level := int(upgrades.get(other_id, 0))
|
|
return clampi(aggregate - other_level, 0, 1)
|
|
return clampi(aggregate, 0, max_level(id))
|
|
|
|
|
|
func max_level(id: String) -> int:
|
|
var item := get_item(id)
|
|
if item.is_empty():
|
|
return 0
|
|
return maxi(1, int(item.get("max_level", 1)))
|
|
|
|
|
|
func price(id: String) -> int:
|
|
var item := get_item(id)
|
|
if item.is_empty():
|
|
return 0
|
|
var current := level(id)
|
|
var prices = item.get("prices", null)
|
|
if prices is Array and not (prices as Array).is_empty():
|
|
var arr: Array = prices
|
|
return maxi(0, int(arr[mini(current, arr.size() - 1)]))
|
|
if item.has("price"):
|
|
return maxi(0, int(item.get("price", 0)))
|
|
return maxi(0, int(item.get("cost", 0)))
|
|
|
|
|
|
func can_buy(id: String) -> bool:
|
|
var item := get_item(id)
|
|
if item.is_empty():
|
|
return false
|
|
if not item_unlocked(item):
|
|
return false
|
|
if level(id) >= max_level(id):
|
|
return false
|
|
var game = _game()
|
|
if game == null:
|
|
return false
|
|
return int(game.money) >= price(id)
|
|
|
|
|
|
func buy(id: String) -> bool:
|
|
if not can_buy(id):
|
|
return false
|
|
var item := get_item(id)
|
|
var game = _game()
|
|
if game == null:
|
|
return false
|
|
var cost := price(id)
|
|
game.money = maxi(0, int(game.money) - cost)
|
|
if game.has_signal("money_changed"):
|
|
game.money_changed.emit(game.money)
|
|
if not (game.upgrades is Dictionary):
|
|
game.upgrades = {}
|
|
var current := level(id)
|
|
game.upgrades[id] = current + 1
|
|
var effect := str(item.get("effect", ""))
|
|
var target := str(EFFECT_TARGETS.get(effect, ""))
|
|
if not target.is_empty():
|
|
game.upgrades[target] = int(game.upgrades.get(target, 0)) + 1
|
|
if effect == "sections":
|
|
_unlock_section(game)
|
|
elif effect == "sla_add":
|
|
# SLA +24 за уровень; Game.add_sla сам клампит до MAX_SLA.
|
|
var value := float(item.get("value", 24))
|
|
if game.has_method("add_sla"):
|
|
game.add_sla(value, "shop")
|
|
_play_sfx(&"money_down")
|
|
purchased.emit(id)
|
|
if game.has_signal("toast"):
|
|
game.toast.emit("Куплено: %s" % str(item.get("name_ru", id)))
|
|
return true
|
|
|
|
|
|
## Агрегированный уровень эффекта (для UI/тестов).
|
|
func effect_level(effect: String) -> int:
|
|
var game = _game()
|
|
if game == null:
|
|
return 0
|
|
var target := str(EFFECT_TARGETS.get(effect, ""))
|
|
if target.is_empty():
|
|
return 0
|
|
return int(game.upgrades.get(target, 0))
|
|
|
|
|
|
# --- служебное -------------------------------------------------------------
|
|
|
|
## Ключ открытия позиции. Данные — из Data, иначе локальная копия правила.
|
|
func _unlock_key(id: String) -> String:
|
|
var data = _data_node()
|
|
if data != null and data.has_method("shop_unlock_key"):
|
|
return str(data.shop_unlock_key(id))
|
|
match id:
|
|
"shop_sla":
|
|
return ""
|
|
"shop_search":
|
|
return "search"
|
|
"shop_shield":
|
|
return "shield"
|
|
_:
|
|
return "shop"
|
|
|
|
|
|
func _unlock_section(game) -> void:
|
|
if game.registry == null or not game.registry.has_method("create_section"):
|
|
return
|
|
for letter in ["D", "E", "F", "G", "H"]:
|
|
if not game.registry.sections.has(letter):
|
|
game.registry.create_section(letter)
|
|
return
|
|
|
|
|
|
func _game():
|
|
var loop := Engine.get_main_loop()
|
|
if loop is SceneTree:
|
|
return (loop as SceneTree).root.get_node_or_null("Game")
|
|
return null
|
|
|
|
|
|
func _data_node():
|
|
var loop := Engine.get_main_loop()
|
|
if loop is SceneTree:
|
|
return (loop as SceneTree).root.get_node_or_null("Data")
|
|
return null
|
|
|
|
|
|
func _play_sfx(id: StringName) -> void:
|
|
var loop := Engine.get_main_loop()
|
|
if not (loop is SceneTree):
|
|
return
|
|
var audio = (loop as SceneTree).root.get_node_or_null("Audio")
|
|
if audio != null and audio.has_method("play_sfx"):
|
|
audio.play_sfx(str(id))
|