233 lines
13 KiB
GDScript
233 lines
13 KiB
GDScript
class_name ClassEditor
|
||
extends PanelContainer
|
||
|
||
var ui: GameUI
|
||
var slot := 0
|
||
var weapon := 0
|
||
var secondary := false
|
||
var draft: Dictionary
|
||
var showcase: WeaponShowcase
|
||
var options_panel: VBoxContainer
|
||
var list_panel: VBoxContainer
|
||
var title_label: Label
|
||
var stats_label: Label
|
||
var notice: Label
|
||
var class_name_edit: LineEdit
|
||
var tabs: OptionButton
|
||
var selected_sticker := 0
|
||
var overview: LoadoutOverview
|
||
var workshop: Control
|
||
|
||
func _ready() -> void:
|
||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
var style := StyleBoxFlat.new()
|
||
style.bg_color = Color("050707")
|
||
style.content_margin_left = 28; style.content_margin_right = 28
|
||
style.content_margin_top = 20; style.content_margin_bottom = 18
|
||
add_theme_stylebox_override("panel", style)
|
||
var theme_copy := ui.root.theme.duplicate() as Theme
|
||
theme_copy.default_font_size = 17
|
||
for state in ["normal", "hover", "pressed", "focus"]:
|
||
var b := StyleBoxFlat.new()
|
||
b.bg_color = Color("111518") if state == "normal" else Color("233740")
|
||
b.border_width_bottom = 2; b.border_color = Color("86bfd0") if state != "normal" else Color("3a474c")
|
||
b.content_margin_left = 12; b.content_margin_right = 12
|
||
b.content_margin_top = 9; b.content_margin_bottom = 9
|
||
theme_copy.set_stylebox(state, "Button", b)
|
||
theme_copy.set_stylebox(state, "OptionButton", b)
|
||
theme = theme_copy
|
||
var column := VBoxContainer.new(); workshop = column; column.add_theme_constant_override("separation", 14); add_child(column)
|
||
var heading := Label.new(); heading.text = "ARSENAL / KLASSENEDITOR"; heading.add_theme_font_size_override("font_size", 32); heading.modulate = Color("86bfd0"); column.add_child(heading)
|
||
var row := HBoxContainer.new(); row.size_flags_vertical = Control.SIZE_EXPAND_FILL; row.add_theme_constant_override("separation", 22); column.add_child(row)
|
||
var left := VBoxContainer.new(); left.custom_minimum_size.x = 225; row.add_child(left)
|
||
add_label(left, "01 / EIGENE KLASSEN")
|
||
var scroll := ScrollContainer.new(); scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL; left.add_child(scroll)
|
||
list_panel = VBoxContainer.new(); list_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL; scroll.add_child(list_panel)
|
||
add_label(left, "VORLAGE IN DIESEN SLOT KOPIEREN")
|
||
var preset := OptionButton.new()
|
||
for name in ClassLoadouts.PRESETS: preset.add_item(name)
|
||
left.add_child(preset)
|
||
add_button(left, "VORLAGE ÜBERNEHMEN", func(): draft = ClassLoadouts.template(preset.selected).duplicate(true); refresh())
|
||
add_button(left, "KLASSE DUPLIZIEREN", duplicate_class)
|
||
var center := VBoxContainer.new(); center.size_flags_horizontal = Control.SIZE_EXPAND_FILL; row.add_child(center)
|
||
title_label = add_label(center, ""); title_label.add_theme_font_size_override("font_size", 26)
|
||
class_name_edit = LineEdit.new(); class_name_edit.max_length = 24; center.add_child(class_name_edit)
|
||
class_name_edit.text_changed.connect(func(value): draft.name = value)
|
||
var switch_row := HBoxContainer.new(); center.add_child(switch_row)
|
||
add_button(switch_row, "PRIMÄRWAFFE", func(): secondary = false; refresh())
|
||
add_button(switch_row, "P12 / SEKUNDÄR", func(): secondary = true; refresh())
|
||
showcase = WeaponShowcase.new(); center.add_child(showcase)
|
||
showcase.position = Vector2.ZERO
|
||
showcase.custom_minimum_size = Vector2(300, 240)
|
||
showcase.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
showcase.stretch = true
|
||
add_label(center, "MAUS ZIEHEN: DREHEN • MAUSRAD: ZOOM")
|
||
stats_label = add_label(center, ""); stats_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
add_label(center, "AUSRÜSTUNG 2 × FRAG / 1 × FLASH / ENERGIESCHILD")
|
||
var right := VBoxContainer.new(); right.custom_minimum_size.x = 340; row.add_child(right)
|
||
tabs = OptionButton.new()
|
||
for name in ["02 / WAFFE & AUFSÄTZE", "03 / LACKIERUNG", "04 / BILDSTICKER"]: tabs.add_item(name)
|
||
right.add_child(tabs); tabs.item_selected.connect(func(_i): rebuild_options())
|
||
var option_scroll := ScrollContainer.new(); option_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL; right.add_child(option_scroll)
|
||
options_panel = VBoxContainer.new(); options_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL; options_panel.add_theme_constant_override("separation", 10); option_scroll.add_child(options_panel)
|
||
notice = add_label(column, "Änderungen gelten für den nächsten Spawn. Klassen bleiben lokal gespeichert.")
|
||
notice.modulate = Color("86bfd0")
|
||
var footer := HBoxContainer.new(); footer.add_theme_constant_override("separation", 16); column.add_child(footer)
|
||
add_button(footer, "SPEICHERN & AUSRÜSTEN", save_class)
|
||
add_button(footer, "← ZURÜCK", ui.main_menu)
|
||
var player := LineEdit.new(); player.text = ui.game.nickname; player.max_length = 20; player.custom_minimum_size.x = 190; player.placeholder_text = "Operatorname"; footer.add_child(player)
|
||
player.text_changed.connect(func(value): ui.game.nickname = value.strip_edges())
|
||
slot = ClassLoadouts.selected
|
||
draft = ClassLoadouts.entries[slot].duplicate(true)
|
||
refresh()
|
||
overview = LoadoutOverview.new()
|
||
overview.editor = self
|
||
add_child(overview)
|
||
workshop.hide()
|
||
add_button(footer, "← KLASSENÜBERSICHT", func(): workshop.hide(); overview.show(); overview.refresh())
|
||
|
||
func show_workshop() -> void:
|
||
overview.hide()
|
||
workshop.show()
|
||
|
||
func add_label(parent: Node, value: String) -> Label:
|
||
var label := Label.new(); label.text = value; label.add_theme_font_size_override("font_size", 16); parent.add_child(label); return label
|
||
|
||
func add_button(parent: Node, value: String, callback: Callable) -> Button:
|
||
var button := Button.new(); button.text = value; button.pressed.connect(callback); parent.add_child(button); return button
|
||
|
||
func clear_children(parent: Node) -> void:
|
||
for child in parent.get_children(): parent.remove_child(child); child.queue_free()
|
||
|
||
func refresh() -> void:
|
||
weapon = 4 if secondary else int(draft.primary)
|
||
class_name_edit.text = draft.name
|
||
title_label.text = Arsenal.DATA[weapon].name
|
||
clear_children(list_panel)
|
||
for i in ClassLoadouts.entries.size():
|
||
var label := "%02d %s" % [i + 1, ClassLoadouts.entries[i].name]
|
||
if i == ClassLoadouts.selected: label = "● " + label
|
||
var button := add_button(list_panel, label, func():
|
||
ClassLoadouts.entries[slot] = ClassLoadouts.clean(draft)
|
||
slot = i; draft = ClassLoadouts.entries[i].duplicate(true); refresh())
|
||
button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||
button.custom_minimum_size.x = 220
|
||
button.modulate = Color("86bfd0") if i == slot else Color.WHITE
|
||
showcase.select_weapon(weapon)
|
||
showcase.camera.size = 0.48 if weapon == 4 else (0.95 if weapon in [3, 5, 6] else 0.75)
|
||
update_model()
|
||
rebuild_options()
|
||
if is_instance_valid(overview): overview.refresh()
|
||
|
||
func design() -> Dictionary:
|
||
var key := str(weapon)
|
||
if not draft.skins.has(key): draft.skins[key] = WeaponSkins.clean({})
|
||
return draft.skins[key]
|
||
|
||
func update_model() -> void:
|
||
WeaponSkins.apply(showcase.model, design())
|
||
var w := WeaponAttachments.stats(weapon, design())
|
||
stats_label.text = "SCHADEN %s • MAGAZIN %d • REICHWEITE %.0f m\nNACHLADEN %.2f s • ZIELZEIT %.2f s • TEMPO %.0f %%" % [str(w.damage), w.mag, w.range, w.reload, w.ads, w.speed * 100]
|
||
|
||
func rebuild_options() -> void:
|
||
clear_children(options_panel)
|
||
var d := design()
|
||
if tabs.selected == 0:
|
||
add_label(options_panel, "WAFFENAUSWAHL")
|
||
var select := OptionButton.new()
|
||
var ids: Array = [4] if secondary else Arsenal.PRIMARY_IDS
|
||
for id in ids: select.add_item(Arsenal.DATA[id].name)
|
||
select.selected = ids.find(weapon); options_panel.add_child(select)
|
||
select.item_selected.connect(func(i): draft.primary = ids[i]; refresh())
|
||
if weapon == 4:
|
||
add_label(options_panel,"SOWJETISCHES MODELL")
|
||
var variant := OptionButton.new()
|
||
variant.add_item("Makarov PM")
|
||
variant.add_item("Tokarev TT-33")
|
||
variant.selected = d.get("soviet_variant",0)
|
||
options_panel.add_child(variant)
|
||
variant.item_selected.connect(func(value): d.soviet_variant = value; update_model())
|
||
if not d.has("attachments"): d.attachments = {}
|
||
for i in WeaponAttachments.SLOTS.size():
|
||
var key: String = WeaponAttachments.SLOTS[i]
|
||
add_label(options_panel, WeaponAttachments.LABELS[i].to_upper())
|
||
var choice := OptionButton.new()
|
||
for name in WeaponAttachments.OPTIONS[key]: choice.add_item(name)
|
||
choice.selected = d.attachments.get(key, 0); options_panel.add_child(choice)
|
||
if weapon == 4 and key == "stock": choice.selected = 0; choice.disabled = true
|
||
var hint := add_label(options_panel, WeaponAttachments.HELP[key][choice.selected]); hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; hint.custom_minimum_size.x = 310; hint.modulate = Color("97b6af")
|
||
if weapon == 4 and key == "stock": hint.text = "P12 besitzt keinen austauschbaren Schaft."
|
||
choice.item_selected.connect(func(value): d.attachments[key] = value; hint.text = WeaponAttachments.HELP[key][value]; update_model())
|
||
elif tabs.selected == 1:
|
||
for i in WeaponSkins.PARTS.size():
|
||
var key: String = WeaponSkins.PARTS[i]
|
||
var row := HBoxContainer.new(); options_panel.add_child(row)
|
||
var label := add_label(row, WeaponSkins.LABELS[i]); label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
var picker := ColorPickerButton.new(); picker.custom_minimum_size = Vector2(96, 28); picker.edit_alpha = false; picker.color = d.get(key, Color("586267")); row.add_child(picker)
|
||
picker.color_changed.connect(func(value): d[key] = value; update_model())
|
||
add_label(options_panel, "BESCHRIFTUNG")
|
||
var words := LineEdit.new(); words.max_length = 32; words.text = d.get("text", ""); options_panel.add_child(words)
|
||
words.text_changed.connect(func(value): d.text = value; update_model())
|
||
add_label(options_panel, "TEXTFARBE")
|
||
var text_color := ColorPickerButton.new()
|
||
text_color.custom_minimum_size.y = 28
|
||
text_color.edit_alpha = false
|
||
text_color.color = d.get("text_color", Color.WHITE)
|
||
options_panel.add_child(text_color)
|
||
text_color.color_changed.connect(func(value): d.text_color = value; update_model())
|
||
add_button(options_panel, "LACKIERUNG ZURÜCKSETZEN", func():
|
||
for key in WeaponSkins.PARTS + ["text", "text_color"]: d.erase(key)
|
||
update_model(); rebuild_options())
|
||
else:
|
||
add_label(options_panel, "3 STICKERPLÄTZE / BEIDE WAFFENSEITEN")
|
||
var positions := OptionButton.new()
|
||
for name in ["Gehäuse hinten", "Gehäuse Mitte", "Gehäuse vorne"]: positions.add_item(name)
|
||
positions.selected = selected_sticker; options_panel.add_child(positions)
|
||
positions.item_selected.connect(func(i): selected_sticker = i; rebuild_options())
|
||
d.stickers = WeaponStickers.clean(d.get("stickers", []))
|
||
var preview := TextureRect.new(); preview.custom_minimum_size = Vector2(256, 180); preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE; preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED; preview.texture = WeaponStickers.texture(d.stickers[selected_sticker]); options_panel.add_child(preview)
|
||
add_button(options_panel, "BILD IMPORTIEREN …", import_sticker)
|
||
add_button(options_panel, "STICKER ENTFERNEN", func(): d.stickers[selected_sticker] = ""; update_model(); rebuild_options())
|
||
var hint := add_label(options_panel, "PNG / JPG / WebP · maximal 8 MB\nTransparente PNGs empfohlen.\nImport wird auf 256 Pixel verkleinert."); hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
add_label(options_panel, "GESPEICHERTE STICKER")
|
||
var gallery := GridContainer.new(); gallery.columns = 4; options_panel.add_child(gallery)
|
||
var folder := DirAccess.open(WeaponStickers.directory)
|
||
if folder:
|
||
for file in folder.get_files():
|
||
var id := file.get_basename()
|
||
if not WeaponStickers.valid(id) or gallery.get_child_count() >= 32: continue
|
||
var tile := TextureButton.new()
|
||
tile.texture_normal = WeaponStickers.texture(id)
|
||
tile.ignore_texture_size = true
|
||
tile.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
|
||
tile.custom_minimum_size = Vector2(68, 68)
|
||
tile.tooltip_text = "Sticker auf ausgewählten Platz setzen"
|
||
gallery.add_child(tile)
|
||
tile.pressed.connect(func(): d.stickers[selected_sticker] = id; update_model(); rebuild_options())
|
||
|
||
func import_sticker() -> void:
|
||
var dialog := FileDialog.new(); dialog.access = FileDialog.ACCESS_FILESYSTEM; dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||
dialog.filters = PackedStringArray(["*.png,*.jpg,*.jpeg,*.webp ; Bilder"])
|
||
add_child(dialog); dialog.popup_centered_ratio(0.75)
|
||
dialog.file_selected.connect(func(path):
|
||
var id := WeaponStickers.import_file(path)
|
||
if id.is_empty(): notice.text = WeaponStickers.error
|
||
else: design().stickers[selected_sticker] = id; update_model(); rebuild_options(); notice.text = "Sticker importiert. Zum Behalten Klasse speichern."
|
||
dialog.queue_free())
|
||
dialog.canceled.connect(dialog.queue_free)
|
||
|
||
func save_class() -> void:
|
||
ClassLoadouts.entries[slot] = ClassLoadouts.clean(draft)
|
||
ClassLoadouts.selected = slot
|
||
if ClassLoadouts.save() != OK: notice.text = "Speichern fehlgeschlagen."; return
|
||
ClassLoadouts.activate(ui.game, slot)
|
||
notice.text = "Klasse gespeichert und ausgerüstet. Wird beim nächsten Spawn verwendet."
|
||
refresh()
|
||
|
||
func duplicate_class() -> void:
|
||
var next := (slot + 1) % 10
|
||
ClassLoadouts.entries[slot] = ClassLoadouts.clean(draft)
|
||
slot = next; draft = draft.duplicate(true); draft.name = str(draft.name).left(17) + " Kopie"
|
||
notice.text = "Kopie in Slot %d vorbereitet. Speichern bestätigt das Ersetzen." % (slot + 1)
|
||
refresh()
|