feat: adding the skin_shader_generator, and gacha base barebone
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
extends Control
|
||||
## FragmentCraftPanel — Shows all craftable skins and their fragment requirements.
|
||||
## Linked from GachaPanel via the "🧩 Fragment Craft" button.
|
||||
|
||||
signal closed
|
||||
|
||||
# ─── Node refs ───────────────────────────────────────────────────────────────
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var recipe_list := %RecipeList as VBoxContainer
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
@onready var frag_balance := %FragBalance as Label
|
||||
|
||||
const FRAG_ICONS := {
|
||||
"frag_common": "⬜",
|
||||
"frag_uncommon": "🟩",
|
||||
"frag_rare": "🟦"
|
||||
}
|
||||
|
||||
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
||||
func _ready() -> void:
|
||||
back_btn.pressed.connect(_on_close)
|
||||
|
||||
func show_panel() -> void:
|
||||
show()
|
||||
_refresh()
|
||||
|
||||
func _refresh() -> void:
|
||||
_update_frag_balance()
|
||||
_rebuild_recipe_list()
|
||||
|
||||
# ─── Fragment balance header ──────────────────────────────────────────────────
|
||||
func _update_frag_balance() -> void:
|
||||
var frags: Dictionary = UserProfileManager.fragments
|
||||
var parts: Array = []
|
||||
for fid in ["frag_common", "frag_uncommon", "frag_rare"]:
|
||||
var icon: String = FRAG_ICONS.get(fid, "?")
|
||||
var count: int = frags.get(fid, 0)
|
||||
parts.append("%s ×%d" % [icon, count])
|
||||
frag_balance.text = " ".join(parts)
|
||||
|
||||
# ─── Recipe cards ─────────────────────────────────────────────────────────────
|
||||
func _rebuild_recipe_list() -> void:
|
||||
for c in recipe_list.get_children(): c.queue_free()
|
||||
await get_tree().process_frame
|
||||
|
||||
var recipes: Dictionary = GachaManager.get_all_recipes()
|
||||
for recipe_id in recipes:
|
||||
var recipe: Dictionary = recipes[recipe_id]
|
||||
var card := _make_recipe_card(recipe_id, recipe)
|
||||
recipe_list.add_child(card)
|
||||
|
||||
func _make_recipe_card(recipe_id: String, recipe: Dictionary) -> PanelContainer:
|
||||
var can_craft: bool = GachaManager.can_craft(recipe_id)
|
||||
var frags: Dictionary = UserProfileManager.fragments
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 14)
|
||||
margin.add_theme_constant_override("margin_top", 10)
|
||||
margin.add_theme_constant_override("margin_right", 14)
|
||||
margin.add_theme_constant_override("margin_bottom", 10)
|
||||
panel.add_child(margin)
|
||||
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 14)
|
||||
margin.add_child(hbox)
|
||||
|
||||
# Left info
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_theme_constant_override("separation", 4)
|
||||
hbox.add_child(vbox)
|
||||
|
||||
var name_lbl := Label.new()
|
||||
name_lbl.text = recipe.get("name", recipe_id)
|
||||
name_lbl.add_theme_font_size_override("font_size", 16)
|
||||
name_lbl.add_theme_color_override("font_color", Color(0.9, 0.8, 0.3))
|
||||
vbox.add_child(name_lbl)
|
||||
|
||||
var char_cat_lbl := Label.new()
|
||||
char_cat_lbl.text = "%s • %s" % [recipe.get("character", "All"), recipe.get("category", "").capitalize()]
|
||||
char_cat_lbl.add_theme_font_size_override("font_size", 11)
|
||||
char_cat_lbl.add_theme_color_override("font_color", Color(0.65, 0.65, 0.65))
|
||||
vbox.add_child(char_cat_lbl)
|
||||
|
||||
# Fragment cost row
|
||||
var cost_hbox := HBoxContainer.new()
|
||||
cost_hbox.add_theme_constant_override("separation", 12)
|
||||
vbox.add_child(cost_hbox)
|
||||
|
||||
var cost: Dictionary = recipe.get("cost", {})
|
||||
for fid in ["frag_common", "frag_uncommon", "frag_rare"]:
|
||||
if not cost.has(fid): continue
|
||||
var needed: int = cost[fid]
|
||||
var have: int = frags.get(fid, 0)
|
||||
var icon: String = FRAG_ICONS.get(fid, "?")
|
||||
var cost_lbl := Label.new()
|
||||
cost_lbl.text = "%s %d/%d" % [icon, have, needed]
|
||||
cost_lbl.add_theme_font_size_override("font_size", 13)
|
||||
cost_lbl.add_theme_color_override("font_color",
|
||||
Color(0.4, 1.0, 0.5) if have >= needed else Color(1.0, 0.4, 0.4))
|
||||
cost_hbox.add_child(cost_lbl)
|
||||
|
||||
# Craft button
|
||||
var craft_btn := Button.new()
|
||||
craft_btn.text = "🔨 Craft"
|
||||
craft_btn.custom_minimum_size = Vector2(100, 40)
|
||||
craft_btn.disabled = not can_craft
|
||||
if not can_craft:
|
||||
craft_btn.modulate = Color(0.5, 0.5, 0.5, 0.7)
|
||||
craft_btn.pressed.connect(_on_craft_pressed.bind(recipe_id, panel))
|
||||
hbox.add_child(craft_btn)
|
||||
|
||||
return panel
|
||||
|
||||
# ─── Craft action ─────────────────────────────────────────────────────────────
|
||||
func _on_craft_pressed(recipe_id: String, _card: PanelContainer) -> void:
|
||||
var ok := GachaManager.craft(recipe_id)
|
||||
if ok:
|
||||
var recipes := GachaManager.get_all_recipes()
|
||||
var name: String = recipes.get(recipe_id, {}).get("name", recipe_id)
|
||||
_set_status("✅ Crafted: %s!" % name, Color(0.4, 1.0, 0.4))
|
||||
_refresh()
|
||||
else:
|
||||
_set_status("❌ Not enough fragments.", Color(1.0, 0.4, 0.4))
|
||||
|
||||
func _set_status(msg: String, col: Color = Color.WHITE) -> void:
|
||||
status_label.add_theme_color_override("font_color", col)
|
||||
status_label.text = msg
|
||||
await get_tree().create_timer(3.0).timeout
|
||||
if status_label.text == msg: status_label.text = ""
|
||||
|
||||
func _on_close() -> void:
|
||||
hide()
|
||||
closed.emit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cnenx8f7cftrs
|
||||
@@ -0,0 +1,212 @@
|
||||
extends Control
|
||||
## GachaPanel — Two-banner gacha interface.
|
||||
## Banners: Star (✦) and Gold (▤)
|
||||
## Pull results shown in animated card reveal.
|
||||
|
||||
signal closed
|
||||
|
||||
# ─── Node refs ───────────────────────────────────────────────────────────────
|
||||
@onready var back_btn := %BackBtn as Button
|
||||
@onready var star_tab_btn := %StarTabBtn as Button
|
||||
@onready var gold_tab_btn := %GoldTabBtn as Button
|
||||
@onready var banner_label := %BannerLabel as Label
|
||||
@onready var balance_label := %BalanceLabel as Label
|
||||
@onready var pity_label := %PityLabel as Label
|
||||
@onready var pull_1_btn := %Pull1Btn as Button
|
||||
@onready var pull_10_btn := %Pull10Btn as Button
|
||||
@onready var cost_1_label := %Cost1Label as Label
|
||||
@onready var cost_10_label := %Cost10Label as Label
|
||||
@onready var rates_label := %RatesLabel as Label
|
||||
@onready var result_panel := %ResultPanel as PanelContainer
|
||||
@onready var result_grid := %ResultGrid as GridContainer
|
||||
@onready var close_result_btn := %CloseResultBtn as Button
|
||||
@onready var craft_btn := %CraftBtn as Button
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
|
||||
# ─── State ───────────────────────────────────────────────────────────────────
|
||||
var _current_banner: String = "star"
|
||||
var _pulling: bool = false
|
||||
|
||||
const RARITY_COLORS := {
|
||||
"common": Color(0.80, 0.80, 0.80),
|
||||
"uncommon": Color(0.30, 0.85, 0.35),
|
||||
"rare": Color(0.20, 0.55, 1.00),
|
||||
"real_prize": Color(1.00, 0.75, 0.10)
|
||||
}
|
||||
const RARITY_LABELS := {
|
||||
"common": "Common",
|
||||
"uncommon": "Uncommon",
|
||||
"rare": "Rare",
|
||||
"real_prize": "✨ REAL PRIZE ✨"
|
||||
}
|
||||
|
||||
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
||||
func _ready() -> void:
|
||||
back_btn.pressed.connect(_on_close)
|
||||
star_tab_btn.pressed.connect(func(): _switch_banner("star"))
|
||||
gold_tab_btn.pressed.connect(func(): _switch_banner("gold"))
|
||||
pull_1_btn.pressed.connect(func(): _do_pull(1))
|
||||
pull_10_btn.pressed.connect(func(): _do_pull(10))
|
||||
close_result_btn.pressed.connect(func(): result_panel.hide())
|
||||
craft_btn.pressed.connect(_on_open_craft)
|
||||
result_panel.hide()
|
||||
_switch_banner("star")
|
||||
|
||||
func show_panel() -> void:
|
||||
show()
|
||||
_refresh_ui()
|
||||
|
||||
# ─── Banner switching ─────────────────────────────────────────────────────────
|
||||
func _switch_banner(id: String) -> void:
|
||||
_current_banner = id
|
||||
star_tab_btn.modulate = Color(1.3, 1.1, 0.3) if id == "star" else Color.WHITE
|
||||
gold_tab_btn.modulate = Color(1.3, 1.1, 0.3) if id == "gold" else Color.WHITE
|
||||
_refresh_ui()
|
||||
|
||||
func _refresh_ui() -> void:
|
||||
var gd: Dictionary = GachaManager.data
|
||||
var banner: Dictionary = gd.get("banners", {}).get(_current_banner, {})
|
||||
if banner.is_empty(): return
|
||||
|
||||
var currency: String = banner.get("currency", "star")
|
||||
var icon: String = "✦" if currency == "star" else "▤"
|
||||
var bal: int = GachaManager.get_balance(_current_banner)
|
||||
var pity: int = GachaManager.get_pity(_current_banner)
|
||||
var pity_at: int = banner.get("pity_at", 90)
|
||||
var c1: int = banner.get("pull_1_cost", 0)
|
||||
var c10: int = banner.get("pull_10_cost", 0)
|
||||
var rates: Dictionary = banner.get("rates", {})
|
||||
|
||||
banner_label.text = banner.get("name", "Banner")
|
||||
balance_label.text = "%s %d" % [icon, bal]
|
||||
pity_label.text = "Pity: %d / %d" % [pity, pity_at]
|
||||
cost_1_label.text = "%s %d" % [icon, c1]
|
||||
cost_10_label.text = "%s %d" % [icon, c10]
|
||||
pull_1_btn.disabled = (bal < c1) or _pulling
|
||||
pull_10_btn.disabled = (bal < c10) or _pulling
|
||||
|
||||
var real_pool: Array = gd.get("pools", {}).get("real_prize", [])
|
||||
var real_names: Array = []
|
||||
for rid in real_pool:
|
||||
var rd = gd.get("real_prize_catalog", {}).get(rid, {})
|
||||
real_names.append(rd.get("name", rid))
|
||||
|
||||
rates_label.text = (
|
||||
"Common %.0f%% Uncommon %.0f%% Rare %.0f%% ✨Real Prize %.1f%%\n" +
|
||||
"Guaranteed Real Prize every %d pulls (current pity: %d)\n\n" +
|
||||
"✨ Exclusive Real Prizes:\n" + "\n".join(real_names.map(func(n): return " • " + n))
|
||||
) % [
|
||||
rates.get("common", 0) * 100,
|
||||
rates.get("uncommon", 0) * 100,
|
||||
rates.get("rare", 0) * 100,
|
||||
rates.get("real_prize", 0) * 100,
|
||||
pity_at, pity
|
||||
]
|
||||
|
||||
# ─── Pull ─────────────────────────────────────────────────────────────────────
|
||||
func _do_pull(count: int) -> void:
|
||||
if _pulling: return
|
||||
_pulling = true
|
||||
status_label.text = "Rolling..."
|
||||
var results: Array = await _run_pull(count)
|
||||
_pulling = false
|
||||
if results.is_empty():
|
||||
status_label.text = "❌ Not enough currency!"
|
||||
_refresh_ui()
|
||||
return
|
||||
status_label.text = ""
|
||||
_refresh_ui()
|
||||
_show_results(results)
|
||||
|
||||
func _run_pull(count: int) -> Array:
|
||||
# Yield one frame so UI updates first
|
||||
await get_tree().process_frame
|
||||
return GachaManager.pull(_current_banner, count)
|
||||
|
||||
# ─── Result display ───────────────────────────────────────────────────────────
|
||||
func _show_results(results: Array) -> void:
|
||||
# Clear old cards
|
||||
for c in result_grid.get_children(): c.queue_free()
|
||||
await get_tree().process_frame
|
||||
|
||||
result_panel.show()
|
||||
for res in results:
|
||||
var card := _make_result_card(res)
|
||||
result_grid.add_child(card)
|
||||
# Staggered reveal
|
||||
await get_tree().create_timer(0.08).timeout
|
||||
|
||||
func _make_result_card(res: Dictionary) -> PanelContainer:
|
||||
var rarity: String = res.get("rarity", "common")
|
||||
var col: Color = RARITY_COLORS.get(rarity, Color.WHITE)
|
||||
var label_txt: String = RARITY_LABELS.get(rarity, rarity.capitalize())
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size = Vector2(110, 130)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 8)
|
||||
margin.add_theme_constant_override("margin_top", 8)
|
||||
margin.add_theme_constant_override("margin_right", 8)
|
||||
margin.add_theme_constant_override("margin_bottom", 8)
|
||||
panel.add_child(margin)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
margin.add_child(vbox)
|
||||
|
||||
# Rarity icon
|
||||
var icon_lbl := Label.new()
|
||||
icon_lbl.text = _rarity_icon(rarity)
|
||||
icon_lbl.add_theme_font_size_override("font_size", 36)
|
||||
icon_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
icon_lbl.add_theme_color_override("font_color", col)
|
||||
vbox.add_child(icon_lbl)
|
||||
|
||||
# Item name
|
||||
var name_lbl := Label.new()
|
||||
name_lbl.text = res.get("name", "?")
|
||||
name_lbl.add_theme_font_size_override("font_size", 11)
|
||||
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
name_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
name_lbl.add_theme_color_override("font_color", col)
|
||||
vbox.add_child(name_lbl)
|
||||
|
||||
# Rarity label
|
||||
var rar_lbl := Label.new()
|
||||
rar_lbl.text = label_txt
|
||||
rar_lbl.add_theme_font_size_override("font_size", 9)
|
||||
rar_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
rar_lbl.add_theme_color_override("font_color", col.lerp(Color.WHITE, 0.3))
|
||||
vbox.add_child(rar_lbl)
|
||||
|
||||
# Flash animation
|
||||
panel.modulate.a = 0.0
|
||||
var tween := create_tween()
|
||||
tween.tween_property(panel, "modulate:a", 1.0, 0.25)
|
||||
if rarity == "real_prize":
|
||||
tween.tween_property(panel, "modulate", Color(1.4, 1.3, 0.5, 1.0), 0.2)
|
||||
tween.tween_property(panel, "modulate", Color.WHITE, 0.3)
|
||||
|
||||
return panel
|
||||
|
||||
func _rarity_icon(rarity: String) -> String:
|
||||
match rarity:
|
||||
"common": return "⬜"
|
||||
"uncommon": return "🟩"
|
||||
"rare": return "🟦"
|
||||
"real_prize": return "✨"
|
||||
return "❓"
|
||||
|
||||
# ─── Craft link ───────────────────────────────────────────────────────────────
|
||||
func _on_open_craft() -> void:
|
||||
hide()
|
||||
# Find or load the fragment craft panel in the scene tree
|
||||
var main = get_tree().current_scene
|
||||
var fcp = main.get_node_or_null("FragmentCraftPanel")
|
||||
if fcp:
|
||||
fcp.show_panel()
|
||||
|
||||
func _on_close() -> void:
|
||||
hide()
|
||||
closed.emit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://clkxaudy5hxfj
|
||||
@@ -33,6 +33,7 @@ signal profile_updated
|
||||
@onready var costume_tab_btn := %CostumeTabBtn as Button
|
||||
@onready var glove_tab_btn := %GloveTabBtn as Button
|
||||
@onready var acc_tab_btn := %AccTabBtn as Button
|
||||
@onready var frag_tab_btn := %FragTabBtn as Button
|
||||
@onready var drag_zone := %DragZone as Control
|
||||
@onready var character_root := %CharacterRoot as Node3D
|
||||
@onready var char_left_btn := %CharLeftBtn as Button
|
||||
@@ -173,6 +174,7 @@ func _connect_signals() -> void:
|
||||
costume_tab_btn.pressed.connect(func(): _on_category_tab_pressed("costume"))
|
||||
glove_tab_btn.pressed.connect(func(): _on_category_tab_pressed("glove"))
|
||||
acc_tab_btn.pressed.connect(func(): _on_category_tab_pressed("accessory"))
|
||||
frag_tab_btn.pressed.connect(func(): _on_category_tab_pressed("fragment"))
|
||||
|
||||
# Item grid slot buttons
|
||||
for i in _item_slots.size():
|
||||
@@ -248,13 +250,18 @@ func _highlight_active_tab() -> void:
|
||||
"head": head_tab_btn,
|
||||
"costume": costume_tab_btn,
|
||||
"glove": glove_tab_btn,
|
||||
"accessory": acc_tab_btn
|
||||
"accessory": acc_tab_btn,
|
||||
"fragment": frag_tab_btn
|
||||
}
|
||||
for cat: String in map:
|
||||
(map[cat] as Button).modulate = Color(1.3, 1.3, 0.4, 1) if cat == _current_category else Color.WHITE
|
||||
|
||||
func _rebuild_category_items() -> void:
|
||||
_category_items.clear()
|
||||
# Fragment tab handled separately
|
||||
if _current_category == "fragment":
|
||||
_rebuild_fragment_items()
|
||||
return
|
||||
var prefix := _current_category + "_"
|
||||
# Resolve the current character's node name (e.g. "Copper" → "Oldpop")
|
||||
var current_char_display: String = CHARACTERS[_loadout_index]
|
||||
@@ -283,6 +290,12 @@ func _rebuild_category_items() -> void:
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Item grid
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
func _rebuild_fragment_items() -> void:
|
||||
var frags: Dictionary = UserProfileManager.fragments
|
||||
for fid in ["frag_common", "frag_uncommon", "frag_rare"]:
|
||||
var count: int = frags.get(fid, 0)
|
||||
if count > 0: _category_items.append(fid)
|
||||
|
||||
func _populate_item_grid() -> void:
|
||||
var start: int = _current_page * ITEMS_PER_PAGE
|
||||
var total: int = _category_items.size()
|
||||
|
||||
Reference in New Issue
Block a user