feat: adding the skin_shader_generator, and gacha base barebone

This commit is contained in:
2026-04-24 00:17:00 +08:00
parent 16c82a48b8
commit 7e4b707e84
196 changed files with 4883 additions and 200 deletions
+137
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://cnenx8f7cftrs
+212
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://clkxaudy5hxfj
+14 -1
View File
@@ -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()