From bb9eb71770592f1a07d8a2194c7036589b60d387 Mon Sep 17 00:00:00 2001 From: slyphax Date: Wed, 28 Oct 2026 09:10:16 +0100 Subject: [PATCH] Map-Loadout: Loadout-Bereich im Map-Tab (Spieler-Loadout oder festes Map-Loadout mit Aufsaetzen), V18, Undo/Redo Co-Authored-By: Claude Sonnet 5.5 --- library/loadout_katalog.json | 3206 ++++++++++++++++++++++++++++ src/svmap/loadout_katalog.gd | 149 ++ src/svmap/loadout_katalog.gd.uid | 1 + src/svmap/svmap_io.gd | 2 + src/ui/panels/loadout_panel.gd | 225 ++ src/ui/panels/loadout_panel.gd.uid | 1 + src/ui/panels/map_panel.gd | 4 + src/ui/panels/registry.gd | 2 + tests/test_loadout.gd | 119 ++ tests/test_loadout.gd.uid | 1 + tools/run_tests.sh | 2 +- tools/sync_loadout_katalog.py | 32 + 12 files changed, 3743 insertions(+), 1 deletion(-) create mode 100644 library/loadout_katalog.json create mode 100644 src/svmap/loadout_katalog.gd create mode 100644 src/svmap/loadout_katalog.gd.uid create mode 100644 src/ui/panels/loadout_panel.gd create mode 100644 src/ui/panels/loadout_panel.gd.uid create mode 100644 tests/test_loadout.gd create mode 100644 tests/test_loadout.gd.uid create mode 100644 tools/sync_loadout_katalog.py diff --git a/library/loadout_katalog.json b/library/loadout_katalog.json new file mode 100644 index 0000000..9e2c58f --- /dev/null +++ b/library/loadout_katalog.json @@ -0,0 +1,3206 @@ +{ + "version": 1, + "waffen": { + "ar1": { + "name": "AR-1 Sturmgewehr", + "slots": { + "bar_bottom": { + "default": "ar1_handle_bottom", + "optional": true, + "options": [ + "ar1_handle_bottom", + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "ar1_handle_left", + "optional": true, + "options": [ + "ar1_handle_left", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "ar1_handle_right", + "optional": true, + "options": [ + "ar1_handle_right", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "gunstock": { + "default": "ar1_buttstock_light", + "optional": false, + "options": [ + "ar1_buttstock_heavy", + "ar1_buttstock_light" + ] + }, + "magazine": { + "default": "ar1_magazine_extended", + "optional": false, + "options": [ + "ar1_magazine", + "ar1_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "ar1_sight", + "optional": true, + "options": [ + "ar1_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar10": { + "name": "AR-10 Kompakt", + "slots": { + "bar_bottom": { + "default": "ar10_handguard", + "optional": true, + "options": [ + "ar10_handguard", + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "ar10_magazine_extended", + "optional": false, + "options": [ + "ar10_magazine", + "ar10_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "ar10_sight", + "optional": true, + "options": [ + "ar10_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar2": { + "name": "AR-2", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "barrel": { + "default": "ar2_barrel_mod", + "optional": false, + "options": [ + "ar2_barrel_mod", + "ar2_barrel_stock" + ] + }, + "gunstock": { + "default": "ar2_buttstock_heavy", + "optional": false, + "options": [ + "ar2_buttstock_heavy", + "ar2_buttstock_light" + ] + }, + "magazine": { + "default": "ar2_magazine", + "optional": false, + "options": [ + "ar2_magazine", + "ar2_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "ar2_sight_01", + "optional": true, + "options": [ + "ar2_sight_01", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar3": { + "name": "AR-3 Lang", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "barrel": { + "default": "ar3_barrel_mod", + "optional": false, + "options": [ + "ar3_barrel_mod", + "ar3_barrel_stock" + ] + }, + "gunstock": { + "default": "ar3_buttstock_heavy", + "optional": false, + "options": [ + "ar3_buttstock_heavy", + "ar3_buttstock_light" + ] + }, + "magazine": { + "default": "ar3_magazine", + "optional": false, + "options": [ + "ar3_magazine", + "ar3_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar4": { + "name": "AR-4 Werfer", + "slots": { + "bar_bottom": { + "default": "ar4_grenade_launcher", + "optional": true, + "options": [ + "ar4_grenade_launcher", + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "magazine": { + "default": "ar4_magazine", + "optional": false, + "options": [ + "ar4_magazine", + "ar4_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar5": { + "name": "AR-5", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "gunstock": { + "default": "ar5_buttstock_heavy", + "optional": false, + "options": [ + "ar5_buttstock_heavy", + "ar5_buttstock_light" + ] + }, + "magazine": { + "default": "ar5_magazine", + "optional": false, + "options": [ + "ar5_magazine", + "ar5_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar6": { + "name": "AR-6 Kurz", + "slots": { + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "magazine": { + "default": "ar6_magazine_extended", + "optional": false, + "options": [ + "ar6_magazine", + "ar6_magazine_extended" + ] + }, + "muzzle": { + "default": "ar6_muzzle", + "optional": true, + "options": [ + "ar6_muzzle", + "ar6_grenade_launcher", + "ar6_handguard", + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "ar6_sight", + "optional": true, + "options": [ + "ar6_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar7": { + "name": "AR-7 Schwer", + "slots": { + "bar_bottom_01": { + "default": "ar7_grenade_launcher", + "optional": false, + "options": [ + "ar7_grenade_launcher" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "ar7_magazine", + "optional": false, + "options": [ + "ar7_magazine", + "ar7_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar8": { + "name": "AR-8", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "magazine": { + "default": "ar8_magazine", + "optional": false, + "options": [ + "ar8_magazine", + "ar8_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "ar8_sight", + "optional": true, + "options": [ + "ar8_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "ar9": { + "name": "AR-9", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "ar9_magazine", + "optional": false, + "options": [ + "ar9_magazine", + "ar9_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "ar9_sight", + "optional": true, + "options": [ + "ar9_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "flare_gun": { + "name": "Leuchtpistole", + "slots": {} + }, + "gl1": { + "name": "Granatwerfer M79", + "slots": {} + }, + "gl2": { + "name": "Granatwerfer MGL", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "gl3": { + "name": "Granatwerfer M320", + "slots": { + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "gl4": { + "name": "Granatwerfer Grosskaliber", + "slots": { + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "gl5": { + "name": "Granatwerfer Rohr 40mm", + "slots": { + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "gl6": { + "name": "Granatwerfer RPG", + "slots": { + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "m40": { + "name": "M40", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bipod": { + "default": "", + "optional": true, + "options": [ + "sr1_bipod" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + } + } + }, + "mg1": { + "name": "M249", + "slots": { + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bipod": { + "default": "mg1_bipod", + "optional": true, + "options": [ + "mg1_bipod" + ] + }, + "handle": { + "default": "mg1_handle", + "optional": true, + "options": [ + "mg1_handle" + ] + }, + "magazine": { + "default": "mg1_magazine", + "optional": false, + "options": [ + "mg1_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "mg2": { + "name": "PKM", + "slots": { + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "bipod": { + "default": "mg2_bipod", + "optional": true, + "options": [ + "mg2_bipod" + ] + }, + "gunstock": { + "default": "mg2_buttstock_heavy", + "optional": false, + "options": [ + "mg2_buttstock_heavy", + "mg2_buttstock_light" + ] + }, + "handle": { + "default": "mg2_handle", + "optional": true, + "options": [ + "mg2_handle" + ] + }, + "magazine": { + "default": "mg2_magazine", + "optional": false, + "options": [ + "mg2_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "mg2_sight", + "optional": true, + "options": [ + "mg2_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "mg3": { + "name": "M60", + "slots": { + "bipod": { + "default": "mg3_bipod", + "optional": true, + "options": [ + "mg3_bipod" + ] + }, + "magazine": { + "default": "mg3_magazine", + "optional": false, + "options": [ + "mg3_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "mg4": { + "name": "MG4", + "slots": { + "bipod": { + "default": "mg4_bipod", + "optional": true, + "options": [ + "mg4_bipod" + ] + }, + "gunstock": { + "default": "mg4_buttstock_heavy", + "optional": false, + "options": [ + "mg4_buttstock_heavy", + "mg4_buttstock_light" + ] + }, + "handle": { + "default": "mg4_handle", + "optional": true, + "options": [ + "mg4_handle" + ] + }, + "magazine": { + "default": "mg4_magazine", + "optional": false, + "options": [ + "mg4_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "mg5": { + "name": "RPD", + "slots": {} + }, + "mg6": { + "name": "M2", + "slots": { + "bipod": { + "default": "mg6_bipod", + "optional": true, + "options": [ + "mg6_bipod" + ] + }, + "magazine": { + "default": "mg6_magazine", + "optional": false, + "options": [ + "mg6_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + } + } + }, + "pistol1": { + "name": "Pistole", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "pistol1_magazine", + "optional": false, + "options": [ + "pistol1_magazine", + "pistol1_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "pistol2": { + "name": "Pistole P226", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "pistol2_magazine", + "optional": false, + "options": [ + "pistol2_magazine", + "pistol2_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "pistol3": { + "name": "Pistole G17", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "pistol3_magazine_extended", + "optional": false, + "options": [ + "pistol3_magazine", + "pistol3_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "pistol4": { + "name": "Pistole M1911", + "slots": {} + }, + "pistol5": { + "name": "Revolver .357", + "slots": {} + }, + "pistol6": { + "name": "Pistole USP .45", + "slots": { + "magazine": { + "default": "pistol6_magazine", + "optional": false, + "options": [ + "pistol6_magazine", + "pistol6_magazine_extended" + ] + } + } + }, + "shotgun1": { + "name": "Pumpe 12ga", + "slots": { + "gunstock": { + "default": "shotgun1_buttstock_heavy", + "optional": false, + "options": [ + "shotgun1_buttstock_heavy", + "shotgun1_buttstock_light" + ] + }, + "magazine": { + "default": "shotgun1_magazine", + "optional": false, + "options": [ + "shotgun1_magazine", + "shotgun1_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "shotgun2": { + "name": "Kurzflinte 12ga", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "shotgun2_magazine", + "optional": false, + "options": [ + "shotgun2_magazine", + "shotgun2_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "shotgun3": { + "name": "Pumpe 20ga", + "slots": { + "gunstock": { + "default": "shotgun3_buttstock_light", + "optional": false, + "options": [ + "shotgun3_buttstock_heavy", + "shotgun3_buttstock_light" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "shotgun4": { + "name": "Pumpe 12ga Lang", + "slots": { + "gunstock": { + "default": "shotgun4_buttstock_heavy", + "optional": false, + "options": [ + "shotgun4_buttstock_heavy", + "shotgun4_buttstock_light" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "shotgun5": { + "name": "Halbauto 12ga", + "slots": {} + }, + "shotgun6": { + "name": "Auto-Shotgun", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "shotgun6_magazine", + "optional": false, + "options": [ + "shotgun6_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg1": { + "name": "MP5", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "gunstock": { + "default": "smg1_buttstock_light", + "optional": false, + "options": [ + "smg1_buttstock_heavy", + "smg1_buttstock_light" + ] + }, + "magazine": { + "default": "smg1_magazine", + "optional": false, + "options": [ + "smg1_magazine_extended", + "smg1_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg10": { + "name": "Scorpion Evo", + "slots": { + "magazine": { + "default": "smg10_magazine_extended", + "optional": false, + "options": [ + "smg10_magazine_extended", + "smg10_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg11": { + "name": "Sterling", + "slots": { + "gunstock": { + "default": "smg11_buttstock", + "optional": false, + "options": [ + "smg11_buttstock" + ] + }, + "magazine": { + "default": "smg11_magazine", + "optional": false, + "options": [ + "smg11_magazine", + "smg11_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg12": { + "name": "Micro Uzi", + "slots": { + "bar_handle": { + "default": "smg12_handle", + "optional": true, + "options": [ + "smg12_handle" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_top": { + "default": "", + "optional": true, + "options": [ + "tactical_laser_aiming", + "tactical_laser_aiming_system", + "tactical_flashlight", + "tactical_flashlight_02" + ] + }, + "magazine": { + "default": "smg12_magazine", + "optional": false, + "options": [ + "smg12_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + } + } + }, + "smg2": { + "name": "MP5K", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "gunstock": { + "default": "smg2_buttstock_heavy", + "optional": false, + "options": [ + "smg2_buttstock_heavy", + "smg2_buttstock_light" + ] + }, + "magazine": { + "default": "smg2_magazine_extended", + "optional": false, + "options": [ + "smg2_magazine", + "smg2_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg3": { + "name": "MP7", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "gunstock": { + "default": "smg3_buttstock_light", + "optional": false, + "options": [ + "smg3_buttstock_heavy", + "smg3_buttstock_light" + ] + }, + "magazine": { + "default": "smg3_magazine_extended", + "optional": false, + "options": [ + "smg3_magazine_extended", + "smg3_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg4": { + "name": "MP9", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "gunstock": { + "default": "smg4_buttstock_heavy", + "optional": false, + "options": [ + "smg4_buttstock_heavy", + "smg4_buttstock_light" + ] + }, + "magazine": { + "default": "smg4_magazine", + "optional": false, + "options": [ + "smg4_magazine_extended", + "smg4_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg5": { + "name": "Uzi", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "gunstock": { + "default": "smg5_buttstock_light", + "optional": false, + "options": [ + "smg5_buttstock_heavy", + "smg5_buttstock_light" + ] + }, + "magazine": { + "default": "smg5_magazine", + "optional": false, + "options": [ + "smg5_magazine_extended", + "smg5_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "smg5_sight", + "optional": true, + "options": [ + "smg5_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg6": { + "name": "MAC-10", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "gunstock": { + "default": "smg6_buttstock_light", + "optional": false, + "options": [ + "smg6_buttstock_heavy", + "smg6_buttstock_light" + ] + }, + "magazine": { + "default": "smg6_magazine", + "optional": false, + "options": [ + "smg6_magazine_extended", + "smg6_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "smg6_sight", + "optional": true, + "options": [ + "smg6_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg7": { + "name": "PP-19 Bizon", + "slots": { + "gunstock": { + "default": "smg7_buttstock_heavy", + "optional": false, + "options": [ + "smg7_buttstock_heavy", + "smg7_buttstock_light" + ] + }, + "magazine": { + "default": "smg7_magazine", + "optional": false, + "options": [ + "smg7_magazine_extended", + "smg7_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "smg7_sight", + "optional": true, + "options": [ + "smg7_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg8": { + "name": "P90", + "slots": { + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "smg8_magazine", + "optional": false, + "options": [ + "smg8_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "smg8_sight", + "optional": true, + "options": [ + "smg8_sight", + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "smg9": { + "name": "Kriss Vector", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "magazine": { + "default": "smg9_magazine", + "optional": false, + "options": [ + "smg9_magazine_extended", + "smg9_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "sr1": { + "name": "AWM", + "slots": { + "bar_bottom": { + "default": "", + "optional": true, + "options": [ + "foregrip_horisontal", + "foregrip_small", + "foregrip_vertical_01", + "foregrip_vertical_02", + "foregrip_vertical_03", + "foregrip_vertical_04", + "foregrip_vertical_05", + "foregrip_vertical_06", + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bipod": { + "default": "sr1_bipod", + "optional": true, + "options": [ + "sr1_bipod" + ] + }, + "magazine": { + "default": "sr1_magazine_extended", + "optional": false, + "options": [ + "sr1_magazine", + "sr1_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "sight_10", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "sr2": { + "name": "SR-25", + "slots": { + "bar_left": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bar_right": { + "default": "", + "optional": true, + "options": [ + "tactical_flashlight", + "tactical_flashlight_02", + "tactical_laser_aiming", + "tactical_laser_aiming_system" + ] + }, + "bipod": { + "default": "sr2_bipod", + "optional": true, + "options": [ + "sr2_bipod" + ] + }, + "gunstock": { + "default": "sr2_buttstock_heavy", + "optional": false, + "options": [ + "sr2_buttstock_heavy", + "sr2_buttstock_light" + ] + }, + "magazine": { + "default": "sr2_magazine", + "optional": false, + "options": [ + "sr2_magazine", + "sr2_magazine_extended" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "sight_11", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "sr3": { + "name": "M24", + "slots": { + "bipod": { + "default": "sr3_bipod", + "optional": true, + "options": [ + "sr3_bipod" + ] + }, + "magazine": { + "default": "sr3_magazine", + "optional": false, + "options": [ + "sr3_magazine_extended", + "sr3_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "sight_10", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "sr4": { + "name": "SVD Dragunow", + "slots": { + "magazine": { + "default": "sr4_magazine", + "optional": false, + "options": [ + "sr4_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "sight_08", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "sr5": { + "name": "Remington 700", + "slots": { + "magazine": { + "default": "sr5_magazine", + "optional": false, + "options": [ + "sr5_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "sight_06", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "sr6": { + "name": "TAC-50", + "slots": { + "bipod": { + "default": "sr6_bipod", + "optional": true, + "options": [ + "sr6_bipod" + ] + }, + "magazine": { + "default": "sr6_magazine_extended", + "optional": false, + "options": [ + "sr6_magazine_extended", + "sr6_magazine" + ] + }, + "muzzle": { + "default": "", + "optional": true, + "options": [ + "muzzle_compensator_01", + "muzzle_compensator_02", + "muzzle_flash_hider_01", + "muzzle_flash_hider_02", + "muzzle_suppressor_01", + "muzzle_suppressor_02", + "muzzle_suppressor_03" + ] + }, + "sight": { + "default": "sight_07", + "optional": true, + "options": [ + "sight_01", + "sight_02", + "sight_03", + "sight_04", + "sight_05", + "sight_06", + "sight_07", + "sight_08", + "sight_09", + "sight_10", + "sight_11", + "sight_collimator", + "sight_holographic", + "sight_offset" + ] + } + } + }, + "taser_gun": { + "name": "Taser", + "slots": {} + } + } +} \ No newline at end of file diff --git a/src/svmap/loadout_katalog.gd b/src/svmap/loadout_katalog.gd new file mode 100644 index 0000000..e61c8c2 --- /dev/null +++ b/src/svmap/loadout_katalog.gd @@ -0,0 +1,149 @@ +class_name LoadoutKatalog +extends RefCounted +## Map-Loadout (Karte „Loadout pro Map"): Waffen-/Aufsatz-Katalog des Editors +## und die Regeln für den optionalen `loadout`-Block der .svmap (V18, 1:1 wie +## MapLoadout im Match-Server, docs/map_format.md). +## +## doc["loadout"] fehlt oder mode "player" = Spieler-Loadout aus der Lobby. +## mode "fixed" = {primary, secondary ("" = keine), attachments:{waffe: "slot=teil;…"}} +## — Aufsatz-Text nur mit Abweichungen vom Standard (Draht-Format wie im Client). +## Katalog-Quelle: library/loadout_katalog.json (tools/sync_loadout_katalog.py). + +const PATH := "res://library/loadout_katalog.json" +const MODE_FIXED := "fixed" +const MODE_PLAYER := "player" + +static var _data: Dictionary = {} +static var _loaded := false + + +static func _ensure() -> void: + if _loaded: + return + _loaded = true + var f := FileAccess.open(PATH, FileAccess.READ) + if f == null: + push_warning("LoadoutKatalog: %s fehlt — Loadout-Auswahl bleibt leer" % PATH) + return + var parsed: Variant = JSON.parse_string(f.get_as_text()) + if parsed is Dictionary: + _data = (parsed as Dictionary).get("waffen", {}) + + +## Alle Waffen-IDs, nach Anzeigename sortiert. +static func waffen_ids() -> Array[String]: + _ensure() + var ids: Array[String] = [] + for k in _data.keys(): + ids.append(str(k)) + ids.sort_custom(func(a: String, b: String) -> bool: + return anzeigename(a).naturalnocasecmp_to(anzeigename(b)) < 0) + return ids + + +static func hat_waffe(id: String) -> bool: + _ensure() + return _data.has(id) + + +static func anzeigename(id: String) -> String: + _ensure() + return str((_data.get(id, {}) as Dictionary).get("name", id)) + + +## Aufsatz-Slots einer Waffe: {slot: {default, optional, options}} (leer = keine). +static func slots(id: String) -> Dictionary: + _ensure() + return (_data.get(id, {}) as Dictionary).get("slots", {}) + + +static func decode(text: String) -> Dictionary: + var out := {} + for chunk in text.split(";", false): + var kv := chunk.split("=", true, 1) + if kv.size() == 2 and not kv[0].is_empty(): + out[kv[0]] = kv[1] + return out + + +## Auswahl {slot: teil} → Draht-Text, nur Abweichungen vom Standard, Slots sortiert. +static func encode(id: String, sel: Dictionary) -> String: + var sl := slots(id) + var keys: Array = sel.keys() + keys.sort() + var parts: PackedStringArray = [] + for s in keys: + if sl.has(s) and str(sel[s]) != str((sl[s] as Dictionary).get("default", "")): + parts.append("%s=%s" % [s, sel[s]]) + return ";".join(parts) + + +## Aktuelle Teil-Wahl je Slot (Standard, überlagert vom gespeicherten Text). +static func auswahl(id: String, wire: String) -> Dictionary: + var out := {} + var sl := slots(id) + var dec := decode(wire) + for s in sl.keys(): + out[s] = str(dec.get(s, (sl[s] as Dictionary).get("default", ""))) + return out + + +## Grund, warum der Aufsatz-Text für die Waffe ungültig ist ("" = ok). +static func pruefe_aufsaetze(id: String, wire: String) -> String: + if wire.is_empty(): + return "" + var sl := slots(id) + if sl.is_empty(): + return "Waffe '%s' hat keine Aufsätze" % id + var dec := decode(wire) + if dec.is_empty(): + return "Aufsatz-Text unlesbar" + for s in dec.keys(): + if not sl.has(s): + return "Slot '%s' gibt es an '%s' nicht" % [s, id] + var p := str(dec[s]) + var sd: Dictionary = sl[s] + if p.is_empty(): + if not bool(sd.get("optional", false)): + return "Slot '%s' darf nicht leer sein" % s + elif not (sd.get("options", []) as Array).has(p): + return "Teil '%s' passt nicht an Slot '%s' von '%s'" % [p, s, id] + return "" + + +## V18-Fehlerliste (leer = gültig oder nicht vorhanden). +static func validate(lo: Variant) -> PackedStringArray: + var errors := PackedStringArray() + if lo == null: + return errors + if not (lo is Dictionary): + errors.append("V18: loadout muss ein Objekt sein.") + return errors + var d: Dictionary = lo + var mode := str(d.get("mode", MODE_PLAYER)) + if mode != MODE_FIXED and mode != MODE_PLAYER: + errors.append("V18: loadout.mode muss 'fixed' oder 'player' sein (ist '%s')." % mode) + return errors + if mode == MODE_PLAYER: + return errors + var prim := str(d.get("primary", "")).strip_edges().to_lower() + var sec := str(d.get("secondary", "")).strip_edges().to_lower() + if prim.is_empty(): + errors.append("V18: loadout.primary fehlt (Modus 'fixed').") + elif not hat_waffe(prim): + errors.append("V18: loadout.primary '%s' ist keine bekannte Waffe." % prim) + if not sec.is_empty() and not hat_waffe(sec): + errors.append("V18: loadout.secondary '%s' ist keine bekannte Waffe." % sec) + var att: Variant = d.get("attachments", {}) + if not (att is Dictionary): + errors.append("V18: loadout.attachments muss ein Objekt sein.") + return errors + for wid in (att as Dictionary).keys(): + var w := str(wid).strip_edges().to_lower() + if w != prim and w != sec: + errors.append("V18: loadout.attachments['%s'] gehört zu keiner Loadout-Waffe." % w) + continue + var why := pruefe_aufsaetze(w, str(att[wid])) + if why != "": + errors.append("V18: loadout.attachments['%s']: %s." % [w, why]) + return errors diff --git a/src/svmap/loadout_katalog.gd.uid b/src/svmap/loadout_katalog.gd.uid new file mode 100644 index 0000000..1df16fa --- /dev/null +++ b/src/svmap/loadout_katalog.gd.uid @@ -0,0 +1 @@ +uid://jm1cubdr25s8 diff --git a/src/svmap/svmap_io.gd b/src/svmap/svmap_io.gd index ef1f867..7d032e3 100644 --- a/src/svmap/svmap_io.gd +++ b/src/svmap/svmap_io.gd @@ -220,6 +220,8 @@ static func _validate_core(data: Dictionary, manifest: Dictionary = {}) -> Packe ids.append(e["id"]) errors.append("V17: weather_sky '%s' ist keine gültige Skybox (%s)." % [ str(weather_sky), ", ".join(ids)]) + # V18: loadout (optional) — festes Map-Loadout, siehe LoadoutKatalog. + errors.append_array(LoadoutKatalog.validate(data.get("loadout", null))) return errors diff --git a/src/ui/panels/loadout_panel.gd b/src/ui/panels/loadout_panel.gd new file mode 100644 index 0000000..904fadb --- /dev/null +++ b/src/ui/panels/loadout_panel.gd @@ -0,0 +1,225 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Loadout-Bereich im Map-Tab (Karte „Loadout pro Map"): pro Map wählbar, ob die +## Spieler ihr eigenes Lobby-Loadout mitbringen oder die Map ein festes Loadout +## (Primär-/Sekundärwaffe + Aufsätze) vorschreibt. Gespeichert als optionaler +## Block doc["loadout"] (Regeln: src/svmap/loadout_katalog.gd, V18). Der Server +## setzt ein festes Loadout durch, der Client zeigt es an. +## +## Jede Änderung ist ein Undo-Schritt (History-Block "loadout"; fehlt der Block, +## ist das Spieler-Loadout gemeint). + +const KEINE := "" + +var mode_option: OptionButton +var primary_option: OptionButton +var secondary_option: OptionButton +var fixed_box: VBoxContainer +var attach_box: VBoxContainer +var hint_label: Label + +var _loading := false +var _ids: Array[String] = [] + + +static func spec() -> Dictionary: + return {"titel": "Loadout", "gruppe": "map", "order": 25} + + +func panel_title() -> String: + return "Loadout" + + +func build() -> void: + add_child(_header_label("Loadout")) + add_child(_muted_label("Legt fest, mit welchen Waffen die Spieler diese Karte spielen.")) + + mode_option = OptionButton.new() + mode_option.tooltip_text = "Eigenes Loadout: Spieler nehmen ihre Auswahl aus der Lobby mit. Festes Loadout: alle bekommen genau diese Waffen, der Server setzt das durch." + mode_option.add_item("Eigenes Loadout der Spieler (Lobby)") + mode_option.add_item("Festes Map-Loadout") + add_child(mode_option) + + fixed_box = VBoxContainer.new() + fixed_box.add_theme_constant_override("separation", UIScale.px(8)) + add_child(fixed_box) + + fixed_box.add_child(_field_label("Primärwaffe (Rücken)")) + primary_option = OptionButton.new() + fixed_box.add_child(primary_option) + fixed_box.add_child(_field_label("Sekundärwaffe (Hüfte)")) + secondary_option = OptionButton.new() + fixed_box.add_child(secondary_option) + + _ids = LoadoutKatalog.waffen_ids() + for id in _ids: + primary_option.add_item(LoadoutKatalog.anzeigename(id)) + secondary_option.add_item(LoadoutKatalog.anzeigename(id)) + secondary_option.add_item("— keine —") + + attach_box = VBoxContainer.new() + attach_box.add_theme_constant_override("separation", UIScale.px(6)) + fixed_box.add_child(attach_box) + + hint_label = _muted_label("Messer, Minen und Granaten bleiben unverändert.") + fixed_box.add_child(hint_label) + + mode_option.item_selected.connect(func(i: int) -> void: set_fixed(i == 1)) + primary_option.item_selected.connect(func(i: int) -> void: + set_weapons(_ids[i], _secondary_id())) + secondary_option.item_selected.connect(func(_i: int) -> void: + set_weapons(_primary_id(), _secondary_id())) + refresh_values() + + +# ------------------------------------------------------------- Lesen ------- + +func _primary_id() -> String: + return _ids[primary_option.selected] if primary_option.selected >= 0 else "" + + +func _secondary_id() -> String: + var i := secondary_option.selected + return _ids[i] if i >= 0 and i < _ids.size() else KEINE + + +func current() -> Dictionary: + return main.doc.get("loadout", {}) if main != null and not main.doc.is_empty() else {} + + +func is_fixed() -> bool: + return str(current().get("mode", LoadoutKatalog.MODE_PLAYER)) == LoadoutKatalog.MODE_FIXED + + +# ------------------------------------------------------------- Ändern ------ + +## Modus wechseln. Auf „fest" ohne vorherige Wahl: M40 + Pistole 1 als Start. +func set_fixed(fixed: bool) -> void: + if main.doc.is_empty(): + return + if not fixed: + _commit(null, "Loadout: Spieler-Loadout") + return + var cur := current() + if cur.get("mode", "") == LoadoutKatalog.MODE_FIXED: + return + var prim := "m40" if LoadoutKatalog.hat_waffe("m40") else (_ids[0] if not _ids.is_empty() else "") + var sec := "pistol1" if LoadoutKatalog.hat_waffe("pistol1") else KEINE + _commit({"mode": LoadoutKatalog.MODE_FIXED, "primary": prim, "secondary": sec, "attachments": {}}, + "Loadout: festes Map-Loadout") + + +## Waffen setzen. Aufsätze von Waffen, die nicht mehr im Loadout sind, fallen weg. +func set_weapons(primary: String, secondary: String) -> void: + if main.doc.is_empty() or not is_fixed(): + return + if not LoadoutKatalog.hat_waffe(primary) or (secondary != KEINE and not LoadoutKatalog.hat_waffe(secondary)): + return + var lo := current().duplicate(true) + lo["primary"] = primary + lo["secondary"] = secondary + var att: Dictionary = lo.get("attachments", {}) + for w in att.keys(): + if w != primary and w != secondary: + att.erase(w) + lo["attachments"] = att + _commit(lo, "Loadout: %s / %s" % [LoadoutKatalog.anzeigename(primary), + LoadoutKatalog.anzeigename(secondary) if secondary != KEINE else "keine"]) + + +## Ein Aufsatz-Slot einer Loadout-Waffe setzen (part = Teil-ID, "" = nichts). +func set_part(weapon: String, slot: String, part: String) -> void: + if main.doc.is_empty() or not is_fixed(): + return + var lo := current().duplicate(true) + if weapon != str(lo.get("primary", "")) and weapon != str(lo.get("secondary", "")): + return + var att: Dictionary = lo.get("attachments", {}) + var sel := LoadoutKatalog.auswahl(weapon, str(att.get(weapon, ""))) + if not sel.has(slot): + return + sel[slot] = part + var wire := LoadoutKatalog.encode(weapon, sel) + if LoadoutKatalog.pruefe_aufsaetze(weapon, wire) != "": + return + if wire.is_empty(): + att.erase(weapon) + else: + att[weapon] = wire + lo["attachments"] = att + _commit(lo, "Loadout: Aufsatz %s" % slot) + + +func _commit(neu: Variant, text: String) -> void: + var alt: Variant = main.doc.get("loadout", null) + if alt != null: + alt = (alt as Dictionary).duplicate(true) + if neu == null: + main.doc.erase("loadout") + else: + main.doc["loadout"] = (neu as Dictionary).duplicate(true) + var danach: Variant = main.doc.get("loadout", null) + if danach != null: + danach = (danach as Dictionary).duplicate(true) + if alt == danach: + return + main.history.commit(text, {"loadout": alt}, {"loadout": danach}) + main.dirty = true + main.call("_refresh_file_label") + main.call("_refresh_history_buttons") + refresh_values() + main.call("_set_status", "%s." % text) + + +# ------------------------------------------------------------ Anzeige ------ + +## UI aus dem doc auffüllen (Laden, Undo/Redo) — löst keinen Schreibzugriff aus. +func refresh_values() -> void: + if mode_option == null or main == null: + return + _loading = true + var lo := current() + var fixed := is_fixed() + mode_option.select(1 if fixed else 0) + fixed_box.visible = fixed + if fixed: + var prim := str(lo.get("primary", "")) + var sec := str(lo.get("secondary", "")) + primary_option.select(maxi(0, _ids.find(prim))) + secondary_option.select(_ids.find(sec) if sec != KEINE and _ids.has(sec) else _ids.size()) + _rebuild_attachments(prim, sec, lo.get("attachments", {})) + _loading = false + + +func _rebuild_attachments(prim: String, sec: String, att: Dictionary) -> void: + for c in attach_box.get_children(): + attach_box.remove_child(c) + c.queue_free() + for w in [prim, sec]: + if w == "" or LoadoutKatalog.slots(w).is_empty(): + continue + attach_box.add_child(_header_label("Aufsätze: %s" % LoadoutKatalog.anzeigename(w))) + var sel := LoadoutKatalog.auswahl(w, str(att.get(w, ""))) + var sl := LoadoutKatalog.slots(w) + var slot_names: Array = sl.keys() + slot_names.sort() + for s in slot_names: + var sd: Dictionary = sl[s] + var row := HBoxContainer.new() + var lab := _field_label(str(s).replace("_", " ")) + lab.custom_minimum_size = UIScale.v2(90, 0) + row.add_child(lab) + var ob := OptionButton.new() + ob.name = "Teil_%s_%s" % [w, s] + ob.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var parts: Array = [] + if bool(sd.get("optional", false)): + parts.append("") + parts.append_array(sd.get("options", [])) + for p in parts: + ob.add_item("— nichts —" if str(p) == "" else str(p)) + ob.select(maxi(0, parts.find(sel.get(s, "")))) + ob.item_selected.connect(func(i: int) -> void: + if not _loading: + set_part(w, str(s), str(parts[i]))) + row.add_child(ob) + attach_box.add_child(row) diff --git a/src/ui/panels/loadout_panel.gd.uid b/src/ui/panels/loadout_panel.gd.uid new file mode 100644 index 0000000..dfc7fac --- /dev/null +++ b/src/ui/panels/loadout_panel.gd.uid @@ -0,0 +1 @@ +uid://t00n1cb8be2w diff --git a/src/ui/panels/map_panel.gd b/src/ui/panels/map_panel.gd index 8e2144b..c2a076e 100644 --- a/src/ui/panels/map_panel.gd +++ b/src/ui/panels/map_panel.gd @@ -254,4 +254,8 @@ func refresh_values() -> void: var wl: Variant = main.doc.get("water_level", null) prop_water.set_value_no_signal( float(wl) if (wl is float or wl is int) else KEIN_WASSER_WERT) + # Loadout-Bereich (Registry-Gruppe "map") aus dem doc nachziehen. + var loadout_panel: Node = _sections.get_node_or_null("loadout") if _sections != null else null + if loadout_panel != null: + loadout_panel.call("refresh_values") _loading_ui_values = false diff --git a/src/ui/panels/registry.gd b/src/ui/panels/registry.gd index 0af47da..4750a82 100644 --- a/src/ui/panels/registry.gd +++ b/src/ui/panels/registry.gd @@ -44,6 +44,8 @@ const PANELS: Array = [ "skript": "res://src/ui/panels/check_panel.gd"}, {"id": "teststart", "titel": "Test-Start", "gruppe": "map", "order": 20, "skript": "res://src/ui/panels/test_start_panel.gd"}, + {"id": "loadout", "titel": "Loadout", "gruppe": "map", "order": 25, + "skript": "res://src/ui/panels/loadout_panel.gd"}, {"id": "symmetrie", "titel": "Symmetrie", "gruppe": "map", "order": 30, "skript": "res://src/ui/panels/symmetry_panel.gd"}, # --- dialog: Inhalt des Einstellungen-Fensters (Knopf in der Kopfleiste) --- diff --git a/tests/test_loadout.gd b/tests/test_loadout.gd new file mode 100644 index 0000000..c5c17b1 --- /dev/null +++ b/tests/test_loadout.gd @@ -0,0 +1,119 @@ +extends SceneTree +## Editor-Test (headless): Map-Loadout (V18) — Katalog, Validierung, Panel mit +## Undo/Redo und Speichern/Laden. Ausführen: +## godot --headless --path . --script res://tests/test_loadout.gd + +var failures := 0 +var checks := 0 +var main: Control + + +func _initialize() -> void: + _run_all.call_deferred() + + +func _check(cond: bool, label: String) -> void: + checks += 1 + if cond: + print(" ok: %s" % label) + else: + failures += 1 + print(" FAIL: %s" % label) + + +func _run_all() -> void: + _test_katalog() + await _test_panel() + if failures == 0: + print("ALL OK: %d checks" % checks) + quit(0) + else: + print("FAILED: %d/%d checks" % [failures, checks]) + quit(1) + + +func _test_katalog() -> void: + print("Testgruppe: Katalog + Validierung") + _check(LoadoutKatalog.waffen_ids().size() >= 50, "Katalog hat Waffen") + _check(LoadoutKatalog.hat_waffe("m40") and LoadoutKatalog.hat_waffe("pistol1"), "m40 + pistol1 bekannt") + _check(LoadoutKatalog.validate(null).is_empty(), "fehlend gültig") + _check(LoadoutKatalog.validate({"mode": "player"}).is_empty(), "player gültig") + _check(not LoadoutKatalog.validate({"mode": "fixed"}).is_empty(), "fixed ohne Primär ungültig") + _check(not LoadoutKatalog.validate({"mode": "fixed", "primary": "nix"}).is_empty(), "unbekannte Waffe ungültig") + _check(not LoadoutKatalog.validate({"mode": "x"}).is_empty(), "unbekannter Modus ungültig") + _check(LoadoutKatalog.validate({"mode": "fixed", "primary": "sr1", "secondary": ""}).is_empty(), "fixed ohne Sekundär gültig") + _check(not LoadoutKatalog.validate({"mode": "fixed", "primary": "sr1", + "attachments": {"ar1": ""}}).is_empty(), "Aufsatz für Fremdwaffe ungültig") + _check(not LoadoutKatalog.validate({"mode": "fixed", "primary": "sr1", + "attachments": {"sr1": "sight=nix"}}).is_empty(), "unbekanntes Teil ungültig") + var sel := LoadoutKatalog.auswahl("sr1", "") + sel["muzzle"] = "muzzle_suppressor_01" + var wire := LoadoutKatalog.encode("sr1", sel) + _check(wire == "muzzle=muzzle_suppressor_01", "encode nur Abweichungen: " + wire) + _check(LoadoutKatalog.pruefe_aufsaetze("sr1", wire) == "", "encodierter Text gültig") + + +func _test_panel() -> void: + print("Testgruppe: Panel") + var scene: PackedScene = load("res://scenes/main.tscn") + main = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + main.doc = SvmapDocument.create_new("LoadoutTest", 512.0, "mittag") + main.history.doc = main.doc + main.call("_refresh_all") + var panel: Node = main.map_mod.sections_host().get_node_or_null("loadout") + _check(panel != null, "Loadout-Panel hängt im Map-Tab") + if panel == null: + return + _check(not panel.is_fixed() and not main.doc.has("loadout"), "Start: Spieler-Loadout, kein Block") + _check(not panel.fixed_box.visible, "Waffen-Auswahl verborgen") + + panel.set_fixed(true) + _check(panel.is_fixed() and main.doc["loadout"]["primary"] == "m40", "fest: M40 als Start") + _check(panel.fixed_box.visible, "Waffen-Auswahl sichtbar") + panel.set_weapons("sr1", "") + _check(main.doc["loadout"]["primary"] == "sr1" and main.doc["loadout"]["secondary"] == "", "Waffen gesetzt, Sekundär leer") + panel.set_part("sr1", "muzzle", "muzzle_suppressor_01") + _check(main.doc["loadout"]["attachments"].get("sr1", "") == "muzzle=muzzle_suppressor_01", "Aufsatz gespeichert") + panel.set_part("ar1", "muzzle", "muzzle_suppressor_01") + _check(not main.doc["loadout"]["attachments"].has("ar1"), "Aufsatz für Nicht-Loadout-Waffe ignoriert") + _check(SvmapIO.validate_map(_signed(main.doc)).is_empty(), "Map mit Loadout besteht V1–V18") + + panel.set_weapons("ar1", "") + _check(not main.doc["loadout"]["attachments"].has("sr1"), "Waffenwechsel verwirft Aufsätze der alten Waffe") + + # Undo/Redo + main.history.undo() + main.call("_refresh_all") + _check(main.doc["loadout"]["primary"] == "sr1" and main.doc["loadout"]["attachments"].has("sr1"), "Undo: Waffenwechsel zurück") + main.history.undo() + main.history.undo() + main.history.undo() + main.call("_refresh_all") + _check(not main.doc.has("loadout") and not panel.fixed_box.visible, "Undo bis Start: Block weg, UI nachgezogen") + main.history.redo() + main.call("_refresh_all") + _check(main.doc.has("loadout") and panel.is_fixed(), "Redo: fest wieder da") + + # Zurück auf Spieler-Loadout entfernt den Block + panel.set_fixed(false) + _check(not main.doc.has("loadout"), "Spieler-Loadout entfernt den Block") + + # Speichern/Laden + panel.set_fixed(true) + panel.set_weapons("sr2", "pistol2") + var path := "user://loadout_test.svmap" + var doc2: Dictionary = main.doc.duplicate(true) + SvmapIO.sign_map(doc2) + var res := SvmapIO.save_to_file(path, doc2) + _check(res["ok"], "Speichern ok: " + str(res["errors"])) + var back := SvmapIO.load_from_file(path) + _check(back["ok"] and back["data"]["loadout"]["primary"] == "sr2", "Laden: Loadout bleibt erhalten") + + +func _signed(d: Dictionary) -> Dictionary: + var c := d.duplicate(true) + SvmapIO.sign_map(c) + return c diff --git a/tests/test_loadout.gd.uid b/tests/test_loadout.gd.uid new file mode 100644 index 0000000..3dcceaf --- /dev/null +++ b/tests/test_loadout.gd.uid @@ -0,0 +1 @@ +uid://cqys3opdjiba7 diff --git a/tools/run_tests.sh b/tools/run_tests.sh index bda5600..891f747 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." GODOT=${GODOT:-/opt/data/home/bin/godot4} rc=0 -for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_build_spawn_mirror test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector test_pinsel_werkzeuge test_terrain_mirror test_mal_fein test_ui_aufraeumen test_ui_layout test_ui_scale; do +for t in test_ux test_shortcuts test_editor_svmap test_terrain test_library test_glb_view test_spawns test_testrun test_bauen test_build_spawn_mirror test_resize_dialog test_updater test_gui_smoke test_rmb_vorrang test_inspector test_pinsel_werkzeuge test_terrain_mirror test_mal_fein test_ui_aufraeumen test_ui_layout test_ui_scale test_loadout; do "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1 code=$? line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1) diff --git a/tools/sync_loadout_katalog.py b/tools/sync_loadout_katalog.py new file mode 100644 index 0000000..5d0da95 --- /dev/null +++ b/tools/sync_loadout_katalog.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Waffen- und Aufsatz-Katalog fürs Map-Loadout aus dem Match-Server-Repo holen. + +Aufruf: python3 tools/sync_loadout_katalog.py [pfad/zu/scopedvr-match-server] +Schreibt library/loadout_katalog.json (Waffen-ID -> Name, Aufsatz-Slots). +Nach neuen Waffen/Aufsätzen im Server erneut ausführen. +""" +import json +import os +import sys + +hier = os.path.dirname(os.path.abspath(__file__)) +server = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/godot-projects/scopedvr-match-server") +profile = json.load(open(os.path.join(server, "library/weapons.json"), encoding="utf-8"))["profiles"] +aufsaetze = json.load(open(os.path.join(server, "library/attachments.json"), encoding="utf-8"))["weapons"] + +waffen = {} +for wid in sorted(profile): + p = profile[wid] + eintrag = {"name": str(p.get("name", wid))} + if wid in aufsaetze: + eintrag["slots"] = { + s: {"default": d.get("default", ""), "optional": bool(d.get("optional", False)), + "options": list(d.get("options", []))} + for s, d in sorted(aufsaetze[wid].items()) + } + waffen[wid] = eintrag + +ziel = os.path.join(hier, "..", "library", "loadout_katalog.json") +with open(ziel, "w", encoding="utf-8") as f: + json.dump({"version": 1, "waffen": waffen}, f, ensure_ascii=False, indent=1, sort_keys=True) +print("%d Waffen -> %s" % (len(waffen), os.path.normpath(ziel)))