From 51d9a6546dbdd24a4021e1fc9188d875b3712b19 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Mon, 18 May 2026 21:08:30 +0200 Subject: [PATCH 01/55] Copilot CLI session 8689e9a8-e26a-41c6-84c1-77d5b487bdd5 changes --- ChapterMaster.yyp | 34 ++- datafiles/data/mobility.json | 20 ++ datafiles/main/chapters/1.JSON | 2 +- datafiles/main/squads/base_squads.json | 25 +- datafiles/main/squads/lightning_warriors.json | 242 ++++++++++++++++++ scripts/scr_UnitGroup/scr_UnitGroup.gml | 117 ++++++++- .../scr_initialize_custom.gml | 35 +-- scripts/scr_squads/scr_squads.gml | 10 + scripts/scr_start_load/scr_start_load.gml | 4 +- 9 files changed, 423 insertions(+), 66 deletions(-) create mode 100644 datafiles/main/squads/lightning_warriors.json diff --git a/ChapterMaster.yyp b/ChapterMaster.yyp index ae73d2af0f..d2c238c6bb 100644 --- a/ChapterMaster.yyp +++ b/ChapterMaster.yyp @@ -2,9 +2,9 @@ "$GMProject":"v1", "%Name":"ChapterMaster", "AudioGroups":[ - {"$GMAudioGroup":"v1","%Name":"audiogroup_default","exportDir":"","name":"audiogroup_default","resourceType":"GMAudioGroup","resourceVersion":"2.0","targets":-1,}, - {"$GMAudioGroup":"v1","%Name":"audiogroup_sfx","exportDir":"","name":"audiogroup_sfx","resourceType":"GMAudioGroup","resourceVersion":"2.0","targets":-1,}, - {"$GMAudioGroup":"v1","%Name":"audiogroup_music","exportDir":"","name":"audiogroup_music","resourceType":"GMAudioGroup","resourceVersion":"2.0","targets":-1,}, + {"$GMAudioGroup":"","%Name":"audiogroup_default","name":"audiogroup_default","resourceType":"GMAudioGroup","resourceVersion":"2.0","targets":-1,}, + {"$GMAudioGroup":"","%Name":"audiogroup_sfx","name":"audiogroup_sfx","resourceType":"GMAudioGroup","resourceVersion":"2.0","targets":-1,}, + {"$GMAudioGroup":"","%Name":"audiogroup_music","name":"audiogroup_music","resourceType":"GMAudioGroup","resourceVersion":"2.0","targets":-1,}, ], "configs":{ "children":[], @@ -22,6 +22,7 @@ {"$GMFolder":"","%Name":"Objects","folderPath":"folders/Objects.yy","name":"Objects","resourceType":"GMFolder","resourceVersion":"2.0",}, {"$GMFolder":"","%Name":"Combat_Fleet","folderPath":"folders/Objects/Combat_Fleet.yy","name":"Combat_Fleet","resourceType":"GMFolder","resourceVersion":"2.0",}, {"$GMFolder":"","%Name":"New Combat","folderPath":"folders/Objects/New Combat.yy","name":"New Combat","resourceType":"GMFolder","resourceVersion":"2.0",}, + {"$GMFolder":"","%Name":"New Ground","folderPath":"folders/Objects/New Ground.yy","name":"New Ground","resourceType":"GMFolder","resourceVersion":"2.0",}, {"$GMFolder":"","%Name":"New UI","folderPath":"folders/Objects/New UI.yy","name":"New UI","resourceType":"GMFolder","resourceVersion":"2.0",}, {"$GMFolder":"","%Name":"Temp","folderPath":"folders/Objects/Temp.yy","name":"Temp","resourceType":"GMFolder","resourceVersion":"2.0",}, {"$GMFolder":"","%Name":"Paths","folderPath":"folders/Paths.yy","name":"Paths","resourceType":"GMFolder","resourceVersion":"2.0",}, @@ -578,6 +579,7 @@ {"$GMIncludedFile":"","%Name":"equal_scouts.json","CopyToMask":-1,"filePath":"datafiles/main/squads","name":"equal_scouts.json","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"equal_specialists.json","CopyToMask":-1,"filePath":"datafiles/main/squads","name":"equal_specialists.json","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"equal_spescout.json","CopyToMask":-1,"filePath":"datafiles/main/squads","name":"equal_spescout.json","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, + {"$GMIncludedFile":"","%Name":"lightning_warriors.json","CopyToMask":-1,"filePath":"datafiles/main/squads","name":"lightning_warriors.json","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"version.json","CopyToMask":-1,"filePath":"datafiles/main","name":"version.json","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"data.json","CopyToMask":-1,"filePath":"datafiles/main/visual_sets/Company_marks_roman_right_knee","name":"data.json","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"12.png","CopyToMask":-1,"filePath":"datafiles/main/visual_sets/Company_marks_roman_right_knee/one","name":"12.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, @@ -604,7 +606,7 @@ "isEcma":false, "LibraryEmitters":[], "MetaData":{ - "IDEVersion":"2024.1400.5.1065", + "IDEVersion":"2024.1400.5.1055", }, "name":"ChapterMaster", "resources":[ @@ -656,6 +658,7 @@ {"id":{"name":"obj_en_pulse","path":"objects/obj_en_pulse/obj_en_pulse.yy",},}, {"id":{"name":"obj_en_round","path":"objects/obj_en_round/obj_en_round.yy",},}, {"id":{"name":"obj_en_ship","path":"objects/obj_en_ship/obj_en_ship.yy",},}, + {"id":{"name":"obj_enemy_leftest","path":"objects/obj_enemy_leftest/obj_enemy_leftest.yy",},}, {"id":{"name":"obj_enunit","path":"objects/obj_enunit/obj_enunit.yy",},}, {"id":{"name":"obj_event_log","path":"objects/obj_event_log/obj_event_log.yy",},}, {"id":{"name":"obj_event","path":"objects/obj_event/obj_event.yy",},}, @@ -675,10 +678,12 @@ {"id":{"name":"obj_main_menu_buttons","path":"objects/obj_main_menu_buttons/obj_main_menu_buttons.yy",},}, {"id":{"name":"obj_main_menu","path":"objects/obj_main_menu/obj_main_menu.yy",},}, {"id":{"name":"obj_managment_panel","path":"objects/obj_managment_panel/obj_managment_panel.yy",},}, + {"id":{"name":"obj_marine","path":"objects/obj_marine/obj_marine.yy",},}, {"id":{"name":"obj_mass_equip","path":"objects/obj_mass_equip/obj_mass_equip.yy",},}, {"id":{"name":"obj_ncombat","path":"objects/obj_ncombat/obj_ncombat.yy",},}, {"id":{"name":"obj_new_button","path":"objects/obj_new_button/obj_new_button.yy",},}, {"id":{"name":"obj_nfort","path":"objects/obj_nfort/obj_nfort.yy",},}, + {"id":{"name":"obj_ork","path":"objects/obj_ork/obj_ork.yy",},}, {"id":{"name":"obj_p_assra","path":"objects/obj_p_assra/obj_p_assra.yy",},}, {"id":{"name":"obj_p_capital","path":"objects/obj_p_capital/obj_p_capital.yy",},}, {"id":{"name":"obj_p_cruiser","path":"objects/obj_p_cruiser/obj_p_cruiser.yy",},}, @@ -688,6 +693,8 @@ {"id":{"name":"obj_p_ship","path":"objects/obj_p_ship/obj_p_ship.yy",},}, {"id":{"name":"obj_p_small","path":"objects/obj_p_small/obj_p_small.yy",},}, {"id":{"name":"obj_p_th","path":"objects/obj_p_th/obj_p_th.yy",},}, + {"id":{"name":"obj_p1_bullet_miss","path":"objects/obj_p1_bullet_miss/obj_p1_bullet_miss.yy",},}, + {"id":{"name":"obj_p1_bullet","path":"objects/obj_p1_bullet/obj_p1_bullet.yy",},}, {"id":{"name":"obj_persistent","path":"objects/obj_persistent/obj_persistent.yy",},}, {"id":{"name":"obj_planet_map","path":"objects/obj_planet_map/obj_planet_map.yy",},}, {"id":{"name":"obj_pnunit","path":"objects/obj_pnunit/obj_pnunit.yy",},}, @@ -724,10 +731,25 @@ {"id":{"name":"rm_tutorial","path":"rooms/rm_tutorial/rm_tutorial.yy",},}, {"id":{"name":"__global_object_depths","path":"scripts/__global_object_depths/__global_object_depths.yy",},}, {"id":{"name":"__gml_pragma_global","path":"scripts/__gml_pragma_global/__gml_pragma_global.yy",},}, + {"id":{"name":"__init_action","path":"scripts/__init_action/__init_action.yy",},}, + {"id":{"name":"__init_d3d","path":"scripts/__init_d3d/__init_d3d.yy",},}, + {"id":{"name":"__init_view","path":"scripts/__init_view/__init_view.yy",},}, {"id":{"name":"__init","path":"scripts/__init/__init.yy",},}, + {"id":{"name":"__view_get","path":"scripts/__view_get/__view_get.yy",},}, + {"id":{"name":"__view_set_internal","path":"scripts/__view_set_internal/__view_set_internal.yy",},}, + {"id":{"name":"__view_set","path":"scripts/__view_set/__view_set.yy",},}, + {"id":{"name":"action_another_room","path":"scripts/action_another_room/action_another_room.yy",},}, + {"id":{"name":"action_color","path":"scripts/action_color/action_color.yy",},}, + {"id":{"name":"action_if_number","path":"scripts/action_if_number/action_if_number.yy",},}, + {"id":{"name":"action_if_variable","path":"scripts/action_if_variable/action_if_variable.yy",},}, + {"id":{"name":"action_kill_object","path":"scripts/action_kill_object/action_kill_object.yy",},}, + {"id":{"name":"action_restart_game","path":"scripts/action_restart_game/action_restart_game.yy",},}, + {"id":{"name":"action_set_alarm","path":"scripts/action_set_alarm/action_set_alarm.yy",},}, + {"id":{"name":"action_set_relative","path":"scripts/action_set_relative/action_set_relative.yy",},}, {"id":{"name":"Armamentarium","path":"scripts/Armamentarium/Armamentarium.yy",},}, {"id":{"name":"ColourItem","path":"scripts/ColourItem/ColourItem.yy",},}, {"id":{"name":"ColourPicker","path":"scripts/ColourPicker/ColourPicker.yy",},}, + {"id":{"name":"d3d_set_fog","path":"scripts/d3d_set_fog/d3d_set_fog.yy",},}, {"id":{"name":"DebugView","path":"scripts/DebugView/DebugView.yy",},}, {"id":{"name":"DiploBasicNodes","path":"scripts/DiploBasicNodes/DiploBasicNodes.yy",},}, {"id":{"name":"DiploCommonComponents","path":"scripts/DiploCommonComponents/DiploCommonComponents.yy",},}, @@ -735,6 +757,7 @@ {"id":{"name":"DiscordPool","path":"scripts/DiscordPool/DiscordPool.yy",},}, {"id":{"name":"DiscordWebhook","path":"scripts/DiscordWebhook/DiscordWebhook.yy",},}, {"id":{"name":"draw_line_dashed","path":"scripts/draw_line_dashed/draw_line_dashed.yy",},}, + {"id":{"name":"draw_set_blend_mode","path":"scripts/draw_set_blend_mode/draw_set_blend_mode.yy",},}, {"id":{"name":"ErrorHandler","path":"scripts/ErrorHandler/ErrorHandler.yy",},}, {"id":{"name":"exp_and_exp_growth","path":"scripts/exp_and_exp_growth/exp_and_exp_growth.yy",},}, {"id":{"name":"explode_script","path":"scripts/explode_script/explode_script.yy",},}, @@ -914,7 +937,6 @@ {"id":{"name":"scr_save_chapter","path":"scripts/scr_save_chapter/scr_save_chapter.yy",},}, {"id":{"name":"scr_save","path":"scripts/scr_save/scr_save.yy",},}, {"id":{"name":"scr_scrollbar","path":"scripts/scr_scrollbar/scr_scrollbar.yy",},}, - {"id":{"name":"scr_secret_lair_view","path":"scripts/scr_secret_lair_view/scr_secret_lair_view.yy",},}, {"id":{"name":"scr_serialization_functions","path":"scripts/scr_serialization_functions/scr_serialization_functions.yy",},}, {"id":{"name":"scr_shader_initialize","path":"scripts/scr_shader_initialize/scr_shader_initialize.yy",},}, {"id":{"name":"scr_ship_battle","path":"scripts/scr_ship_battle/scr_ship_battle.yy",},}, @@ -951,9 +973,9 @@ {"id":{"name":"scr_ui_display_weapons","path":"scripts/scr_ui_display_weapons/scr_ui_display_weapons.yy",},}, {"id":{"name":"scr_ui_formation_bars","path":"scripts/scr_ui_formation_bars/scr_ui_formation_bars.yy",},}, {"id":{"name":"scr_ui_manage","path":"scripts/scr_ui_manage/scr_ui_manage.yy",},}, + {"id":{"name":"scr_ui_popup","path":"scripts/scr_ui_popup/scr_ui_popup.yy",},}, {"id":{"name":"scr_ui_refresh","path":"scripts/scr_ui_refresh/scr_ui_refresh.yy",},}, {"id":{"name":"scr_ui_settings","path":"scripts/scr_ui_settings/scr_ui_settings.yy",},}, - {"id":{"name":"scr_ui_tooltip","path":"scripts/scr_ui_tooltip/scr_ui_tooltip.yy",},}, {"id":{"name":"scr_unit_detail_text","path":"scripts/scr_unit_detail_text/scr_unit_detail_text.yy",},}, {"id":{"name":"scr_unit_equip_functions","path":"scripts/scr_unit_equip_functions/scr_unit_equip_functions.yy",},}, {"id":{"name":"scr_unit_quick_find_pane","path":"scripts/scr_unit_quick_find_pane/scr_unit_quick_find_pane.yy",},}, diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index 2bfe3862cc..cb0e2b68c8 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -18,6 +18,26 @@ "value": 35, "requires_to_forge": ["combi_1"] }, + "Attack Bike": { + "abbreviation": "At Bike", + "damage_resistance_mod": { + "artifact": 10, + "master_crafted": 10, + "standard": 5 + }, + "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action. Sports an additional sidecar with a heavy weapon of choice [WIP only HB present].", + "hp_mod": { + "artifact": 35, + "master_crafted": 25, + "standard": 25 + }, + "second_profiles": [ + "Twin Linked Bolters", + "Heavy Bolter" + ], + "value": 35, + "requires_to_forge": ["combi_1"] + }, "Conversion Beamer Pack": { "abbreviation": "CnvBmr", "buyable": false, diff --git a/datafiles/main/chapters/1.JSON b/datafiles/main/chapters/1.JSON index 96f484826a..949a7a06b8 100644 --- a/datafiles/main/chapters/1.JSON +++ b/datafiles/main/chapters/1.JSON @@ -1815,7 +1815,7 @@ "role": "Black Knight", "loadout": { "required": { - "wep1": ["Chainsword", 4], + "wep1": ["Power Sword", 4], "wep2": ["Bolt Pistol", 2], "mobi": ["Bike", 4] }, diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index 7163ec86e5..f7146100b4 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -399,17 +399,22 @@ } }, "bike_squad": { - "Assault": { - "max": 9, - "min": 4, - "role": "Biker", - "loadout": { - "required": { - "wep1": ["", "max"], - "wep2": ["Chainsword", "max"], - "mobi": ["Bike", "max"] - } + "Biker": { + "max": 7, + "min": 2, + "role": "Biker", + "alternative_roles": ["Tactical", "Assault"], + "loadout": { + "required": { + "armour": ["",0], + "wep1": ["", 0], + "wep2": ["Bolt Pistol", 2], + "mobi": ["Bike", "max"] + }, + "option": { + "wep1": [[["","Chainsword","Chainaxe"],5,{"wep2":"Bolt Pistol"}]] } + } }, "Sergeant": { "max": 1, diff --git a/datafiles/main/squads/lightning_warriors.json b/datafiles/main/squads/lightning_warriors.json new file mode 100644 index 0000000000..b036184609 --- /dev/null +++ b/datafiles/main/squads/lightning_warriors.json @@ -0,0 +1,242 @@ +{ + "companies": [ + { + "company": 1, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "terminator_squad", + "proportion": 2 + }, + { + "squad": "terminator_assault_squad", + "proportion": 2 + }, + { + "squad": "veteran_squad", + "proportion": 1 + } + ] + }, + { + "company": 2, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "tactical_squad", + "proportion": 0 + }, + { + "squad": "bike_squad", + "proportion": 7 + }, + { + "squad": "attack_bike_squad", + "proportion": 3 + }, + { + "squad": "devastator_squad", + "proportion": 0 + }, + { + "squad": "assault_squad", + "proportion": 0 + } + ] + }, + { + "company": 3, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "tactical_squad", + "proportion": 0 + }, + { + "squad": "bike_squad", + "proportion": 7 + }, + { + "squad": "attack_bike_squad", + "proportion": 3 + }, + { + "squad": "devastator_squad", + "proportion": 0 + }, + { + "squad": "assault_squad", + "proportion": 0 + } + ] + }, + { + "company": 4, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "tactical_squad", + "proportion": 0 + }, + { + "squad": "bike_squad", + "proportion": 7 + }, + { + "squad": "attack_bike_squad", + "proportion": 3 + }, + { + "squad": "devastator_squad", + "proportion": 0 + }, + { + "squad": "assault_squad", + "proportion": 0 + } + ] + }, + { + "company": 5, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "tactical_squad", + "proportion": 0 + }, + { + "squad": "bike_squad", + "proportion": 7 + }, + { + "squad": "attack_bike_squad", + "proportion": 3 + }, + { + "squad": "devastator_squad", + "proportion": 0 + }, + { + "squad": "assault_squad", + "proportion": 0 + } + ] + }, + { + "company": 6, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "tactical_squad", + "proportion": 0 + }, + { + "squad": "bike_squad", + "proportion": 7 + }, + { + "squad": "attack_bike_squad", + "proportion": 3 + }, + { + "squad": "devastator_squad", + "proportion": 0 + }, + { + "squad": "assault_squad", + "proportion": 0 + } + ] + }, + { + "company": 7, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "tactical_squad", + "proportion": 1 + } + ] + }, + { + "company": 8, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "assault_squad", + "proportion": 1 + } + ] + }, + { + "company": 9, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "devastator_squad", + "proportion": 1 + } + ] + }, + { + "company": 10, + "squads": [ + { + "squad": "command_squad", + "max_count": 1, + "min_count": 1, + "require": true + }, + { + "squad": "scout_squad", + "proportion": 1 + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/scr_UnitGroup/scr_UnitGroup.gml b/scripts/scr_UnitGroup/scr_UnitGroup.gml index 35e8259574..7885b48bb8 100644 --- a/scripts/scr_UnitGroup/scr_UnitGroup.gml +++ b/scripts/scr_UnitGroup/scr_UnitGroup.gml @@ -202,7 +202,8 @@ function UnitGroup(units) constructor { static sgt_types = role_groups(SPECIALISTS_SQUAD_LEADERS); static create_squad = function(squad_type, squad_loadout = true, squad_uid = "", game_start = false) { - // LOGGER.info($"sgts : ${sgt_types}"); + //LOGGER.info($"sgts : ${sgt_types}"); + var roles = active_roles(); @@ -222,20 +223,55 @@ function UnitGroup(units) constructor { var _fill_squad = obj_ini.squad_types[$ squad_type]; var _fulfilled = false; + //adding multiple role sources on gen within a single squad + var _all_roles_to_fetch = array_create(0); + for (var r = 0; r < array_length(squad_unit_types); r++) { + var _primary_role = squad_unit_types[r]; + var _primary_role_def = _fill_squad[$ _primary_role]; + var _primary_role_name = struct_exists(_primary_role_def, "role") ? _primary_role_def.role : _primary_role; + LOGGER.info($"Squad type: {squad_type}, Looking for roles: {_all_roles_to_fetch}"); + if (!array_contains(_all_roles_to_fetch, _primary_role_name)) { + array_push(_all_roles_to_fetch, _primary_role_name); + } + + //add alternative source roles to fetch list if this squad role defines them + if (struct_exists(_primary_role_def, "alternative_roles")) { + var _alternatives = _primary_role_def.alternative_roles; + for (var a = 0; a < array_length(_alternatives); a++) { + if (!array_contains(_all_roles_to_fetch, _alternatives[a])) { + array_push(_all_roles_to_fetch, _alternatives[a]); + } + } + } + } + - var _squadless = get_from({squadless: true, roles: squad_unit_types}); + var _squadless = get_from({squadless: true, roles: _all_roles_to_fetch}); + LOGGER.info($"Squadless count before: {_squadless.number()}"); for (var s = 0; s < 2; s++) { var _sgt_type = sgt_types[s]; - var _available_sgt = _squadless.get_from({role: _sgt_type, max_wanted: 1}); + var _available_sgt = _squadless.get_from({role: _sgt_type, max_wanted: 1}, true, true); if (_available_sgt.number() == 0) { continue; } var _sgt = _available_sgt.units[0]; - squad.add_member(_sgt); - squad_fulfilment[$ _sgt_type]++; + squad.add_member(_sgt.company, _sgt.marine_number); + var _sgt_group = ""; + for (var r = 0; r < array_length(squad_unit_types); r++) { + var _role_name = squad_unit_types[r]; + var _role_def = _fill_squad[$ _role_name]; + var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; + if (_primary_role_name == _sgt_type) { + _sgt_group = _role_name; + break; + } + } + if (_sgt_group != "") { + squad_fulfilment[$ _sgt_group]++; + } sergeant_found = true; } @@ -252,8 +288,17 @@ function UnitGroup(units) constructor { var _has_sgt_requirements = false; for (var s = 0; s < 2; s++) { var _sgt_type = sgt_types[s]; - if (array_contains(squad_unit_types, _sgt_type)) { - _has_sgt_requirements = true; + for (var r = 0; r < array_length(squad_unit_types); r++) { + var _role_name = squad_unit_types[r]; + var _role_def = _fill_squad[$ _role_name]; + var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; + if (_sgt_type == _primary_role_name) { + _has_sgt_requirements = true; + break; + } + } + if (_has_sgt_requirements) { + break; } } @@ -262,14 +307,47 @@ function UnitGroup(units) constructor { } //clone or else keeps pushing up number - var _max = variable_clone(_fill_squad[$ _unit.role()][$ "max"]); + //EXPERIMENTAL Determine which role GROUP this marine belongs to + var _target_role = _unit.role(); + var _role_group = ""; // default to the marine's own role + + // Search through defined role groups to find which one this marine belongs to + for (var r = 0; r < array_length(squad_unit_types); r++) { + var _role_name = squad_unit_types[r]; + var _role_def = _fill_squad[$ _role_name]; + var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; + + + // Check if marine matches this primary role + if (_target_role == _primary_role_name) { + _role_group = _role_name; + break; + } + + // Check if marine matches any alternative roles for this group + if (struct_exists(_role_def, "alternative_roles")) { + var _alts = _role_def.alternative_roles; + if (array_contains(_alts, _target_role)) { + _role_group = _role_name; + break; + } + } + } + + if (_role_group == "") { + continue; + } + + // NEW: Check max capacity for the ROLE GROUP (not the source role) + var _max = variable_clone(_fill_squad[$ _role_group][$ "max"]); if (_has_sgt_requirements) { _max += 1; } - if (squad_fulfilment[$ _unit.role()] < _max) { + // NEW: Track fulfillment by role GROUP, allowing alternatives to fill the same slot + if (squad_fulfilment[$ _role_group] < _max) { //if sergeants not required - squad_fulfilment[$ _unit.role()]++; + squad_fulfilment[$ _role_group]++; squad.add_member(_unit.company, _unit.marine_number); } } @@ -285,10 +363,23 @@ function UnitGroup(units) constructor { } for (var s = 0; s < 2; s++) { var _sgt_type = sgt_types[s]; - if (struct_exists(squad_fulfilment, _sgt_type) && (!sergeant_found)) { - _exp_unit = _members.highest_exp(); + var _sgt_group = ""; + for (var r = 0; r < array_length(squad_unit_types); r++) { + var _role_name = squad_unit_types[r]; + var _role_def = _fill_squad[$ _role_name]; + var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; + if (_primary_role_name == _sgt_type) { + _sgt_group = _role_name; + break; + } + } if (_sgt_group != "" && struct_exists(squad_fulfilment, _sgt_group) && (!sergeant_found)) { + var _exp_unit = _members.highest_exp(); - squad_fulfilment[$ _sgt_type]++; + _exp_unit.update_role(_sgt_type); + squad_fulfilment[$ _sgt_group]++; + if (game_start && irandom(1) == 0) { + _exp_unit.add_trait("lead_example"); + } } } diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 7bee2adb51..b5122aa0bf 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -1830,40 +1830,7 @@ function scr_initialize_custom() { }; } - /*if (scr_has_adv("Lightning Warriors")) { - variable_struct_set( - custom_squads, - "bikers", - [ - [ - roles.assault, - { - "max": 9, - "min": 4, - "loadout": { - //tactical marine - "required": {"wep1": ["", "max"], "wep2": ["Chainsword", "max"], "mobi": ["Bike", "max"]}, - }, - "role": $"Biker", - } - ], - [ - roles.sergeant, - { - "max": 1, - "min": 1, - "loadout": { - //sergeant - "required": {"wep1": ["", "max"], "wep2": ["Chainsword", "max"], "mobi": ["Bike", 1]}, - }, - "role": $"Biker {roles.sergeant}", - } - ], - ["type_data", {"display_data": $"Bike {_squad_name}", "class": ["bike"], "formation_options": ["assault", "tactical"]}] - ] - ); - } - + /* if (scr_has_adv("Boarders")) { variable_struct_set( custom_squads, diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 7cabad2454..b7cb6f711e 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -302,6 +302,16 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { } squad_fulfilment[$ _wanted_unit_role] = 0; //create a fulfilment structure to log members of squad } + //Mapping for role groups in alternative source; + squad_role_alternatives = {}; + for (var i =0; i < array_length(squad_unit_types); i++) { + var _role_name = squad_unit_types[i]; + var _role_def = fill_squad[$ _role_name]; + //alternative source presence check + if (struct_exists(_role_def, "alternative_roles")) { + squad_role_alternatives[$ _role_name] = _role_def.alternative_roles; + } + } return squad_unit_types; }; diff --git a/scripts/scr_start_load/scr_start_load.gml b/scripts/scr_start_load/scr_start_load.gml index f35b38b27d..691e494458 100644 --- a/scripts/scr_start_load/scr_start_load.gml +++ b/scripts/scr_start_load/scr_start_load.gml @@ -33,7 +33,7 @@ function scr_start_load(fleet, load_from_star, load_options) { if (comp_split > 7 || !comp_has_units[comp_split + 2]) { comp_split = 0; } - var _squad = fetch_squad(_squad_ids[i]); + var _squad = fetch_squad(_squad_ids[squads]); if (_squad.base_company == 1) { array_push(total_distribute_squads[comp_split], _squad); comp_split++; @@ -47,7 +47,7 @@ function scr_start_load(fleet, load_from_star, load_options) { if (comp_split > 7 || !comp_has_units[comp_split + 2]) { comp_split = 0; } - var _squad = fetch_squad(_squad_ids[i]); + var _squad = fetch_squad(_squad_ids[squads]); if (_squad.base_company == 10) { array_push(total_distribute_squads[comp_split], _squad); comp_split++; From 6306746074d8d93488943e9a84eb3f12ec876944 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 19 May 2026 19:39:49 +0200 Subject: [PATCH 02/55] stuff --- datafiles/main/squads/base_squads.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index f7146100b4..ae98977e26 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -210,7 +210,7 @@ }, "devastator_squad": { - "Devastator": { + "Devastator Marine": { "max": 9, "min": 4, "loadout": { @@ -255,7 +255,7 @@ }, "tactical_squad": { - "Tactical": { + "Tactical Marine": { "max": 9, "min": 4, "loadout": { @@ -304,7 +304,7 @@ }, "assault_squad": { - "Assault": { + "Assault Marine": { "max": 9, "min": 4, "loadout": { @@ -435,7 +435,7 @@ } }, "breacher_squad": { - "Tactical": { + "Tactical Marine": { "max": 9, "min": 4, "role": "Breacher", From 87a8d472f06ca28dbf0e24f9cf400d3930e748c2 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:08:27 +0200 Subject: [PATCH 03/55] fix: monastery typo --- datafiles/main/chapters/34.json | 2 +- scripts/scr_chapter_new/scr_chapter_new.gml | 2 +- scripts/scr_planetary_feature/scr_planetary_feature.gml | 2 +- scripts/scr_save_chapter/scr_save_chapter.gml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datafiles/main/chapters/34.json b/datafiles/main/chapters/34.json index 76d8c9a535..2dc7a424f8 100644 --- a/datafiles/main/chapters/34.json +++ b/datafiles/main/chapters/34.json @@ -139,7 +139,7 @@ "culture_styles": [], "home_planets": 1.0, "flagship_name": "Victus", - "monastary_name": "", + "monastery_name": "", "advantages": [ "", "Assault Doctrine", diff --git a/scripts/scr_chapter_new/scr_chapter_new.gml b/scripts/scr_chapter_new/scr_chapter_new.gml index 8b5c7af2af..b91fb3ecad 100644 --- a/scripts/scr_chapter_new/scr_chapter_new.gml +++ b/scripts/scr_chapter_new/scr_chapter_new.gml @@ -30,7 +30,7 @@ function ChapterData() constructor { home_planets = 1; flagship_name = global.name_generator.GenerateFromSet("imperial_ship"); - monastary_name = ""; + monastery_name = ""; advantages = array_create(9); disadvantages = array_create(9); discipline = "librarius"; // todo convert to enum diff --git a/scripts/scr_planetary_feature/scr_planetary_feature.gml b/scripts/scr_planetary_feature/scr_planetary_feature.gml index 58300f6705..672dba85c7 100644 --- a/scripts/scr_planetary_feature/scr_planetary_feature.gml +++ b/scripts/scr_planetary_feature/scr_planetary_feature.gml @@ -152,7 +152,7 @@ function NewPlanetFeature(feature_type, other_data = {}) constructor { tier = 1; break; case eP_FEATURES.MONASTERY: - planet_display = "Fortress Monastary"; + planet_display = "Fortress Monastery"; player_hidden = 0; forge = 0; name = global.name_generator.GenerateFromSet("imperial_ship"); diff --git a/scripts/scr_save_chapter/scr_save_chapter.gml b/scripts/scr_save_chapter/scr_save_chapter.gml index f8131611f8..413103a553 100644 --- a/scripts/scr_save_chapter/scr_save_chapter.gml +++ b/scripts/scr_save_chapter/scr_save_chapter.gml @@ -104,7 +104,7 @@ function scr_save_chapter(chapter_id) { chap.disposition = disposition; if (variable_instance_exists(self.id, "monastery_name")) { - chap.monastary_name = monastery_name; + chap.monastery_name = monastery_name; } chap.chapter_master = { name: chapter_master_name, From c836498486667e0f045a9a58a8093242bef8c813 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:08:42 +0200 Subject: [PATCH 04/55] feat: new template --- datafiles/main/chapters/template.JSON | 1782 ++++++++++++++++++++----- 1 file changed, 1481 insertions(+), 301 deletions(-) diff --git a/datafiles/main/chapters/template.JSON b/datafiles/main/chapters/template.JSON index 7c64b377f4..291e21eb95 100644 --- a/datafiles/main/chapters/template.JSON +++ b/datafiles/main/chapters/template.JSON @@ -1,309 +1,1489 @@ { - "chapter": { - "id": 1, - "name": "Chapter Name", - "flavor": "Chapter Lore", - "origin": 3, // 1 - Founding, 2 - Successor, 3 - Other/non-canon/fanmade, 4 - Custom - "points": 150, - "founding": 0, // The id of the founding chapter, 0 for unknown or none, 10 for random - "splash": 1, // in \images\creation\chapters\splash folder, the img number, 1 being Dark Angels etc. - "icon_name": "unknown", - "fleet_type": 1, // 1= Homeworld, 2 = Fleet based, 3 = Penitent - "strength": 5, // 1-10 - "purity": 5, // 1-10 - "stability": 50, // 1-99 - "cooperation": 5, // 1-10 - "homeworld_exists": 1, // 1 = true - "recruiting_exists": 1, // 1 = true - "homeworld_rule": 1, // 1 = Govenor, 2 = Countries, 3 = Personal Rule - "homeworld": "Hive", // one of "Lava" "Desert" "Forge" "Hive" "Death" "Agri" "Feudal" "Temperate" "Ice" "Dead" "Shrine" - "homeworld_name": "", // Homeworld Planet name, leave blank to autogenerate - "recruiting": "Death", // one of "Lava" "Desert" "Forge" "Hive" "Death" "Agri" "Feudal" "Temperate" "Ice" "Dead" "Shrine" - "recruiting_name": "", // Recruiting Planet name, leave blank to autogenerate - "discipline": "librarius", // one of 'default' 'biomancy' 'pyromancy' 'telekinesis' 'rune_magic' - "aspirant_trial": "SURVIVAL", // one of "BLOODDUEL" "HUNTING" "SURVIVAL" "EXPOSURE" "KNOWLEDGE" "CHALLENGE" "APPRENTICESHIP" - "advantages": [ - "", // leave the first entry blank for now - "", - "", - "", - "", - "", - "", - "", - "" + "chapter": + { + "id": 0, //number for your JSON file to be referenced in game files + "name": "", //name of the chapter + "flavor": "", //the short description that appears on popup when you hover over a chapter image + "battle_cry": "", //this is where you cry. I mean this is where you say what are you crying about. I mean... fuck it just type shit here to scream at people + "origin": "", //[1] Founding [2] Successor [3] Other [4] Custom + "points": 0, //creation points, feel free to cheat here for more wild stuff in custom if you don't like doing it via files + "founding": 0, //ID of the progenitor chapter, as per fiels in datafiles/main/chapters + "successors": 0, //write how many successors your chapter has. Go bonkers. Show them you got the seed to have 666 successors + "splash": 0, //the more artsy image on the left side of selection screen. Highly advised to use the same number as chapter ID + "icon_name": "", //name for the chapter icon you'll be using. Standing + "fleet_type": 0, //[1] Homeworld [2] Fleetbased [3] Penitent + "strength": 0, //1-10 + "purity": 0, //1-10 + "stability": 0, //1-99 + "cooperation": 0, //1-10 + "homeworld_exists:": 1, //only is there for an obscure bit of code, but to avoid bugs and crashes keep it as is until we fix it + "recruiting_exists": 1, //same here + "home_warp": 1, //[0] low/no connections [1] connected [2] warp hub + "home_planets": 1, //how many planets in home system, [0]=1 [3]=4 + "homeworld": "Forge", //Homeworld planet type "Lava" "Desert" "Forge" "Hive" "Death" "Agri" "Feudal" "Temperate" "Ice" "Dead" "Shrine" + "homeworld_rule": 1, //[1] Governor [2] Semi-independent [3] Direct Rule + "home_spawn_loc": 1, //unsure which is it, probably 0=fringe, 1=central + "homeworld_name": "", //name of the homeworld + "monastery_name": "", //name of the F-M structure on the homeplanet. + "recruiting": "", //Recruiting world planet type "Lava" "Desert" "Forge" "Hive" "Death" "Agri" "Feudal" "Temperate" "Ice" "Dead" "Shrine" + "recruiting_name": "", //recruiting world name + "recruit_home_relationship": 1, //[0] same planet [1] same system [2] different system + "discipline": "librarius", //one of 'default' 'biomancy' 'pyromancy' 'telekinesis' 'rune_magic' + "aspirant_trial": 5, //[0]"BLOODDUEL" [1]"HUNTING" [2]"SURVIVAL" [3]"EXPOSURE" [4]"KNOWLEDGE" [5]"CHALLENGE" [6]"APPRENTICESHIP" + "advantages": //Self-Explanatory + [ + "", //first blank cause reasons + "", + "", + "", + "", + "", + "", + "", + "" + ], + "disadvantages": //S-E + [ + "", //first blank cause reasons + "", + "", + "", + "", + "", + "", + "", + "" + ], + "mutations": //defines which mutations are present + { + "mucranoid":0, + "zygote":0, + "secretions":0, + "doomed":0, + "occulobe":0, + "membrane":0, + "lyman":0, + "betchers":0, + "ossmodula":0, + "omophagea":0, + "voice":0, + "catalepsean":0, + "preomnor":0 + }, + "disposition": + [ + 0, //nothing, probably self + 0, //Progenitor + 0, //Imperium + 0, //Admech + 0, //=][= + 0, //Ecclesiarchy + 0, //Astartes + 0 //Nothing + ], + "colors": //main or default colours for your chapter. Check a pasted table or scr_colors_initialize.gml for the color array + { + "special": 0, // 0 - normal, 1 - Breastplate, 2 - Vertical, 3 - Quadrant + "main": "Dark Red", + "weapon": "Dark Red", + "pauldron_l": "Dark Red", + "pauldron_r": "Dark Red", + "trim": "Dark Gold", + "secondary": "Dark Gold", + "lens": "Red" + }, + "culture_styles": //write in the styles you want + [ + "" + ], + "chapter_master": + { + "name": "", //name for your chapter master, leave blank for random + "traits": //currently kinda not working, he's guaranteed to have Lead by Example though + [ + "" ], - "disadvantages": [ - "", // leave the first entry blank for now - "", - "", - "", - "", - "", - "", - "", - "" - ], - "colors": { - "main": "Black", - "secondary": "Black", - "pauldron_r": "Black", - "pauldron_l": "Black", - "trim": "Silver", - "lens": "Red", - "weapon": "Dark Red", - "special": 0, // 0 - normal, 1 - Breastplate, 2 - Vertical, 3 - Quadrant - //"trim_on": 0 // 0 no, 1 yes for pauldron trim colours. Trim colour will still be used for certain complex livery items - }, - "names": { - //Chapter Staff - "hchaplain": "", // Head Chaplain - "clibrarian": "", // Chief Librarian - "fmaster": "", // Forge Master - "hapothecary": "", // Head Apothecary - //Company Captains 1 - 10 - "honorcapt": "", - "watchmaster": "", - "arsenalmaster": "", - "admiral": "", - "marchmaster": "", - "ritesmaster": "", - "victualler": "", - "lordexec": "", - "relmaster": "", - "recruiter": "" - }, - "battle_cry": "For the Emperor!", - "squad_distribution": 0, // 0 if no, 1 if yes. If yes, will distribute specialist roles like Assaults and Devastators equally between companies. Otherwise all Assaults go in Company 8 and all Devastators in Company 9 - "load_to_ships": { - "escort_load": 2, // 0 no, 2 yes, 1 doesnt do anything :) - "split_scouts": 0, // 0 no, 1 yes. If yes, splits scouts between ships equally. Otherwise all scouts are kept on the homeworld. - "split_vets": 0 // 0 no, 1 yes. If yes, all veterans are distrubuted equally between ships. Otherwise all veterans are kept in the flagship - }, - "successors": 0, //total number of successor chapters - "mutations": { - "preomnor": 0, - "voice": 0, - "doomed": 0, - "lyman": 0, - "omophagea": 0, - "ossmodula": 0, - "membrane": 0, - "zygote": 0, - "betchers": 0, - "catalepsean": 0, - "secretions": 0, - "occulobe": 0, - "mucranoid": 0 - }, - "disposition": [ - 0, // nothing - 0, // Progenitor faction - 50, // Imperium - 50, // Admech - 50, //Inquisition - 50, // Ecclesiarchy - 50, // Astartes - 0 // nothing - ], - "chapter_master": { - "name": "", - "specialty": 1, //1 Leader, 2 Champion, 3 Psyker, - "melee": 1, // 1 twin power fists ... 8 force staff - "ranged": 1, // 1 boltstorm gauntlets ... 7 storm shield - // All chapter masters have the trait `Lead by Example` by default - "traits": [ - "" - ], - "gear": "", - "mobi": "", - "armour": "" // default is Artificer armour, only needed to set if changing to something else. - }, - "artifact": [ + "specialty": 1, //1 Leader, 2 Champion, 3 Psyker + "melee": 1, //[1]twin power fists [2]twin lighting claws [3]Relic blade [4]Thunder Hammer [5]Power Sword [6]Power axe [7]Eviscerator [8]Force staff + "ranged": 1, //[1]boltstorm gauntlet [2]Infernus pistol [3]Plasma pistol [4]Plasma gun [5]MC Heavy Bolter [6]Mc Meltagun [7]Storm Shield + "armour": "", //default is Artificer, dont' need to put anything unless you want something different + "gear": "" //default is Iron Halo, same as above + }, + "artifact": //copy and paste the template below as many times as you like + [ + { + "name": "Artifact Name", //the most holy name of your long schlong weapon + "description": "Artefact Lore", //write fun stuff + "base_weapon_type": "Power Sword", //doesn't matter if it's armour, gear, mobi or anything else, you always use base_weapon_type to define it + "slot": "wep1" //leave empty if you want it to be stored in the Librarium without assignment + } + ], + "names": + { + //Chapter Staff + "hchaplain": "", // Head Chaplain + "clibrarian": "", // Chief Librarian + "fmaster": "", // Forge Master + "hapothecary": "", // Head Apothecary + //Company Captains 1 - 10 + "honorcapt": "", + "watchmaster": "", + "arsenalmaster": "", + "admiral": "", + "marchmaster": "", + "ritesmaster": "", + "victualler": "", + "lordexec": "", + "relmaster": "", + "recruiter": "" + }, + "company_titles": + [ + "", //leave blank cause reasons + "", //1st Company + "", //2nd + "", //3rd + "", //4th + "", //5th + "", //6th + "", //7th + "", //8th + "", //9th + "" //10th + ], + "equal_specialists": 0,// 0 if no, 1 if yes. If yes, will distribute specialist roles like Assaults and Devastators equally between companies. Otherwise all Assaults go in Company 8 and all Devastators in Company 9 + "flagship_name": "Flagship Name", // leave blank to autogenerate + "load_to_ships": + { + "escort_load": 2, // 0 no, 2 yes, 1 doesnt do anything :) + "split_scouts": 0, // 0 no, 1 yes. If yes, splits scouts between ships equally. Otherwise all scouts are kept on the homeworld. + "split_vets": 0 // 0 no, 1 yes. If yes, all veterans are distrubuted equally between ships. Otherwise all veterans are kept in the flagship + }, + "full_liveries": + [ + {//[1] Chapter Master + "weapon_primary":11, + "metallic_trim":11, + "weapon_secondary":20, + "is_changed":true, + "left_arm":20, + "left_backpack":20, + "left_chest":20, + "right_arm":20, + "right_backpack":20, + "right_chest":20, + "left_hand":11, + "left_head":20, + "left_leg_knee":20, + "right_hand":11, + "eye_lense":9, + "right_head":20, + "left_leg_lower":20, + "left_leg_upper":20, + "left_muzzle":20, + "right_leg_knee":20, + "left_pauldron":11, + "right_leg_lower":20, + "right_leg_upper":20, + "right_muzzle":20, + "right_pauldron":11, + "left_thorax":20, + "left_trim":20, + "right_thorax":20, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[2] Brown dude, so far unknown what is this defining + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":14, + "left_backpack":14, + "left_chest":14, + "right_arm":14, + "right_backpack":14, + "right_chest":14, + "left_hand":14, + "left_head":14, + "left_leg_knee":14, + "right_hand":14, + "eye_lense":22, + "right_head":14, + "left_leg_lower":14, + "left_leg_upper":14, + "left_muzzle":14, + "right_leg_knee":14, + "left_pauldron":14, + "right_leg_lower":14, + "right_leg_upper":14, + "right_muzzle":14, + "right_pauldron":14, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[3] Honour Guard + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":20, + "left_backpack":20, + "left_chest":20, + "right_arm":20, + "right_backpack":20, + "right_chest":20, + "left_hand":20, + "left_head":20, + "left_leg_knee":20, + "right_hand":20, + "eye_lense":9, + "right_head":20, + "left_leg_lower":20, + "left_leg_upper":20, + "left_muzzle":20, + "right_leg_knee":20, + "left_pauldron":20, + "right_leg_lower":20, + "right_leg_upper":20, + "right_muzzle":20, + "right_pauldron":20, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[4] Veteran + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":20, + "left_backpack":11, + "left_chest":8, + "right_arm":20, + "right_backpack":11, + "right_chest":8, + "left_hand":11, + "left_head":20, + "left_leg_knee":20, + "right_hand":11, + "eye_lense":9, + "right_head":20, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":20, + "right_leg_knee":20, + "left_pauldron":11, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":20, + "right_pauldron":11, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[5] Terminator + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":11, + "left_backpack":11, + "left_chest":11, + "right_arm":11, + "right_backpack":11, + "right_chest":11, + "left_hand":20, + "left_head":8, + "left_leg_knee":11, + "right_hand":20, + "eye_lense":9, + "right_head":8, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":20, + "right_leg_knee":20, + "left_pauldron":20, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":20, + "right_pauldron":20, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[6] Captain + "weapon_primary":11, + "metallic_trim":11, + "weapon_secondary":20, + "is_changed":true, + "left_arm":20, + "left_backpack":20, + "left_chest":20, + "right_arm":20, + "right_backpack":20, + "right_chest":20, + "left_hand":20, + "left_head":20, + "left_leg_knee":11, + "right_hand":20, + "eye_lense":9, + "right_head":20, + "left_leg_lower":20, + "left_leg_upper":20, + "left_muzzle":20, + "right_leg_knee":11, + "left_pauldron":20, + "right_leg_lower":20, + "right_leg_upper":20, + "right_muzzle":20, + "right_pauldron":20, + "left_thorax":0, + "left_trim":11, + "right_thorax":0, + "right_trim":11, + "company_marks":0, + "company_marks_loc":0 + }, + {//[7] Dreadnought + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":20, + "left_backpack":20, + "left_chest":20, + "right_arm":20, + "right_backpack":20, + "right_chest":20, + "left_hand":20, + "left_head":20, + "left_leg_knee":20, + "right_hand":20, + "eye_lense":9, + "right_head":20, + "left_leg_lower":20, + "left_leg_upper":20, + "left_muzzle":20, + "right_leg_knee":20, + "left_pauldron":20, + "right_leg_lower":20, + "right_leg_upper":20, + "right_muzzle":20, + "right_pauldron":20, + "left_thorax":20, + "left_trim":20, + "right_thorax":20, + "right_trim":20, + "company_marks":20, + "company_marks_loc":20 + }, + {//[8] Champion + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":20, + "left_backpack":20, + "left_chest":11, + "right_arm":20, + "right_backpack":20, + "right_chest":11, + "left_hand":20, + "left_head":11, + "left_leg_knee":20, + "right_hand":20, + "eye_lense":22, + "right_head":11, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":11, + "right_leg_knee":20, + "left_pauldron":20, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":11, + "right_pauldron":20, + "left_thorax":0, + "left_trim":11, + "right_thorax":0, + "right_trim":11, + "company_marks":0, + "company_marks_loc":0 + }, + {//[9] Tactical + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":11, + "left_backpack":11, + "left_chest":11, + "right_arm":11, + "right_backpack":11, + "right_chest":11, + "left_hand":11, + "left_head":11, + "left_leg_knee":11, + "right_hand":11, + "eye_lense":22, + "right_head":11, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":11, + "right_leg_knee":11, + "left_pauldron":11, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":11, + "right_pauldron":11, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[10] Devastator + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":11, + "left_backpack":11, + "left_chest":11, + "right_arm":11, + "right_backpack":11, + "right_chest":11, + "left_hand":11, + "left_head":20, + "left_leg_knee":11, + "right_hand":11, + "eye_lense":22, + "right_head":20, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":11, + "right_leg_knee":11, + "left_pauldron":11, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":11, + "right_pauldron":11, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[11] Assault + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":11, + "left_backpack":11, + "left_chest":11, + "right_arm":11, + "right_backpack":11, + "right_chest":11, + "left_hand":11, + "left_head":11, + "left_leg_knee":11, + "right_hand":11, + "eye_lense":22, + "right_head":11, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":20, + "right_leg_knee":11, + "left_pauldron":11, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":20, + "right_pauldron":11, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[12] Ancient + "weapon_primary":20, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":11, + "left_backpack":20, + "left_chest":8, + "right_arm":11, + "right_backpack":20, + "right_chest":8, + "left_hand":20, + "left_head":11, + "left_leg_knee":20, + "right_hand":20, + "eye_lense":9, + "right_head":11, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":8, + "right_leg_knee":20, + "left_pauldron":11, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":8, + "right_pauldron":11, + "left_thorax":0, + "left_trim":8, + "right_thorax":0, + "right_trim":8, + "company_marks":0, + "company_marks_loc":0 + }, + {//[13] Scout + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":11, + "left_backpack":11, + "left_chest":11, + "right_arm":11, + "right_backpack":11, + "right_chest":11, + "left_hand":11, + "left_head":11, + "left_leg_knee":11, + "right_hand":11, + "eye_lense":22, + "right_head":11, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":11, + "right_leg_knee":11, + "left_pauldron":11, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":11, + "right_pauldron":11, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[14] Green dude + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":23, + "left_backpack":23, + "left_chest":23, + "right_arm":23, + "right_backpack":23, + "right_chest":23, + "left_hand":23, + "left_head":23, + "left_leg_knee":23, + "right_hand":23, + "eye_lense":22, + "right_head":23, + "left_leg_lower":23, + "left_leg_upper":23, + "left_muzzle":23, + "right_leg_knee":23, + "left_pauldron":23, + "right_leg_lower":23, + "right_leg_upper":23, + "right_muzzle":23, + "right_pauldron":23, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[15] Chaplain + "weapon_primary":8, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":8, + "left_backpack":8, + "left_chest":8, + "right_arm":8, + "right_backpack":8, + "right_chest":8, + "left_hand":8, + "left_head":8, + "left_leg_knee":8, + "right_hand":8, + "eye_lense":9, + "right_head":8, + "left_leg_lower":8, + "left_leg_upper":8, + "left_muzzle":8, + "right_leg_knee":8, + "left_pauldron":11, + "right_leg_lower":8, + "right_leg_upper":8, + "right_muzzle":8, + "right_pauldron":8, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[16] Apothecary + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":18, + "left_backpack":18, + "left_chest":18, + "right_arm":18, + "right_backpack":18, + "right_chest":18, + "left_hand":18, + "left_head":18, + "left_leg_knee":18, + "right_hand":18, + "eye_lense":9, + "right_head":18, + "left_leg_lower":18, + "left_leg_upper":18, + "left_muzzle":18, + "right_leg_knee":18, + "left_pauldron":11, + "right_leg_lower":18, + "right_leg_upper":18, + "right_muzzle":18, + "right_pauldron":18, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[17] Techmarine + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":9, + "left_backpack":9, + "left_chest":9, + "right_arm":9, + "right_backpack":9, + "right_chest":9, + "left_hand":9, + "left_head":9, + "left_leg_knee":9, + "right_hand":9, + "eye_lense":22, + "right_head":9, + "left_leg_lower":9, + "left_leg_upper":9, + "left_muzzle":9, + "right_leg_knee":9, + "left_pauldron":11, + "right_leg_lower":9, + "right_leg_upper":9, + "right_muzzle":9, + "right_pauldron":9, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[18] Librarian + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":34, + "left_backpack":34, + "left_chest":34, + "right_arm":34, + "right_backpack":34, + "right_chest":34, + "left_hand":34, + "left_head":34, + "left_leg_knee":34, + "right_hand":34, + "eye_lense":22, + "right_head":34, + "left_leg_lower":34, + "left_leg_upper":34, + "left_muzzle":34, + "right_leg_knee":34, + "left_pauldron":1, + "right_leg_lower":34, + "right_leg_upper":34, + "right_muzzle":34, + "right_pauldron":34, + "left_thorax":0, + "left_trim":0, + "right_thorax":0, + "right_trim":0, + "company_marks":0, + "company_marks_loc":0 + }, + {//[19] Sergeant + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":11, + "left_backpack":11, + "left_chest":11, + "right_arm":11, + "right_backpack":11, + "right_chest":11, + "left_hand":11, + "left_head":11, + "left_leg_knee":11, + "right_hand":11, + "eye_lense":22, + "right_head":11, + "left_leg_lower":11, + "left_leg_upper":11, + "left_muzzle":11, + "right_leg_knee":20, + "left_pauldron":20, + "right_leg_lower":11, + "right_leg_upper":11, + "right_muzzle":11, + "right_pauldron":20, + "left_thorax":0, + "left_trim":11, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[20] Veteran Sergeant + "weapon_primary":8, + "metallic_trim":20, + "weapon_secondary":11, + "is_changed":true, + "left_arm":20, + "left_backpack":8, + "left_chest":8, + "right_arm":20, + "right_backpack":8, + "right_chest":8, + "left_hand":20, + "left_head":11, + "left_leg_knee":20, + "right_hand":20, + "eye_lense":22, + "right_head":11, + "left_leg_lower":8, + "left_leg_upper":8, + "left_muzzle":11, + "right_leg_knee":20, + "left_pauldron":11, + "right_leg_lower":8, + "right_leg_upper":8, + "right_muzzle":11, + "right_pauldron":20, + "left_thorax":0, + "left_trim":11, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + }, + {//[21] Blue Chest + "weapon_primary":11, + "metallic_trim":20, + "weapon_secondary":20, + "is_changed":true, + "left_arm":31, + "left_backpack":31, + "left_chest":31, + "right_arm":31, + "right_backpack":31, + "right_chest":31, + "left_hand":31, + "left_head":31, + "left_leg_knee":31, + "right_hand":31, + "eye_lense":22, + "right_head":31, + "left_leg_lower":31, + "left_leg_upper":31, + "left_muzzle":31, + "right_leg_knee":31, + "left_pauldron":31, + "right_leg_lower":31, + "right_leg_upper":31, + "right_muzzle":31, + "right_pauldron":31, + "left_thorax":0, + "left_trim":20, + "right_thorax":0, + "right_trim":20, + "company_marks":0, + "company_marks_loc":0 + } + ], + "complex_livery_data": //helm_pattern - paint setup, [0] Full colour primary [1] Stripe in the middle secondary [2] Upper main lower second [3] same as 1 for some reason + { + "sgt": + { + "helm_detail":20, + "helm_lens":9, + "helm_pattern":0, + "helm_primary":20, + "helm_secondary":0 + }, + "veteran": + { + "helm_detail":20, + "helm_lens":22, + "helm_pattern":1, + "helm_primary":11, + "helm_secondary":8 + }, + "captain": + { + "helm_detail":20, + "helm_lens":9, + "helm_pattern":1, + "helm_primary":20, + "helm_secondary":11 + }, + "vet_sgt": + { + "helm_detail":20, + "helm_lens":9, + "helm_pattern":1, + "helm_primary":8, + "helm_secondary":11 + } + }, + "company_liveries"://if you put -1 as value that means there is no change from the base colour scheme. Beware that compnay markings overwrite specialists as well + [ + {// HQ + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 1 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 2 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 3 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 4 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 5 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 6 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 7 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 8 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 9 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + }, + {// 10 + "weapon_primary":-1, + "metallic_trim":-1, + "weapon_secondary":-1, + "is_changed":true, + "left_arm":-1, + "left_backpack":-1, + "left_chest":-1, + "right_arm":-1, + "right_backpack":-1, + "right_chest":-1, + "left_hand":-1, + "left_head":-1, + "left_leg_knee":-1, + "right_hand":-1, + "eye_lense":-1, + "right_head":-1, + "left_leg_lower":-1, + "left_leg_upper":-1, + "left_muzzle":-1, + "right_leg_knee":-1, + "left_pauldron":-1, + "right_leg_lower":-1, + "right_leg_upper":-1, + "right_muzzle":-1, + "right_pauldron":-1, + "left_thorax":-1, + "left_trim":-1, + "right_thorax":-1, + "right_trim":-1, + "company_marks":-1, + "company_marks_loc":-1 + } + ], + "custom_roles": + { + "chapter_master"://feel free to add specific equipment, name and whatnot + { + "name": "" + } + /*"template": + { + "wep1":"", + "wep2":"", + "gear":"", + "armour":"", + "name":"template", + "mobi":"" + },*/ + }, + "extra_equipment": + [ + // ["Bolter", 10] is an example of how this is called + ], + "extra_marines": //default is 100 marines per company, the first company works via adding veterans or terminators + {//beware, only adds Tacticals + "second": 0, + "third": 0, + "fourth": 0, + "fifth": 0, + "sixth": 0, + "seventh": 0, + "eighth": 0, + "ninth": 0, + "tenth": 0 + }, + "extra_ships": + /** + * * Default fleet composition + * * Homeworld + * - 2 Battle Barges, 8 Strike cruisers, 7 Gladius, 3 Hunters + * * Fleet based and Penitent + * - 4 Battle Barges, 3 Strike Cruisers, 7 Gladius, 3 Hunters + * + * use negative numbers to subtract ships + * * Stacks with advantages/disadvantages + */ + { + "gloriana": 0, + "battle_barges": 0, + "strike_cruisers": 0, + "gladius": 0, + "hunters": 0 + }, + /** + * * Default HQ Layout (Does not include company specialists) + * - 8 Chaplains, 8 Techmarines, 8 Apothecary, 2 Epistolary (librarian), + * - 2 Codiciery, 4 Lexicanum + * * Default Company specialists (divided based on `load_to_ships.split_vets` setting) + * - 20 Terminators, 85 Veterans, 20 Devastators, 20 Assault + * Use negative numbers to subtract + * Stacks with advantages/disadvantages + */ + "extra_specialists": + { + "chaplains": 0,//adds them to the Reclusiam + "chaplains_per_company": 0,//adds them to company, X per company + "techmarines": 0, + "techmarines_per_company": 0, + "apothecary": 0, + "apothecary_per_company": 0, + "epistolary": 0, + "epistolary_per_company": 0, + "codiciery": 0, + "lexicanum": 0, + "terminator": 0, + "assault": 0, + "veteran": 0, + "devastator": 0, + "dreadnought": 0 + }, + "extra_vehicles": + { + "rhino": 0, + "predator": 0, + "land_speeder": 0, + "whirlwind": 0, + "land_raider": 0 + }, + "companies":// you may use this to override default company manpower and vehicles on start. + { + "first": "", + "second":"", + "third":"", + "fourth":"", + "fifth":"", + "sixth":"", + "seventh":"", + "eighth":"", + "ninth":"", + "tenth":"" + }, + "custom_advisors"://here you define what image [by typing the png name] are your custom advisors using. If you add nothing here, the game uses defaults + {/* + "forge_master": , + "apothecary": , + "librarian": , + "chaplain": , + "admiral": , + "recruiter": + */}, + "squad_name": "Squad", //how will any squad be referenced + "custom_squads": + {/*Example how this works, showcasing the Deathwing Terminator Squads + "deathwing_squad": + [ + // Terminator Sergeant + [ + "Veteran Sergeant", + { + "max": 1, + "min": 1, + "role": "Deathwing Sergeant", + "loadout": { - "name": "Artifact Name", - "description": "Artefact Lore", - "base_weapon_type": "Power Sword", - "slot": "wep1" + "required": + { + "wep1": + [ + "Power Sword", + 1 + ] + } } + } ], - "company_titles": [ - "", - "1st Company", - "2nd Company", - "3rd Company", - "4th Company", - "5th Company", - "6th Company", - "7th Company", - "8th Company", - "9th Company", - "10th Company" - ], - "flagship_name": "Flagship Name", // leave blank to autogenerate - /** - * * Default fleet composition - * * Homeworld - * - 2 Battle Barges, 8 Strike cruisers, 7 Gladius, 3 Hunters - * * Fleet based and Penitent - * - 4 Battle Barges, 3 Strike Cruisers, 7 Gladius, 3 Hunters - * - * use negative numbers to subtract ships - * * Stacks with advantages/disadvantages - */ - "extra_ships": { - "battle_barges": 0, - "gladius": 0, - "strike_cruisers": 0, - "hunters": 0 - }, - /** - * * Default HQ Layout (Does not include company specialists) - * - 8 Chaplains, 8 Techmarines, 8 Apothecary, 2 Epistolary (librarian), - * - 2 Codiciery, 4 Lexicanum - * * Default Company specialists (divided based on `load_to_ships.split_vets` setting) - * - 20 Terminators, 85 Veterans, 20 Devastators, 20 Assault - * Use negative numbers to subtract - * * Stacks with advantages/disadvantages - */ - "extra_specialists": { - "chaplains": 0, - "chaplains_per_company": 0, - "techmarines": 0, - "techmarines_per_company": 0, - "apothecary": 0, - "apothecary_per_company": 0, - "epistolary": 0, - "epistolary_per_company": 0, - "codiciery": 0, - "lexicanum": 0, - "terminator": 0, - "assault": 0, - "veteran": 0, - "devastator": 0, - "dreadnought": 0 - }, - /** - * * Default Marine strength - * - 100 marines per company - * use negative numbers to subtract - * * Stacks with strength for non-founding chapters only - */ - "extra_marines": { - "second": 0, - "third": 0, - "fourth": 0, - "fifth": 0, - "sixth": 0, - "seventh": 0, - "eighth": 0, - "ninth": 0, - "tenth": 0 - }, - /** - * All vehicles are added to the 10th company at the start of the game - */ - "extra_vehicles": { - "rhino": 0, - "whirlwind": 0, - "predator": 0, - "land_raider": 0, - "land_speeder": 0 - }, - /** Add extra starting items ["Item Name", Number to add] */ - "extra_equipment": [ - // [ - // "Bolter", - // 20 - // ] + // Terminator + [ + "Terminator", + { + "max": 4, + "min": 2, + "role": "Deathwing Terminator", + "loadout": + { + "required": + { + "wep1": + [ + "", + 0 + ], + "wep2": + [ + "Storm Bolter", + 3 + ] + }, + "option": + { + "wep1": + [ + [ + [ + "Power Fist", + "Chainfist" + ], + 4 + ] + ], + "wep2": + [ + [ + [ + "Heavy Flamer", + "Heavy Flamer", + "Heavy Flamer", + "Assault Cannon", + "Assault Cannon", + "Plasma Cannon" + ], + 1 + ] + ] + } + } + } ], - /** - * Provide a place to change the default name and equipment preferences of roles for this chapter - * `custom_roles` should be used for specialist/leadership type roles, - * To affect an entire squad, see `custom_squads` below - */ - "custom_roles": { - // "honour_guard": { - // "name": "Honour Guard", - // "wep1": "Power Sword", - // "wep2": "Bolter" - // }, - // "veteran": { - // "name": "Veteran", - // "wep1": "Chainaxe" - // }, - // "captain": { - // "name": "Captain", - // "wep1": "Power Sword" - // }, - // "tactical": { - // "name": "Tactical" - // }, - // "devastator": { - // "name": "Devastator" - // }, - // "assault": { - // "name": "Assault", - // "wep1": "Chainsword" - // }, - // "scout": { - // "name": "Scout", - // "wep1": "Sniper Rifle" - // }, - // "chaplain": { - // "name": "Chaplain", - // }, - // "techmarine": { - // "name": "Techmarine", - // "wep1": "Power Axe" - // }, - // "apothecary": { - // "name": "Apothecary", - // "wep1": "Power Axe" - // }, - // "librarian": { - // "name": "Librarian", - // "wep1": "Force Staff" - // }, - // "sergeant": { - // "name": "Sergeant", - // "wep1": "Chainaxe" - // }, - // "veteran_sergeant": { - // "name": "Veteran Sergeant", - // "wep1": "Chainaxe" - // } - }, - /** - * * Custom squad roles, loadouts and formations - * When companies are made, squads are formed based on these rules: - * - squad name: one of captain, terminator, terminator_assault, sternguard_veteran, - vanguard_veteran, devastator, tactical, assault, scout, scout_sniper - * - squad array layout [Role, Role, ...Role, type_data] - * - each element of the array is a default Role, and their settings. - - if you changed the role using `custom_roles` you need to reference the role with this new name - - for non-leader roles it is better to change the name of the role in the squad layout instead - * - unit layout [Role Name, Settings Struct] - * - settings struct: - * - max: The most amount of this unit is allowed per squad - * - min: The squad can't be formed unless at least 1 of this unit is in it - * - role: The name of the unit when it is a member of the squad. This is where you rename roles e.g. - "Terminator" > "Deathwing Terminator" - ** - loadout: Struct containing Required and Optional weaponry this unit can equip - a required loadout always follows this syntax :[,] - so "wep1":["Bolter",4], will mean four marines are always equipped with 4 bolters in the wep1 slot - * option loadouts follow the following syntax :[[],] - for example [["Flamer", "Meltagun"],1], means the role can have a max of one flamer or meltagun - [["Plasma Pistol","Bolt Pistol"], 4] means the role can have a mix of 4 plasma pistols and bolt pistols on top - of all required loadout options - - wep1: right hand weapon - - wep2: left hand weapon - - mobi: Mobility item, e.g. Jump Packs. - - armour: required armour - - gear: special equipment needed for certain roles, like a Roasrius or Narthecium - * - type_data: names the squad, allows certain formations - */ - "squad_name": "Squad", - "custom_squads": {} + [ + "type_data", + { + "display_data": "Deathwing Terminator Squad", + "formation_options": + [ + "terminator", + "veteran", + "assault", + "devastator", + "scout", + "tactical" + ], + "allowed_companies": [1] //this is specifically for when squads are supposed to spawn only in specific companies. Ignore for chapter-wide changes + } + ] + ]*/ } +/* * Custom squad roles, loadouts and formations + * When companies are made, squads are formed based on these rules: + * - squad name: one of captain, terminator, terminator_assault, sternguard_veteran, + vanguard_veteran, devastator, tactical, assault, scout, scout_sniper + * - squad array layout [Role, Role, ...Role, type_data] + * - each element of the array is a default Role, and their settings. + - if you changed the role using `custom_roles` you need to reference the role with this new name + - for non-leader roles it is better to change the name of the role in the squad layout instead + * - unit layout [Role Name, Settings Struct] + * - settings struct: + * - max: The most amount of this unit is allowed per squad + * - min: The squad can't be formed unless at least 1 of this unit is in it + * - role: The name of the unit when it is a member of the squad. This is where you rename roles e.g. + "Terminator" > "Deathwing Terminator" + ** - loadout: Struct containing Required and Optional weaponry this unit can equip + a required loadout always follows this syntax :[,] + so "wep1":["Bolter",4], will mean four marines are always equipped with 4 bolters in the wep1 slot + * option loadouts follow the following syntax :[[],] + for example [["Flamer", "Meltagun"],1], means the role can have a max of one flamer or meltagun + [["Plasma Pistol","Bolt Pistol"], 4] means the role can have a mix of 4 plasma pistols and bolt pistols on top + of all required loadout options + - wep1: right hand weapon + - wep2: left hand weapon + - mobi: Mobility item, e.g. Jump Packs. + - armour: required armour + - gear: special equipment needed for certain roles, like a Roasrius or Narthecium + * - type_data: names the squad, allows certain formations ? idk what that does yet*/ + } } \ No newline at end of file From 3946d9a315480f37019e859a6e3d52b785e2b609 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:52:15 +0200 Subject: [PATCH 05/55] style: Contemptor Dread description lol --- datafiles/data/armour.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafiles/data/armour.json b/datafiles/data/armour.json index b591e3cbd4..f56706ca38 100644 --- a/datafiles/data/armour.json +++ b/datafiles/data/armour.json @@ -86,7 +86,7 @@ "master_crafted": 25, "standard": 20 }, - "description": "PLACEHOLDER", + "description": "Once the standard Dreadnought pattern when Primarchs walked the stars and Legions conquered worlds, now one of the closest-guarded secrets in Mechanicum tech enclaves. With an Atomantic arc-reactor at its heart that gave it superior speed, strength and even shielding capabilities, it was far superior to any other Dreadnought pattern save for its maintenance demands and the possible meltdown of the reactor.", "hp_mod": { "artifact": 50, "master_crafted": 35, From 6433cdd653a539d62c4ef99edeafa17d555794cd Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:55:51 +0200 Subject: [PATCH 06/55] feat: Attack Bike added and Bike amended --- datafiles/data/mobility.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index cb0e2b68c8..c5d12dc33b 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -9,7 +9,7 @@ "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action.", "hp_mod": { "artifact": 35, - "master_crafted": 25, + "master_crafted": 30, "standard": 25 }, "second_profiles": [ @@ -27,13 +27,12 @@ }, "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action. Sports an additional sidecar with a heavy weapon of choice [WIP only HB present].", "hp_mod": { - "artifact": 35, - "master_crafted": 25, - "standard": 25 + "artifact": 50, + "master_crafted": 40, + "standard": 35 }, "second_profiles": [ - "Twin Linked Bolters", - "Heavy Bolter" + "Twin Linked Bolters" ], "value": 35, "requires_to_forge": ["combi_1"] From 7c02137864820c57e675e2e66da102ed2385fac0 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 2 Jun 2026 17:48:08 +0200 Subject: [PATCH 07/55] feat: Bikes damage modifier --- datafiles/data/mobility.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index c5d12dc33b..73e6fe9954 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -12,6 +12,11 @@ "master_crafted": 30, "standard": 25 }, + "melee_mod":{ + "artifact": 30, + "master_crafted": 25, + "standard": 20 + }, "second_profiles": [ "Twin Linked Bolters" ], @@ -31,6 +36,11 @@ "master_crafted": 40, "standard": 35 }, + "melee_mod":{ + "artifact": 20, + "master_crafted": 15, + "standard": 10 + }, "second_profiles": [ "Twin Linked Bolters" ], From 7b64b1bd4831d814c871c337a449d921cb5aa9ab Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:46:27 +0200 Subject: [PATCH 08/55] fix: Attack Bike higher cost --- datafiles/data/mobility.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index 73e6fe9954..bed1718c5b 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -30,7 +30,7 @@ "master_crafted": 10, "standard": 5 }, - "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action. Sports an additional sidecar with a heavy weapon of choice [WIP only HB present].", + "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action. Sports an additional sidecar with a heavy weapon of choice.", "hp_mod": { "artifact": 50, "master_crafted": 40, @@ -44,7 +44,7 @@ "second_profiles": [ "Twin Linked Bolters" ], - "value": 35, + "value": 95, "requires_to_forge": ["combi_1"] }, "Conversion Beamer Pack": { From 2dc024d40b07d6f78acf9d7094a88bdfbfe4e118 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:07:31 +0200 Subject: [PATCH 09/55] feat: Attack Bike Squad and Bike Squad --- datafiles/main/squads/base_squads.json | 151 +++++++++++++++++-------- 1 file changed, 106 insertions(+), 45 deletions(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index ae98977e26..44f436d13f 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -7,27 +7,32 @@ "Champion": { "max": 1, "min": 0, - "role": "Company Champion" + "role": "Company Champion", + "alternative_roles": ["Champion"] }, "Apothecary": { "max": 1, "min": 0, - "role": "Company Apothecary" + "role": "Company Apothecary", + "alternative_roles": ["Apothecary"] }, "Chaplain": { "max": 1, "min": 0, - "role": "Company Chaplain" + "role": "Company Chaplain", + "alternative_roles": ["Chaplain"] }, "Ancient": { "max": 1, "min": 1, - "role": "Company Ancient" + "role": "Company Ancient", + "alternative_roles": ["Ancient"] }, "Veteran": { "max": 5, "min": 0, "role": "Company Veteran", + "alternative_roles": ["Veteran"], "loadout": { "required": { "wep1": ["", 0], @@ -43,12 +48,14 @@ "Techmarine": { "max": 2, "min": 0, - "role": "Company Techmarine" + "role": "Company Techmarine", + "alternative_roles": ["Techmarine"] }, "Librarian": { "max": 1, "min": 0, - "role": "Company Librarian" + "role": "Company Librarian", + "alternative_roles": ["Librarian"] }, "type_data": { "display_data": "Command {squad_name}", @@ -213,11 +220,11 @@ "Devastator Marine": { "max": 9, "min": 4, + "role": "Devastator", "loadout": { "required": { "wep1": ["Bolter", 5], - "wep2": ["Combat Knife", 9], - "mobi": ["", 5] + "wep2": ["Combat Knife", 9] }, "option": { "wep1": [ @@ -258,6 +265,7 @@ "Tactical Marine": { "max": 9, "min": 4, + "role": "Tactical", "loadout": { "required": { "wep1": ["wep1[8]", 7], @@ -307,6 +315,7 @@ "Assault Marine": { "max": 9, "min": 4, + "role": "Assault", "loadout": { "required": { "wep1": ["wep1[10]", 5], @@ -358,6 +367,7 @@ "Scout": { "max": 9, "min": 4, + "role": "Scout", "loadout": { "required": { "wep1": ["", 0], @@ -398,42 +408,7 @@ ] } }, - "bike_squad": { - "Biker": { - "max": 7, - "min": 2, - "role": "Biker", - "alternative_roles": ["Tactical", "Assault"], - "loadout": { - "required": { - "armour": ["",0], - "wep1": ["", 0], - "wep2": ["Bolt Pistol", 2], - "mobi": ["Bike", "max"] - }, - "option": { - "wep1": [[["","Chainsword","Chainaxe"],5,{"wep2":"Bolt Pistol"}]] - } - } - }, - "Sergeant": { - "max": 1, - "min": 1, - "role": "Biker Sergeant", - "loadout": { - "required": { - "wep1": ["", "max"], - "wep2": ["Chainsword", "max"], - "mobi": ["Bike", 1] - } - } - }, - "type_data": { - "display_data": "Bike {squad_name}", - "class": ["bike"], - "formation_options": ["assault", "tactical"] - } - }, + "breacher_squad": { "Tactical Marine": { "max": 9, @@ -476,5 +451,91 @@ "display_data": "Breacher {squad_name}", "formation_options": ["tactical", "assault", "devastator", "scout"] } - } + }, + + "bike_squad": { + "Tactical": { + "max": 7, + "min": 2, + "role": "Biker", + "alternative_roles": ["Assault","Devastator","Tactical"], + "loadout": { + "required": { + "armour": ["", 0], + "wep1": ["", 0], + "wep2": ["", 0], + "mobi": ["Bike", 7] + }, + "option": { + "wep1": [[["Chainsword","Chainaxe"],7,{"wep2":"Bolt Pistol"}]] + } + } + }, + "Sergeant": { + "max": 1, + "min": 1, + "role": "Biker Sergeant", + "loadout": { + "required": { + "wep1": ["", 0], + "wep2": ["", 0], + "mobi": ["Bike", 1] + }, + "option": { + "wep1": [ + [["Power Sword", "Power Spear", "Power Axe", "Power Fist", "Chainaxe", "Chainsword"],1], + [["Lightning Claw"],1,{"wep2":"Lightning Claw"}] + ], + "wep2": [[["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "RyzPlsmPis"],1]] + } + } + }, + "type_data": { + "display_data": "Bike {squad_name}", + "class": ["bike"], + "formation_options": ["assault", "tactical","devastator","scout"] + } + }, + + + "attack_bike_squad": { + "Tactical": { + "max": 2, + "min": 1, + "role": "Attack Biker", + "alternative_roles": ["Devastator", "Assault", "Tactical"], + "loadout": { + "required": { + "wep1": ["", 0], + "wep2": ["", 0], + "mobi": ["Attack Bike", "max"] + }, + "option": { + "wep1": [[["Multi-Melta", "Heavy Bolter"], 2]], + "wep2": [[["Chainsword", "Chainaxe", "Combat Knife"],2]] + } + } + }, + "Sergeant": { + "max": 1, + "min": 1, + "role": "Attack Bike Sergeant", + "loadout": { + "required": { + "wep1": ["", 0], + "wep2": ["", 0], + "mobi": ["Attack Bike", 1] + }, + "option": { + "wep1": [[["Multi-Melta", "Heavy Bolter", "Plasma Cannon", "Lascannon"],1]], + "wep2": [[["Chainsword", "Chainaxe", "Power Sword", "Power Spear", "Power Axe"],1]] + } + } + }, + "type_data": { + "display_data": "Attack Bike {squad_name}", + "class": ["bike"], + "formation_options": ["assault", "tactical", "devastator", "scout"] + } +} } \ No newline at end of file From 966c268b380d7b6baa4be04ac2c55c258181f195 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:08:00 +0200 Subject: [PATCH 10/55] feat: multi-role origin squads --- scripts/scr_UnitGroup/scr_UnitGroup.gml | 61 ++++++--- .../scr_company_order/scr_company_order.gml | 72 ++++++++++- .../scr_marine_struct/scr_marine_struct.gml | 22 ++-- scripts/scr_squads/scr_squads.gml | 121 ++++++++++++------ 4 files changed, 210 insertions(+), 66 deletions(-) diff --git a/scripts/scr_UnitGroup/scr_UnitGroup.gml b/scripts/scr_UnitGroup/scr_UnitGroup.gml index 7885b48bb8..492ab5a3e0 100644 --- a/scripts/scr_UnitGroup/scr_UnitGroup.gml +++ b/scripts/scr_UnitGroup/scr_UnitGroup.gml @@ -199,11 +199,11 @@ function UnitGroup(units) constructor { var _roles = active_roles(); - static sgt_types = role_groups(SPECIALISTS_SQUAD_LEADERS); + static sgt_types = []; static create_squad = function(squad_type, squad_loadout = true, squad_uid = "", game_start = false) { //LOGGER.info($"sgts : ${sgt_types}"); - + sgt_types = role_groups(SPECIALISTS_SQUAD_LEADERS); var roles = active_roles(); @@ -229,10 +229,14 @@ function UnitGroup(units) constructor { var _primary_role = squad_unit_types[r]; var _primary_role_def = _fill_squad[$ _primary_role]; var _primary_role_name = struct_exists(_primary_role_def, "role") ? _primary_role_def.role : _primary_role; - LOGGER.info($"Squad type: {squad_type}, Looking for roles: {_all_roles_to_fetch}"); if (!array_contains(_all_roles_to_fetch, _primary_role_name)) { array_push(_all_roles_to_fetch, _primary_role_name); } + // Always include the JSON key itself as a source — units carry their base role + // (e.g. "Terminator") before being renamed to the squad-specific role ("Deathwing Terminator") + if (!array_contains(_all_roles_to_fetch, _primary_role)) { + array_push(_all_roles_to_fetch, _primary_role); + } //add alternative source roles to fetch list if this squad role defines them if (struct_exists(_primary_role_def, "alternative_roles")) { @@ -244,10 +248,15 @@ function UnitGroup(units) constructor { } } } - + //ensure generic sergeant role names are always included in the pool + //JSON overrides with specific names "Tactical Sergeant" + for (var s = 0; s < 2; s++) { + if (!array_contains(_all_roles_to_fetch, sgt_types[s])) { + array_push(_all_roles_to_fetch, sgt_types[s]); + } + } var _squadless = get_from({squadless: true, roles: _all_roles_to_fetch}); - LOGGER.info($"Squadless count before: {_squadless.number()}"); for (var s = 0; s < 2; s++) { var _sgt_type = sgt_types[s]; @@ -264,13 +273,19 @@ function UnitGroup(units) constructor { var _role_name = squad_unit_types[r]; var _role_def = _fill_squad[$ _role_name]; var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; - if (_primary_role_name == _sgt_type) { + if (_primary_role_name == _sgt_type || (_role_name == "Sergeant" && _sgt_type == sgt_types[0]) || (_role_name == "Veteran Sergeant" && _sgt_type == sgt_types[1])) { _sgt_group = _role_name; break; } } if (_sgt_group != "") { squad_fulfilment[$ _sgt_group]++; + // Rename pre-existing sergeant to squad-specific role if needed + var _sgt_slot_def = _fill_squad[$ _sgt_group]; + var _target_sgt_role = struct_exists(_sgt_slot_def, "role") ? _sgt_slot_def.role : _sgt_type; + if (_target_sgt_role != _sgt.role()) { + _sgt.update_role(_target_sgt_role); + } } sergeant_found = true; } @@ -292,7 +307,7 @@ function UnitGroup(units) constructor { var _role_name = squad_unit_types[r]; var _role_def = _fill_squad[$ _role_name]; var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; - if (_sgt_type == _primary_role_name) { + if (_primary_role_name == _sgt_type || _role_name == "Sergeant" && _sgt_type == sgt_types[0] || _role_name == "Veteran Sergeant" && _sgt_type == sgt_types[1]) { _has_sgt_requirements = true; break; } @@ -318,12 +333,17 @@ function UnitGroup(units) constructor { var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; - // Check if marine matches this primary role + // Check if marine matches this primary role (squad-specific rename target) if (_target_role == _primary_role_name) { _role_group = _role_name; break; } - + // Check if marine matches the JSON key itself (base role before squad rename) + if (_target_role == _role_name) { + _role_group = _role_name; + break; + } + // Check if marine matches any alternative roles for this group if (struct_exists(_role_def, "alternative_roles")) { var _alts = _role_def.alternative_roles; @@ -349,6 +369,14 @@ function UnitGroup(units) constructor { //if sergeants not required squad_fulfilment[$ _role_group]++; squad.add_member(_unit.company, _unit.marine_number); + // Rename unit to the squad-specific role only for rank-and-file marines + // (Tactical/Assault/Devastator/Scout → Biker, Breacher, etc.) + // Specialists (Chaplain, Ancient, Champion, Veteran, etc.) keep their existing role + var _slot_def = _fill_squad[$ _role_group]; + if (struct_exists(_slot_def, "role") && _slot_def.role != _unit.role() + && _unit.IsSpecialist(SPECIALISTS_RANK_AND_FILE)) { + _unit.update_role(_slot_def.role); + } } } @@ -364,18 +392,21 @@ function UnitGroup(units) constructor { for (var s = 0; s < 2; s++) { var _sgt_type = sgt_types[s]; var _sgt_group = ""; + var _exp_unit = undefined; for (var r = 0; r < array_length(squad_unit_types); r++) { var _role_name = squad_unit_types[r]; var _role_def = _fill_squad[$ _role_name]; var _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name; - if (_primary_role_name == _sgt_type) { + if (_primary_role_name == _sgt_type || (_role_name == "Sergeant" && _sgt_type == sgt_types[0]) || (_role_name == "Veteran Sergeant" && _sgt_type == sgt_types[1])) { _sgt_group = _role_name; break; } - } if (_sgt_group != "" && struct_exists(squad_fulfilment, _sgt_group) && (!sergeant_found)) { - var _exp_unit = _members.highest_exp(); - - _exp_unit.update_role(_sgt_type); + } + if (_sgt_group != "" && struct_exists(squad_fulfilment, _sgt_group) && (!sergeant_found)) { + _exp_unit = _members.highest_exp(); + var _sgt_role_def = _fill_squad[$ _sgt_group]; + var _actual_sgt_role = struct_exists(_sgt_role_def, "role") ? _sgt_role_def.role : _sgt_type; + _exp_unit.update_role(_actual_sgt_role); squad_fulfilment[$ _sgt_group]++; if (game_start && irandom(1) == 0) { _exp_unit.add_trait("lead_example"); @@ -395,7 +426,7 @@ function UnitGroup(units) constructor { } if (_fulfilled) { for (var s = 0; s < 2; s++) { - if (struct_exists(squad_fulfilment, sgt_types[s]) && (sergeant_found == false)) { + if (struct_exists(squad_fulfilment, sgt_types[s]) && (sergeant_found == false) && (_exp_unit != undefined)) { _exp_unit.update_role(sgt_types[s]); //if squad is viable promote marine to sergeant if (game_start && irandom(1) == 0) { _exp_unit.add_trait("lead_example"); diff --git a/scripts/scr_company_order/scr_company_order.gml b/scripts/scr_company_order/scr_company_order.gml index b7996fc3cc..005a67e44b 100644 --- a/scripts/scr_company_order/scr_company_order.gml +++ b/scripts/scr_company_order/scr_company_order.gml @@ -178,6 +178,76 @@ function role_hierarchy() { "Death Company", _roles[eROLE.VETERANSERGEANT], _roles[eROLE.SERGEANT], + ]; + + // Dynamically collect squad-specific sergeant role variants (e.g. "Biker Sergeant", "Tactical Sergeant") + // so they sort immediately after the generic sergeant, keeping them at the top of their squads + var _sgt_base = _roles[eROLE.SERGEANT]; + var _vsgt_base = _roles[eROLE.VETERANSERGEANT]; + var _squad_type_names = struct_get_names(obj_ini.squad_types); + for (var _si = 0; _si < array_length(_squad_type_names); _si++) { + var _sq_data = obj_ini.squad_types[$ _squad_type_names[_si]]; + var _sq_keys = struct_get_names(_sq_data); + for (var _ki = 0; _ki < array_length(_sq_keys); _ki++) { + var _k = _sq_keys[_ki]; + if (_k == "type_data") continue; + var _role_def = _sq_data[$ _k]; + if (!struct_exists(_role_def, "role")) continue; + var _specific_role = _role_def.role; + if (!array_contains(hierarchy, _specific_role)) { + if (string_count(_vsgt_base, _specific_role) > 0) { + // Veteran-sergeant variant — insert just before _vsgt_base position + var _vpos = array_get_index(hierarchy, _vsgt_base); + array_insert(hierarchy, max(0, _vpos), _specific_role); + } else if (string_count(_sgt_base, _specific_role) > 0) { + // Regular sergeant variant — insert just after _sgt_base position + var _spos = array_get_index(hierarchy, _sgt_base); + array_insert(hierarchy, _spos + 1, _specific_role); + } + } + } + } + + // Also add non-sergeant squad-specific role variants so they appear after their base role + var _base_roles = [ + _roles[eROLE.TERMINATOR], _roles[eROLE.VETERAN], + _roles[eROLE.TACTICAL], _roles[eROLE.ASSAULT], + _roles[eROLE.DEVASTATOR], _roles[eROLE.SCOUT], + _roles[eROLE.ANCIENT], _roles[eROLE.CHAMPION], + _roles[eROLE.CHAPLAIN], _roles[eROLE.APOTHECARY], + _roles[eROLE.TECHMARINE], _roles[eROLE.LIBRARIAN] + ]; + for (var _si = 0; _si < array_length(_squad_type_names); _si++) { + var _sq_data = obj_ini.squad_types[$ _squad_type_names[_si]]; + var _sq_keys = struct_get_names(_sq_data); + for (var _ki = 0; _ki < array_length(_sq_keys); _ki++) { + var _k = _sq_keys[_ki]; + if (_k == "type_data") continue; + var _role_def = _sq_data[$ _k]; + if (!struct_exists(_role_def, "role")) continue; + var _specific_role = _role_def.role; + if (array_contains(hierarchy, _specific_role)) continue; + // Skip sergeant variants (already handled above) + if (string_count(_sgt_base, _specific_role) > 0) continue; + // Find the closest matching base role and insert after it + for (var _bi = 0; _bi < array_length(_base_roles); _bi++) { + if (struct_exists(_role_def, "alternative_roles") && + array_contains(_role_def.alternative_roles, _base_roles[_bi])) { + var _bpos = array_get_index(hierarchy, _base_roles[_bi]); + if (_bpos >= 0) { + array_insert(hierarchy, _bpos + 1, _specific_role); + break; + } + } + } + // If not inserted via alternative_roles, just append before rank-and-file + if (!array_contains(hierarchy, _specific_role)) { + array_push(hierarchy, _specific_role); + } + } + } + + array_push(hierarchy, _roles[eROLE.TERMINATOR], _roles[eROLE.VETERAN], _roles[eROLE.TACTICAL], @@ -192,7 +262,7 @@ function role_hierarchy() { "Sister of Battle", "Flash Git", "Ork Sniper" - ]; + ); return hierarchy; } diff --git a/scripts/scr_marine_struct/scr_marine_struct.gml b/scripts/scr_marine_struct/scr_marine_struct.gml index df88687d24..2e5908bd1c 100644 --- a/scripts/scr_marine_struct/scr_marine_struct.gml +++ b/scripts/scr_marine_struct/scr_marine_struct.gml @@ -440,16 +440,18 @@ function TTRPG_stats(faction, comp, mar, class = "marine", other_spawn_data = {} stat_boosts({strength: 4, constitution: 4, dexterity: 4}); //will decide on if these are needed } } - if (!is_specialist(role())) { - //logs changes too and from specialist status - if (is_specialist(new_role)) { - obj_controller.marines -= 1; - obj_controller.command += 1; - } - } else { - if (!is_specialist(new_role)) { - obj_controller.marines += 1; - obj_controller.command -= 1; + if (instance_exists(obj_controller)) { + if (!is_specialist(role())) { + //logs changes too and from specialist status + if (is_specialist(new_role)) { + obj_controller.marines -= 1; + obj_controller.command += 1; + } + } else { + if (!is_specialist(new_role)) { + obj_controller.marines += 1; + obj_controller.command -= 1; + } } } } diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index b7cb6f711e..c70458316b 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -45,10 +45,19 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co squad_type = target_squad.type; squad_unit_types = squad.find_squad_unit_types(); full_squad_data = obj_ini.squad_types[$ squad_type]; + //build a map from JSON key + role_key_to_actual = {}; + for (var _i = 0; _i < array_length(squad_unit_types); _i++) { + var _key = squad_unit_types[_i]; + var _def = full_squad_data[$ _key]; + var _actual = struct_exists(_def, "role") ? _def.role : _key; + role_key_to_actual[$ _key] = _actual; + } unit_role = ""; members_UnitGroup = squad.get_members(true); members_UnitGroup.shuffle(); optional_load = undefined; + optional_fill_counts = {}; // flat struct: "slot_groupIndex" -> filled count required_load = undefined; target_squad.update_fulfilment(); @@ -69,18 +78,24 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co "mobi" ]; - static structure_role_optional_loadout = function(optional_data) { - optional_load = variable_clone(optional_data); //create a fulfillment object for optional loadouts + static structure_role_optional_loadout = function(optional_data){ - var _optional_loadout_slots = struct_get_names(optional_load); + // Use the original data directly — no clone needed since optional_load is now read-only + // (all mutable state lives in optional_fill_counts). variable_clone can incorrectly + // flatten doubly-nested arrays in this GML version, corrupting the group structure. + optional_load = optional_data; + // Initialise fill-count tracking in a flat struct (struct field writes are always + // in-place in GML — no copy-on-write issues unlike nested array element writes) + optional_fill_counts = {}; + var _optional_loadout_slots = struct_get_names(optional_load); for (var slot = 0; slot < array_length(_optional_loadout_slots); slot++) { var _load_out_slot = _optional_loadout_slots[slot]; for (var i = 0; i < array_length(optional_load[$ _load_out_slot]); i++) { - array_insert(optional_load[$ _load_out_slot][i], 2, 0); + optional_fill_counts[$ _load_out_slot + "_" + string(i)] = 0; } } - }; + } static structure_role_required_loadout = function(required_data) { //find out if the _unit type for the squad has required equipment thresholds @@ -117,13 +132,13 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co var _optional_groups = optional_load[$ current_load_slot]; for (var i = 0; i < array_length(_optional_groups); i++) { - var _optional_load_data = _optional_groups[i]; - var _optionals_filled = _optional_load_data[2]; - var _optionals_max_allowed = _optional_load_data[1]; - var _optionals_equipment = _optional_load_data[0]; + var _count_key = current_load_slot + "_" + string(i); + var _optionals_filled = optional_fill_counts[$ _count_key]; // read from flat struct + var _optionals_max_allowed = _optional_groups[i][1]; + var _optionals_equipment = _optional_groups[i][0]; var _item_to_add; if (_optionals_filled < _optionals_max_allowed) { - var _is_equipment_set = array_length(_optional_load_data) > 3; + var _is_equipment_set = array_length(_optional_groups[i]) > 2; if (is_array(_optionals_equipment)) { //if the array items are varibale e.g a struct @@ -155,9 +170,10 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co var _opt_load_out = {}; _opt_load_out[$ current_load_slot] = _item_to_add; _unit.alter_equipment(_opt_load_out, from_armoury, to_armoury); - _optional_load_data[1]++; + // Struct field write — guaranteed in-place, no copy-on-write in GML + optional_fill_counts[$ _count_key]++; if (_is_equipment_set) { - var _equip_set_data = _optional_load_data[3]; + var _equip_set_data = _optional_groups[i][2]; if (is_struct(_equip_set_data)) { _unit.alter_equipment(_equip_set_data, from_armoury, to_armoury); array_push(ignore_units, _unit.uid); @@ -168,8 +184,9 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co } }; - static equip_loudouts_specific_equip_slot = function() { - var _members_with_role = members_UnitGroup.get_from({role: unit_role}); + static equip_loudouts_specific_equip_slot = function(){ + var _actual_role = role_key_to_actual[$ unit_role]; + var _members_with_role = members_UnitGroup.get_from({role: _actual_role}); if (!struct_exists(current_unit_squad_data, "loadout")) { return; } @@ -177,13 +194,11 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co var _loudouts = current_unit_squad_data[$ "loadout"]; while (_members_with_role.number() > 0) { _unit = _members_with_role.pop(); - if (array_contains(ignore_units, _unit.uid)) { - continue; - } - if (_unit.role() != unit_role) { + if (_unit.role() != _actual_role) { continue; } + // Required loadout is always applied — ignore_units only gates optional extras if (required_load != undefined && struct_exists(required_load, current_load_slot)) { var _needed_required = equip_required_for_role(_unit); if (_needed_required) { @@ -191,6 +206,11 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co } } + // Optional loadout respects ignore_units (units that already got a full equipment set) + if (array_contains(ignore_units, _unit.uid)) { + continue; + } + if (optional_load != undefined && struct_exists(optional_load, current_load_slot)) { equip_optional_for_role(_unit); } @@ -330,10 +350,10 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { }; // for creating a new sergeant from existing squad members - static new_sergeant = function(veteran = false) { + static new_sergeant = function(veteran = false, target_role = undefined) { var exp_unit = ""; var _unit; - var highest_exp = 0; + var highest_exp = -1; var member_length = array_length(members); for (var i = 0; i < member_length; i++) { _unit = fetch_unit(members[i]); @@ -343,7 +363,7 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { i--; continue; } - if (_unit.experience > highest_exp) { + if (exp_unit == "" || _unit.experience > highest_exp) { highest_exp = _unit.experience; exp_unit = _unit; } @@ -351,7 +371,9 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { if ((array_length(members) > 0) && is_struct(exp_unit)) { if (exp_unit.name() != "") { var new_role; - if (veteran == true) { + if (target_role != undefined) { + new_role = target_role; + } else if (veteran == true) { new_role = obj_ini.role[100][19]; } else { new_role = obj_ini.role[100][18]; @@ -384,6 +406,8 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { var squad_unit_types = struct_get_names(fill_squad); //find out what type of units squad consists of var unit_type_count = array_length(squad_unit_types); + // build actual_role → json_key map to handle slots with a "role" override + var _actual_to_key = {}; for (var i = unit_type_count - 1; i >= 0; i--) { var _wanted_unit_role = squad_unit_types[i]; if (_wanted_unit_role == "type_data") { @@ -391,6 +415,9 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { continue; } squad_fulfilment[$ _wanted_unit_role] = 0; //create a fulfilment structure to log members of squad + var _role_def = fill_squad[$ _wanted_unit_role]; + var _mapped = struct_exists(_role_def, "role") ? _role_def.role : _wanted_unit_role; + _actual_to_key[$ _mapped] = _wanted_unit_role; } var member_length = array_length(members); for (var i = member_length - 1; i >= 0; i--) { @@ -400,10 +427,13 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { array_delete(members, i, 1); continue; } - if (struct_exists(squad_fulfilment, _unit.role())) { - squad_fulfilment[$ _unit.role()]++; + // map actual role to json key so role-overridden slots are counted correctly + var _unit_role = _unit.role(); + var _slot_key = struct_exists(_actual_to_key, _unit_role) ? _actual_to_key[$ _unit_role] : _unit_role; + if (struct_exists(squad_fulfilment, _slot_key)) { + squad_fulfilment[$ _slot_key]++; } else { - squad_fulfilment[$ _unit.role()] = 1; + squad_fulfilment[$ _slot_key] = 1; } } fulfilled = true; @@ -418,8 +448,15 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { var _min_role_allowed = fill_squad[$ _wanted_unit_role][$ "min"]; if (fill_from != undefined) { - while (fill_from.has_role(_wanted_unit_role) && _squad_role_current < _max_role_count) { - var _new_member = fill_from.pop_role_member(_wanted_unit_role); + var _fill_role = struct_exists(fill_squad[$ _wanted_unit_role], "role") + ? fill_squad[$ _wanted_unit_role].role : _wanted_unit_role; + // Also try the JSON key itself as a source role (base role before squad rename) + var _fill_role_base = _wanted_unit_role; + while (_squad_role_current < _max_role_count) { + var _pick_role = fill_from.has_role(_fill_role) ? _fill_role + : (fill_from.has_role(_fill_role_base) ? _fill_role_base : ""); + if (_pick_role == "") break; + var _new_member = fill_from.pop_role_member(_pick_role); add_member(_new_member.company, _new_member.marine_number); squad_fulfilment[$ _wanted_unit_role]++; _squad_role_current = squad_fulfilment[$ _wanted_unit_role]; @@ -437,19 +474,23 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { required[$ _wanted_unit_role] = _min_role_allowed - _squad_role_current; } } - var _sarge = obj_ini.role[100][eROLE.SERGEANT]; - if (struct_exists(required, _sarge)) { - if (required[$ _sarge] > 0) { - new_sergeant(); - required[$ _sarge]--; - } - } - //find a new veteran sergeant - var _vet_sarge = obj_ini.role[100][eROLE.VETERANSERGEANT]; - if (struct_exists(required, _vet_sarge)) { - if (required[$ _vet_sarge] > 0) { - new_sergeant(true); - required[$ _vet_sarge]--; + var _default_sarge = obj_ini.role[100][eROLE.SERGEANT]; + var _default_vet_sarge = obj_ini.role[100][eROLE.VETERANSERGEANT]; + var _required_keys = struct_get_names(required); + for (var _ri = 0; _ri < array_length(_required_keys); _ri++) { + var _req_key = _required_keys[_ri]; + if (required[$ _req_key] <= 0) continue; + var _role_def = fill_squad[$ _req_key]; + if (_role_def == undefined) continue; + var _actual_role = struct_exists(_role_def, "role") ? _role_def.role : _req_key; + if (_req_key == _default_sarge || _actual_role == _default_sarge + || string_lower(_req_key) == "sergeant") { + new_sergeant(false, _actual_role); + required[$ _req_key]--; + } else if (_req_key == _default_vet_sarge || _actual_role == _default_vet_sarge + || string_lower(_req_key) == "veteran sergeant") { + new_sergeant(true, _actual_role); + required[$ _req_key]--; } } }; From c527543a88d5349708402d3bbcf914a2d06f3b2c Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:43:02 +0200 Subject: [PATCH 11/55] fix: squad gen issues and company view bug --- scripts/scr_initialize_custom/scr_initialize_custom.gml | 5 +++++ scripts/scr_start_load/scr_start_load.gml | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index b5122aa0bf..7f07bf8b9f 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -2944,6 +2944,11 @@ function scr_initialize_custom() { LOGGER.info("set up the starting squads"); obj_ini.squads = {}; game_start_squads(); + + // Populate TTRPG display arrays immediately so the management screen + // shows unit data before any fleet event fires sort_all_companies + LOGGER.info("initial company sort"); + sort_all_companies(); } /// @description helper function to streamline code inside of scr_initialize_custom, should only be used as part of game setup and not during normal gameplay diff --git a/scripts/scr_start_load/scr_start_load.gml b/scripts/scr_start_load/scr_start_load.gml index 691e494458..f35b38b27d 100644 --- a/scripts/scr_start_load/scr_start_load.gml +++ b/scripts/scr_start_load/scr_start_load.gml @@ -33,7 +33,7 @@ function scr_start_load(fleet, load_from_star, load_options) { if (comp_split > 7 || !comp_has_units[comp_split + 2]) { comp_split = 0; } - var _squad = fetch_squad(_squad_ids[squads]); + var _squad = fetch_squad(_squad_ids[i]); if (_squad.base_company == 1) { array_push(total_distribute_squads[comp_split], _squad); comp_split++; @@ -47,7 +47,7 @@ function scr_start_load(fleet, load_from_star, load_options) { if (comp_split > 7 || !comp_has_units[comp_split + 2]) { comp_split = 0; } - var _squad = fetch_squad(_squad_ids[squads]); + var _squad = fetch_squad(_squad_ids[i]); if (_squad.base_company == 10) { array_push(total_distribute_squads[comp_split], _squad); comp_split++; From 29bf8ba50b9881f01403bf25087fc43a07e04d26 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:57:06 +0200 Subject: [PATCH 12/55] feat: random pick equipment loadout --- datafiles/main/squads/base_squads.json | 17 +++++++----- scripts/scr_squads/scr_squads.gml | 37 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index 44f436d13f..f6278782f7 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -481,13 +481,16 @@ "wep2": ["", 0], "mobi": ["Bike", 1] }, - "option": { - "wep1": [ - [["Power Sword", "Power Spear", "Power Axe", "Power Fist", "Chainaxe", "Chainsword"],1], - [["Lightning Claw"],1,{"wep2":"Lightning Claw"}] - ], - "wep2": [[["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "RyzPlsmPis"],1]] - } + "random_pick": [ + { + "wep1": ["Power Sword", "Power Spear", "Power Axe", "Power Fist", "Chainaxe", "Chainsword"], + "wep2": ["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "RyzPlsmPis"] + }, + { + "wep1": "Lightning Claw", + "wep2": "Lightning Claw" + } + ] } }, "type_data": { diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index c70458316b..fa26c82470 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -217,6 +217,38 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co } }; + // Picks ONE entry (loadout category) at random, then resolves each slot: + // string values are used directly; array values get one item picked at random. + // Any slot omitted from an entry is left unchanged on the unit. + // + // JSON example: + // "random_pick": [ + // { "wep1": ["Sword","Axe","Mace"], "wep2": ["Pistol","Plasma","Volkite"] }, + // { "wep1": "Lightning Claw", "wep2": "Lightning Claw" } + // ] + static equip_random_pick_for_role = function(pick_options) { + var _actual_role = role_key_to_actual[$ unit_role]; + var _members_with_role = members_UnitGroup.get_from({role: _actual_role}); + while (_members_with_role.number() > 0) { + var _unit = _members_with_role.pop(); + if (array_contains(ignore_units, _unit.uid)) continue; + if (_unit.role() != _actual_role) continue; + + // Pick a random loadout category + var _chosen = pick_options[irandom(array_length(pick_options) - 1)]; + + // Resolve slots: array values → random element; strings → used as-is + var _resolved = {}; + var _slots = struct_get_names(_chosen); + for (var _s = 0; _s < array_length(_slots); _s++) { + var _slot = _slots[_s]; + var _value = _chosen[$ _slot]; + _resolved[$ _slot] = is_array(_value) ? array_random_element(_value) : _value; + } + _unit.alter_equipment(_resolved, from_armoury, to_armoury); + } + }; + static role_squad_loadout = function() { required_load = undefined; optional_load = undefined; @@ -242,6 +274,11 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co current_load_slot = load_out_areas[i]; equip_loudouts_specific_equip_slot(); } + + // random_pick runs after required/option — picks one complete loadout at random + if (struct_exists(_loudout_data, "random_pick")) { + equip_random_pick_for_role(_loudout_data[$ "random_pick"]); + } }; } From fb13d99b915a98e6ab99062c7ff17a43a5a1ec18 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:12:38 +0200 Subject: [PATCH 13/55] refactor: Company display tiles --- .../scr_initialize_custom.gml | 5 --- scripts/scr_management/scr_management.gml | 39 +++++-------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 7f07bf8b9f..b5122aa0bf 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -2944,11 +2944,6 @@ function scr_initialize_custom() { LOGGER.info("set up the starting squads"); obj_ini.squads = {}; game_start_squads(); - - // Populate TTRPG display arrays immediately so the management screen - // shows unit data before any fleet event fires sort_all_companies - LOGGER.info("initial company sort"); - sort_all_companies(); } /// @description helper function to streamline code inside of scr_initialize_custom, should only be used as part of game setup and not during normal gameplay diff --git a/scripts/scr_management/scr_management.gml b/scripts/scr_management/scr_management.gml index 4a5befcff6..eb71629c3b 100644 --- a/scripts/scr_management/scr_management.gml +++ b/scripts/scr_management/scr_management.gml @@ -6,6 +6,11 @@ function scr_management(argument0) { var chapter_name = global.chapter_name; if (argument0 == 1) { + // Ensure TTRPG display data is up to date before drawing the overview + with (obj_ini) { + sort_all_companies(); + } + with (obj_managment_panel) { instance_destroy(); } @@ -85,36 +90,10 @@ function scr_management(argument0) { pane.header = 1; pane.title = t; - var _company_group = collect_company(company).index_roles(); - - pane.line = array_join(pane.line, _company_group.create_plural_strings_array()); - - var num = array_create(5, 0); - var nam = [ - "Land Raider", - "Predator", - "Rhino", - "Land Speeder", - "Whirlwind" - ]; - // Vehicles - for (var i = 0; i < array_length(obj_ini.veh_role[company]); i++) { - for (var s = 0; s < array_length(nam); s++) { - if (obj_ini.veh_role[company][i] == nam[s]) { - num[s]++; - } - } - } - - for (var d = 0; d < 5; d++) { - if (num[d] > 0) { - if (d == 1) { - array_push(pane.line, {str1: nam[d], bold: true, italic: false}); - } else { - array_push(pane.line, nam[d], string_plural_count(nam[d], num[d], false)); - } - } - } + // Populate company panel — same pattern as the HQ specialty panels above + var _co_units = collect_company(company).index_roles(); + pane.line = array_join(pane.line, _co_units.create_plural_strings_array()); + xx += 156; } } From 9b87f156b066bd7f0cb02fd3482a16811687a950 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 6 Jun 2026 13:50:34 +0200 Subject: [PATCH 14/55] fixes: various display related bugs and cleanups of small bugs --- datafiles/main/chapters/34.json | 1 + .../scr_chapter_random/scr_chapter_random.gml | 2 +- .../scr_company_order/scr_company_order.gml | 31 +++++++++---------- .../scr_initialize_custom.gml | 1 + 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/datafiles/main/chapters/34.json b/datafiles/main/chapters/34.json index 2dc7a424f8..4766bef98a 100644 --- a/datafiles/main/chapters/34.json +++ b/datafiles/main/chapters/34.json @@ -1,5 +1,6 @@ { "chapter": { + "id": 34, "name": "Flesh Tearers", "mutations": { "betchers": 0.0, diff --git a/scripts/scr_chapter_random/scr_chapter_random.gml b/scripts/scr_chapter_random/scr_chapter_random.gml index 54ea1b5800..19e854a6f0 100644 --- a/scripts/scr_chapter_random/scr_chapter_random.gml +++ b/scripts/scr_chapter_random/scr_chapter_random.gml @@ -955,7 +955,7 @@ function scr_chapter_random(custom_or_random) { if (ran2 == 93) { phrase2 = "Wizards"; col_second = "Purple"; - name[100][eROLE.LIBRARIAN] = "Wizard"; + role[100][eROLE.LIBRARIAN] = "Wizard"; chapter_master_specialty = 3; } if (ran2 == 94) { diff --git a/scripts/scr_company_order/scr_company_order.gml b/scripts/scr_company_order/scr_company_order.gml index 005a67e44b..7575f40ebe 100644 --- a/scripts/scr_company_order/scr_company_order.gml +++ b/scripts/scr_company_order/scr_company_order.gml @@ -178,6 +178,20 @@ function role_hierarchy() { "Death Company", _roles[eROLE.VETERANSERGEANT], _roles[eROLE.SERGEANT], + _roles[eROLE.TERMINATOR], + _roles[eROLE.VETERAN], + _roles[eROLE.TACTICAL], + _roles[eROLE.ASSAULT], + _roles[eROLE.DEVASTATOR], + _roles[eROLE.SCOUT], + $"Venerable {_roles[eROLE.DREADNOUGHT]}", + _roles[eROLE.DREADNOUGHT], + "Skitarii", + "Crusader", + "Ranger", + "Sister of Battle", + "Flash Git", + "Ork Sniper" ]; // Dynamically collect squad-specific sergeant role variants (e.g. "Biker Sergeant", "Tactical Sergeant") @@ -247,22 +261,5 @@ function role_hierarchy() { } } - array_push(hierarchy, - _roles[eROLE.TERMINATOR], - _roles[eROLE.VETERAN], - _roles[eROLE.TACTICAL], - _roles[eROLE.ASSAULT], - _roles[eROLE.DEVASTATOR], - _roles[eROLE.SCOUT], - $"Venerable {_roles[eROLE.DREADNOUGHT]}", - _roles[eROLE.DREADNOUGHT], - "Skitarii", - "Crusader", - "Ranger", - "Sister of Battle", - "Flash Git", - "Ork Sniper" - ); - return hierarchy; } diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index b5122aa0bf..e2e75fdfea 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -3195,3 +3195,4 @@ function load_chapter_master_equipment() { } return chapter_master_equip; } +} From d58960ec16ffa7ea4def11481c3eb0bdff77f01c Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:47:26 +0200 Subject: [PATCH 15/55] feat: JSON compression on compositions --- .../main/squads/company_squad_builds.json | 199 +-------- datafiles/main/squads/equal_scouts.json | 204 ++-------- datafiles/main/squads/equal_specialists.json | 226 +---------- datafiles/main/squads/equal_spescout.json | 264 +----------- datafiles/main/squads/lightning_warriors.json | 378 ++++++++---------- .../scr_company_order/scr_company_order.gml | 14 +- .../scr_initialize_custom.gml | 69 +++- scripts/scr_squads/scr_squads.gml | 56 ++- 8 files changed, 342 insertions(+), 1068 deletions(-) diff --git a/datafiles/main/squads/company_squad_builds.json b/datafiles/main/squads/company_squad_builds.json index 705b36f420..17504439ff 100644 --- a/datafiles/main/squads/company_squad_builds.json +++ b/datafiles/main/squads/company_squad_builds.json @@ -1,210 +1,55 @@ { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 6 }, + { "squad": "breacher_squad", "proportion": 0 }, + { "squad": "devastator_squad", "proportion": 2 }, + { "squad": "assault_squad", "proportion": 2 } + ], "companies": [ { "company": 1, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "terminator_squad", - "proportion": 2 - }, - { - "squad": "terminator_assault_squad", - "proportion": 2 - }, - { - "squad": "veteran_squad", - "proportion": 1 - } - ] - }, - { - "company": 2, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "breacher_squad", - "proportion": 0 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 3, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "breacher_squad", - "proportion": 0 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 4, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "breacher_squad", - "proportion": 0 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 5, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "breacher_squad", - "proportion": 0 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "terminator_squad", "proportion": 2 }, + { "squad": "terminator_assault_squad", "proportion": 2 }, + { "squad": "veteran_squad", "proportion": 1 } ] }, { "company": 6, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 1 } ] }, { "company": 7, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 1 } ] }, { "company": 8, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "assault_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "assault_squad", "proportion": 1 } ] }, { "company": 9, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "devastator_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "devastator_squad", "proportion": 1 } ] }, { "company": 10, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "scout_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 1 } ] } ] -} \ No newline at end of file +} diff --git a/datafiles/main/squads/equal_scouts.json b/datafiles/main/squads/equal_scouts.json index 770df43fa8..ca00b88406 100644 --- a/datafiles/main/squads/equal_scouts.json +++ b/datafiles/main/squads/equal_scouts.json @@ -1,214 +1,56 @@ { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 4 }, + { "squad": "devastator_squad", "proportion": 2 }, + { "squad": "assault_squad", "proportion": 2 }, + { "squad": "scout_squad", "proportion": 2 } + ], "companies": [ { "company": 1, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "terminator_squad", - "proportion": 2 - }, - { - "squad": "terminator_assault_squad", - "proportion": 2 - }, - { - "squad": "veteran_squad", - "proportion": 1 - } - ] - }, - { - "company": 2, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 4 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 2 - } - ] - }, - { - "company": 3, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 4 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 2 - } - ] - }, - { - "company": 4, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 4 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 2 - } - ] - }, - { - "company": 5, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 4 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 2 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "terminator_squad", "proportion": 2 }, + { "squad": "terminator_assault_squad", "proportion": 2 }, + { "squad": "veteran_squad", "proportion": 1 } ] }, { "company": 6, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 1 } ] }, { "company": 7, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 1 } ] }, { "company": 8, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "assault_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "assault_squad", "proportion": 1 } ] }, { "company": 9, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "devastator_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "devastator_squad", "proportion": 1 } ] }, { "company": 10, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "scout_squad", - "proportion": 5 - }, - { - "squad": "tactical_squad", - "proportion": 4 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 5 }, + { "squad": "tactical_squad", "proportion": 4 } ] } ] -} \ No newline at end of file +} diff --git a/datafiles/main/squads/equal_specialists.json b/datafiles/main/squads/equal_specialists.json index fb8f1c55eb..552880f543 100644 --- a/datafiles/main/squads/equal_specialists.json +++ b/datafiles/main/squads/equal_specialists.json @@ -1,226 +1,26 @@ { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 6 }, + { "squad": "devastator_squad", "proportion": 2 }, + { "squad": "assault_squad", "proportion": 2 } + ], "companies": [ { "company": 1, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "terminator_squad", - "proportion": 2 - }, - { - "squad": "terminator_assault_squad", - "proportion": 2 - }, - { - "squad": "veteran_squad", - "proportion": 1 - } - ] - }, - { - "company": 2, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 3, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 4, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 5, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 6, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 7, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 8, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } - ] - }, - { - "company": 9, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 6 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "terminator_squad", "proportion": 2 }, + { "squad": "terminator_assault_squad", "proportion": 2 }, + { "squad": "veteran_squad", "proportion": 1 } ] }, { "company": 10, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "scout_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 1 } ] } ] -} \ No newline at end of file +} diff --git a/datafiles/main/squads/equal_spescout.json b/datafiles/main/squads/equal_spescout.json index e95d1eb94f..5fd0acbf98 100644 --- a/datafiles/main/squads/equal_spescout.json +++ b/datafiles/main/squads/equal_spescout.json @@ -1,262 +1,28 @@ { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 5 }, + { "squad": "devastator_squad", "proportion": 2 }, + { "squad": "assault_squad", "proportion": 2 }, + { "squad": "scout_squad", "proportion": 1 } + ], "companies": [ { "company": 1, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "terminator_squad", - "proportion": 2 - }, - { - "squad": "terminator_assault_squad", - "proportion": 2 - }, - { - "squad": "veteran_squad", - "proportion": 1 - } - ] - }, - { - "company": 2, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } - ] - }, - { - "company": 3, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } - ] - }, - { - "company": 4, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } - ] - }, - { - "company": 5, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } - ] - }, - { - "company": 6, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } - ] - }, - { - "company": 7, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } - ] - }, - { - "company": 8, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } - ] - }, - { - "company": 9, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 5 - }, - { - "squad": "devastator_squad", - "proportion": 2 - }, - { - "squad": "assault_squad", - "proportion": 2 - }, - { - "squad": "scout_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "terminator_squad", "proportion": 2 }, + { "squad": "terminator_assault_squad", "proportion": 2 }, + { "squad": "veteran_squad", "proportion": 1 } ] }, { "company": 10, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "scout_squad", - "proportion": 5 - }, - { - "squad": "tactical_squad", - "proportion": 4 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 5 }, + { "squad": "tactical_squad", "proportion": 4 } ] } ] -} \ No newline at end of file +} diff --git a/datafiles/main/squads/lightning_warriors.json b/datafiles/main/squads/lightning_warriors.json index b036184609..ed95eb9c5a 100644 --- a/datafiles/main/squads/lightning_warriors.json +++ b/datafiles/main/squads/lightning_warriors.json @@ -1,242 +1,198 @@ { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 0 }, + { "squad": "bike_squad", "proportion": 7 }, + { "squad": "attack_bike_squad", "proportion": 3 }, + { "squad": "devastator_squad", "proportion": 0 }, + { "squad": "assault_squad", "proportion": 0 } + ], "companies": [ { "company": 1, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "terminator_squad", - "proportion": 2 - }, - { - "squad": "terminator_assault_squad", - "proportion": 2 - }, - { - "squad": "veteran_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "terminator_squad", "proportion": 2 }, + { "squad": "terminator_assault_squad", "proportion": 2 }, + { "squad": "veteran_squad", "proportion": 1 } ] }, { - "company": 2, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 0 - }, - { - "squad": "bike_squad", - "proportion": 7 - }, - { - "squad": "attack_bike_squad", - "proportion": 3 - }, - { - "squad": "devastator_squad", - "proportion": 0 - }, - { - "squad": "assault_squad", - "proportion": 0 - } - ] - }, - { - "company": 3, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 0 - }, - { - "squad": "bike_squad", - "proportion": 7 - }, - { - "squad": "attack_bike_squad", - "proportion": 3 - }, - { - "squad": "devastator_squad", - "proportion": 0 - }, - { - "squad": "assault_squad", - "proportion": 0 - } - ] - }, - { - "company": 4, + "company": 7, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 0 - }, - { - "squad": "bike_squad", - "proportion": 7 - }, - { - "squad": "attack_bike_squad", - "proportion": 3 - }, - { - "squad": "devastator_squad", - "proportion": 0 - }, - { - "squad": "assault_squad", - "proportion": 0 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 1 } ] }, { - "company": 5, + "company": 8, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 0 - }, - { - "squad": "bike_squad", - "proportion": 7 - }, - { - "squad": "attack_bike_squad", - "proportion": 3 - }, - { - "squad": "devastator_squad", - "proportion": 0 - }, - { - "squad": "assault_squad", - "proportion": 0 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "assault_squad", "proportion": 1 } ] }, { - "company": 6, + "company": 9, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 0 - }, - { - "squad": "bike_squad", - "proportion": 7 - }, - { - "squad": "attack_bike_squad", - "proportion": 3 - }, - { - "squad": "devastator_squad", - "proportion": 0 - }, - { - "squad": "assault_squad", - "proportion": 0 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "devastator_squad", "proportion": 1 } ] }, { - "company": 7, + "company": 10, "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "tactical_squad", - "proportion": 1 - } + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 1 } ] - }, - { - "company": 8, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "assault_squad", - "proportion": 1 + } + ], + "distribution_overrides": { + "equal_specialists": { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 4 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 } + ], + "companies": [ + { + "company": 7, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 4 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 } + ] + }, + { + "company": 8, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 4 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 } + ] + }, + { + "company": 9, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 4 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 } + ] } ] }, - { - "company": 9, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "devastator_squad", - "proportion": 1 + "equal_scouts": { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 3 }, + { "squad": "bike_squad", "proportion": 5 }, + { "squad": "scout_squad", "proportion": 2 } + ], + "companies": [ + { + "company": 7, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 3 }, + { "squad": "bike_squad", "proportion": 5 }, + { "squad": "scout_squad", "proportion": 2 } + ] + }, + { + "company": 8, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 3 }, + { "squad": "bike_squad", "proportion": 5 }, + { "squad": "scout_squad", "proportion": 2 } + ] + }, + { + "company": 9, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 3 }, + { "squad": "bike_squad", "proportion": 5 }, + { "squad": "scout_squad", "proportion": 2 } + ] + }, + { + "company": 10, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 5 }, + { "squad": "bike_squad", "proportion": 4 } + ] } ] }, - { - "company": 10, - "squads": [ - { - "squad": "command_squad", - "max_count": 1, - "min_count": 1, - "require": true - }, - { - "squad": "scout_squad", - "proportion": 1 + "equal_spescout": { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 3 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "scout_squad", "proportion": 1 } + ], + "companies": [ + { + "company": 7, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 3 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "scout_squad", "proportion": 1 } + ] + }, + { + "company": 8, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 3 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "scout_squad", "proportion": 1 } + ] + }, + { + "company": 9, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "attack_bike_squad", "proportion": 2 }, + { "squad": "bike_squad", "proportion": 3 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "scout_squad", "proportion": 1 } + ] + }, + { + "company": 10, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 5 }, + { "squad": "bike_squad", "proportion": 4 } + ] } ] } - ] -} \ No newline at end of file + } +} diff --git a/scripts/scr_company_order/scr_company_order.gml b/scripts/scr_company_order/scr_company_order.gml index 7575f40ebe..1d2e386bc8 100644 --- a/scripts/scr_company_order/scr_company_order.gml +++ b/scripts/scr_company_order/scr_company_order.gml @@ -70,18 +70,8 @@ function scr_company_order(company) { if (_squadless.number() > 3) { var _squad_index = _company_marines.index_squads(); - var _data_match = false; - var _data; - if (struct_exists(obj_ini.chapter_squad_arrangement, "companies")) { - var _comp_datas = obj_ini.chapter_squad_arrangement.companies; - for (var i = 0; i < array_length(_comp_datas); i++) { - if (_comp_datas[i].company == co) { - _data_match = true; - _data = _comp_datas[i]; - } - } - } - if (_data_match) { + var _data = resolve_company_arrangement(obj_ini.chapter_squad_arrangement, co); + if (_data != undefined) { _squadless.organise_by_template(_data, _squad_index, _empty_index, false); } diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index e2e75fdfea..814befd97a 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -1639,23 +1639,41 @@ function scr_initialize_custom() { #endregion #region Squad Loadouts - switch (obj_creation.squad_distribution) { - case 1: // equal specialists only - obj_ini.chapter_squad_arrangement = json_to_gamemaker( - working_directory + $"main/squads/equal_specialists.json", json_parse); - break; - case 2: // equal scouts only - obj_ini.chapter_squad_arrangement = json_to_gamemaker( - working_directory + $"main/squads/equal_scouts.json", json_parse); - break; - case 3: // equal specialists and equal scouts - obj_ini.chapter_squad_arrangement = json_to_gamemaker( - working_directory + $"main/squads/equal_spescout.json", json_parse); - break; - default: // 0 = standard - obj_ini.chapter_squad_arrangement = json_to_gamemaker( - working_directory + $"main/squads/company_squad_builds.json", json_parse); - break; + if (scr_has_adv("Lightning Warriors")) { + obj_ini.chapter_squad_arrangement = json_to_gamemaker( + working_directory + $"main\\squads\\lightning_warriors.json", json_parse); + var _dist_key = ""; + switch (obj_creation.squad_distribution) { + case 1: _dist_key = "equal_specialists"; break; + case 2: _dist_key = "equal_scouts"; break; + case 3: _dist_key = "equal_spescout"; break; + } + if (_dist_key != "" + && struct_exists(obj_ini.chapter_squad_arrangement, "distribution_overrides") + && struct_exists(obj_ini.chapter_squad_arrangement.distribution_overrides, _dist_key)) { + apply_squad_distribution_override( + obj_ini.chapter_squad_arrangement, + obj_ini.chapter_squad_arrangement.distribution_overrides[$ _dist_key]); + } + } else { + switch (obj_creation.squad_distribution) { + case 1: // equal specialists only + obj_ini.chapter_squad_arrangement = json_to_gamemaker( + working_directory + $"main\\squads\\equal_specialists.json", json_parse); + break; + case 2: // equal scouts only + obj_ini.chapter_squad_arrangement = json_to_gamemaker( + working_directory + $"main\\squads\\equal_scouts.json", json_parse); + break; + case 3: // equal specialists and equal scouts + obj_ini.chapter_squad_arrangement = json_to_gamemaker( + working_directory + $"main\\squads\\equal_spescout.json", json_parse); + break; + default: // 0 = standard + obj_ini.chapter_squad_arrangement = json_to_gamemaker( + working_directory + $"main\\squads\\company_squad_builds.json", json_parse); + break; + } } var _squad_name = "Squad"; @@ -1777,15 +1795,26 @@ function scr_initialize_custom() { array_push(_swaps, _set); } - if (variable_instance_exists(obj_creation, "squad_builder")) { + // LOGGER.debug($"squads object for chapter {chapter_name}"); + // LOGGER.debug($"{custom_squads}"); + + if (struct_exists(obj_creation, "squad_builder")) { + if (!struct_exists(obj_ini.chapter_squad_arrangement, "companies")) { + obj_ini.chapter_squad_arrangement.companies = []; + } for (var s = 0; s < array_length(obj_creation.squad_builder); s++) { var _custom_build = obj_creation.squad_builder[s]; + var _found = false; for (var i = 0; i < array_length(obj_ini.chapter_squad_arrangement.companies); i++) { - var _default_build = obj_ini.chapter_squad_arrangement.companies[i]; - if (_custom_build.company == _default_build.company) { + if (obj_ini.chapter_squad_arrangement.companies[i].company == _custom_build.company) { obj_ini.chapter_squad_arrangement.companies[i] = _custom_build; + _found = true; + break; } } + if (!_found) { + array_push(obj_ini.chapter_squad_arrangement.companies, _custom_build); + } } } diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index fa26c82470..51e31ca5b5 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -793,6 +793,52 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { }; } +// Resolves the squad arrangement data for a specific company number from an arrangement struct. +// Checks explicit companies entries first, falls back to default_squads if present. +// Returns undefined if neither is available. +function resolve_company_arrangement(arrangement, company_number) { + if (struct_exists(arrangement, "companies")) { + var _companies = arrangement.companies; + for (var i = 0; i < array_length(_companies); i++) { + if (_companies[i].company == company_number) { + return _companies[i]; + } + } + } + if (struct_exists(arrangement, "default_squads")) { + return { company: company_number, squads: arrangement.default_squads }; + } + return undefined; +} + +// Applies a distribution_overrides entry onto a loaded arrangement in-place. +// Replaces default_squads if the override defines them, and upserts any explicit company entries. +function apply_squad_distribution_override(arrangement, override) { + if (struct_exists(override, "default_squads")) { + arrangement.default_squads = override.default_squads; + } + if (struct_exists(override, "companies")) { + if (!struct_exists(arrangement, "companies")) { + arrangement.companies = []; + } + var _ovr_companies = override.companies; + for (var oi = 0; oi < array_length(_ovr_companies); oi++) { + var _ovr = _ovr_companies[oi]; + var _found = false; + for (var ai = 0; ai < array_length(arrangement.companies); ai++) { + if (arrangement.companies[ai].company == _ovr.company) { + arrangement.companies[ai] = _ovr; + _found = true; + break; + } + } + if (!_found) { + array_push(arrangement.companies, _ovr); + } + } + } +} + // creates the origional distribution of squads accross the chapter // lots of room for customisation of different chapters here @@ -1212,11 +1258,11 @@ function SquadArrangementEditor(company) constructor { function game_start_squads() { obj_ini.squads = {}; - if (struct_exists(chapter_squad_arrangement, "companies")) { - var _comp_datas = obj_ini.chapter_squad_arrangement.companies; - for (var i = 0; i < array_length(_comp_datas); i++) { - var _company = collect_company(_comp_datas[i].company); - _company.organise_by_template(_comp_datas[i]); + for (var co = 1; co <= obj_ini.companies; co++) { + var _data = resolve_company_arrangement(obj_ini.chapter_squad_arrangement, co); + if (_data != undefined) { + var _company = collect_company(co); + _company.organise_by_template(_data); } } } From b9bf61b50721fc2b11f68250f763367d63f9a3b3 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:50:10 +0200 Subject: [PATCH 16/55] fix: syntax lol --- scripts/scr_initialize_custom/scr_initialize_custom.gml | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 814befd97a..a53050c83f 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -3224,4 +3224,3 @@ function load_chapter_master_equipment() { } return chapter_master_equip; } -} From 42bb426a55e333c8853f525c7263ee4ec8e5f9df Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:18:17 +0200 Subject: [PATCH 17/55] feat: LW support for company gen cause I forgot --- .../scr_initialize_custom.gml | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index a53050c83f..c5417368e9 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -2299,6 +2299,7 @@ function scr_initialize_custom() { var equal_scouts = (squad_distribution == 2 || squad_distribution == 3); obj_ini.equal_scouts = equal_scouts; // for use in squad creation later + var _lw = scr_has_adv("Lightning Warriors"); var _moved_scouts = 0; var _coys = struct_get_names(companies); @@ -2417,14 +2418,40 @@ function scr_initialize_custom() { _coy.devastators = 0; } if (real(_coy.coy) == 8) { - _coy.tacticals = 0; - _coy.assaults = _coy.total; - _coy.devastators = 0; + if (_lw && equal_scouts) { + if (companies.tenth.scouts > 10) { + _coy.scouts = 10; + _moved_scouts += 10; + _coy.tacticals = _coy.total - _coy.scouts; + companies.tenth.scouts -= 10; + } else { + _coy.tacticals = _coy.total; + } + _coy.assaults = 0; + _coy.devastators = 0; + } else { + _coy.tacticals = 0; + _coy.assaults = _coy.total; + _coy.devastators = 0; + } } if (real(_coy.coy) == 9) { - _coy.tacticals = 0; - _coy.assaults = 0; - _coy.devastators = _coy.total; + if (_lw && equal_scouts) { + if (companies.tenth.scouts > 10) { + _coy.scouts = 10; + _moved_scouts += 10; + _coy.tacticals = _coy.total - _coy.scouts; + companies.tenth.scouts -= 10; + } else { + _coy.tacticals = _coy.total; + } + _coy.assaults = 0; + _coy.devastators = 0; + } else { + _coy.tacticals = 0; + _coy.assaults = 0; + _coy.devastators = _coy.total; + } } if (real(_coy.coy) == 10 && equal_scouts) { _coy.tacticals = _moved_scouts; From e15b2e958e8136f62cac66cfb5a2e7e5a97c2bad Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:30:17 +0200 Subject: [PATCH 18/55] fix: Scouts in Scout column --- scripts/scr_roster/scr_roster.gml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/scr_roster/scr_roster.gml b/scripts/scr_roster/scr_roster.gml index 990002aa56..d288452fa6 100644 --- a/scripts/scr_roster/scr_roster.gml +++ b/scripts/scr_roster/scr_roster.gml @@ -682,6 +682,9 @@ function add_unit_to_battle(unit, meeting, is_local) { case "command": col = obj_controller.bat_command_column; break; + case "scout": + col = obj_controller.bat_scout_column; + break; } } if (col == 0) { From 41036a48f0b618237847b9841a5d83acf766adee Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:06:43 +0200 Subject: [PATCH 19/55] fix: formation overrides+docs --- scripts/scr_squads/scr_squads.gml | 54 +++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 51e31ca5b5..d9b886cf37 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -793,9 +793,16 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { }; } -// Resolves the squad arrangement data for a specific company number from an arrangement struct. -// Checks explicit companies entries first, falls back to default_squads if present. -// Returns undefined if neither is available. +/// @function resolve_company_arrangement +/// @description Resolves the squad template for a specific company number from a loaded +/// arrangement struct. Explicit per-company entries take priority; if none matches, +/// the arrangement's default_squads array is wrapped and returned. Returns undefined +/// if the arrangement contains neither a matching company entry nor a default_squads. +/// @param {Struct} arrangement A parsed squad-arrangement struct (e.g. from lightning_warriors.json). +/// Expected fields: optional {Array} companies, optional {Array} default_squads. +/// @param {Real} company_number The 1-based company index to resolve a template for. +/// @return {Struct|Undefined} A company template struct with fields {Real} company and {Array} squads, +/// or undefined if no template can be resolved. function resolve_company_arrangement(arrangement, company_number) { if (struct_exists(arrangement, "companies")) { var _companies = arrangement.companies; @@ -811,11 +818,34 @@ function resolve_company_arrangement(arrangement, company_number) { return undefined; } -// Applies a distribution_overrides entry onto a loaded arrangement in-place. -// Replaces default_squads if the override defines them, and upserts any explicit company entries. +/// @function apply_squad_distribution_override +/// @description Merges a distribution_overrides entry into a loaded arrangement struct in-place. +/// Two operations are performed: +/// 1. If the override defines default_squads, a deep clone of that array replaces +/// arrangement.default_squads. Cloning keeps the two references independent so +/// any future in-place mutation of one cannot corrupt the other. +/// 2. If the override defines a companies array, each entry is upserted into +/// arrangement.companies — matching on the company number field, replacing an +/// existing entry if found or appending if not. +/// Squad order within default_squads and company squads arrays matters: squads that +/// only accept their own marine role (e.g. devastator_squad, assault_squad) must be +/// listed before squads that use alternative_roles (e.g. bike_squad, attack_bike_squad) +/// so that specific squads claim their marines before greedy squads can absorb them. +/// @param {Struct} arrangement The live chapter_squad_arrangement struct to mutate. +/// @param {Struct} override One distribution_overrides child struct from the same JSON +/// (e.g. arrangement.distribution_overrides.equal_specialists). +/// Expected optional fields: {Array} default_squads, {Array} companies. +/// @return {Undefined} function apply_squad_distribution_override(arrangement, override) { if (struct_exists(override, "default_squads")) { - arrangement.default_squads = override.default_squads; + // Deep-clone so arrangement.default_squads is independent of the override sub-struct, + // preventing any future in-place mutation of the array from corrupting both references. + var _src = override.default_squads; + var _clone = array_create(array_length(_src)); + for (var _i = 0; _i < array_length(_src); _i++) { + _clone[_i] = variable_clone(_src[_i]); + } + arrangement.default_squads = _clone; } if (struct_exists(override, "companies")) { if (!struct_exists(arrangement, "companies")) { @@ -839,9 +869,15 @@ function apply_squad_distribution_override(arrangement, override) { } } -// creates the origional distribution of squads accross the chapter -// lots of room for customisation of different chapters here - +/// @function game_start_squads +/// @description Populates obj_ini.squads at game start by iterating every company and calling +/// organise_by_template with the resolved squad template for that company. +/// Templates are resolved from obj_ini.chapter_squad_arrangement via +/// resolve_company_arrangement; companies with no resolvable template are skipped. +/// Must be called after obj_ini.chapter_squad_arrangement has been fully built +/// (including any apply_squad_distribution_override calls) and after all marine +/// individuals have been created by the count-based initialisation pass. +/// @return {Undefined} function get_compay_squad_arrangement(company){ var _comp_datas = obj_ini.chapter_squad_arrangement.companies; for (var i = 0; i < array_length(_comp_datas); i++) { From de127d6dbc5ca7467f5d72310533c8ad8a9ff26a Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:08:37 +0200 Subject: [PATCH 20/55] this is the previous commit, the file beforehand is from the next commit-relate changes --- scripts/scr_roster/scr_roster.gml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/scr_roster/scr_roster.gml b/scripts/scr_roster/scr_roster.gml index d288452fa6..19ac0e2945 100644 --- a/scripts/scr_roster/scr_roster.gml +++ b/scripts/scr_roster/scr_roster.gml @@ -475,6 +475,9 @@ function PurgeButton(purge_image, xx, yy, purge_type) constructor { }; } +/// @desc Resolves the chosen formation (formation_set, e.g. Attack/Defend/Raid) into concrete +/// per-role battle columns on obj_controller, which add_unit_to_battle/add_vehicle_to_battle +/// then use to place individual units. function setup_battle_formations() { // Formation here var new_combat = obj_ncombat; @@ -497,6 +500,17 @@ function setup_battle_formations() { obj_controller.bat_scout_column = obj_controller.bat_scou_for[new_combat.formation_set]; } +/// @desc Determines which formation column (block) a single marine is placed into for the +/// current battle and adds them to the nearest obj_pnunit at that column. +/// Resolution order: 1) the unit's individual role (sergeant variants, Scout, Tactical, +/// Veteran, Devastator, Assault, Librarian/Techmarine specialists, Honour Guard, +/// Dreadnought, Terminator, Chapter Master/heads, Death Company), then 2) if the unit +/// belongs to a squad, the squad's formation_place overrides the role-based column +/// (assault/veteran/tactical/devastator/terminator/command/scout), and finally +/// 3) anything still unresolved (col == 0) defaults to the hireling column. +/// @param {Id.Instance} unit The marine instance being added to the battle. +/// @param {Bool} meeting Whether this is a meeting/temp roster (uses obj_temp_meeting lookups) rather than a normal company roster. +/// @param {Bool} is_local Whether this unit belongs to the local (player-controlled) side. function add_unit_to_battle(unit, meeting, is_local) { var new_combat = obj_ncombat; var man_size = 1; From ea21184ab7a43e3335f21ea9741a722c4a7c99d0 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:09:12 +0200 Subject: [PATCH 21/55] feat: Lightning Warriors Advantage 1.0 --- datafiles/main/squads/equal_spescout.json | 3 +- datafiles/main/squads/lightning_warriors.json | 124 +++++++++++------- .../scr_initialize_custom.gml | 81 +++++++----- 3 files changed, 124 insertions(+), 84 deletions(-) diff --git a/datafiles/main/squads/equal_spescout.json b/datafiles/main/squads/equal_spescout.json index 5fd0acbf98..1afcb85a8c 100644 --- a/datafiles/main/squads/equal_spescout.json +++ b/datafiles/main/squads/equal_spescout.json @@ -20,8 +20,7 @@ "company": 10, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "scout_squad", "proportion": 5 }, - { "squad": "tactical_squad", "proportion": 4 } + { "squad": "devastator_squad", "proportion": 1 } ] } ] diff --git a/datafiles/main/squads/lightning_warriors.json b/datafiles/main/squads/lightning_warriors.json index ed95eb9c5a..f4914d250f 100644 --- a/datafiles/main/squads/lightning_warriors.json +++ b/datafiles/main/squads/lightning_warriors.json @@ -2,10 +2,10 @@ "default_squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, { "squad": "tactical_squad", "proportion": 0 }, - { "squad": "bike_squad", "proportion": 7 }, - { "squad": "attack_bike_squad", "proportion": 3 }, + { "squad": "bike_squad", "proportion": 9 }, + { "squad": "attack_bike_squad", "proportion": 6 }, { "squad": "devastator_squad", "proportion": 0 }, - { "squad": "assault_squad", "proportion": 0 } + { "squad": "assault_squad", "proportion": 1 } ], "companies": [ { @@ -17,6 +17,13 @@ { "squad": "veteran_squad", "proportion": 1 } ] }, + { + "company": 6, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count":1, "require": true }, + { "squad": "tactical_squad", "proportion": 1 } + ] + }, { "company": 7, "squads": [ @@ -50,44 +57,55 @@ "equal_specialists": { "default_squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 4 }, - { "squad": "tactical_squad", "proportion": 2 }, { "squad": "devastator_squad", "proportion": 1 }, - { "squad": "assault_squad", "proportion": 1 } + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ], "companies": [ { - "company": 7, + "company": 6, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 4 }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 }, { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } + ] + }, + { + "company": 7, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, { "squad": "devastator_squad", "proportion": 1 }, - { "squad": "assault_squad", "proportion": 1 } + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ] }, { "company": 8, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 4 }, - { "squad": "tactical_squad", "proportion": 2 }, { "squad": "devastator_squad", "proportion": 1 }, - { "squad": "assault_squad", "proportion": 1 } + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ] }, { "company": 9, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 4 }, - { "squad": "tactical_squad", "proportion": 2 }, { "squad": "devastator_squad", "proportion": 1 }, - { "squad": "assault_squad", "proportion": 1 } + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 2 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ] } ] @@ -95,36 +113,37 @@ "equal_scouts": { "default_squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 3 }, - { "squad": "bike_squad", "proportion": 5 }, + { "squad": "attack_bike_squad", "proportion": 6 }, + { "squad": "bike_squad", "proportion": 8 }, { "squad": "scout_squad", "proportion": 2 } ], "companies": [ + { + "company": 6, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 6 } + ] + }, { "company": 7, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 3 }, - { "squad": "bike_squad", "proportion": 5 }, - { "squad": "scout_squad", "proportion": 2 } + { "squad": "tactical_squad", "proportion": 6 } ] }, { "company": 8, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 3 }, - { "squad": "bike_squad", "proportion": 5 }, - { "squad": "scout_squad", "proportion": 2 } + { "squad": "assault_squad", "proportion": 1 } ] }, { "company": 9, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 3 }, - { "squad": "bike_squad", "proportion": 5 }, - { "squad": "scout_squad", "proportion": 2 } + { "squad": "devastator_squad", "proportion": 1 } ] }, { @@ -140,55 +159,66 @@ "equal_spescout": { "default_squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 3 }, - { "squad": "tactical_squad", "proportion": 2 }, { "squad": "devastator_squad", "proportion": 1 }, { "squad": "assault_squad", "proportion": 1 }, - { "squad": "scout_squad", "proportion": 1 } + { "squad": "scout_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 1 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ], "companies": [ + { + "company": 6, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "devastator_squad", "proportion": 1 }, + { "squad": "assault_squad", "proportion": 1 }, + { "squad": "scout_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 1 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } + ] + }, { "company": 7, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 3 }, - { "squad": "tactical_squad", "proportion": 2 }, { "squad": "devastator_squad", "proportion": 1 }, { "squad": "assault_squad", "proportion": 1 }, - { "squad": "scout_squad", "proportion": 1 } + { "squad": "scout_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 1 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ] }, { "company": 8, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 3 }, - { "squad": "tactical_squad", "proportion": 2 }, { "squad": "devastator_squad", "proportion": 1 }, { "squad": "assault_squad", "proportion": 1 }, - { "squad": "scout_squad", "proportion": 1 } + { "squad": "scout_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 1 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ] }, { "company": 9, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "attack_bike_squad", "proportion": 2 }, - { "squad": "bike_squad", "proportion": 3 }, - { "squad": "tactical_squad", "proportion": 2 }, { "squad": "devastator_squad", "proportion": 1 }, { "squad": "assault_squad", "proportion": 1 }, - { "squad": "scout_squad", "proportion": 1 } + { "squad": "scout_squad", "proportion": 1 }, + { "squad": "tactical_squad", "proportion": 1 }, + { "squad": "attack_bike_squad", "proportion": 4 }, + { "squad": "bike_squad", "proportion": 6 } ] }, { "company": 10, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "scout_squad", "proportion": 5 }, { "squad": "bike_squad", "proportion": 4 } ] } diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index c5417368e9..27acd9e887 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -2358,7 +2358,22 @@ function scr_initialize_custom() { /// comp 10: tac 40: scout 50; if (squad_distribution == 1 || squad_distribution == 3) { if (_coy.coy >= 2 && _coy.coy <= 9) { - if (equal_scouts) { + // Scout distribution logic for equal_scouts (sd==2) and equal_spescout (sd==3). + // + // For standard equal_scouts (sd==2) or equal_spescout without LW (sd==3, !_lw): + // 10 scouts are moved from the 10th company bank into each battle company so + // that the JSON template's scout_squad proportion can fill at game start. + // + // For LW + equal_spescout (sd==3, _lw==true) scouts are NOT drained from 10th: + // - 10th company retains its full scout bank (~90) so its own scout_squad + // proportions (from the equal_spescout override) fill correctly. + // - Companies 2-9 receive no scout marines at game start; the scout_squad(1) + // proportion in their JSON template simply produces no squads initially. + // Scouts can be recruited into those companies naturally during the campaign. + // + // Note: for LW + equal_scouts (sd==2) this branch is not reached at all because + // sd==2 does not satisfy (sd==1 || sd==3), so it falls to the else block below. + if (equal_scouts && !(squad_distribution == 3 && _lw)) { if (companies.tenth.scouts > 10) { //theoretically this keeps track of moving scouts from the bank of them in 10th _coy.scouts = 10; @@ -2375,7 +2390,13 @@ function scr_initialize_custom() { _coy.assaults = assault; _coy.devastators = devastator; } - if (equal_scouts && _coy.coy == 10) { + // For equal_scouts or equal_spescout (without LW), replace the scouts that were moved + // out of 10th company with an equivalent number of tacticals so the 10th's total + // marine count stays consistent. _moved_scouts tracks the cumulative scouts transferred + // to other companies during the loop above. + // Skipped for LW + equal_spescout (sd==3, _lw==true) because no scouts were moved in + // that path; 10th company retains its scouts and needs no tactical swap. + if (equal_scouts && _coy.coy == 10 && !(squad_distribution == 3 && _lw)) { // theoretically this swaps moved scouts with tacticals _coy.tacticals = _moved_scouts; } @@ -2400,8 +2421,15 @@ function scr_initialize_custom() { _coy.devastators = devastator; } + // Companies 6-7: only receive scouts under the non-LW equal_scouts arrangement + // (company_squad_builds/equal_scouts.json gives 6-7 nothing but tactical_squad + // when Lightning Warriors is active - lightning_warriors.json's equal_scouts + // override does the same: companies 6 and 7 are tactical_squad-only, with no + // scout_squad entry at all). Granting _coy.scouts here for LW would create scout + // marines that the LW template can never organise into squads, leaving them as + // stray squadless scouts in companies that should be scout-free. if (real(_coy.coy) >= 6 && real(_coy.coy) <= 7) { - if (equal_scouts) { + if (equal_scouts && !_lw) { if (companies.tenth.scouts > 10) { _coy.scouts = 10; _moved_scouts += _coy.scouts; @@ -2417,41 +2445,24 @@ function scr_initialize_custom() { _coy.assaults = 0; _coy.devastators = 0; } + // Company 8 and 9: always pure assault / devastator reserves for equal_scouts + // (sd==2), regardless of Lightning Warriors. Both company_squad_builds/ + // equal_scouts.json AND lightning_warriors.json's equal_scouts override define + // company 8 as assault_squad-only and company 9 as devastator_squad-only - neither + // lists a scout_squad. The previous `_lw && equal_scouts` branches incorrectly + // handed these companies scout marines that the LW override's templates have no + // scout_squad to absorb, producing stray squadless scouts (the "all companies get + // scouts" symptom). Scouts for equal_scouts + LW must stay confined to companies + // 2-5, matching the override's default_squads scout_squad proportion. if (real(_coy.coy) == 8) { - if (_lw && equal_scouts) { - if (companies.tenth.scouts > 10) { - _coy.scouts = 10; - _moved_scouts += 10; - _coy.tacticals = _coy.total - _coy.scouts; - companies.tenth.scouts -= 10; - } else { - _coy.tacticals = _coy.total; - } - _coy.assaults = 0; - _coy.devastators = 0; - } else { - _coy.tacticals = 0; - _coy.assaults = _coy.total; - _coy.devastators = 0; - } + _coy.tacticals = 0; + _coy.assaults = _coy.total; + _coy.devastators = 0; } if (real(_coy.coy) == 9) { - if (_lw && equal_scouts) { - if (companies.tenth.scouts > 10) { - _coy.scouts = 10; - _moved_scouts += 10; - _coy.tacticals = _coy.total - _coy.scouts; - companies.tenth.scouts -= 10; - } else { - _coy.tacticals = _coy.total; - } - _coy.assaults = 0; - _coy.devastators = 0; - } else { - _coy.tacticals = 0; - _coy.assaults = 0; - _coy.devastators = _coy.total; - } + _coy.tacticals = 0; + _coy.assaults = 0; + _coy.devastators = _coy.total; } if (real(_coy.coy) == 10 && equal_scouts) { _coy.tacticals = _moved_scouts; From 000f3f5b85a0f16f1f9d655ee89298225bf51914 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:13:15 +0200 Subject: [PATCH 22/55] fix: default weapon encumbrance bug --- scripts/scr_squads/scr_squads.gml | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index d9b886cf37..80900e767d 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -63,6 +63,50 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co target_squad.update_fulfilment(); static sort = function() { + // Build the set of weapon slots this squad's loadout actively manages, + // across all roles (required + option + random_pick). + // Only wep1/wep2 are cleared — other slots (armour, gear, mobi) are + // intentionally left as-is since they may carry meaningful defaults. + var _weapon_slots = ["wep1", "wep2"]; + var _managed_slots = {}; + for (var _ri = 0; _ri < array_length(squad_unit_types); _ri++) { + var _role_data = full_squad_data[$ squad_unit_types[_ri]]; + if (!struct_exists(_role_data, "loadout")) continue; + var _ld = _role_data.loadout; + if (struct_exists(_ld, "required")) { + var _slots = struct_get_names(_ld.required); + for (var _s = 0; _s < array_length(_slots); _s++) + _managed_slots[$ _slots[_s]] = true; + } + if (struct_exists(_ld, "option")) { + var _slots = struct_get_names(_ld.option); + for (var _s = 0; _s < array_length(_slots); _s++) + _managed_slots[$ _slots[_s]] = true; + } + if (struct_exists(_ld, "random_pick")) { + var _picks = _ld.random_pick; + for (var _p = 0; _p < array_length(_picks); _p++) { + var _slots = struct_get_names(_picks[_p]); + for (var _s = 0; _s < array_length(_slots); _s++) + _managed_slots[$ _slots[_s]] = true; + } + } + } + // Clear managed weapon slots on every member so pre-initialization + // defaults (e.g. a Bolt Pistol carried before squad assignment) don't + // interfere with the encumbrance check during loadout assignment. + var _all_members = target_squad.get_members(true); + for (var _mi = 0; _mi < _all_members.number(); _mi++) { + var _u = _all_members.units[_mi]; + for (var _s = 0; _s < array_length(_weapon_slots); _s++) { + if (struct_exists(_managed_slots, _weapon_slots[_s])) { + var _clear = {}; + _clear[$ _weapon_slots[_s]] = ""; + _u.alter_equipment(_clear, false, false); + } + } + } + for (var i = 0; i < array_length(squad_unit_types); i++) { unit_role = squad_unit_types[i]; role_squad_loadout(); From dc7826493a5c29ceb30aa7e45bbddcf6b2e0f662 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:44:48 +0200 Subject: [PATCH 23/55] feat: better weapon options for Biker Sergeant --- datafiles/main/squads/base_squads.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index f6278782f7..b7f7ed45a9 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -530,7 +530,7 @@ "mobi": ["Attack Bike", 1] }, "option": { - "wep1": [[["Multi-Melta", "Heavy Bolter", "Plasma Cannon", "Lascannon"],1]], + "wep1": [[["Multi-Melta", "Plasma Cannon", "Lascannon"],1]], "wep2": [[["Chainsword", "Chainaxe", "Power Sword", "Power Spear", "Power Axe"],1]] } } From 910356d5eb19fd287bc8ba27d160bd16f309349a Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:18:28 +0200 Subject: [PATCH 24/55] feat: bikers formation --- ChapterMaster.yyp | 1 + datafiles/images/ui/formation18.png | Bin 0 -> 108634 bytes datafiles/main/squads/base_squads.json | 8 +++++--- objects/obj_controller/Create_0.gml | 2 ++ scripts/scr_roster/scr_roster.gml | 4 ++++ 5 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 datafiles/images/ui/formation18.png diff --git a/ChapterMaster.yyp b/ChapterMaster.yyp index d2c238c6bb..d17bed6032 100644 --- a/ChapterMaster.yyp +++ b/ChapterMaster.yyp @@ -463,6 +463,7 @@ {"$GMIncludedFile":"","%Name":"formation15.png","CopyToMask":-1,"filePath":"datafiles/images/ui","name":"formation15.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"formation16.png","CopyToMask":-1,"filePath":"datafiles/images/ui","name":"formation16.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"formation17.png","CopyToMask":-1,"filePath":"datafiles/images/ui","name":"formation17.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, + {"$GMIncludedFile":"","%Name":"formation18.png","CopyToMask":-1,"filePath":"datafiles/images/ui","name":"formation18.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"formation2.png","CopyToMask":-1,"filePath":"datafiles/images/ui","name":"formation2.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"formation3.png","CopyToMask":-1,"filePath":"datafiles/images/ui","name":"formation3.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, {"$GMIncludedFile":"","%Name":"formation4.png","CopyToMask":-1,"filePath":"datafiles/images/ui","name":"formation4.png","resourceType":"GMIncludedFile","resourceVersion":"2.0",}, diff --git a/datafiles/images/ui/formation18.png b/datafiles/images/ui/formation18.png new file mode 100644 index 0000000000000000000000000000000000000000..d1a2d2d1f874087f7473bf098c7123d9dc816554 GIT binary patch literal 108634 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 u@g%5jd0pS0q4&@@bmROOIs9K;b=3nWD Date: Wed, 10 Jun 2026 13:35:31 +0200 Subject: [PATCH 25/55] fix:typo --- datafiles/main/squads/base_squads.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index 67961f4df6..a68c2e5aeb 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -484,7 +484,7 @@ "random_pick": [ { "wep1": ["Power Sword", "Power Spear", "Power Axe", "Power Fist", "Chainaxe", "Chainsword"], - "wep2": ["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "RyzPlsmPis"] + "wep2": ["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "Ryza Plasma Pistol"] }, { "wep1": "Lightning Claw", From 76a051c8970162d5e3f06a21f5c5545779584226 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:51:10 +0200 Subject: [PATCH 26/55] feat: Speed Force ability for Bikes --- datafiles/data/mobility.json | 24 ++++++++--- scripts/scr_flavor/scr_flavor.gml | 40 +++++++++++++++++++ .../scr_marine_struct/scr_marine_struct.gml | 12 ++++++ .../scr_player_combat_weapon_stacks.gml | 9 +++++ .../scr_ui_formation_bars.gml | 1 + 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index bed1718c5b..711881b25a 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -2,9 +2,9 @@ "Bike": { "abbreviation": "Bike", "damage_resistance_mod": { - "artifact": 10, - "master_crafted": 10, - "standard": 5 + "artifact": 40, + "master_crafted": 30, + "standard": 20 }, "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action.", "hp_mod": { @@ -17,6 +17,12 @@ "master_crafted": 25, "standard": 20 }, + "special_properties":[ + "Speed Force" + ], + "tags":[ + "bike" + ], "second_profiles": [ "Twin Linked Bolters" ], @@ -26,9 +32,9 @@ "Attack Bike": { "abbreviation": "At Bike", "damage_resistance_mod": { - "artifact": 10, - "master_crafted": 10, - "standard": 5 + "artifact": 40, + "master_crafted": 30, + "standard": 25 }, "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action. Sports an additional sidecar with a heavy weapon of choice.", "hp_mod": { @@ -41,9 +47,15 @@ "master_crafted": 15, "standard": 10 }, + "special_properties": [ + "Speed Force" + ], "second_profiles": [ "Twin Linked Bolters" ], + "tags":[ + "bike" + ], "value": 95, "requires_to_forge": ["combi_1"] }, diff --git a/scripts/scr_flavor/scr_flavor.gml b/scripts/scr_flavor/scr_flavor.gml index b26bd358e5..5b8c55de69 100644 --- a/scripts/scr_flavor/scr_flavor.gml +++ b/scripts/scr_flavor/scr_flavor.gml @@ -185,6 +185,46 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot } } } + } else if (weapon_name == "Speed Force" || weapon_name == "Speed Force(M)") { + flavoured = true; + if (!character_shot) { + if (number_of_shots < 20) { + attack_message += $"{number_of_shots} Astartes on Bikes speed ahead, their Bikes roaring like beasts of old- "; + } else if (number_of_shots >= 20 && number_of_shots < 100) { + attack_message += $"Squads of {number_of_shots} Astartes thunder ahead on their Bikes. They descend upon the enemy- "; + } else { + attack_message += $"A massive wave of {number_of_shots} Astartes rolls ahead on top of their mighty Bikes. They crash into enemy lines, smashing their foe- "; + } + if (target.dudes_num[targeh] == 1) { + if (casulties == 0) { + attack_message += $"but the {target_name} endures the onslaught."; + } else { + attack_message += $"the {target_name} falls to the charge."; + } + } else { + if (casulties == 0) { + attack_message += $"{target_name} ranks are hit, but no casualties are confirmed."; + } else { + attack_message += $"{target_name} ranks are hit, killing {casulties} in an instant."; + } + } + } else { + if (target.dudes_num[targeh] == 1) { + attack_message += string(unit_name) + $" speeds on his bike, soaring and crashing into the {target_name}- "; + if (casulties == 0) { + attack_message += $"but it endures the onslaught."; + } else { + attack_message += $"and it falls to the charge."; + } + } else { + attack_message += string(unit_name) + $" speeds on his bike, slamming into {target_name} ranks- "; + if (casulties == 0) { + attack_message += $"but all survive the impact."; + } else { + attack_message += $"killing {casulties} perish in the attack."; + } + } + } } else if (weapon_name == "Assault Cannon") { flavoured = true; if (!character_shot) { diff --git a/scripts/scr_marine_struct/scr_marine_struct.gml b/scripts/scr_marine_struct/scr_marine_struct.gml index 2e5908bd1c..2c3eecfe46 100644 --- a/scripts/scr_marine_struct/scr_marine_struct.gml +++ b/scripts/scr_marine_struct/scr_marine_struct.gml @@ -1664,6 +1664,18 @@ function TTRPG_stats(faction, comp, mar, class = "marine", other_spawn_data = {} return wrath; }; + static speed_force = function() { + var _melee_attack = melee_damage_data[0]; + var _melee_weapon = melee_damage_data[3]; + + var speed = new EquipmentStruct({attack: _melee_attack * 2, name: "Speed Force", range: 14, ammo: 12, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); + + var speed_melee = new EquipmentStruct({attack: _melee_attack * 4, name: "Speed Force(M)", range: 1, ammo: 16, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); + + speed.second_profiles = [speed_melee]; + + return speed; + }; static armour_calc = function() { armour_rating = 0; diff --git a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml index b24a39a08d..fee30a8786 100644 --- a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml +++ b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml @@ -182,6 +182,15 @@ function scr_player_combat_weapon_stacks() { } } } + if (is_struct(mobi_item) && mobi_item.has_tag("bike")) { + var stack_index = find_stack_index("Speed Force", head_role, unit); + if (stack_index > -1) { + add_data_to_stack(stack_index, unit.speed_force(), false, head_role, unit); + if (head_role) { + player_head_role_stack(stack_index, unit); + } + } + } if (is_struct(mobi_item)) { add_second_profiles_to_stack(mobi_item); diff --git a/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml b/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml index c902f4b290..9a7909347a 100644 --- a/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml +++ b/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml @@ -74,6 +74,7 @@ function scr_ui_formation_bars() { if (temp[4800 + bar] > 10) { bat_deva_for[bar] = 1; bat_assa_for[bar] = 4; + bat_bike_for[bar] = 4; bat_tact_for[bar] = 2; bat_vete_for[bar] = 2; bat_hire_for[bar] = 3; From 6be368fa649e72864a24fd14fd8c941c11245113 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:23:01 +0200 Subject: [PATCH 27/55] feat: unique role support for equipment checks --- scripts/scr_squads/scr_squads.gml | 44 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 80900e767d..fc94b06233 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -45,13 +45,11 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co squad_type = target_squad.type; squad_unit_types = squad.find_squad_unit_types(); full_squad_data = obj_ini.squad_types[$ squad_type]; - //build a map from JSON key role_key_to_actual = {}; for (var _i = 0; _i < array_length(squad_unit_types); _i++) { - var _key = squad_unit_types[_i]; - var _def = full_squad_data[$ _key]; - var _actual = struct_exists(_def, "role") ? _def.role : _key; - role_key_to_actual[$ _key] = _actual; + var _key = squad_unit_types[_i]; + var _def = full_squad_data[$ _key]; + role_key_to_actual[$ _key] = struct_exists(_def, "role") ? _def.role : _key; } unit_role = ""; members_UnitGroup = squad.get_members(true); @@ -92,17 +90,23 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co } } } - // Clear managed weapon slots on every member so pre-initialization - // defaults (e.g. a Bolt Pistol carried before squad assignment) don't - // interfere with the encumbrance check during loadout assignment. - var _all_members = target_squad.get_members(true); - for (var _mi = 0; _mi < _all_members.number(); _mi++) { - var _u = _all_members.units[_mi]; - for (var _s = 0; _s < array_length(_weapon_slots); _s++) { - if (struct_exists(_managed_slots, _weapon_slots[_s])) { - var _clear = {}; - _clear[$ _weapon_slots[_s]] = ""; - _u.alter_equipment(_clear, false, false); + // Clear managed weapon slots only on members whose role defines a loadout, + // so pre-initialization defaults don't interfere with the encumbrance check. + // Roles without a loadout (e.g. Captain, Ancient) are skipped entirely. + // Search by key name (e.g. "Terminator") not the "role" field rename + // (e.g. "Assault Terminator") — marines are stored under the key name. + for (var _ri = 0; _ri < array_length(squad_unit_types); _ri++) { + var _role_key = squad_unit_types[_ri]; + if (!struct_exists(full_squad_data[$ _role_key], "loadout")) continue; + var _role_members = members_UnitGroup.get_from({roles: [_role_key, role_key_to_actual[$ _role_key]]}); + while (_role_members.number() > 0) { + var _u = _role_members.pop(); + for (var _s = 0; _s < array_length(_weapon_slots); _s++) { + if (struct_exists(_managed_slots, _weapon_slots[_s])) { + var _clear = {}; + _clear[$ _weapon_slots[_s]] = ""; + _u.alter_equipment(_clear, false, false); + } } } } @@ -230,7 +234,7 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co static equip_loudouts_specific_equip_slot = function(){ var _actual_role = role_key_to_actual[$ unit_role]; - var _members_with_role = members_UnitGroup.get_from({role: _actual_role}); + var _members_with_role = members_UnitGroup.get_from({roles: [unit_role, _actual_role]}); if (!struct_exists(current_unit_squad_data, "loadout")) { return; } @@ -238,7 +242,7 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co var _loudouts = current_unit_squad_data[$ "loadout"]; while (_members_with_role.number() > 0) { _unit = _members_with_role.pop(); - if (_unit.role() != _actual_role) { + if (_unit.role() != unit_role && _unit.role() != _actual_role) { continue; } @@ -272,11 +276,11 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co // ] static equip_random_pick_for_role = function(pick_options) { var _actual_role = role_key_to_actual[$ unit_role]; - var _members_with_role = members_UnitGroup.get_from({role: _actual_role}); + var _members_with_role = members_UnitGroup.get_from({roles: [unit_role, _actual_role]}); while (_members_with_role.number() > 0) { var _unit = _members_with_role.pop(); if (array_contains(ignore_units, _unit.uid)) continue; - if (_unit.role() != _actual_role) continue; + if (_unit.role() != unit_role && _unit.role() != _actual_role) continue; // Pick a random loadout category var _chosen = pick_options[irandom(array_length(pick_options) - 1)]; From 3b8cc5952532a07cbf1fc2ecd614fdd590a7686c Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:23:55 +0200 Subject: [PATCH 28/55] feat: LW final squad change pre-testing --- datafiles/main/squads/lightning_warriors.json | 3 +- .../scr_initialize_custom.gml | 38 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/datafiles/main/squads/lightning_warriors.json b/datafiles/main/squads/lightning_warriors.json index f4914d250f..41f6993946 100644 --- a/datafiles/main/squads/lightning_warriors.json +++ b/datafiles/main/squads/lightning_warriors.json @@ -150,7 +150,7 @@ "company": 10, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "scout_squad", "proportion": 5 }, + { "squad": "scout_squad", "proportion": 1 }, { "squad": "bike_squad", "proportion": 4 } ] } @@ -219,6 +219,7 @@ "company": 10, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "scout_squad", "proportion": 1 }, { "squad": "bike_squad", "proportion": 4 } ] } diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 27acd9e887..772425c60f 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -2358,22 +2358,16 @@ function scr_initialize_custom() { /// comp 10: tac 40: scout 50; if (squad_distribution == 1 || squad_distribution == 3) { if (_coy.coy >= 2 && _coy.coy <= 9) { - // Scout distribution logic for equal_scouts (sd==2) and equal_spescout (sd==3). + // Scout distribution logic for equal_spescout (sd==3). // - // For standard equal_scouts (sd==2) or equal_spescout without LW (sd==3, !_lw): - // 10 scouts are moved from the 10th company bank into each battle company so - // that the JSON template's scout_squad proportion can fill at game start. - // - // For LW + equal_spescout (sd==3, _lw==true) scouts are NOT drained from 10th: - // - 10th company retains its full scout bank (~90) so its own scout_squad - // proportions (from the equal_spescout override) fill correctly. - // - Companies 2-9 receive no scout marines at game start; the scout_squad(1) - // proportion in their JSON template simply produces no squads initially. - // Scouts can be recruited into those companies naturally during the campaign. + // 10 scouts are moved from the 10th company bank into each battle company (2-9) + // so that the JSON template's scout_squad proportion can fill at game start. + // This applies regardless of whether Lightning Warriors is active; LW+equal_spescout + // distributes 10 scouts per company just like the non-LW case. // // Note: for LW + equal_scouts (sd==2) this branch is not reached at all because // sd==2 does not satisfy (sd==1 || sd==3), so it falls to the else block below. - if (equal_scouts && !(squad_distribution == 3 && _lw)) { + if (equal_scouts) { if (companies.tenth.scouts > 10) { //theoretically this keeps track of moving scouts from the bank of them in 10th _coy.scouts = 10; @@ -2390,13 +2384,10 @@ function scr_initialize_custom() { _coy.assaults = assault; _coy.devastators = devastator; } - // For equal_scouts or equal_spescout (without LW), replace the scouts that were moved - // out of 10th company with an equivalent number of tacticals so the 10th's total - // marine count stays consistent. _moved_scouts tracks the cumulative scouts transferred - // to other companies during the loop above. - // Skipped for LW + equal_spescout (sd==3, _lw==true) because no scouts were moved in - // that path; 10th company retains its scouts and needs no tactical swap. - if (equal_scouts && _coy.coy == 10 && !(squad_distribution == 3 && _lw)) { + // Replace the scouts that were moved out of 10th company with an equivalent number + // of tacticals so the 10th's total marine count stays consistent. + // _moved_scouts tracks the cumulative scouts transferred to other companies above. + if (equal_scouts && _coy.coy == 10) { // theoretically this swaps moved scouts with tacticals _coy.tacticals = _moved_scouts; } @@ -2406,10 +2397,12 @@ function scr_initialize_custom() { if (_coy.coy >= 2 && _coy.coy <= 5) { if (equal_scouts) { if (companies.tenth.scouts > 10) { - _coy.scouts = 10; + // LW needs 20 scouts per company to fill proportion:2 scout squads. + // Non-LW equal_scouts uses 10 (proportion:1). + _coy.scouts = _lw ? 20 : 10; _moved_scouts += _coy.scouts; _coy.tacticals = max(0, (_coy.total - (assault + devastator + _coy.scouts))); - companies.tenth.scouts -= _moved_scouts; + companies.tenth.scouts -= _coy.scouts; // fix: subtract this company's amount, not the cumulative total } else { // if 10th is run out somehow, revert to normal behaviour _coy.tacticals = max(0, (_coy.total - (assault + devastator))); @@ -2466,7 +2459,8 @@ function scr_initialize_custom() { } if (real(_coy.coy) == 10 && equal_scouts) { _coy.tacticals = _moved_scouts; - _coy.scouts = _coy.scouts - _coy.tacticals; + // _coy.scouts is already the correct bank remainder after per-company + // deductions above — do not subtract tacticals again or it double-counts. } } From 77a968c8f2a4ba387541b0a031ebc0d7df813e4c Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:24:23 +0200 Subject: [PATCH 29/55] feat: final touches for the new Bike properties --- scripts/scr_marine_struct/scr_marine_struct.gml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/scr_marine_struct/scr_marine_struct.gml b/scripts/scr_marine_struct/scr_marine_struct.gml index 2c3eecfe46..05e93a16e4 100644 --- a/scripts/scr_marine_struct/scr_marine_struct.gml +++ b/scripts/scr_marine_struct/scr_marine_struct.gml @@ -1668,13 +1668,13 @@ function TTRPG_stats(faction, comp, mar, class = "marine", other_spawn_data = {} var _melee_attack = melee_damage_data[0]; var _melee_weapon = melee_damage_data[3]; - var speed = new EquipmentStruct({attack: _melee_attack * 2, name: "Speed Force", range: 14, ammo: 12, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); + var speedf = new EquipmentStruct({attack: _melee_attack * 2, name: "Speed Force", range: 14, ammo: 12, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); - var speed_melee = new EquipmentStruct({attack: _melee_attack * 4, name: "Speed Force(M)", range: 1, ammo: 16, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); + var speedf_melee = new EquipmentStruct({attack: _melee_attack * 4, name: "Speed Force(M)", range: 1, ammo: 16, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); - speed.second_profiles = [speed_melee]; + speedf.second_profiles = [speedf_melee]; - return speed; + return speedf; }; static armour_calc = function() { From 8889412899d35eb5bae4117c8cc72fa08da04f42 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:28:24 +0200 Subject: [PATCH 30/55] fix: Terminator squad count --- datafiles/main/squads/base_squads.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index a68c2e5aeb..06043b86fc 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -93,7 +93,7 @@ } }, "Terminator": { - "max": 4, + "max": 9, "min": 2, "loadout": { "required": { @@ -138,7 +138,7 @@ } }, "Terminator": { - "max": 4, + "max": 9, "min": 2, "role": "Assault Terminator", "loadout": { From f1beee907d8d1a3529f04bb4ad1996d27120337e Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:02:53 +0200 Subject: [PATCH 31/55] fix: terminator equipment issues --- datafiles/main/squads/base_squads.json | 39 +++++++++++++++++--------- scripts/scr_squads/scr_squads.gml | 23 ++++++--------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index 06043b86fc..f92602c28a 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -102,13 +102,20 @@ }, "option": { "wep1": [ - [["Power Fist", "Chainfist"], 4] - ], - "wep2": [ - ["{WEAPON_LIST_RANGED_COMBI}", 3], - ["{WEAPON_LIST_RANGED_HEAVY_TERMINATOR}", 1] + [["Power Fist", "Chainfist"], 7, {"wep2": "Storm Bolter"}] //if option group 3rd element is struct random pick ignores it ] - } + }, + "random_pick": [ + { + "wep1": ["Power Fist", "Chainfist"], + "wep2": "Storm Bolter", + "mobi": "Cyclone Missile System" + }, + { + "wep1": ["Power Fist", "Chainfist"], + "wep2": ["Heavy Flamer", "Assault Cannon"] + } + ] } }, "type_data": { @@ -143,14 +150,19 @@ "role": "Assault Terminator", "loadout": { "required": { - "wep1": ["Thunder Hammer", 1], - "wep2": ["Storm Shield", 1] + "wep1": ["", 0], + "wep2": ["", 0] }, - "option": { - "wep1": [ - [["Lightning Claw"], 3, { "wep2": "Lightning Claw" }] - ] - } + "random_pick": [ + { + "wep1": ["Thunder Hammer"], + "wep2": ["Storm Shield"] + }, + { + "wep1": ["Lightning Claw"], + "wep2": ["Lightning Claw"] + } + ] } }, "type_data": { @@ -400,6 +412,7 @@ "type_data": { "display_data": "Scout {squad_name}", "class": ["scout"], + "base": "scout", "formation_options": [ "scout", "tactical", diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index fc94b06233..0a7f062ea3 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -61,15 +61,16 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co target_squad.update_fulfilment(); static sort = function() { - // Build the set of weapon slots this squad's loadout actively manages, - // across all roles (required + option + random_pick). - // Only wep1/wep2 are cleared — other slots (armour, gear, mobi) are - // intentionally left as-is since they may carry meaningful defaults. + // For each role, clear only the weapon slots that role's loadout actively manages + // (required + option + random_pick). Slots not mentioned in a role's own loadout + // are left untouched — e.g. a role that only defines armour won't have wep1/wep2 cleared. var _weapon_slots = ["wep1", "wep2"]; - var _managed_slots = {}; for (var _ri = 0; _ri < array_length(squad_unit_types); _ri++) { - var _role_data = full_squad_data[$ squad_unit_types[_ri]]; + var _role_key = squad_unit_types[_ri]; + var _role_data = full_squad_data[$ _role_key]; if (!struct_exists(_role_data, "loadout")) continue; + + var _managed_slots = {}; var _ld = _role_data.loadout; if (struct_exists(_ld, "required")) { var _slots = struct_get_names(_ld.required); @@ -89,15 +90,7 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co _managed_slots[$ _slots[_s]] = true; } } - } - // Clear managed weapon slots only on members whose role defines a loadout, - // so pre-initialization defaults don't interfere with the encumbrance check. - // Roles without a loadout (e.g. Captain, Ancient) are skipped entirely. - // Search by key name (e.g. "Terminator") not the "role" field rename - // (e.g. "Assault Terminator") — marines are stored under the key name. - for (var _ri = 0; _ri < array_length(squad_unit_types); _ri++) { - var _role_key = squad_unit_types[_ri]; - if (!struct_exists(full_squad_data[$ _role_key], "loadout")) continue; + var _role_members = members_UnitGroup.get_from({roles: [_role_key, role_key_to_actual[$ _role_key]]}); while (_role_members.number() > 0) { var _u = _role_members.pop(); From 1fb45fcf1a115be6c38640fcb4103c696fa7804d Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:02:22 +0200 Subject: [PATCH 32/55] fix: merge leftovers --- ChapterMaster.yyp | 24 ++----------------- objects/obj_formation_bar/Create_0.gml | 3 +++ .../scr_ui_formation_bars.gml | 3 ++- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/ChapterMaster.yyp b/ChapterMaster.yyp index d17bed6032..4ca4b505d3 100644 --- a/ChapterMaster.yyp +++ b/ChapterMaster.yyp @@ -659,7 +659,6 @@ {"id":{"name":"obj_en_pulse","path":"objects/obj_en_pulse/obj_en_pulse.yy",},}, {"id":{"name":"obj_en_round","path":"objects/obj_en_round/obj_en_round.yy",},}, {"id":{"name":"obj_en_ship","path":"objects/obj_en_ship/obj_en_ship.yy",},}, - {"id":{"name":"obj_enemy_leftest","path":"objects/obj_enemy_leftest/obj_enemy_leftest.yy",},}, {"id":{"name":"obj_enunit","path":"objects/obj_enunit/obj_enunit.yy",},}, {"id":{"name":"obj_event_log","path":"objects/obj_event_log/obj_event_log.yy",},}, {"id":{"name":"obj_event","path":"objects/obj_event/obj_event.yy",},}, @@ -679,12 +678,10 @@ {"id":{"name":"obj_main_menu_buttons","path":"objects/obj_main_menu_buttons/obj_main_menu_buttons.yy",},}, {"id":{"name":"obj_main_menu","path":"objects/obj_main_menu/obj_main_menu.yy",},}, {"id":{"name":"obj_managment_panel","path":"objects/obj_managment_panel/obj_managment_panel.yy",},}, - {"id":{"name":"obj_marine","path":"objects/obj_marine/obj_marine.yy",},}, {"id":{"name":"obj_mass_equip","path":"objects/obj_mass_equip/obj_mass_equip.yy",},}, {"id":{"name":"obj_ncombat","path":"objects/obj_ncombat/obj_ncombat.yy",},}, {"id":{"name":"obj_new_button","path":"objects/obj_new_button/obj_new_button.yy",},}, {"id":{"name":"obj_nfort","path":"objects/obj_nfort/obj_nfort.yy",},}, - {"id":{"name":"obj_ork","path":"objects/obj_ork/obj_ork.yy",},}, {"id":{"name":"obj_p_assra","path":"objects/obj_p_assra/obj_p_assra.yy",},}, {"id":{"name":"obj_p_capital","path":"objects/obj_p_capital/obj_p_capital.yy",},}, {"id":{"name":"obj_p_cruiser","path":"objects/obj_p_cruiser/obj_p_cruiser.yy",},}, @@ -694,8 +691,6 @@ {"id":{"name":"obj_p_ship","path":"objects/obj_p_ship/obj_p_ship.yy",},}, {"id":{"name":"obj_p_small","path":"objects/obj_p_small/obj_p_small.yy",},}, {"id":{"name":"obj_p_th","path":"objects/obj_p_th/obj_p_th.yy",},}, - {"id":{"name":"obj_p1_bullet_miss","path":"objects/obj_p1_bullet_miss/obj_p1_bullet_miss.yy",},}, - {"id":{"name":"obj_p1_bullet","path":"objects/obj_p1_bullet/obj_p1_bullet.yy",},}, {"id":{"name":"obj_persistent","path":"objects/obj_persistent/obj_persistent.yy",},}, {"id":{"name":"obj_planet_map","path":"objects/obj_planet_map/obj_planet_map.yy",},}, {"id":{"name":"obj_pnunit","path":"objects/obj_pnunit/obj_pnunit.yy",},}, @@ -732,25 +727,10 @@ {"id":{"name":"rm_tutorial","path":"rooms/rm_tutorial/rm_tutorial.yy",},}, {"id":{"name":"__global_object_depths","path":"scripts/__global_object_depths/__global_object_depths.yy",},}, {"id":{"name":"__gml_pragma_global","path":"scripts/__gml_pragma_global/__gml_pragma_global.yy",},}, - {"id":{"name":"__init_action","path":"scripts/__init_action/__init_action.yy",},}, - {"id":{"name":"__init_d3d","path":"scripts/__init_d3d/__init_d3d.yy",},}, - {"id":{"name":"__init_view","path":"scripts/__init_view/__init_view.yy",},}, {"id":{"name":"__init","path":"scripts/__init/__init.yy",},}, - {"id":{"name":"__view_get","path":"scripts/__view_get/__view_get.yy",},}, - {"id":{"name":"__view_set_internal","path":"scripts/__view_set_internal/__view_set_internal.yy",},}, - {"id":{"name":"__view_set","path":"scripts/__view_set/__view_set.yy",},}, - {"id":{"name":"action_another_room","path":"scripts/action_another_room/action_another_room.yy",},}, - {"id":{"name":"action_color","path":"scripts/action_color/action_color.yy",},}, - {"id":{"name":"action_if_number","path":"scripts/action_if_number/action_if_number.yy",},}, - {"id":{"name":"action_if_variable","path":"scripts/action_if_variable/action_if_variable.yy",},}, - {"id":{"name":"action_kill_object","path":"scripts/action_kill_object/action_kill_object.yy",},}, - {"id":{"name":"action_restart_game","path":"scripts/action_restart_game/action_restart_game.yy",},}, - {"id":{"name":"action_set_alarm","path":"scripts/action_set_alarm/action_set_alarm.yy",},}, - {"id":{"name":"action_set_relative","path":"scripts/action_set_relative/action_set_relative.yy",},}, {"id":{"name":"Armamentarium","path":"scripts/Armamentarium/Armamentarium.yy",},}, {"id":{"name":"ColourItem","path":"scripts/ColourItem/ColourItem.yy",},}, {"id":{"name":"ColourPicker","path":"scripts/ColourPicker/ColourPicker.yy",},}, - {"id":{"name":"d3d_set_fog","path":"scripts/d3d_set_fog/d3d_set_fog.yy",},}, {"id":{"name":"DebugView","path":"scripts/DebugView/DebugView.yy",},}, {"id":{"name":"DiploBasicNodes","path":"scripts/DiploBasicNodes/DiploBasicNodes.yy",},}, {"id":{"name":"DiploCommonComponents","path":"scripts/DiploCommonComponents/DiploCommonComponents.yy",},}, @@ -758,7 +738,6 @@ {"id":{"name":"DiscordPool","path":"scripts/DiscordPool/DiscordPool.yy",},}, {"id":{"name":"DiscordWebhook","path":"scripts/DiscordWebhook/DiscordWebhook.yy",},}, {"id":{"name":"draw_line_dashed","path":"scripts/draw_line_dashed/draw_line_dashed.yy",},}, - {"id":{"name":"draw_set_blend_mode","path":"scripts/draw_set_blend_mode/draw_set_blend_mode.yy",},}, {"id":{"name":"ErrorHandler","path":"scripts/ErrorHandler/ErrorHandler.yy",},}, {"id":{"name":"exp_and_exp_growth","path":"scripts/exp_and_exp_growth/exp_and_exp_growth.yy",},}, {"id":{"name":"explode_script","path":"scripts/explode_script/explode_script.yy",},}, @@ -938,6 +917,7 @@ {"id":{"name":"scr_save_chapter","path":"scripts/scr_save_chapter/scr_save_chapter.yy",},}, {"id":{"name":"scr_save","path":"scripts/scr_save/scr_save.yy",},}, {"id":{"name":"scr_scrollbar","path":"scripts/scr_scrollbar/scr_scrollbar.yy",},}, + {"id":{"name":"scr_secret_lair_view","path":"scripts/scr_secret_lair_view/scr_secret_lair_view.yy",},}, {"id":{"name":"scr_serialization_functions","path":"scripts/scr_serialization_functions/scr_serialization_functions.yy",},}, {"id":{"name":"scr_shader_initialize","path":"scripts/scr_shader_initialize/scr_shader_initialize.yy",},}, {"id":{"name":"scr_ship_battle","path":"scripts/scr_ship_battle/scr_ship_battle.yy",},}, @@ -974,9 +954,9 @@ {"id":{"name":"scr_ui_display_weapons","path":"scripts/scr_ui_display_weapons/scr_ui_display_weapons.yy",},}, {"id":{"name":"scr_ui_formation_bars","path":"scripts/scr_ui_formation_bars/scr_ui_formation_bars.yy",},}, {"id":{"name":"scr_ui_manage","path":"scripts/scr_ui_manage/scr_ui_manage.yy",},}, - {"id":{"name":"scr_ui_popup","path":"scripts/scr_ui_popup/scr_ui_popup.yy",},}, {"id":{"name":"scr_ui_refresh","path":"scripts/scr_ui_refresh/scr_ui_refresh.yy",},}, {"id":{"name":"scr_ui_settings","path":"scripts/scr_ui_settings/scr_ui_settings.yy",},}, + {"id":{"name":"scr_ui_tooltip","path":"scripts/scr_ui_tooltip/scr_ui_tooltip.yy",},}, {"id":{"name":"scr_unit_detail_text","path":"scripts/scr_unit_detail_text/scr_unit_detail_text.yy",},}, {"id":{"name":"scr_unit_equip_functions","path":"scripts/scr_unit_equip_functions/scr_unit_equip_functions.yy",},}, {"id":{"name":"scr_unit_quick_find_pane","path":"scripts/scr_unit_quick_find_pane/scr_unit_quick_find_pane.yy",},}, diff --git a/objects/obj_formation_bar/Create_0.gml b/objects/obj_formation_bar/Create_0.gml index 14d65d3f8e..fa095551bb 100644 --- a/objects/obj_formation_bar/Create_0.gml +++ b/objects/obj_formation_bar/Create_0.gml @@ -191,6 +191,9 @@ mouse_release = function(){ if (unit_id == 17) { obj_controller.bat_whirl_for[obj_controller.formating] = mah_target.col_parent; } + if (unit_id == 18) { + obj_controller.bat_bike_for[obj_controller.formating] = mah_target.col_parent; + } obj_cursor.dragging = 0; obj_cursor.image_index = 0; diff --git a/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml b/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml index 9a7909347a..971dd3fbf7 100644 --- a/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml +++ b/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml @@ -31,6 +31,7 @@ function scr_ui_formation_bars() { { unit_id: 11, for_arr: bat_drea_for, size: 2, image_index: 11, unit_type: "Dread", tooltip: "Dreadnoughts", tooltip2: "Dreadnoughts are the most durable and tough marines within your chapter. They are best suited for the front lines." }, { unit_id: 12, for_arr: bat_hire_for, size: 1, image_index: 7, unit_type: "???", tooltip: "Hirelings", tooltip2: "Any and all units that you recieve from other factions are placed within this block." }, { unit_id: 16, for_arr: bat_landspee_for, size: 2, image_index: 14, unit_type: "Land Speeder", tooltip: "Land Speeders", tooltip2: "Land Speeders are incredibly agile attack vehicles that offer a light highly mobile heavy weapon platform." }, + { unit_id: 18, for_arr: bat_bike_for, size: 3, image_index: 14, unit_type: "Biker", tooltip: "Bikers", tooltip2: "Bikers are the swift deathbringers of the Astartes Chapters. Descending upon a foe with speed impossible to follow by mortal means, they decimate enemies in close range by a powerful assortment of melee weapons or heavy ranged weapons on their Attack Bikes." }, ]; var _attack_only_options = [ { unit_id: 13, for_arr: bat_rhin_for, size: 4, image_index: 12, unit_type: "Rhino", tooltip: "Rhinos", tooltip2: "Rhinos offer protection for units behind them but are not well armoured and lacking in firepower." }, @@ -48,7 +49,7 @@ function scr_ui_formation_bars() { temp[ui_formations_data.te] = 0; temp[ui_formations_data.te + 100] = 0; - for (var unit_id = 0; unit_id <= 17; unit_id++) { + for (var unit_id = 0; unit_id <= 18; unit_id++) { for (var _i = 0; _i < array_length(_bar_configs); _i++) { var _cfg = _bar_configs[_i]; From 1127311cce99195a4889326ee3ba0fdb38307d9b Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:15:59 +0200 Subject: [PATCH 33/55] fix: bike formation block --- scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml b/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml index 971dd3fbf7..07b3a88f8b 100644 --- a/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml +++ b/scripts/scr_ui_formation_bars/scr_ui_formation_bars.gml @@ -31,7 +31,7 @@ function scr_ui_formation_bars() { { unit_id: 11, for_arr: bat_drea_for, size: 2, image_index: 11, unit_type: "Dread", tooltip: "Dreadnoughts", tooltip2: "Dreadnoughts are the most durable and tough marines within your chapter. They are best suited for the front lines." }, { unit_id: 12, for_arr: bat_hire_for, size: 1, image_index: 7, unit_type: "???", tooltip: "Hirelings", tooltip2: "Any and all units that you recieve from other factions are placed within this block." }, { unit_id: 16, for_arr: bat_landspee_for, size: 2, image_index: 14, unit_type: "Land Speeder", tooltip: "Land Speeders", tooltip2: "Land Speeders are incredibly agile attack vehicles that offer a light highly mobile heavy weapon platform." }, - { unit_id: 18, for_arr: bat_bike_for, size: 3, image_index: 14, unit_type: "Biker", tooltip: "Bikers", tooltip2: "Bikers are the swift deathbringers of the Astartes Chapters. Descending upon a foe with speed impossible to follow by mortal means, they decimate enemies in close range by a powerful assortment of melee weapons or heavy ranged weapons on their Attack Bikes." }, + { unit_id: 18, for_arr: bat_bike_for, size: 2, image_index: 14, unit_type: "Biker", tooltip: "Bikers", tooltip2: "Bikers are the swift deathbringers of the Astartes Chapters. Descending upon a foe with speed impossible to follow by mortal means, they decimate enemies in close range by a powerful assortment of melee weapons or heavy ranged weapons on their Attack Bikes." }, ]; var _attack_only_options = [ { unit_id: 13, for_arr: bat_rhin_for, size: 4, image_index: 12, unit_type: "Rhino", tooltip: "Rhinos", tooltip2: "Rhinos offer protection for units behind them but are not well armoured and lacking in firepower." }, From 4170301ef567e79eb3cd14374ef91dc646bac73d Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:51:44 +0200 Subject: [PATCH 34/55] feat: Bikes balance changes --- datafiles/data/mobility.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index 711881b25a..cb214a99a8 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -12,6 +12,7 @@ "master_crafted": 30, "standard": 25 }, + "ranged_hands": -1, "melee_mod":{ "artifact": 30, "master_crafted": 25, @@ -38,10 +39,11 @@ }, "description": "A robust bike that can propel an Astartes at very high speeds. Boasts highly responsive controls that allow for fluid movement on the battlefield and respectable Twin-Linked Bolters for offensive action. Sports an additional sidecar with a heavy weapon of choice.", "hp_mod": { - "artifact": 50, - "master_crafted": 40, - "standard": 35 + "artifact": 150, + "master_crafted": 120, + "standard": 100 }, + "ranged_hands": 1, "melee_mod":{ "artifact": 20, "master_crafted": 15, From 84d93078cd00818f277cbd5a855c9b1ec0f210f1 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:04:06 +0200 Subject: [PATCH 35/55] refactor: opening game text composition --- objects/obj_controller/Create_0.gml | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/objects/obj_controller/Create_0.gml b/objects/obj_controller/Create_0.gml index b04ae145e5..a672c521b9 100644 --- a/objects/obj_controller/Create_0.gml +++ b/objects/obj_controller/Create_0.gml @@ -1434,7 +1434,7 @@ temp[33] = string_upper(scr_thought()); // Thought of the day LOGGER.info("Game start welcoming message"); var njm = 34, com = 0, vih = 0, word = "", masta = 0, forga = 0, chapla = 0, apa = 0, liba = 0, techa = 0, libra = 0, coda = 0, lexa = 0, apotha = 0, old_dudes = 0; -var honoh = 0, termi = 0, veter = 0, capt = 0, chap = 0, apoth = 0, stand = 0, dread = 0, champ = 0, tact = 0, assa = 0, deva = 0, rhino = 0, speeder = 0, raider = 0, standard = 0, bike = 0, scou = 0, whirl = 0, pred = 0, lib = 0, serg = 0, vet_serg = 0; +var honoh = 0, termi = 0, veter = 0, capt = 0, chap = 0, apoth = 0, stand = 0, dread = 0, champ = 0, tact = 0, assa = 0, deva = 0, rhino = 0, speeder = 0, raider = 0, standard = 0, bike = 0, scou = 0, whirl = 0, pred = 0, lib = 0, serg = 0, vet_serg = 0, bikers = 0, attack_bikers = 0; for (var mm = 0; mm <= 100; mm++) { if (obj_ini.role[com][mm] == obj_ini.role[100][eROLE.CHAPTERMASTER]) { masta = 1; @@ -1545,12 +1545,14 @@ for (var company = 0; company < 10; company++) { standard = 0; bike = 0; scou = 0; + bikers = 0; + attack_bikers = 0; whirl = 0; pred = 0; lib = 0; serg = 0; vet_serg = 0; - for (var mm = 1; mm <= 400; mm++) { + for (var mm = 0; mm <= 400; mm++) { if (obj_ini.role[com][mm] == obj_ini.role[100][3]) { veter += 1; } @@ -1570,21 +1572,27 @@ for (var company = 0; company < 10; company++) { champ += 1; } - if (obj_ini.role[com][mm] == obj_ini.role[100][8]) { + if (obj_ini.role[com][mm] == obj_ini.role[100][8] || obj_ini.role[com][mm] == "Tactical Sergeant") { tact += 1; } - if (obj_ini.role[com][mm] == obj_ini.role[100][9]) { + if (obj_ini.role[com][mm] == obj_ini.role[100][9] || obj_ini.role[com][mm] == "Devastator Sergeant") { deva += 1; } - if (obj_ini.role[com][mm] == obj_ini.role[100][10]) { + if (obj_ini.role[com][mm] == obj_ini.role[100][10] || obj_ini.role[com][mm] == "Assault Sergeant") { assa += 1; } if (obj_ini.role[com][mm] == obj_ini.role[100][11]) { standard += 1; } - if (obj_ini.role[com][mm] == obj_ini.role[100][12]) { + if (obj_ini.role[com][mm] == obj_ini.role[100][12] || obj_ini.role[com][mm] == "Scout Sergeant") { scou += 1; } + if (obj_ini.role[com][mm] == obj_ini.role[100][13] || obj_ini.role[com][mm] == "Biker Sergeant") { + bikers += 1; + } + if (obj_ini.role[com][mm] == obj_ini.role[100][20] || obj_ini.role[com][mm] == "Attack Bike Sergeant") { + attack_bikers += 1; + } if (obj_ini.role[com][mm] == obj_ini.role[100][14]) { chap += 1; @@ -1680,6 +1688,12 @@ for (var company = 0; company < 10; company++) { if (scou > 0) { temp[njm] += $", {string_plural_count(obj_ini.role[100][12], scou)}"; } + if (bikers > 0) { + temp[njm] += $", {string_plural_count(obj_ini.role[100][13], bikers)}"; + } + if (attack_bikers > 0) { + temp[njm] += $", {string_plural_count(obj_ini.role[100][20], attack_bikers)}"; + } if (dread > 0) { temp[njm] += $", {string_plural_count(obj_ini.role[100][6], dread)}"; } From ad5b436e329d0900902a4866fa8e43c50499ac6f Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Sat, 20 Jun 2026 23:04:29 +0200 Subject: [PATCH 36/55] feat: Lightning Warriors Beta 1.0 --- objects/obj_creation/Create_0.gml | 3 ++- objects/obj_popup/Create_0.gml | 26 +++++++++++++++++++ scripts/is_specialist/is_specialist.gml | 2 ++ scripts/macros/macros.gml | 2 ++ scripts/scr_chapter_new/scr_chapter_new.gml | 2 ++ scripts/scr_creation/scr_creation.gml | 2 +- .../scr_initialize_custom.gml | 7 +++-- .../scr_popup_functions.gml | 2 +- scripts/scr_promote/scr_promote.gml | 10 +++++++ scripts/scr_ui_settings/scr_ui_settings.gml | 2 ++ 10 files changed, 53 insertions(+), 5 deletions(-) diff --git a/objects/obj_creation/Create_0.gml b/objects/obj_creation/Create_0.gml index 0ecaca9b9d..7a72eb5f49 100644 --- a/objects/obj_creation/Create_0.gml +++ b/objects/obj_creation/Create_0.gml @@ -466,13 +466,14 @@ load_default_gear(eROLE.DEVASTATOR, "Devastator", "", "Combat Knife", STR_ANY_PO load_default_gear(eROLE.ASSAULT, "Assault", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "Jump Pack", ""); load_default_gear(eROLE.ANCIENT, "Ancient", "Company Standard", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", ""); load_default_gear(eROLE.SCOUT, "Scout", "Bolter", "Combat Knife", "Scout Armour", "", ""); +load_default_gear(eROLE.BIKER, "Biker", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "Bike", ""); load_default_gear(eROLE.CHAPLAIN, "Chaplain", "Crozius Arcanum", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Rosarius"); load_default_gear(eROLE.APOTHECARY, "Apothecary", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Narthecium"); load_default_gear(eROLE.TECHMARINE, "Techmarine", "Power Axe", "Bolt Pistol", "Artificer Armour", "Servo-arm", ""); load_default_gear(eROLE.LIBRARIAN, "Librarian", "Force Staff", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Psychic Hood"); load_default_gear(eROLE.SERGEANT, "Sergeant", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", ""); load_default_gear(eROLE.VETERANSERGEANT, "Veteran Sergeant", "Chainsword", "Plasma Pistol", STR_ANY_POWER_ARMOUR, "", ""); - +load_default_gear(eROLE.ATTACK_BIKER, "Attack Biker", "Heavy Bolter", "Chainsword", STR_ANY_POWER_ARMOUR, "Attack Bike", ""); if (global.restart > 0) { fade_in = -1; slate1 = -1; diff --git a/objects/obj_popup/Create_0.gml b/objects/obj_popup/Create_0.gml index 633e6523b4..c8d78e0391 100644 --- a/objects/obj_popup/Create_0.gml +++ b/objects/obj_popup/Create_0.gml @@ -214,6 +214,24 @@ get_unit_promotion_options = function() { } } + if (array_contains([2, 3, 4, 5], target_comp)) { + i += 1; + role_name[i] = obj_ini.role[100][13]; //bikers + role_exp[i] = company_promote_data[target_comp].exp; + if (obj_controller.command_set[2] == 0) { + role_exp[i] = 0; + } + } + + if (array_contains([2, 3, 4, 5], target_comp)) { + i += 1; + role_name[i] = obj_ini.role[100][20]; //attack bikers + role_exp[i] = company_promote_data[target_comp].exp; + if (obj_controller.command_set[2] == 0) { + role_exp[i] = 0; + } + } + if (target_comp == 1) { i += 1; role_name[i] = obj_ini.role[100][4]; //terminators @@ -255,6 +273,14 @@ get_unit_promotion_options = function() { role_name[i] = obj_ini.role[100][12]; //scouts role_exp[i] = 0; + i += 1; + role_name[i] = obj_ini.role[100][13]; //bikers + role_exp[i] = 0; + + i += 1; + role_name[i] = obj_ini.role[100][20]; //attack bikers + role_exp[i] = 0; + i += 1; role_name[i] = obj_ini.role[100][3]; //veterans role_exp[i] = 0; diff --git a/scripts/is_specialist/is_specialist.gml b/scripts/is_specialist/is_specialist.gml index 0581a0c37b..9b818a1534 100644 --- a/scripts/is_specialist/is_specialist.gml +++ b/scripts/is_specialist/is_specialist.gml @@ -172,6 +172,8 @@ function role_groups(group, include_trainee = false, include_heads = true) { _roles[eROLE.TACTICAL], _roles[eROLE.DEVASTATOR], _roles[eROLE.ASSAULT], + _roles[eROLE.BIKER], + _roles[eROLE.ATTACK_BIKER], _roles[eROLE.SCOUT] ]; break; diff --git a/scripts/macros/macros.gml b/scripts/macros/macros.gml index d5a8c7c01d..2d5de7e11e 100644 --- a/scripts/macros/macros.gml +++ b/scripts/macros/macros.gml @@ -70,12 +70,14 @@ enum eROLE { ASSAULT = 10, ANCIENT = 11, SCOUT = 12, + BIKER = 13, CHAPLAIN = 14, APOTHECARY = 15, TECHMARINE = 16, LIBRARIAN = 17, SERGEANT = 18, VETERANSERGEANT = 19, + ATTACK_BIKER = 20, LANDRAIDER = 50, RHINO = 51, PREDATOR = 52, diff --git a/scripts/scr_chapter_new/scr_chapter_new.gml b/scripts/scr_chapter_new/scr_chapter_new.gml index b91fb3ecad..faef18b5d0 100644 --- a/scripts/scr_chapter_new/scr_chapter_new.gml +++ b/scripts/scr_chapter_new/scr_chapter_new.gml @@ -231,12 +231,14 @@ function scr_chapter_new(chapter_identifier) { load_default_gear(eROLE.ASSAULT, "Assault", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "Jump Pack", ""); load_default_gear(eROLE.ANCIENT, "Ancient", "Company Standard", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", ""); load_default_gear(eROLE.SCOUT, "Scout", "Bolter", "Combat Knife", "Scout Armour", "", ""); + load_default_gear(eROLE.BIKER, "Biker", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR,"Bike",""); load_default_gear(eROLE.CHAPLAIN, "Chaplain", "Crozius Arcanum", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Rosarius"); load_default_gear(eROLE.APOTHECARY, "Apothecary", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Narthecium"); load_default_gear(eROLE.TECHMARINE, "Techmarine", "Power Axe", "Bolt Pistol", "Artificer Armour", "Servo-arm", ""); load_default_gear(eROLE.LIBRARIAN, "Librarian", "Force Staff", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Psychic Hood"); load_default_gear(eROLE.SERGEANT, "Sergeant", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", ""); load_default_gear(eROLE.VETERANSERGEANT, "Veteran Sergeant", "Chainsword", "Plasma Pistol", STR_ANY_POWER_ARMOUR, "", ""); + load_default_gear(eROLE.ATTACK_BIKER, "Attack Biker", "Heavy Bolter", "Chainsword", STR_ANY_POWER_ARMOUR, "Attack Bike", ""); for (var c = 0; c < array_length(obj_creation.all_chapters); c++) { if (chapter_identifier == obj_creation.all_chapters[c].name && obj_creation.all_chapters[c].json == true) { diff --git a/scripts/scr_creation/scr_creation.gml b/scripts/scr_creation/scr_creation.gml index 8f0add29e8..fbccefd23f 100644 --- a/scripts/scr_creation/scr_creation.gml +++ b/scripts/scr_creation/scr_creation.gml @@ -39,7 +39,7 @@ function set_complex_livery_buttons() { function update_creation_roles_radio(start_role = 1) { var _role_data = []; - for (var i = start_role; i <= 19; i++) { + for (var i = start_role; i <= 20; i++) { if (race[100][i] != 0 && role[100][i] != "") { array_push(_role_data, {str1: role[100][i], font: fnt_40k_14b, role_id: i}); } diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 772425c60f..83aafdb05d 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -1482,12 +1482,14 @@ function scr_initialize_custom() { load_default_gear(eROLE.ASSAULT, "Assault", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "Jump Pack", ""); load_default_gear(eROLE.ANCIENT, "Ancient", "Company Standard", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", ""); load_default_gear(eROLE.SCOUT, "Scout", "Bolter", "Combat Knife", "Scout Armour", "", ""); + load_default_gear(eROLE.BIKER, "Biker", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "Bike", ""); load_default_gear(eROLE.CHAPLAIN, "Chaplain", "Crozius Arcanum", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Rosarius"); load_default_gear(eROLE.APOTHECARY, "Apothecary", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Narthecium"); load_default_gear(eROLE.TECHMARINE, "Techmarine", "Omnissian Axe", "Bolt Pistol", _hi_qual_armour, "Servo-arm", ""); load_default_gear(eROLE.LIBRARIAN, "Librarian", "Force Staff", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", "Psychic Hood"); load_default_gear(eROLE.SERGEANT, "Sergeant", "Chainsword", "Bolt Pistol", STR_ANY_POWER_ARMOUR, "", ""); load_default_gear(eROLE.VETERANSERGEANT, "Veteran Sergeant", "Chainsword", "Plasma Pistol", STR_ANY_POWER_ARMOUR, "", ""); + load_default_gear(eROLE.ATTACK_BIKER, "Attack Biker", "Heavy Bolter", "Chainsword", STR_ANY_POWER_ARMOUR, "Attack Bike", ""); obj_ini.role[101] = obj_ini.role[100]; if (scr_has_disadv("Psyker Intolerant")) { race[defaults_slot][eROLE.LIBRARIAN] = 0; @@ -1767,7 +1769,8 @@ function scr_initialize_custom() { ]; var _roles_player = obj_ini.role[100]; var _default_player = obj_ini.role[101]; - for (var i = 1; i < 20; i++) { + var i; + for (i = 1; i < 21; i++) { if (_roles_player[i] == "") { continue; } @@ -1781,7 +1784,7 @@ function scr_initialize_custom() { array_push(_swaps, _set); } - for (var i = 1; i < 20; i++) { + for (i = 1; i < 21; i++) { var _set = {}; var _key = $"wep1[{i}]"; var _val = obj_ini.wep1[100][i]; diff --git a/scripts/scr_popup_functions/scr_popup_functions.gml b/scripts/scr_popup_functions/scr_popup_functions.gml index 6fa2266ffc..309e5ee264 100644 --- a/scripts/scr_popup_functions/scr_popup_functions.gml +++ b/scripts/scr_popup_functions/scr_popup_functions.gml @@ -303,7 +303,7 @@ function calculate_equipment_needs() { req_wep2 = "Company Standard"; req_wep2_num = units; } else { - for (var i = 2; i < 20; i++) { + for (var i = 2; i < 21; i++) { if (obj_ini.role[100][i] == rall) { req_armour = obj_ini.armour[100][i]; req_armour_num = units; diff --git a/scripts/scr_promote/scr_promote.gml b/scripts/scr_promote/scr_promote.gml index 5c050be414..45d05372db 100644 --- a/scripts/scr_promote/scr_promote.gml +++ b/scripts/scr_promote/scr_promote.gml @@ -45,7 +45,9 @@ function setup_promotion_popup() { variable_struct_set(role_squad_equivilances, obj_ini.role[100][8], "tactical_squad"); variable_struct_set(role_squad_equivilances, obj_ini.role[100][9], "devastator_squad"); variable_struct_set(role_squad_equivilances, obj_ini.role[100][10], "assault_squad"); + variable_struct_set(role_squad_equivilances, obj_ini.role[100][13], "bike_squad"); variable_struct_set(role_squad_equivilances, obj_ini.role[100][12], "scout_squad"); + variable_struct_set(role_squad_equivilances, obj_ini.role[100][20], "attack_bike_squad"); variable_struct_set(role_squad_equivilances, obj_ini.role[100][3], "veteran_squad"); variable_struct_set(role_squad_equivilances, obj_ini.role[100][4], "terminator_squad"); @@ -120,6 +122,14 @@ function setup_promotion_popup() { } // End that [i] } // End repeat + if (struct_exists(role_squad_equivilances, role_name[target_role])) { + var _grp = collect_company(target_comp); + var _result = [true]; + while (_result[0]) { + _result = _grp.create_squad(role_squad_equivilances[$ role_name[target_role]]); + } + } + with (obj_controller) { scr_management(1); } diff --git a/scripts/scr_ui_settings/scr_ui_settings.gml b/scripts/scr_ui_settings/scr_ui_settings.gml index 3239d36915..cd36d393b7 100644 --- a/scripts/scr_ui_settings/scr_ui_settings.gml +++ b/scripts/scr_ui_settings/scr_ui_settings.gml @@ -66,6 +66,8 @@ function setup_ui_chapter_settings(){ eROLE.DEVASTATOR, eROLE.ASSAULT, eROLE.SCOUT, + eROLE.BIKER, + eROLE.ATTACK_BIKER, eROLE.SERGEANT, eROLE.VETERANSERGEANT, ]; From 6680fc3695ddfa489d2c7edcc379cbe59caa4e05 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:04:39 +0200 Subject: [PATCH 37/55] feat: Attack Bikes Speed Force is now ranged-based --- datafiles/data/mobility.json | 4 ++-- scripts/scr_marine_struct/scr_marine_struct.gml | 9 ++++++++- .../scr_player_combat_weapon_stacks.gml | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index cb214a99a8..f26e7b7f49 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -50,13 +50,13 @@ "standard": 10 }, "special_properties": [ - "Speed Force" + "Speed Force (Ranged)" ], "second_profiles": [ "Twin Linked Bolters" ], "tags":[ - "bike" + "bike", "sf_ranged" ], "value": 95, "requires_to_forge": ["combi_1"] diff --git a/scripts/scr_marine_struct/scr_marine_struct.gml b/scripts/scr_marine_struct/scr_marine_struct.gml index 05e93a16e4..e69326ae25 100644 --- a/scripts/scr_marine_struct/scr_marine_struct.gml +++ b/scripts/scr_marine_struct/scr_marine_struct.gml @@ -1664,7 +1664,14 @@ function TTRPG_stats(faction, comp, mar, class = "marine", other_spawn_data = {} return wrath; }; - static speed_force = function() { + static speed_force = function(_ranged = false) { + if (_ranged) { + // Attack Bike: scales off the "sidecar's" ranged weapon - single firepower profile, no melee option. + var _attack = ranged_damage_data[0]; + var _weapon = ranged_damage_data[3]; + return new EquipmentStruct({attack: _attack * 2, name: "Speed Force", range: 14, ammo: 12, spli: _weapon.spli, arp: _weapon.arp}, "weapon"); + } + // Standard Bike: scales off melee, dominant melee (M) profile while engaged in front. var _melee_attack = melee_damage_data[0]; var _melee_weapon = melee_damage_data[3]; diff --git a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml index fee30a8786..3415344c34 100644 --- a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml +++ b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml @@ -185,7 +185,7 @@ function scr_player_combat_weapon_stacks() { if (is_struct(mobi_item) && mobi_item.has_tag("bike")) { var stack_index = find_stack_index("Speed Force", head_role, unit); if (stack_index > -1) { - add_data_to_stack(stack_index, unit.speed_force(), false, head_role, unit); + add_data_to_stack(stack_index, unit.speed_force(mobi_item.has_tag("sf_ranged")), false, head_role, unit); if (head_role) { player_head_role_stack(stack_index, unit); } From 42458ca199c4be4222474b025d90bbe78631b60d Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 24 Jun 2026 20:27:48 +0200 Subject: [PATCH 38/55] refactor: speed force --- scripts/scr_marine_struct/scr_marine_struct.gml | 2 +- .../scr_player_combat_weapon_stacks.gml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/scr_marine_struct/scr_marine_struct.gml b/scripts/scr_marine_struct/scr_marine_struct.gml index e69326ae25..ec9f9253ee 100644 --- a/scripts/scr_marine_struct/scr_marine_struct.gml +++ b/scripts/scr_marine_struct/scr_marine_struct.gml @@ -1669,7 +1669,7 @@ function TTRPG_stats(faction, comp, mar, class = "marine", other_spawn_data = {} // Attack Bike: scales off the "sidecar's" ranged weapon - single firepower profile, no melee option. var _attack = ranged_damage_data[0]; var _weapon = ranged_damage_data[3]; - return new EquipmentStruct({attack: _attack * 2, name: "Speed Force", range: 14, ammo: 12, spli: _weapon.spli, arp: _weapon.arp}, "weapon"); + return new EquipmentStruct({attack: _attack * 2, name: "Speed Force (Ranged)", range: 14, ammo: 12, spli: _weapon.spli, arp: _weapon.arp}, "weapon"); } // Standard Bike: scales off melee, dominant melee (M) profile while engaged in front. var _melee_attack = melee_damage_data[0]; diff --git a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml index 3415344c34..131db6afe7 100644 --- a/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml +++ b/scripts/scr_player_combat_weapon_stacks/scr_player_combat_weapon_stacks.gml @@ -183,9 +183,10 @@ function scr_player_combat_weapon_stacks() { } } if (is_struct(mobi_item) && mobi_item.has_tag("bike")) { - var stack_index = find_stack_index("Speed Force", head_role, unit); + var _speed_force = unit.speed_force(mobi_item.has_tag("sf_ranged")); + var stack_index = find_stack_index(_speed_force.name, head_role, unit); if (stack_index > -1) { - add_data_to_stack(stack_index, unit.speed_force(mobi_item.has_tag("sf_ranged")), false, head_role, unit); + add_data_to_stack(stack_index, _speed_force, false, head_role, unit); if (head_role) { player_head_role_stack(stack_index, unit); } From 5afbd55bcc55685fa6d2457ad0051de398015762 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:41:11 +0200 Subject: [PATCH 39/55] feat: librarian log spam --- scripts/scr_powers/scr_powers.gml | 67 ++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/scripts/scr_powers/scr_powers.gml b/scripts/scr_powers/scr_powers.gml index 99ebf1a53e..65268acada 100644 --- a/scripts/scr_powers/scr_powers.gml +++ b/scripts/scr_powers/scr_powers.gml @@ -58,8 +58,11 @@ function generate_marine_powers_description_string(unit) { /// @desc Psychic powers execution mess. Called in the scope of obj_pnunit. /// @param {real} caster_id - ID of the caster in the player column from obj_pnunit. +/// @param {Struct} [_psy_log] - Per-formation accumulator for attack casts. When provided, ordinary +/// attack casts fold into a per-power summary (see flush_psychic_summary) instead of logging +/// one line each. Leader kills, failed casts and Perils still log individually. /// @self Asset.GMObject.obj_pnunit -function scr_powers(caster_id) { +function scr_powers(caster_id, _psy_log = undefined) { // Gather unit data /// @type {Struct.TTRPG_stats} var _unit = unit_struct[caster_id]; @@ -297,18 +300,19 @@ function scr_powers(caster_id) { compress_enemy_array(_target_data.column); destroy_empty_column(_target_data.column); - // Log battle message to combat feed - _battle_log_message = _cast_flavour_text + _power_flavour_text + _casualties_flavour_text; - if (_casualties == 0) { - _battle_log_priority = _final_damage / 50; // Just to have some priority here, as they don't have the usual "shots fired" + // Battle log: the enemy leader dying always earns its own callout; every other + // attack cast folds into a per-power summary emitted at the end of the casting + // phase (flush_psychic_summary), so a wall of Librarians becomes one line. + // (We're always inside the _casualties > 0 branch here.) + _battle_log_priority = _target_is_vehicle ? (_casualties * 12) : (_casualties * 3); + var _is_leader = (obj_ncombat.enemy <= 10) && (_target_unit_name == obj_controller.faction_leader[obj_ncombat.enemy]); + + if (is_struct(_psy_log) && !_is_leader) { + accumulate_psychic_cast(_psy_log, _power_name, _power_flavour_text, _target_unit_name, _destruction_verb, _target_is_vehicle, _casualties); } else { - if (_target_is_vehicle) { - _battle_log_priority = _casualties * 12; // Vehicles are more juicy - } else { - _battle_log_priority = _casualties * 3; // More casualties = higher priority messages - } + _battle_log_message = _cast_flavour_text + _power_flavour_text + _casualties_flavour_text; + add_battle_log_message(_battle_log_message, _battle_log_priority, 134); } - add_battle_log_message(_battle_log_message, _battle_log_priority, 134); } } } @@ -332,6 +336,47 @@ function scr_powers(caster_id) { display_battle_log_message(); } +/// @desc Folds one attack-power cast into the per-formation psychic summary, keyed by power + target, +/// so many identical Librarian casts collapse into a single battle-log line. +/// @param {Struct} _psy_log The accumulator struct (one per formation casting phase). +function accumulate_psychic_cast(_psy_log, _power_name, _power_flavour, _target_name, _verb, _is_vehicle, _kills) { + var _key = _power_name + "|" + _target_name; + if (!variable_struct_exists(_psy_log, _key)) { + _psy_log[$ _key] = { + power: _power_name, + flavour: _power_flavour, + target: _target_name, + verb: _verb, + vehicle: _is_vehicle, + casts: 0, + kills: 0, + }; + } + var _entry = _psy_log[$ _key]; + _entry.casts += 1; + _entry.kills += _kills; +} + +/// @desc Emits one battle-log line per power+target accumulated during a formation's casting phase. +/// Mirrors scr_powers' own concatenation so spacing matches the individual-cast lines. +/// @param {Struct} _psy_log The accumulator filled by accumulate_psychic_cast. +function flush_psychic_summary(_psy_log) { + if (!is_struct(_psy_log)) { + return; + } + var _keys = variable_struct_get_names(_psy_log); + for (var i = 0; i < array_length(_keys); i++) { + var _e = _psy_log[$ _keys[i]]; + var _cast_word = (_e.casts == 1) ? "casting" : "castings"; + var _message = $"{_e.casts} {_cast_word} of '{_e.power}'{_e.flavour} {_e.kills} {_e.target} are {_e.verb}."; + var _size = _e.vehicle ? (_e.kills * 12) : (_e.kills * 3); + add_battle_log_message(_message, _size, 134); + } + if (array_length(_keys) > 0) { + display_battle_log_message(); + } +} + /// @desc Function to get requested data from the disciplines_data structure. Returns The requested data, or undefined if not found. /// @param _discipline_name - The name of the discipline /// @param _data_name - The specific data attribute you want From 03847e0b62f0e4857d16f8fef55ef5f732bc3ae1 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:49:03 +0200 Subject: [PATCH 40/55] style: combat log text light green colour support --- objects/obj_ncombat/Draw_0.gml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/objects/obj_ncombat/Draw_0.gml b/objects/obj_ncombat/Draw_0.gml index 2d50f1a285..76408f4127 100644 --- a/objects/obj_ncombat/Draw_0.gml +++ b/objects/obj_ncombat/Draw_0.gml @@ -71,6 +71,9 @@ repeat (45) { if (lines_color[l] == "blue") { draw_set_color(c_aqua); } + if (lines_color[l] == "lightgreen") { + draw_set_color(make_color_rgb(150, 255, 150)); + } draw_text(x + 6, y - 10 + (l * 18), string_hash_to_newline(string(lines[l]))); } From 2da7f57742ddb1f0923a652f9257caf0ef52d907 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:32:24 +0200 Subject: [PATCH 41/55] refactor: combat flavour log --- objects/obj_ncombat/Alarm_3.gml | 79 ++--- objects/obj_ncombat/Create_0.gml | 10 + objects/obj_ncombat/Step_0.gml | 16 +- objects/obj_pnunit/Alarm_0.gml | 54 +++- scripts/macros/macros.gml | 8 + scripts/scr_clean/scr_clean.gml | 14 +- scripts/scr_flavor/scr_flavor.gml | 401 ++++++++++++++++++++++--- scripts/scr_shoot/scr_shoot.gml | 475 ++++++++++++++++-------------- 8 files changed, 732 insertions(+), 325 deletions(-) diff --git a/objects/obj_ncombat/Alarm_3.gml b/objects/obj_ncombat/Alarm_3.gml index 6bee766eaf..a73a772141 100644 --- a/objects/obj_ncombat/Alarm_3.gml +++ b/objects/obj_ncombat/Alarm_3.gml @@ -13,7 +13,8 @@ repeat (100) { if (good == 0) { changed = 0; - for (var i = 1; i <= 55; i++) { + repeat (COMBAT_LOG_CAPACITY) { + i += 1; // Collide the messages if needed if ((message[i] == "") && (message[i + 1] != "")) { @@ -27,37 +28,8 @@ repeat (100) { changed = 1; } - // Move larger messages up - if ((message[i] != "") && (message[i + 1] != "") && (message_sz[i] < message_sz[i + 1]) && ((message_priority[i] < message_priority[i + 1]) || (message_priority[i] == 0))) { - message[100] = message[i]; - message_sz[100] = message_sz[i]; - message_priority[100] = message_priority[i]; - - message[i] = message[i + 1]; - message_sz[i] = message_sz[i + 1]; - message_priority[i] = message_priority[i + 1]; - - message[i + 1] = message[100]; - message_sz[i + 1] = message_sz[100]; - message_priority[i + 1] = message_priority[100]; - changed = 1; - } - - // Move messages with higher priority up - if ((message[i] != "") && (message[i + 1] != "") && (message_priority[i] < message_priority[i + 1])) { - message[100] = message[i]; - message_sz[100] = message_sz[i]; - message_priority[100] = message_priority[i]; - - message[i] = message[i + 1]; - message_sz[i] = message_sz[i + 1]; - message_priority[i] = message_priority[i + 1]; - - message[i + 1] = message[100]; - message_sz[i + 1] = message_sz[100]; - message_priority[i + 1] = message_priority[100]; - changed = 1; - } + // Messages are shown in the order they happened, so we only compact gaps upward + // (above) and no longer reorder by size/priority. if (changed == 0) { good = 1; @@ -66,17 +38,20 @@ repeat (100) { } } -if (((messages > 0) && (messages_shown < 24)) && (messages_shown <= 100)) { - var that_sz = 0; +if (messages > 0) { + // Show messages in the order they happened (chronological), with no per-turn cap, so the + // whole exchange is visible right down to the closing "held fire" line. var that = 0; - for (var i = 1; i <= 60; i++) { - if ((message[i] != "") && (message_sz[i] > that_sz)) { - that_sz = message_sz[i]; + i = 0; + repeat (COMBAT_LOG_CAPACITY) { + i += 1; + if (message[i] != "") { that = i; + break; } } - if ((that != 0) && (that_sz > 0)) { + if (that != 0) { newline = message[that]; if (message_priority[that] > 0) { newline_color = "bright"; @@ -98,6 +73,12 @@ if (((messages > 0) && (messages_shown < 24)) && (messages_shown <= 100)) { if (message_priority[that] == 137) { newline_color = "red"; } + if (message_priority[that] == MSG_COLOR_WHITE) { + newline_color = "white"; + } + if (message_priority[that] == MSG_COLOR_LIGHTGREEN) { + newline_color = "lightgreen"; + } scr_newtext(); messages_shown += 1; @@ -111,10 +92,6 @@ if (((messages > 0) && (messages_shown < 24)) && (messages_shown <= 100)) { alarm[3] = 2; } -if ((messages == 0) || (messages_shown >= 24)) { - messages_shown = 999; -} - if (messages == 0) { messages_shown = 999; } @@ -132,23 +109,7 @@ if (!instance_exists(obj_pnunit)) { if (((messages_shown == 999) || (messages == 0)) && (timer_stage == 2)) { newline_color = "yellow"; if (obj_ncombat.enemy != 6) { - if ((enemy_forces > 0) && (obj_ncombat.enemy != 30)) { - newline = "Enemy Forces at " + string(max(1, round((enemy_forces / enemy_max) * 100))) + "%"; - } - if ((obj_ncombat.enemy == 30) && instance_exists(obj_enunit)) { - newline = "Enemy has "; - var yoo; - yoo = instance_nearest(0, 0, obj_enunit); - newline += string(round(yoo.dudes_hp[1])) + "HP remaining"; - } - if ((enemy_forces <= 0) || (!instance_exists(obj_enunit)) && (defeat_message == 0)) { - defeat_message = 1; - newline = "Enemy Forces Defeated"; - timer_maxspeed = 0; - timer_speed = 0; - started = 2; - instance_activate_object(obj_pnunit); - } + combat_emit_enemy_status(); } newline_color = "yellow"; if (obj_ncombat.enemy == 6) { diff --git a/objects/obj_ncombat/Create_0.gml b/objects/obj_ncombat/Create_0.gml index 835918d0b9..16bec098b5 100644 --- a/objects/obj_ncombat/Create_0.gml +++ b/objects/obj_ncombat/Create_0.gml @@ -151,6 +151,16 @@ dead_ene_n = array_create(70, 0); crunch = array_create(70, 0); mucra = array_create(11, 0); +// The combat-log queue must be large enough that a long turn fully drains. The status line +// ("Enemy Forces at X%" / "Defeated") only renders once `messages` reaches 0, and Alarm_3 drains +// the queue through fixed windows — anything past the window strands the tail, leaving messages > 0 +// forever so the status never shows. Size the message arrays generously to match those windows. +for (var _m = 1; _m <= COMBAT_LOG_CAPACITY + 20; _m++) { + message[_m] = ""; + message_sz[_m] = 0; + message_priority[_m] = 0; +} + post_equipment_lost = new EquipmentTracker(); post_equipment_recovered = new EquipmentTracker(); diff --git a/objects/obj_ncombat/Step_0.gml b/objects/obj_ncombat/Step_0.gml index 3830813142..1ce17229e3 100644 --- a/objects/obj_ncombat/Step_0.gml +++ b/objects/obj_ncombat/Step_0.gml @@ -42,14 +42,7 @@ if (((fugg >= 60) || (fugg2 >= 60)) && (messages_shown == 0) && (messages_to_sho if (((messages_shown == 999) || (messages == 0)) && (timer_stage == 2)) { newline_color = "yellow"; if (obj_ncombat.enemy != 6) { - if ((enemy_forces <= 0) || (!instance_exists(obj_enunit)) && (defeat_message == 0)) { - defeat_message = 1; - newline = "Enemy Forces Defeated"; - timer_maxspeed = 0; - timer_speed = 0; - started = 2; - instance_activate_object(obj_pnunit); - } + combat_emit_enemy_status(); } newline_color = "yellow"; if (obj_ncombat.enemy == 6) { @@ -106,7 +99,10 @@ if (((fugg >= 60) || (fugg2 >= 60)) && (messages_shown == 0) && (messages_to_sho if (timer_stage == 2) { fugg += 1; } -if ((timer_stage == 2) && (fugg > 60)) { +// Don't time out of stage 2 until the combat log has finished displaying - otherwise on a long turn +// the stage advances before `messages` drains and the "Enemy Forces at X%" status line is skipped. +// The large hard cap is anti-hang insurance in case the queue ever fails to drain. +if ((timer_stage == 2) && (((fugg > 60) && (messages == 0)) || (fugg > COMBAT_STAGE_TIMEOUT_FRAMES))) { timer_stage = 3; } @@ -116,7 +112,7 @@ if (timer_stage != 2) { if (timer_stage == 4) { fugg2 += 1; } -if ((timer_stage == 4) && (fugg2 > 60)) { +if ((timer_stage == 4) && (((fugg2 > 60) && (messages == 0)) || (fugg2 > COMBAT_STAGE_TIMEOUT_FRAMES))) { timer_stage = 5; } diff --git a/objects/obj_pnunit/Alarm_0.gml b/objects/obj_pnunit/Alarm_0.gml index 2ec58abead..fffae790bd 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -51,7 +51,23 @@ try { } if (instance_exists(obj_enunit)) { + global.ctally_target = undefined; + global.ctally_bounce = []; + global.ctally_injure = []; for (var i = 0; i < array_length(wep); i++) { + // Enemies wiped before every weapon got to fire (e.g. spill-over cleared the line). + // Report who held fire and stop, rather than swinging at empty air. + if (!instance_exists(obj_enunit)) { + var _held_fire = []; + for (var hf = i; hf < array_length(wep); hf++) { + // Only ranged weapons "hold fire"; melee (range 1) never shoots, so skip it. + if (wep[hf] != "" && wep_num[hf] > 0 && range[hf] > 1) { + array_push(_held_fire, wep[hf]); + } + } + report_held_fire(_held_fire); + break; + } if (wep[i] == "") { continue; } @@ -66,6 +82,12 @@ try { enemy = instance_nearest(0, y, obj_enunit); } + // Speed Force sweeps the whole field - bypass normal targeting/range. + if (wep[i] == "Speed Force" || wep[i] == "Speed Force (Ranged)") { + scr_shoot_spread(i); + continue; + } + if ((range[i] >= dist) && (ammo[i] != 0 || range[i] == 1)) { if ((range[i] != 1) && (engaged == 0)) { range_shoot = "ranged"; @@ -244,17 +266,47 @@ try { } } } + } else { + // The field was already clear when this block's turn came up - its whole arsenal holds fire. + var _skipped_fire = []; + for (var s = 0; s < array_length(wep); s++) { + // Only ranged weapons "hold fire"; melee (range 1) never shoots, so skip it. + if (wep[s] != "" && wep_num[s] > 0 && range[s] > 1) { + array_push(_skipped_fire, wep[s]); + } + } + report_held_fire(_skipped_fire); } + combat_tally_flush(); + instance_activate_object(obj_enunit); + // Safety net: drop empty/zombie formations the firing loop never reached, so a lingering corpse + // can't keep the battle alive. + with (obj_enunit) { + var _alive = 0; + for (var _rr = 1; _rr <= 30; _rr++) { + if (dudes_num[_rr] > 0 && dudes_hp[_rr] > 0) { + _alive += dudes_num[_rr]; + } + } + if ((_alive == 0) && (owner != 1)) { + instance_destroy(); + } + } + if (instance_exists(obj_enunit)) { + // Accumulate this formation's attack casts, then emit one summary line per power instead + // of one line per Librarian (see flush_psychic_summary in scr_powers). + var _psy_log = {}; for (var i = 0; i < array_length(unit_struct); i++) { if (marine_dead[i] == 0 && marine_casting[i] == true) { var caster_id = i; - scr_powers(caster_id); + scr_powers(caster_id, _psy_log); } } + flush_psychic_summary(_psy_log); } } catch (_exception) { diff --git a/scripts/macros/macros.gml b/scripts/macros/macros.gml index 2d5de7e11e..199a7f3dd7 100644 --- a/scripts/macros/macros.gml +++ b/scripts/macros/macros.gml @@ -15,6 +15,14 @@ #macro MANAGE_MAN_MAX array_length(obj_controller.display_unit) + 7 #macro LARGE_PLANET_MOD 1000000000 // Population threshold for large planet classification +// Ground combat message log: lines the display fully drains per turn (so the end-of-turn status +// line shows even on long battles), and the per-stage frame timeout before force-advancing. +#macro COMBAT_LOG_CAPACITY 500 +#macro COMBAT_STAGE_TIMEOUT_FRAMES 1200 +// Battle-log message_priority colour codes (extends the existing 134/135/137 set). +#macro MSG_COLOR_WHITE 140 +#macro MSG_COLOR_LIGHTGREEN 141 + #macro STR_ANY_POWER_ARMOUR "Any Power Armour" #macro STR_ANY_TERMINATOR_ARMOUR "Any Terminator Armour" diff --git a/scripts/scr_clean/scr_clean.gml b/scripts/scr_clean/scr_clean.gml index 055c5ec03c..0182271ecf 100644 --- a/scripts/scr_clean/scr_clean.gml +++ b/scripts/scr_clean/scr_clean.gml @@ -75,9 +75,19 @@ function compress_enemy_array(_target_column) { /// @description Destroys the column if it's empty /// @param {id.Instance} _target_column - The column instance to clean up function destroy_empty_column(_target_column) { - // Destroy empty non-player columns to conserve memory and processing + // Destroy empty non-player columns to conserve memory and processing. with (_target_column) { - if ((men + veh + medi == 0) && (owner != 1)) { + // Count living models straight from dudes_num. men/veh/medi are only refreshed on the enemy's + // own alarm, so during the player's firing phase they're stale and would leave a wiped-out + // formation standing - which then keeps getting fired at and blocks "held fire" reporting. + var _alive = 0; + for (var r = 1; r < array_length(dudes_num); r++) { + // A rank chipped to 0 HP but still showing dudes_num is a dead "zombie" - don't count it. + if (dudes_num[r] > 0 && dudes_hp[r] > 0) { + _alive += dudes_num[r]; + } + } + if ((_alive == 0) && (owner != 1)) { instance_destroy(); } } diff --git a/scripts/scr_flavor/scr_flavor.gml b/scripts/scr_flavor/scr_flavor.gml index 5b8c55de69..5ce81478a8 100644 --- a/scripts/scr_flavor/scr_flavor.gml +++ b/scripts/scr_flavor/scr_flavor.gml @@ -22,8 +22,59 @@ function display_battle_log_message() { obj_ncombat.alarm[3] = 5; } -function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shots, casulties) { +/// @desc Plural form of a weapon name. Names that are already plural (end in "s", e.g. +/// "Twin Linked Bolters") are left as-is so we don't print "Bolterss". +/// @param {string} _name The weapon name. +/// @returns {string} +function weapon_name_plural(_name) { + return _name + ((string_char_at(_name, string_length(_name)) == "s") ? "" : "s"); +} + +/// @desc Logs one "held fire" line for weapons that had no live target left to shoot at, e.g. +/// when an earlier volley wiped the enemy before the rest of the squad fired. +/// @param {Array} _weapon_names Raw weapon names (duplicates allowed) that never fired. +function report_held_fire(_weapon_names) { + // Dedupe and pluralise. + var _unique = []; + for (var i = 0; i < array_length(_weapon_names); i++) { + var _p = weapon_name_plural(_weapon_names[i]); + if (array_get_index(_unique, _p) == -1) { + array_push(_unique, _p); + } + } + + var _count = array_length(_unique); + if (_count == 0) { + return; + } + + // Build "A, B, and C" (or "A and B", or "A"). + var _list = _unique[0]; + for (var i = 1; i < _count; i++) { + if (i == _count - 1) { + _list += (_count > 2 ? ", and " : " and ") + _unique[i]; + } else { + _list += ", " + _unique[i]; + } + } + + add_battle_log_message($"{_list} held fire lacking live targets.", 0, 135); + display_battle_log_message(); +} + +function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shots, casulties, shots_bounced = false, _defer = false) { // Generates flavor based on the damage and casualties from scr_shoot, only for the player + // shots_bounced: true when armour stopped the shots outright (AP too low) and nothing died, + // so the log can explain *why* instead of a flat "no casualties". + // _defer: when true, build the message but DON'T post it; return it so the caller can append a + // spill-over kill list and post a single consolidated line (see emit_volley_flavour). + + // Clamp away any negative casualty count so it can never render as "-1". Every volley now earns + // a line: a kill, a wound (injured, no kill), or an armour-bounce. The latter two are consolidated + // per target by emit_volley_flavour / combat_tally_*. + if (casulties < 0) { + casulties = 0; + } var attack_message, kill_message, leader_message, targeh; targeh = target_type; @@ -31,10 +82,19 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot attack_message = $""; kill_message = ""; + // Guard/diagnostic: a non-killing volley against a rank with no living models means we fired at a + // dead target. Shouldn't happen now that emptied formations are destroyed - log it if it does and + // bail, so it can never feed the consolidated non-pen / wound feed. (Spill-over kills, if any, are + // still reported by emit_volley_flavour's undefined-primary path.) + if (casulties <= 0 && (!instance_exists(target) || target.dudes_num[targeh] <= 0)) { + LOGGER.warning($"scr_flavor: shot at a dead target (weapon stack {id_of_attacking_weapons}, rank {targeh})"); + exit; + } + var weapon_name = wep[id_of_attacking_weapons]; if (id_of_attacking_weapons == -51) { - weapon_name = "Heavy Bolter Emplacemelse ent"; + weapon_name = "Heavy Bolter Emplacement"; } if (id_of_attacking_weapons == -52) { weapon_name = "Missile Launcher Emplacement"; @@ -43,6 +103,9 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot weapon_name = "Missile Silo"; } + // Plural form for "{n} {weapon}s ..." lines (see weapon_name_plural). + var weapon_plural = weapon_name_plural(weapon_name); + var weapon_data = gear_weapon_data("weapon", weapon_name, "all"); if (!is_struct(weapon_data)) { weapon_data = new EquipmentStruct({}, ""); @@ -65,7 +128,7 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot if (array_length(full_names) == 1) { unit_name = wep_title[id_of_attacking_weapons] + " " + wep_solo[id_of_attacking_weapons][0]; } else { - unit_name = wep_title[id_of_attacking_weapons] + "'s"; + unit_name = wep_title[id_of_attacking_weapons]; } } if (wep_solo[id_of_attacking_weapons][0] == obj_ini.master_name) { @@ -93,6 +156,22 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot target_name = "flanking " + target_name; } + // Firing subject for consolidated lines: " " for a titled character, "The " + // for a lone shot, or " " for a volley (also used when a unit has no title, e.g. Dreadnoughts). + var firing_subject; + if (character_shot && unit_name != "") { + if (number_of_shots > 1) { + // Grouped titled units (e.g. several Dreadnoughts share one "Dreadnought" title) — show the count. + firing_subject = $"{number_of_shots} {string(unit_name)} {weapon_plural}"; + } else { + firing_subject = $"{string(unit_name)} {weapon_name}"; + } + } else if (number_of_shots == 1) { + firing_subject = $"The {weapon_name}"; + } else { + firing_subject = $"{number_of_shots} {weapon_plural}"; + } + var flavoured = false; if (weapon_data.has_tag("bolt")) { @@ -104,29 +183,29 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot if (number_of_shots < 200) { if (target.dudes_num[targeh] == 1) { if (casulties == 0) { - attack_message += $"{number_of_shots} {weapon_name}s fire. The {target_name} is hit but survives."; + attack_message += $"{number_of_shots} {weapon_plural} fire. The {target_name} is hit but survives."; } else { - attack_message += $"{number_of_shots} {weapon_name}s fire. The {target_name} is struck down."; + attack_message += $"{number_of_shots} {weapon_plural} fire. The {target_name} is struck down."; } } else { if (casulties == 0) { - attack_message += $"{number_of_shots} {weapon_name}s fire hits {target_name} ranks without causing casualties."; + attack_message += $"{number_of_shots} {weapon_plural} fire at {target_name} ranks without causing casualties."; } else { - attack_message += $"{number_of_shots} {weapon_name}s strike {target_name} ranks, taking down {casulties}."; + attack_message += $"{number_of_shots} {weapon_plural} strike {target_name} ranks, taking down {casulties}."; } } } else { if (target.dudes_num[targeh] == 1) { if (casulties == 0) { - attack_message += $"{number_of_shots} {weapon_name}s fire. Explosions rock the {target_name}'s armour but don't kill it."; + attack_message += $"{number_of_shots} {weapon_plural} fire. Explosions rock the {target_name}'s armour but don't kill it."; } else { - attack_message += $"{number_of_shots} {weapon_name}s fire. Explosions take down the {target_name}."; + attack_message += $"{number_of_shots} {weapon_plural} fire. Explosions take down the {target_name}."; } } else { if (casulties == 0) { - attack_message += $"{number_of_shots} {weapon_name}s hit {target_name} ranks, but no casualties are confirmed."; + attack_message += $"{number_of_shots} {weapon_plural} hit {target_name} ranks, but no casualties are confirmed."; } else { - attack_message += $"{number_of_shots} {weapon_name}s tear through {target_name} ranks, instantly killing {casulties}."; + attack_message += $"{number_of_shots} {weapon_plural} tear through {target_name} ranks, instantly killing {casulties}."; } } } @@ -225,20 +304,60 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot } } } + } else if (weapon_name == "Speed Force (Ranged)") { + flavoured = true; + if (!character_shot) { + if (number_of_shots < 20) { + attack_message += $"{number_of_shots} Attack Bikes race across the field, sidecar gunners hosing down the enemy on the move- "; + } else if (number_of_shots >= 20 && number_of_shots < 100) { + attack_message += $"A column of {number_of_shots} Attack Bikes sweeps past, heavy weapons hammering away in a thunderous strafing run- "; + } else { + attack_message += $"A roaring tide of {number_of_shots} Attack Bikes tears along the line, sidecar guns blazing without pause- "; + } + if (target.dudes_num[targeh] == 1) { + if (casulties == 0) { + attack_message += $"but the {target_name} weathers the fusillade."; + } else { + attack_message += $"and the {target_name} is gunned down where it stands."; + } + } else { + if (casulties == 0) { + attack_message += $"{target_name} ranks are raked with fire, but none fall."; + } else { + attack_message += $"cutting down {casulties} {target_name} in the pass."; + } + } + } else { + if (target.dudes_num[targeh] == 1) { + attack_message += string(unit_name) + $" guns his Attack Bike past the {target_name}, sidecar weapon roaring- "; + if (casulties == 0) { + attack_message += $"but it endures the barrage."; + } else { + attack_message += $"and it is torn apart."; + } + } else { + attack_message += string(unit_name) + $" sweeps his Attack Bike along {target_name} ranks, raking them with fire- "; + if (casulties == 0) { + attack_message += $"but all survive the onslaught."; + } else { + attack_message += $"cutting down {casulties} in the pass."; + } + } + } } else if (weapon_name == "Assault Cannon") { flavoured = true; if (!character_shot) { if (target.dudes_num[targeh] == 1) { if (casulties == 0) { - attack_message += $"{number_of_shots} {weapon_name}s roar, explosions clap across the armour of the {target_name} but it remains standing."; + attack_message += $"{number_of_shots} {weapon_plural} roar, explosions clap across the armour of the {target_name} but it remains standing."; } else { - attack_message += $"{number_of_shots} {weapon_name}s fire at the {target_name} and rip it apart."; + attack_message += $"{number_of_shots} {weapon_plural} fire at the {target_name} and rip it apart."; } } else { if (casulties == 0) { - attack_message += $"{number_of_shots} {weapon_name}s thunder, {target_name} are rocked but unharmed."; + attack_message += $"{number_of_shots} {weapon_plural} thunder, {target_name} are rocked but unharmed."; } else { - attack_message += $"{number_of_shots} {weapon_name}s mow down {casulties} {target_name}."; + attack_message += $"{number_of_shots} {weapon_plural} mow down {casulties} {target_name}."; } } } else { @@ -261,15 +380,15 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot if (!character_shot) { if (target.dudes_num[targeh] == 1) { if (casulties == 0) { - attack_message = $"{number_of_shots} {weapon_name}s fire upon the {target_name} but it remains standing."; + attack_message = $"{number_of_shots} {weapon_plural} fire upon the {target_name} but it remains standing."; } else { - attack_message = $"{number_of_shots} {weapon_name}s blast the {target_name} to oblivion."; + attack_message = $"{number_of_shots} {weapon_plural} blast the {target_name} to oblivion."; } } else { if (casulties == 0) { - attack_message = $"{number_of_shots} {weapon_name}s hit {target_name} ranks but they hold firm."; + attack_message = $"{number_of_shots} {weapon_plural} hit {target_name} ranks but they hold firm."; } else { - attack_message = $"{number_of_shots} {weapon_name}s pulverize {casulties} {target_name}."; + attack_message = $"{number_of_shots} {weapon_plural} pulverize {casulties} {target_name}."; } } } else { @@ -431,10 +550,10 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot attack_message = $"A {target_name} is struck down by a Battle Sister's {weapon_name}."; } if ((number_of_shots > 1) && (casulties == 0)) { - attack_message = $"Battle Sisters " + choose("howl out", "roar") + $" and hack at {target_name} ranks with their {weapon_name}s, but they survive."; + attack_message = $"Battle Sisters " + choose("howl out", "roar") + $" and hack at {target_name} ranks with their {weapon_plural}, but they survive."; } if ((number_of_shots > 1) && (casulties > 0)) { - attack_message = $"{number_of_shots} Battle Sisters " + choose("howl out", "roar") + $" as they hack away at the {target_name} ranks, killing {casulties} with their {weapon_name}s."; + attack_message = $"{number_of_shots} Battle Sisters " + choose("howl out", "roar") + $" as they hack away at the {target_name} ranks, killing {casulties} with their {weapon_plural}."; } } else if (weapon_name == "Eviscerator") { flavoured = true; @@ -475,18 +594,18 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot } if ((number_of_shots > 1) && (casulties == 0)) { - attack_message = $"A {target_name} is struck by {number_of_shots} {weapon_name}s but survives."; + attack_message = $"A {target_name} is struck by {number_of_shots} {weapon_plural} but survives."; } if ((number_of_shots > 1) && (casulties == 1)) { - attack_message = $"A {target_name} is struck down by {number_of_shots} {weapon_name}s."; + attack_message = $"A {target_name} is struck down by {number_of_shots} {weapon_plural}."; } } if (target.dudes_num[targeh] > 1) { if ((number_of_shots > 1) && (casulties == 0)) { - attack_message = $"{number_of_shots} {weapon_name}s crackle and spark, striking at the {target_name} ranks, inflicting no damage."; + attack_message = $"{number_of_shots} {weapon_plural} crackle and spark, striking at the {target_name} ranks, inflicting no damage."; } if ((number_of_shots > 1) && (casulties > 0)) { - attack_message = $"{number_of_shots} {weapon_name}s crackle and spark, hewing through the {target_name} ranks, {casulties} are cut down."; + attack_message = $"{number_of_shots} {weapon_plural} crackle and spark, hewing through the {target_name} ranks, {casulties} are cut down."; } } } @@ -501,9 +620,9 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot } else if (number_of_shots == 1 && casulties == 1) { attack_message = $"A {target_name} is struck down by {weapon_name}."; } else if (number_of_shots > 1 && casulties == 0) { - attack_message = $"A {target_name} is struck by {number_of_shots} {weapon_name}s but survives."; + attack_message = $"A {target_name} is struck by {number_of_shots} {weapon_plural} but survives."; } else if (number_of_shots > 1 && casulties == 1) { - attack_message = $"A {target_name} is struck down by {number_of_shots} {weapon_name}s."; + attack_message = $"A {target_name} is struck down by {number_of_shots} {weapon_plural}."; } } else { if (number_of_shots == 1 && casulties == 0) { @@ -511,28 +630,47 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot } else if (number_of_shots == 1 && casulties > 0) { attack_message = $"{weapon_name} strikes at {target_name} and kills {casulties}"; } else if (number_of_shots > 1 && casulties == 0) { - attack_message = $"{number_of_shots} {weapon_name}s strike at the {target_name} ranks, but fail to inflict damage."; + attack_message = $"{number_of_shots} {weapon_plural} strike at the {target_name} ranks, but fail to inflict damage."; } else if (number_of_shots > 1 && casulties > 0) { - attack_message = $"{number_of_shots} {weapon_name}s strike at the {target_name} ranks, killing {casulties}."; + attack_message = $"{number_of_shots} {weapon_plural} strike at the {target_name} ranks, killing {casulties}."; } } } else { if (target.dudes_num[targeh] == 1) { if (casulties == 0) { - attack_message = $"{string(unit_name)} {weapon_name} strikes at a {target_name} but fails to kill it."; + attack_message = $"{firing_subject} strikes at a {target_name} but fails to kill it."; } else { - attack_message = $"{string(unit_name)} {weapon_name} strikes at a {target_name}, killing it."; + attack_message = $"{firing_subject} strikes at a {target_name}, killing it."; } } else { if (casulties == 0) { - attack_message = $"{string(unit_name)} {weapon_name} strikes at the {target_name} ranks, failing to kill any."; + attack_message = $"{firing_subject} strikes at the {target_name} ranks, failing to kill any."; } else { - attack_message = $"{string(unit_name)} {weapon_name} strikes at the {target_name} ranks and kills {casulties}."; + attack_message = $"{firing_subject} strikes at the {target_name} ranks and kills {casulties}."; } } } } + // Reason-aware override: armour stopped the shots cold (AP too low). Replaces whatever + // generic "no casualties" text the branches produced with something that explains why. + if (shots_bounced && casulties == 0) { + flavoured = true; + if (character_shot) { + attack_message = $"{string(unit_name)} {weapon_name} strikes the {target_name} but fails to penetrate its armour."; + } else if (number_of_shots == 1) { + attack_message = $"The {weapon_name} strikes the {target_name} but fails to penetrate its armour."; + } else if (weapon_data.has_tag("bolt")) { + attack_message = $"{number_of_shots} {weapon_plural} hammer the {target_name} but spark harmlessly off its armour."; + } else if (weapon_data.has_tag("flame")) { + attack_message = $"{number_of_shots} {weapon_plural} wash over the {target_name} but its armour endures the flames."; + } else if (weapon_data.has_tag("power")) { + attack_message = $"{number_of_shots} {weapon_plural} strike the {target_name} but glance off its armour."; + } else { + attack_message = $"{number_of_shots} {weapon_plural} strike the {target_name} but fail to penetrate its armour."; + } + } + // if (string_length(attack_message+kill_message+p3)<8) then show_message(weapon_name+" is not displaying anything"); // I don't understand what this was supposed to do either. @@ -585,26 +723,209 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot } } + // Message size drives which lines survive the per-turn display cap (largest win). var message_size = 0; if (defenses == 1) { message_size = 999; } else if (casulties == 0) { - message_size = number_of_shots / 10; + // "Couldn't penetrate" lines must never outrank an actual kill (that's why a lone + // Warboss death used to get culled under its own bounce spam), so keep them tiny. + message_size = 0; } else { + // Weight kills by count *and* toughness so a single hard target (Warboss, Meganob) isn't + // buried under big trash-mob kills. Armour is the stable proxy: a lone survivor's HP gets + // chipped down before death, but its armour rating doesn't change. + message_size = casulties * (1 + target.dudes_ac[targeh]); if (target.dudes_vehicle[targeh] == 1) { - message_size = casulties * 10; + message_size *= 10; + } + } + + // When deferred, hand the parts back to the caller instead of posting them, so the spill-over + // kill list can be appended and the whole volley posted as one line. + if (!_defer) { + if (attack_message != "") { + add_battle_log_message(attack_message, message_size, message_priority); + display_battle_log_message(); + } + + if (leader_message != "") { + add_battle_log_message(leader_message, message_size, message_priority); + display_battle_log_message(); + } + } + + return { + attack: attack_message, + leader: leader_message, + size: message_size, + priority: message_priority, + bounced: (shots_bounced && casulties == 0), + injured: (!shots_bounced && casulties == 0), + target: target_name, + subject: firing_subject, + }; +} + +/// @desc Formats a list of kills into "the X" / "N X", joined as "A, B, and C". +/// @param {Array} _kills Array of { name, count } structs. +/// @returns {string} +function format_kill_list(_kills) { + // Merge entries that share a name so multiple ranks of one unit read as a single tally + // (e.g. "29 Slugga Boy and 223 Slugga Boy" -> "252 Slugga Boy"). + var _merged = []; + for (var m = 0; m < array_length(_kills); m++) { + var _hit = false; + for (var n = 0; n < array_length(_merged); n++) { + if (_merged[n].name == _kills[m].name) { + _merged[n].count += _kills[m].count; + _hit = true; + break; + } + } + if (!_hit) { + array_push(_merged, { name: _kills[m].name, count: _kills[m].count }); + } + } + _kills = _merged; + var _n = array_length(_kills); + if (_n == 0) { + return ""; + } + var _parts = []; + for (var i = 0; i < _n; i++) { + var _k = _kills[i]; + array_push(_parts, (_k.count == 1) ? ("the " + _k.name) : (string(_k.count) + " " + _k.name)); + } + var _list = _parts[0]; + for (var i = 1; i < _n; i++) { + if (i == _n - 1) { + _list += (_n > 2 ? ", and " : " and ") + _parts[i]; } else { - message_size = casulties; + _list += ", " + _parts[i]; } } + return _list; +} + +/// @desc Posts a single consolidated volley line: the deferred rich flavour for the first target, +/// plus an "Also cut down: ..." list of everything the volley's overflow killed afterwards. +/// @param {Struct} _primary Result returned by scr_flavor(..., _defer=true) for the first target (or undefined). +/// @param {Array} _spill_kills Array of { name, count } for targets killed after the first. +function emit_volley_flavour(_primary, _spill_kills) { + var _list = format_kill_list(_spill_kills); + + // Non-killing volley (armour-bounce or a wound that dropped no-one, and nothing spilled): + // consolidate into one chronological line per target instead of one line per weapon. + if (is_struct(_primary) && (_primary.bounced || _primary.injured) && _list == "") { + combat_tally_add(_primary.target, _primary.subject, _primary.injured); + return; + } + + // A killing volley posts immediately; flush any pending bounce/injure tally first so the log + // stays in chronological order. + combat_tally_flush(); + + if (!is_struct(_primary)) { + // First target produced no line (hit but didn't kill, and didn't bounce). Spill-over only + // happens after a wipe, so there should be nothing to report, but stay defensive. + if (_list != "") { + add_battle_log_message("Overflowing fire cuts down " + _list + ".", 0, 0); + display_battle_log_message(); + } + return; + } - if (attack_message != "") { - add_battle_log_message(attack_message, message_size, message_priority); + var _message = _primary.attack; + if (_list != "") { + _message += " In the torrent of fire that reaches beyond those they slaughter: " + _list + "."; + } + + if (_message != "") { + add_battle_log_message(_message, _primary.size, _primary.priority); + display_battle_log_message(); + } + if (_primary.leader != "") { + add_battle_log_message(_primary.leader, _primary.size, _primary.priority); display_battle_log_message(); } +} - if (leader_message != "") { - add_battle_log_message(leader_message, message_size, message_priority); +/// @desc Buffers a non-killing volley (wound or armour-bounce) against a target. Consecutive volleys +/// on the same target merge; switching target flushes the previous one, keeping the log +/// chronological. _injured true = penetrated but no kill; false = bounced off armour. +function combat_tally_add(_target, _subject, _injured) { + if (!variable_global_exists("ctally_target")) { + global.ctally_target = undefined; + global.ctally_bounce = []; + global.ctally_injure = []; + } + if (global.ctally_target != _target) { + combat_tally_flush(); + global.ctally_target = _target; + } + if (_injured) { + array_push(global.ctally_injure, _subject); + } else { + array_push(global.ctally_bounce, _subject); + } +} + +/// @desc Posts the buffered wound/bounce lines for the current target (one each), then clears them. +function combat_tally_flush() { + if (!variable_global_exists("ctally_target") || global.ctally_target == undefined) { + return; + } + var _t = global.ctally_target; + if (array_length(global.ctally_injure) > 0) { + add_battle_log_message($"Fire from {combat_subject_join(global.ctally_injure)} wounds the {_t} but cannot bring it down.", 0, MSG_COLOR_LIGHTGREEN); display_battle_log_message(); } + if (array_length(global.ctally_bounce) > 0) { + add_battle_log_message($"Fire from {combat_subject_join(global.ctally_bounce)} cannot penetrate the {_t}'s armour.", 0, MSG_COLOR_WHITE); + display_battle_log_message(); + } + global.ctally_target = undefined; + global.ctally_bounce = []; + global.ctally_injure = []; +} + +/// @desc Joins firing subjects into "A", "A and B", or "A, B, and C". +function combat_subject_join(_subjects) { + var _n = array_length(_subjects); + if (_n == 0) { + return ""; + } + var _list = _subjects[0]; + for (var i = 1; i < _n; i++) { + if (i == _n - 1) { + _list += (_n > 2 ? ", and " : " and ") + _subjects[i]; + } else { + _list += ", " + _subjects[i]; + } + } + return _list; +} + +/// @self Asset.GMObject.obj_ncombat +/// @desc Sets `newline` to the enemy strength readout (live %, boss HP, or "Defeated") and fires the +/// enemy-defeated side-effects. Shared by obj_ncombat's Alarm_3 and Step_0 so the line can't +/// drift between the two copies (that drift is what hid the % for so long). +function combat_emit_enemy_status() { + if ((enemy_forces > 0) && (enemy != 30)) { + newline = "Enemy Forces at " + string(max(1, round((enemy_forces / enemy_max) * 100))) + "%"; + } + if ((enemy == 30) && instance_exists(obj_enunit)) { + newline = "Enemy has "; + var yoo = instance_nearest(0, 0, obj_enunit); + newline += string(round(yoo.dudes_hp[1])) + "HP remaining"; + } + if ((enemy_forces <= 0) || (!instance_exists(obj_enunit)) && (defeat_message == 0)) { + defeat_message = 1; + newline = "Enemy Forces Defeated"; + timer_maxspeed = 0; + timer_speed = 0; + started = 2; + instance_activate_object(obj_pnunit); + } } diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index e392c7ef80..42d110bbee 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -7,12 +7,6 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_data, melee_or_ranged) { try { // This massive clusterfuck of a script uses the newly determined weapon and target data to attack and assign damage - for (var j = 1; j <= 100; j++) { - obj_ncombat.dead_ene[j] = ""; - obj_ncombat.dead_ene_n[j] = 0; - } - obj_ncombat.dead_enemies = 0; - var hostile_type; var hostile_damage; var hostile_weapon; @@ -240,8 +234,6 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_dat if ((weapon_index_position >= 0) || (weapon_index_position < -40)) { // Normal shooting - var overkill = 0, damage_remaining = 0, shots_remaining = 0; - var that_works = false; if (weapon_index_position >= 0) { @@ -254,258 +246,315 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_dat } if (that_works == true) { - var damage_per_weapon = 0, c = 0, target_armour_value = 0, ap = 0, wii = ""; - var attack_count_mod = 0; + var damage_per_weapon = 0; + attack_count_mod = 0; if (weapon_index_position >= 0) { damage_per_weapon = aggregate_damage / wep_num[weapon_index_position]; - ap = armour_pierce; } // Average damage if (weapon_index_position < -40) { - wii = ""; attack_count_mod = 3; if (weapon_index_position == -51) { - wii = "Heavy Bolter Emplacement"; at = 160; armour_pierce = 0; } if (weapon_index_position == -52) { - wii = "Missile Launcher Emplacement"; at = 200; armour_pierce = -1; } if (weapon_index_position == -53) { - wii = "Missile Silo"; at = 250; armour_pierce = 0; } } - target_armour_value = target_object.dudes_ac[target_type]; + attack_count_mod = max(1, splash[weapon_index_position]); - // Calculate final armor value based on armor piercing (AP) rating against target type - if (target_object.dudes_vehicle[target_type]) { - if (armour_pierce == 4) { - target_armour_value = 0; + // Armour multiplier indexed by AP rating (1..4); any AP outside that range + // leaves armour untouched. Infantry and vehicles scale differently. + var _inf_ap = [1, 3, 2, 1.5, 0]; + var _veh_ap = [1, 6, 4, 2, 0]; + var _ap_valid = (armour_pierce >= 1) && (armour_pierce <= 4); + + // Never open fire on a dead rank/formation. Stale men/veh/medi (only refreshed on + // the enemy's own alarm) and scr_target's rank-1 fallback can aim us at corpses; + // snap to a living rank instead, or clean up the empty formation and bail. + if (!instance_exists(target_object)) { + exit; + } + if (target_object.dudes_num[target_type] <= 0) { + var _alive_rank = find_next_alive_rank(target_object, -1); + if (_alive_rank == -1) { + destroy_empty_column(target_object); + exit; } - if (armour_pierce == 3) { - target_armour_value = target_armour_value * 2; + target_type = _alive_rank; + } + + // Damage spills across ranks and, once a formation is spent, into the + // formation behind it. Every target actually fired upon gets its own flavour + // line with its own casualty count. The loop always terminates: shots_left + // strictly shrinks on every iteration that continues. + var spill_block = target_object; + var spill_rank = target_type; + var shots_left = shots_fired; + var touched_blocks = []; // Spill-over formations only; target_object is cleaned below. + + // The whole volley posts ONE battle-log line: the first target gets the rich + // weapon flavour (deferred), and every later target the overflow kills is + // gathered into a kill list appended to it (see emit_volley_flavour). + var _first_target = true; + var _primary_flavour = undefined; + var _spill_kills = []; // [{ name, count }] for targets killed after the first. + + while (shots_left > 0) { + // This target's armour against our AP rating. + var _armour = spill_block.dudes_ac[spill_rank]; + if (_ap_valid) { + var _ap_table = spill_block.dudes_vehicle[spill_rank] ? _veh_ap : _inf_ap; + _armour *= _ap_table[armour_pierce]; } - if (armour_pierce == 2) { - target_armour_value = target_armour_value * 4; + var final_hit = max(0, (damage_per_weapon - (_armour * attack_count_mod)) * spill_block.dudes_dr[spill_rank]); + + var rank_num = spill_block.dudes_num[spill_rank]; + var rank_hp = spill_block.dudes_hp[spill_rank]; + var total_damage = shots_left * final_hit; + var raw_kills = floor(total_damage / rank_hp); + var casualties = min(raw_kills, rank_num, shots_left * attack_count_mod); + + // Surplus damage only spills once this rank is actually wiped out. + var next_shots = 0; + if ((casualties >= rank_num) && (rank_num > 0) && (raw_kills > rank_num)) { + next_shots = max(0, shots_left - ceil((rank_num * rank_hp) / final_hit)); } - if (armour_pierce == 1) { - target_armour_value = target_armour_value * 6; + + // Gather flavour. The first target carries the rich phrasing (deferred, with + // the full weapon count); later targets just contribute to the kill list. + // final_hit <= 0 means armour stopped the shots cold (AP too low). + if (_first_target) { + _primary_flavour = scr_flavor(weapon_index_position, spill_block, spill_rank, shots_fired, casualties, final_hit <= 0, true); + _first_target = false; + } else if (casualties > 0) { + array_push(_spill_kills, { name: spill_block.dudes[spill_rank], count: casualties }); } - } else { - if (armour_pierce == 4) { - target_armour_value = 0; + + if ((rank_num == 1) && (casualties == 0) && (total_damage > 0)) { + spill_block.dudes_hp[spill_rank] -= total_damage; // Chip a lone survivor + if (spill_block.dudes_hp[spill_rank] <= 0) { + // Chipped to death: remove it now. Otherwise dudes_num stays 1 with + // dudes_hp <= 0 — an unkillable "zombie" that find_next_alive_rank skips + // but destroy_empty_column won't clear, keeping the formation (and the + // whole battle) alive forever. + spill_block.dudes_num[spill_rank] = 0; + obj_ncombat.enemy_forces -= 1; + } } - if (armour_pierce == 3) { - target_armour_value = target_armour_value * 1.5; + if (casualties >= 1) { + spill_block.dudes_num[spill_rank] -= casualties; + obj_ncombat.enemy_forces -= casualties; } - if (armour_pierce == 2) { - target_armour_value = target_armour_value * 2; + + shots_left = next_shots; + if (shots_left <= 0) { + break; } - if (armour_pierce == 1) { - target_armour_value = target_armour_value * 3; + + // Next target: a living rank in this formation, else the formation behind. + var next_rank = find_next_alive_rank(spill_block, spill_block.dudes_vehicle[spill_rank]); + if (next_rank == -1) { + spill_block = get_next_enemy_formation(spill_block); + if (spill_block == noone) { + break; + } + array_push(touched_blocks, spill_block); + next_rank = find_next_alive_rank(spill_block, -1); + if (next_rank == -1) { + break; + } } + spill_rank = next_rank; } - attack_count_mod = max(1, splash[weapon_index_position]); - - final_hit_damage_value = damage_per_weapon - (target_armour_value * attack_count_mod); //damage armour reduction - - final_hit_damage_value *= target_object.dudes_dr[target_type]; //damage_resistance mod + // Post the single consolidated line for the whole volley. + emit_volley_flavour(_primary_flavour, _spill_kills); - if (final_hit_damage_value <= 0) { - final_hit_damage_value = 0; - } // Average after armour + // Clean up the spill-over formations (target_object is handled below). + for (var _tb = 0; _tb < array_length(touched_blocks); _tb++) { + if (instance_exists(touched_blocks[_tb])) { + compress_enemy_array(touched_blocks[_tb]); + destroy_empty_column(touched_blocks[_tb]); + } + } + } + } - c = shots_fired * final_hit_damage_value; // New damage + if (stop == 0) { + compress_enemy_array(target_object); + destroy_empty_column(target_object); + } + } + } catch (_exception) { + ERROR_HANDLER.handle_exception(_exception); + } +} - var casualties, onceh = 0, ponies = 0; +/// @function find_next_alive_rank +/// @description Returns the index of the next living rank (dudes_num > 0 and dudes_hp > 0) in a +/// formation, preferring ranks that match the requested vehicle flag. Returns -1 if +/// none. The dudes_hp > 0 check keeps callers safe from dividing by a rank's HP. +/// @param {Id.Instance} _block The obj_enunit formation to search. +/// @param {Real} _prefer_vehicle 0/1 to prefer that category, or -1 for any living rank. +/// @returns {Real} +function find_next_alive_rank(_block, _prefer_vehicle) { + if (!instance_exists(_block)) { + return -1; + } + var _fallback = -1; + for (var f = 1; f <= 30; f++) { + if (_block.dudes_num[f] <= 0 || _block.dudes_hp[f] <= 0) { + continue; + } + if (_prefer_vehicle == -1 || _block.dudes_vehicle[f] == _prefer_vehicle) { + return f; + } + if (_fallback == -1) { + _fallback = f; + } + } + return _fallback; +} - casualties = min(floor(c / target_object.dudes_hp[target_type]), shots_fired * attack_count_mod); +/// @function get_next_enemy_formation +/// @description Returns the nearest enemy formation (obj_enunit) sitting behind the given one +/// that still contains at least one living rank, or noone if there isn't one. +/// @param {Id.Instance} _block The formation we are spilling out of. +/// @returns {Id.Instance} +function get_next_enemy_formation(_block) { + if (!instance_exists(_block)) { + return noone; + } + var _bx = _block.x; + var _bid = _block.id; + var _best = noone; + var _best_x = 0; + with (obj_enunit) { + if (id == _bid) { + continue; + } + if (x <= _bx) { + continue; + } + if (find_next_alive_rank(id, -1) == -1) { + continue; + } + if (_best == noone || x < _best_x) { + _best = id; + _best_x = x; + } + } + return _best; +} - ponies = target_object.dudes_num[target_type]; - if ((target_object.dudes_num[target_type] == 1) && ((target_object.dudes_hp[target_type] - c) <= 0)) { - casualties = 1; - } +/// @self Asset.GMObject.obj_pnunit +/// @description Speed Force: sweep the whole enemy force, dividing damage proportionally to rank +/// size, and report it as ONE consolidated volley line (see emit_volley_flavour). +/// @param {Real} weapon_index_position The Speed Force weapon stack index. +function scr_shoot_spread(weapon_index_position) { + try { + if (wep_num[weapon_index_position] <= 0 || ammo[weapon_index_position] == 0) { + exit; + } - if (target_object.dudes_num[target_type] - casualties < 0) { - overkill = casualties - target_object.dudes_num[target_type]; - damage_remaining = c - (overkill * target_object.dudes_hp[target_type]); + var _shots = wep_num[weapon_index_position]; + var _ap = apa[weapon_index_position]; + var _dpw = att[weapon_index_position] / _shots; // per-bike damage + var _mod = max(1, splash[weapon_index_position]); + if (ammo[weapon_index_position] > 0) { + ammo[weapon_index_position] -= 1; + } - shots_remaining = round(damage_remaining / damage_per_weapon); - } + // Armour multiplier indexed by AP rating (1..4), matching scr_shoot's normal path. + var _inf_ap = [1, 3, 2, 1.5, 0]; + var _veh_ap = [1, 6, 4, 2, 0]; + var _ap_valid = (_ap >= 1) && (_ap <= 4); + + // Total living models across every formation on the field. + var _formations = []; + var _total = 0; + with (obj_enunit) { + array_push(_formations, id); + for (var r = 1; r <= 30; r++) { + if (dudes[r] != "" && dudes_num[r] > 0) { + _total += dudes_num[r]; + } + } + } + if (_total <= 0) { + exit; + } - if (target_object.dudes_num[target_type] - casualties < 0) { - casualties = ponies; - } - if (casualties < 0) { - casualties = 0; - } + // Apply damage proportionally to each rank's share of the field; record every rank that lost models. + var _hits = []; // [{ name, kills, bounced }] + for (var fi = 0; fi < array_length(_formations); fi++) { + var _f = _formations[fi]; + if (!instance_exists(_f)) { + continue; + } + for (var r = 1; r <= 30; r++) { + if (_f.dudes[r] == "" || _f.dudes_num[r] <= 0) { + continue; + } - if (casualties >= 1) { - var iii = 0, found = 0, openz = 0; - for (iii = 0; iii <= 40; iii++) { - iii += 1; - if (found == 0) { - if ((obj_ncombat.dead_ene[iii] == "") && (openz == 0)) { - openz = iii; - } - if ((obj_ncombat.dead_ene[iii] == target_object.dudes[target_type]) && (found == 0)) { - found = iii; - obj_ncombat.dead_ene_n[obj_ncombat.dead_enemies] += casualties; - } - } - } - if (found == 0) { - obj_ncombat.dead_enemies += 1; - obj_ncombat.dead_ene[openz] = string(target_object.dudes[target_type]); - obj_ncombat.dead_ene_n[openz] = casualties; - } - } + var _armour = _f.dudes_ac[r]; + if (_ap_valid) { + var _ap_table = _f.dudes_vehicle[r] ? _veh_ap : _inf_ap; + _armour *= _ap_table[_ap]; + } - var k = 0; - if ((damage_remaining > 0) && (shots_remaining > 0)) { - repeat (10) { - if ((damage_remaining > 0) && (shots_remaining > 0)) { - var godd; - godd = 0; - k = target_type; - - // Find similar target in this same group - repeat (10) { - k += 1; - if (godd == 0) { - if ((target_object.dudes_num[k] > 0) && (target_object.dudes_vehicle[k] == target_object.dudes_vehicle[target_type])) { - godd = k; - } - } - } - k = target_type; - if (godd == 0) { - repeat (10) { - k -= 1; - if ((godd == 0) && (k >= 1)) { - if ((target_object.dudes_num[k] > 0) && (target_object.dudes_vehicle[k] == target_object.dudes_vehicle[target_type])) { - godd = k; - } - } - } - } - - // Found damage_per_weapon similar target to get the damage - if ((godd > 0) && (damage_remaining > 0) && (shots_remaining > 0)) { - var a2, b2, c2, target_armour_value2, ap2; - ap2 = damage_remaining; - a2 = damage_per_weapon; // Average damage - - target_armour_value2 = target_object.dudes_ac[godd]; - if (target_object.dudes_vehicle[godd] == 0) { - if (ap2 == 1) { - target_armour_value2 = target_armour_value2 * 3; - } - if (ap2 == 2) { - target_armour_value2 = target_armour_value2 * 2; - } - if (ap2 == 3) { - target_armour_value2 = target_armour_value2 * 1.5; - } - if (ap2 == 4) { - target_armour_value2 = 0; - } - } - if (target_object.dudes_vehicle[godd] == 1) { - if (ap2 == 1) { - target_armour_value2 = target_armour_value2 * 6; - } - if (ap2 == 2) { - target_armour_value2 = target_armour_value2 * 4; - } - if (ap2 == 3) { - target_armour_value2 = target_armour_value2 * 2; - } - } - b2 = a2 - target_armour_value2; - if (b2 <= 0) { - b2 = 0; - } // Average after armour - - c2 = b2 * shots_remaining; // New damage - - var casualties2 = 0; - var onceh2 = 0; - var ponies2 = 0; - if (attack_count_mod <= 1) { - casualties2 = min(floor(c2 / target_object.dudes_hp[godd]), shots_remaining); - } - - if (attack_count_mod > 1) { - casualties2 = floor(c2 / target_object.dudes_hp[godd]); - } - ponies2 = target_object.dudes_num[godd]; - if ((target_object.dudes_num[godd] == 1) && ((target_object.dudes_hp[godd] - c2) <= 0)) { - casualties2 = 1; - } - if (target_object.dudes_num[godd] < casualties2) { - casualties2 = target_object.dudes_num[godd]; - } - if (casualties2 < 1) { - casualties2 = 0; - damage_remaining = 0; - overkill = 0; - shots_remaining = 0; - } - - if ((casualties2 >= 1) && (shots_fired > 0)) { - var iii = 0; - var found = 0; - var openz = 0; - repeat (40) { - iii += 1; - if (found == 0) { - if ((obj_ncombat.dead_ene[iii] == "") && (openz == 0)) { - openz = iii; - } - if ((obj_ncombat.dead_ene[iii] == target_object.dudes[godd]) && (found == 0)) { - found = iii; - obj_ncombat.dead_ene_n[obj_ncombat.dead_enemies] += casualties; - } - } - } - if (found == 0) { - obj_ncombat.dead_enemies += 1; - obj_ncombat.dead_ene[openz] = string(target_object.dudes[godd]); - obj_ncombat.dead_ene_n[openz] = casualties; - } - - target_object.dudes_num[godd] -= casualties2; - obj_ncombat.enemy_forces -= casualties2; - } - } - } - } - } // End repeat 10 - scr_flavor(weapon_index_position, target_object, target_type, shots_fired - wep_rnum[weapon_index_position], casualties); + var _rank_shots = _shots * (_f.dudes_num[r] / _total); + var _final_hit = max(0, (_dpw - (_armour * _mod)) * _f.dudes_dr[r]); + var _kills = min(floor((_rank_shots * _final_hit) / _f.dudes_hp[r]), _f.dudes_num[r]); + if (_kills < 0) { + _kills = 0; + } - if ((target_object.dudes_num[target_type] == 1) && (c > 0)) { - target_object.dudes_hp[target_type] -= c; - } // Need special flavor here for just damaging + if (_kills > 0) { + _f.dudes_num[r] -= _kills; + obj_ncombat.enemy_forces -= _kills; + array_push(_hits, { name: _f.dudes[r], kills: _kills, bounced: (_final_hit <= 0), block: _f, rank: r }); + } + } + } - if (casualties >= 1) { - target_object.dudes_num[target_type] -= casualties; - obj_ncombat.enemy_forces -= casualties; - } + // Primary = the rank with the most kills (rich deferred flavour); the rest form the kill list. + var _primary = undefined; + var _spill = []; + if (array_length(_hits) > 0) { + var _best = 0; + for (var i = 1; i < array_length(_hits); i++) { + if (_hits[i].kills > _hits[_best].kills) { + _best = i; } } + for (var i = 0; i < array_length(_hits); i++) { + if (i == _best) { + continue; + } + array_push(_spill, { name: _hits[i].name, count: _hits[i].kills }); + } + var _p = _hits[_best]; + if (instance_exists(_p.block)) { + _primary = scr_flavor(weapon_index_position, _p.block, _p.rank, _shots, _p.kills, _p.bounced, true); + } + } + emit_volley_flavour(_primary, _spill); - if (stop == 0) { - compress_enemy_array(target_object); - destroy_empty_column(target_object); + // Clean up spent ranks/formations (mirrors scr_shoot). + for (var fi = 0; fi < array_length(_formations); fi++) { + if (instance_exists(_formations[fi])) { + compress_enemy_array(_formations[fi]); + destroy_empty_column(_formations[fi]); } } } catch (_exception) { From 1e029b722b0c4f61e842f323e1c44eb977152e1f Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:36:53 +0200 Subject: [PATCH 42/55] fix: stale comments --- objects/obj_ncombat/Create_0.gml | 8 ++++---- scripts/scr_flavor/scr_flavor.gml | 6 +++--- scripts/scr_shoot/scr_shoot.gml | 7 +++---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/objects/obj_ncombat/Create_0.gml b/objects/obj_ncombat/Create_0.gml index 16bec098b5..a3371158a1 100644 --- a/objects/obj_ncombat/Create_0.gml +++ b/objects/obj_ncombat/Create_0.gml @@ -151,10 +151,10 @@ dead_ene_n = array_create(70, 0); crunch = array_create(70, 0); mucra = array_create(11, 0); -// The combat-log queue must be large enough that a long turn fully drains. The status line -// ("Enemy Forces at X%" / "Defeated") only renders once `messages` reaches 0, and Alarm_3 drains -// the queue through fixed windows — anything past the window strands the tail, leaving messages > 0 -// forever so the status never shows. Size the message arrays generously to match those windows. +// The combat-log queue must hold at least COMBAT_LOG_CAPACITY entries so a long turn fully drains. +// The status line ("Enemy Forces at X%" / "Defeated") only renders once `messages` reaches 0, and +// Alarm_3 drains the queue through a COMBAT_LOG_CAPACITY-wide window - anything past it strands the +// tail, leaving messages > 0 forever so the status never shows. The +20 is headroom for compaction. for (var _m = 1; _m <= COMBAT_LOG_CAPACITY + 20; _m++) { message[_m] = ""; message_sz[_m] = 0; diff --git a/scripts/scr_flavor/scr_flavor.gml b/scripts/scr_flavor/scr_flavor.gml index 5ce81478a8..817acd55a4 100644 --- a/scripts/scr_flavor/scr_flavor.gml +++ b/scripts/scr_flavor/scr_flavor.gml @@ -809,7 +809,7 @@ function format_kill_list(_kills) { } /// @desc Posts a single consolidated volley line: the deferred rich flavour for the first target, -/// plus an "Also cut down: ..." list of everything the volley's overflow killed afterwards. +/// plus a trailing list of everything the volley's overflow killed afterwards. /// @param {Struct} _primary Result returned by scr_flavor(..., _defer=true) for the first target (or undefined). /// @param {Array} _spill_kills Array of { name, count } for targets killed after the first. function emit_volley_flavour(_primary, _spill_kills) { @@ -827,8 +827,8 @@ function emit_volley_flavour(_primary, _spill_kills) { combat_tally_flush(); if (!is_struct(_primary)) { - // First target produced no line (hit but didn't kill, and didn't bounce). Spill-over only - // happens after a wipe, so there should be nothing to report, but stay defensive. + // No primary line (scr_flavor bailed on a dead target - shouldn't happen now that emptied + // formations are destroyed). Spill-over only happens after a wipe, so this is just defensive. if (_list != "") { add_battle_log_message("Overflowing fire cuts down " + _list + ".", 0, 0); display_battle_log_message(); diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index 42d110bbee..873745b76b 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -342,10 +342,9 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_dat if ((rank_num == 1) && (casualties == 0) && (total_damage > 0)) { spill_block.dudes_hp[spill_rank] -= total_damage; // Chip a lone survivor if (spill_block.dudes_hp[spill_rank] <= 0) { - // Chipped to death: remove it now. Otherwise dudes_num stays 1 with - // dudes_hp <= 0 — an unkillable "zombie" that find_next_alive_rank skips - // but destroy_empty_column won't clear, keeping the formation (and the - // whole battle) alive forever. + // Chipped to death: remove it now and drop the force count. Otherwise + // dudes_num stays 1 at dudes_hp <= 0 - a "zombie" that find_next_alive_rank + // skips, so it's never finished off and keeps inflating enemy_forces. spill_block.dudes_num[spill_rank] = 0; obj_ncombat.enemy_forces -= 1; } From 0309395cf256581cc3b61aded7b42f2f1fd7a6e0 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Fri, 26 Jun 2026 02:26:33 +0200 Subject: [PATCH 43/55] fix: doubled text --- scripts/scr_flavor/scr_flavor.gml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/scr_flavor/scr_flavor.gml b/scripts/scr_flavor/scr_flavor.gml index 817acd55a4..79500630d6 100644 --- a/scripts/scr_flavor/scr_flavor.gml +++ b/scripts/scr_flavor/scr_flavor.gml @@ -260,7 +260,7 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot if (casulties == 0) { attack_message += $"but all survive the impact."; } else { - attack_message += $"killing {casulties} perish in the attack."; + attack_message += $"and {casulties} are crushed in the impact."; } } } @@ -289,7 +289,7 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot } } else { if (target.dudes_num[targeh] == 1) { - attack_message += string(unit_name) + $" speeds on his bike, soaring and crashing into the {target_name}- "; + attack_message += string(unit_name) + $" speeds on his bike, roaring and crashing into the {target_name}- "; if (casulties == 0) { attack_message += $"but it endures the onslaught."; } else { @@ -300,7 +300,7 @@ function scr_flavor(id_of_attacking_weapons, target, target_type, number_of_shot if (casulties == 0) { attack_message += $"but all survive the impact."; } else { - attack_message += $"killing {casulties} perish in the attack."; + attack_message += $"crushing {casulties} beneath his wheels."; } } } From 76a4c710db4e03608fc8fbbaa8aba817aa458879 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Fri, 26 Jun 2026 03:04:30 +0200 Subject: [PATCH 44/55] feat: combat log now scrollable --- objects/obj_ncombat/Create_0.gml | 10 +++++ objects/obj_ncombat/Draw_0.gml | 58 ++++++++++++++++++++++++---- objects/obj_ncombat/Step_0.gml | 35 +++++++++++++++++ objects/obj_pnunit/Mouse_0.gml | 59 +++++++++++++++++++++++++++-- scripts/scr_newtext/scr_newtext.gml | 15 ++++++++ 5 files changed, 166 insertions(+), 11 deletions(-) diff --git a/objects/obj_ncombat/Create_0.gml b/objects/obj_ncombat/Create_0.gml index a3371158a1..1847fb443b 100644 --- a/objects/obj_ncombat/Create_0.gml +++ b/objects/obj_ncombat/Create_0.gml @@ -188,6 +188,16 @@ dead_jims = 0; newline = ""; newline_color = ""; liness = 0; + +// Combat-log scrollback. lines[] is only a rolling 45-row live window (older rows are discarded by +// scr_lines_increase), so keep a separate capped history the player can scroll back through. +// log_scroll counts rows above the live bottom: 0 = pinned to the newest line (live). +log_history = []; +log_history_max = 300; +log_scroll = 0; +log_view_lines = 45; +log_dragging = false; + world_size = 0; timer = 0; diff --git a/objects/obj_ncombat/Draw_0.gml b/objects/obj_ncombat/Draw_0.gml index 76408f4127..4cd4e4f5ae 100644 --- a/objects/obj_ncombat/Draw_0.gml +++ b/objects/obj_ncombat/Draw_0.gml @@ -50,33 +50,75 @@ if ((display_p2 > 0) && (enemy_forces > 0)) { draw_set_halign(fa_left); +// When pinned to the bottom (log_scroll == 0) the live 45-row window is drawn exactly as before; +// when scrolled up, page back through the retained log_history instead. +var _log_total = array_length(log_history); +var _log_start = (log_scroll <= 0) ? -1 : max(0, _log_total - log_view_lines - log_scroll); + repeat (45) { l += 1; + + var _row_txt, _row_col; + if (_log_start < 0) { + _row_txt = lines[l]; + _row_col = lines_color[l]; + } else { + var _hi = _log_start + (l - 1); + if ((_hi >= 0) && (_hi < _log_total)) { + _row_txt = log_history[_hi].text; + _row_col = log_history[_hi].color; + } else { + _row_txt = ""; + _row_col = ""; + } + } + draw_set_color(CM_GREEN_COLOR); - if (lines_color[l] == "red") { + if (_row_col == "red") { draw_set_color(c_red); } - if (lines_color[l] == "yellow") { + if (_row_col == "yellow") { draw_set_color(3055825); } - if (lines_color[l] == "purple") { + if (_row_col == "purple") { draw_set_color(16646566); } - if (lines_color[l] == "bright") { + if (_row_col == "bright") { draw_set_color(65280); } - if (lines_color[l] == "white") { + if (_row_col == "white") { draw_set_color(c_silver); } - if (lines_color[l] == "blue") { + if (_row_col == "blue") { draw_set_color(c_aqua); } - if (lines_color[l] == "lightgreen") { + if (_row_col == "lightgreen") { draw_set_color(make_color_rgb(150, 255, 150)); } - draw_text(x + 6, y - 10 + (l * 18), string_hash_to_newline(string(lines[l]))); + draw_text(x + 6, y - 10 + (l * 18), string_hash_to_newline(string(_row_txt))); } +// Combat-log scrollbar: a thin draggable thumb in the gutter between the frame and the text column. +// Only shown when there's more history than the visible window. Thumb height tracks visible/total; +// it sits at the bottom when live (log_scroll == 0) and rises as the player pages back. +if (_log_total > log_view_lines) { + var _sb_x1 = x + 2; + var _sb_x2 = x + 4; + var _sb_y1 = y + 8; + var _sb_h = log_view_lines * 18; + var _sb_max_scroll = _log_total - log_view_lines; + var _sb_thumb_h = max(20, _sb_h * (log_view_lines / _log_total)); + var _sb_frac = log_scroll / _sb_max_scroll; // 0 = live bottom, 1 = oldest + var _sb_thumb_y1 = _sb_y1 + (1 - _sb_frac) * (_sb_h - _sb_thumb_h); + + draw_set_color(CM_GREEN_COLOR); + draw_set_alpha(0.3); + draw_rectangle(_sb_x1, _sb_y1, _sb_x2, _sb_y1 + _sb_h, false); + draw_set_alpha(1); + draw_rectangle(_sb_x1, _sb_thumb_y1, _sb_x2, _sb_thumb_y1 + _sb_thumb_h, false); +} +draw_set_alpha(1); + draw_set_color(CM_GREEN_COLOR); if (click_stall_timer <= 0) { if ((fadein < 0) && (fadein > -100) && (started == 0)) { diff --git a/objects/obj_ncombat/Step_0.gml b/objects/obj_ncombat/Step_0.gml index 1ce17229e3..5552091b30 100644 --- a/objects/obj_ncombat/Step_0.gml +++ b/objects/obj_ncombat/Step_0.gml @@ -1,3 +1,38 @@ +// --- Combat-log scroll input ------------------------------------------------- +// Mouse wheel over the left log panel pages through retained history; the thin scrollbar in the +// gutter can also be grabbed and dragged. log_scroll counts rows above the live bottom (0 = live). +var _log_total = array_length(log_history); +var _log_max_scroll = max(0, _log_total - log_view_lines); + +if ((mouse_x >= x) && (mouse_x <= x + 800) && (mouse_y >= y) && (mouse_y <= y + 900)) { + if (mouse_wheel_up()) { + log_scroll += 3; + } + if (mouse_wheel_down()) { + log_scroll -= 3; + } +} + +var _sb_y1 = y + 8; +var _sb_h = log_view_lines * 18; +if (mouse_check_button_pressed(mb_left) && (_log_max_scroll > 0) + && (mouse_x >= x + 1) && (mouse_x <= x + 5) + && (mouse_y >= _sb_y1) && (mouse_y <= _sb_y1 + _sb_h)) { + log_dragging = true; +} +if (!mouse_check_button(mb_left)) { + log_dragging = false; +} +if (log_dragging && (_log_max_scroll > 0)) { + var _sb_thumb_h = max(20, _sb_h * (log_view_lines / _log_total)); + var _sb_usable = max(1, _sb_h - _sb_thumb_h); + var _sb_rel = clamp((mouse_y - _sb_y1 - _sb_thumb_h * 0.5) / _sb_usable, 0, 1); + log_scroll = round((1 - _sb_rel) * _log_max_scroll); +} + +log_scroll = clamp(log_scroll, 0, _log_max_scroll); +// ----------------------------------------------------------------------------- + if (fadein > -30) { fadein -= 1; } diff --git a/objects/obj_pnunit/Mouse_0.gml b/objects/obj_pnunit/Mouse_0.gml index c7536fda5a..590cdab285 100644 --- a/objects/obj_pnunit/Mouse_0.gml +++ b/objects/obj_pnunit/Mouse_0.gml @@ -1,5 +1,58 @@ -for (var i = 1; i <= 50; i++) { - if (marine_type[i] != "") { - show_message(string(i) + ", " + string(marine_type[i]) + ", HP: " + string(marine_hp[i])); +/*show_message("Engaged "+string(engaged)+" +"+string(dudes_num[1])+"x "+string(dudes[1])+" +"+string(dudes_num[2])+"x "+string(dudes[2])+" +"+string(dudes_num[3])+"x "+string(dudes[3])+" +"+string(dudes_num[4])+"x "+string(dudes[4])+" +"+string(dudes_num[5])+"x "+string(dudes[5])+" +"+string(dudes_num[6])+"x "+string(dudes[6])+" +"+string(dudes_num[7])+"x "+string(dudes[7])+" +"+string(dudes_num[8])+"x "+string(dudes[8])+" +"+string(dudes_num[9])+"x "+string(dudes[9])+" +"+string(dudes_num[10])+"x "+string(dudes[10])); +*/ + +/*show_message("Engaged "+string(engaged)+" +"+string(wep_num[1])+"x "+string(wep[1])+": ATT"+string(att[1])+" ARP"+string(apa[1])+" solo:"+string(wep_solo[1])+" +"+string(wep_num[2])+"x "+string(wep[2])+": ATT"+string(att[2])+" ARP"+string(apa[2])+" solo:"+string(wep_solo[2])+" +"+string(wep_num[3])+"x "+string(wep[3])+": ATT"+string(att[3])+" ARP"+string(apa[3])+" solo:"+string(wep_solo[3])+" +"+string(wep_num[4])+"x "+string(wep[4])+": ATT"+string(att[4])+" ARP"+string(apa[4])+" solo:"+string(wep_solo[4])+" +"+string(wep_num[5])+"x "+string(wep[5])+": ATT"+string(att[5])+" ARP"+string(apa[5])+" solo:"+string(wep_solo[5])+" +"+string(wep_num[6])+"x "+string(wep[6])+": ATT"+string(att[6])+" ARP"+string(apa[6])+" solo:"+string(wep_solo[6])+" +"+string(wep_num[7])+"x "+string(wep[7])+": ATT"+string(att[7])+" ARP"+string(apa[7])+" solo:"+string(wep_solo[7])+" +"+string(wep_num[8])+"x "+string(wep[8])+": ATT"+string(att[8])+" ARP"+string(apa[8])+" solo:"+string(wep_solo[8])+" +"+string(wep_num[9])+"x "+string(wep[9])+": ATT"+string(att[9])+" ARP"+string(apa[9])+" solo:"+string(wep_solo[9])+" +"+string(wep_num[10])+"x "+string(wep[10])+": ATT"+string(att[10])+" ARP"+string(apa[10])+" solo:"+string(wep_solo[10])); + +*/ + +/*var blarg;blarg=wep; +show_message(blarg);*/ + +/*show_message("Engaged "+string(engaged)+" +"+string(dudes_num[1])+"x "+string(dudes[1])+" +"+string(dudes_num[2])+"x "+string(dudes[2])+" +"+string(dudes_num[3])+"x "+string(dudes[3])+" +"+string(dudes_num[4])+"x "+string(dudes[4])+" +"+string(dudes_num[5])+"x "+string(dudes[5])+" +"+string(dudes_num[6])+"x "+string(dudes[6])+" +"+string(dudes_num[7])+"x "+string(dudes[7])+" +"+string(dudes_num[8])+"x "+string(dudes[8])+" +"+string(dudes_num[9])+"x "+string(dudes[9])+" +"+string(dudes_num[10])+"x "+string(dudes[10])+" +"+string(dudes_num[11])+"x "+string(dudes[11])+" +"+string(dudes_num[12])+"x "+string(dudes[12])+" +"+string(dudes_num[13])+"x "+string(dudes[13])+" +"+string(dudes_num[14])+"x "+string(dudes[14])+" +"+string(dudes_num[15])+"x "+string(dudes[15])+" +"+string(dudes_num[16])+"x "+string(dudes[16]));*/ + +// Debug-only roster dump. Guarded behind cheat_debug so a stray left-click during combat (e.g. on +// the log scrollbar, which overlaps the leftmost unit's sprite box) can't fire it in normal play, +// and bounded by the real array length so it can't index past marine_type. +if (global.cheat_debug == 1) { + for (var i = 0; i < array_length(marine_type); i++) { + if (marine_type[i] != "") { + show_message(string(i) + ", " + string(marine_type[i]) + ", HP: " + string(marine_hp[i])); + } } } diff --git a/scripts/scr_newtext/scr_newtext.gml b/scripts/scr_newtext/scr_newtext.gml index 67f7317824..065a56ec63 100644 --- a/scripts/scr_newtext/scr_newtext.gml +++ b/scripts/scr_newtext/scr_newtext.gml @@ -21,6 +21,21 @@ function scr_newtext() { } liness += string_count("@", newline); + // Mirror the freshly appended rows into the scrollback history (capped) so the log stays + // scrollable after lines[] rolls them off its 45-row live window. If the player is currently + // reading scrolled-up history, push their view down by the same amount to hold its position. + if (variable_instance_exists(id, "log_history")) { + for (var _h = first_open; _h <= first_open + breaks - 1; _h++) { + array_push(log_history, { text: lines[_h], color: newline_color }); + } + while (array_length(log_history) > log_history_max) { + array_delete(log_history, 0, 1); + } + if (log_scroll > 0) { + log_scroll = min(log_scroll + breaks, max(0, array_length(log_history) - log_view_lines)); + } + } + repeat (100) { // if (liness>30){scr_lines_increase(1);liness-=1;} if (liness > 45) { From 5b6d8d59f3f18afe767f761a30c5aa82a5c1801f Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:11:22 +0200 Subject: [PATCH 45/55] fix: post-merge issues --- objects/obj_ncombat/Alarm_3.gml | 1 + .../scr_initialize_custom.gml | 28 ++++++------------- scripts/scr_squads/scr_squads.gml | 23 ++++++++++++++- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/objects/obj_ncombat/Alarm_3.gml b/objects/obj_ncombat/Alarm_3.gml index a73a772141..cea15d12f0 100644 --- a/objects/obj_ncombat/Alarm_3.gml +++ b/objects/obj_ncombat/Alarm_3.gml @@ -12,6 +12,7 @@ var changed = 0; repeat (100) { if (good == 0) { changed = 0; + i = 0; repeat (COMBAT_LOG_CAPACITY) { i += 1; diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 83aafdb05d..9c3ba60e89 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -2417,27 +2417,15 @@ function scr_initialize_custom() { _coy.devastators = devastator; } - // Companies 6-7: only receive scouts under the non-LW equal_scouts arrangement - // (company_squad_builds/equal_scouts.json gives 6-7 nothing but tactical_squad - // when Lightning Warriors is active - lightning_warriors.json's equal_scouts - // override does the same: companies 6 and 7 are tactical_squad-only, with no - // scout_squad entry at all). Granting _coy.scouts here for LW would create scout - // marines that the LW template can never organise into squads, leaving them as - // stray squadless scouts in companies that should be scout-free. + // Companies 6-7 are tactical-only reserves for every distribution. Both + // company_squad_builds/equal_scouts.json and lightning_warriors.json's equal_scouts + // override define companies 6 and 7 as tactical_squad-only, with no scout_squad entry + // at all - so handing them scout marines here (under any distribution) would leave + // stray squadless scouts the template can never organise into squads. Scouts stay + // confined to the battle companies 2-5 (which fall through to default_squads' + // scout_squad) and the 10th; the scouts not moved here simply remain in the 10th's bank. if (real(_coy.coy) >= 6 && real(_coy.coy) <= 7) { - if (equal_scouts && !_lw) { - if (companies.tenth.scouts > 10) { - _coy.scouts = 10; - _moved_scouts += _coy.scouts; - _coy.tacticals = _coy.total - _coy.scouts; - companies.tenth.scouts -= _coy.scouts; - } else { - // if 10th is run out somehow, revert to normal behaviour - _coy.tacticals = _coy.total; - } - } else { - _coy.tacticals = _coy.total; - } + _coy.tacticals = _coy.total; _coy.assaults = 0; _coy.devastators = 0; } diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 0a7f062ea3..044af294fc 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -377,6 +377,11 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { static change_type = function(new_type) { type = new_type; + if (is_array(type)) { + show_debug_message($"[PROBE] change_type got ARRAY type (len {array_length(type)}): {type}"); + } else if (!struct_exists(obj_ini.squad_types, type)) { + show_debug_message($"[PROBE] change_type unknown squad type: \"{type}\""); + } add_type_data(obj_ini.squad_types[$ type].type_data); }; @@ -920,13 +925,29 @@ function apply_squad_distribution_override(arrangement, override) { /// individuals have been created by the count-based initialisation pass. /// @return {Undefined} function get_compay_squad_arrangement(company){ - var _comp_datas = obj_ini.chapter_squad_arrangement.companies; + var _arrangement = obj_ini.chapter_squad_arrangement; + if (!struct_exists(_arrangement, "companies")) { + _arrangement.companies = []; + } + var _comp_datas = _arrangement.companies; for (var i = 0; i < array_length(_comp_datas); i++) { if (_comp_datas[i].company == company){ return _comp_datas[i]; } } + // No explicit entry: this company currently inherits default_squads. Promote it to its own + // explicit entry, deep-cloning default_squads so the editor's in-place edits can't mutate the + // shared array every other defaulted company also points at. Registering it persists the edits + // and lets resolve_company_arrangement pick this company up by its own entry from now on. + var _src = struct_exists(_arrangement, "default_squads") ? _arrangement.default_squads : []; + var _squads = array_create(array_length(_src)); + for (var _i = 0; _i < array_length(_src); _i++) { + _squads[_i] = variable_clone(_src[_i]); + } + var _entry = { company: company, squads: _squads }; + array_push(_comp_datas, _entry); + return _entry; } function ProportionalSquadEditor(data) constructor { From 0979dc64816d839c67ecd4189d52a13a184becd7 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:03:21 +0200 Subject: [PATCH 46/55] fix: bike encumbrance --- datafiles/data/mobility.json | 2 +- datafiles/main/chapters/1.JSON | 4 ++-- datafiles/main/squads/base_squads.json | 12 +++--------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index f26e7b7f49..4b6f2b5887 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -12,7 +12,7 @@ "master_crafted": 30, "standard": 25 }, - "ranged_hands": -1, + "ranged_hands": -0.5, "melee_mod":{ "artifact": 30, "master_crafted": 25, diff --git a/datafiles/main/chapters/1.JSON b/datafiles/main/chapters/1.JSON index 949a7a06b8..5774322a66 100644 --- a/datafiles/main/chapters/1.JSON +++ b/datafiles/main/chapters/1.JSON @@ -1722,7 +1722,7 @@ }, "option": { "wep2": [ - [["Flamer", "Grav-Gun", "Meltagun", "Plasma Gun", "Plasma Pistol"], 2] + [["Hand Flamer", "Grav-Pistol", "Infernus Pistol", "Plasma Pistol"], 2] ] } } @@ -1821,7 +1821,7 @@ }, "option": { "wep2": [ - [["Flamer", "Grav-Gun", "Meltagun", "Plasma Gun", "Plasma Pistol"], 2] + [["Hand Flamer", "Grav-Pistol", "Infernus Pistol", "Plasma Pistol", "Volkite Serpenta"], 2] ] } } diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index f92602c28a..a8c6442dee 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -494,16 +494,10 @@ "wep2": ["", 0], "mobi": ["Bike", 1] }, - "random_pick": [ - { - "wep1": ["Power Sword", "Power Spear", "Power Axe", "Power Fist", "Chainaxe", "Chainsword"], - "wep2": ["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "Ryza Plasma Pistol"] - }, - { - "wep1": "Lightning Claw", - "wep2": "Lightning Claw" + "option": { + "wep1": [[["Power Sword", "Power Spear", "Power Axe", "Power Fist", "Chainaxe", "Chainsword"],1]], + "wep2": [[["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "Ryza Plasma Pistol"],1]] } - ] } }, "type_data": { From 35d4b8973b80d995a6d61673ae201f1ee47f37c8 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:26:45 +0200 Subject: [PATCH 47/55] fix: ID was in the further part for some reason --- datafiles/main/chapters/34.json | 1 - 1 file changed, 1 deletion(-) diff --git a/datafiles/main/chapters/34.json b/datafiles/main/chapters/34.json index 4766bef98a..b8d075f747 100644 --- a/datafiles/main/chapters/34.json +++ b/datafiles/main/chapters/34.json @@ -39,7 +39,6 @@ "", "" ], - "id": 34, "chapter_master": { "ranged": 1.0, "specialty": 2.0, From bfc65b5e4c9d6af7fb32e57ac6cf133b77aeeb03 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:58:29 +0200 Subject: [PATCH 48/55] fix: equal specialist+scouts --- datafiles/main/squads/equal_spescout.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datafiles/main/squads/equal_spescout.json b/datafiles/main/squads/equal_spescout.json index 1afcb85a8c..5fd0acbf98 100644 --- a/datafiles/main/squads/equal_spescout.json +++ b/datafiles/main/squads/equal_spescout.json @@ -20,7 +20,8 @@ "company": 10, "squads": [ { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, - { "squad": "devastator_squad", "proportion": 1 } + { "squad": "scout_squad", "proportion": 5 }, + { "squad": "tactical_squad", "proportion": 4 } ] } ] From dd22861c45dd64b84ceaebf2786bb67443b36a66 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:14:28 +0200 Subject: [PATCH 49/55] fix: cubic's issues and various oversights --- datafiles/main/chapters/template.JSON | 2 +- objects/obj_controller/Create_0.gml | 2 +- objects/obj_ncombat/Alarm_3.gml | 8 ++- objects/obj_ncombat/Create_0.gml | 4 ++ objects/obj_ncombat/Draw_0.gml | 7 +-- objects/obj_ncombat/Step_0.gml | 12 +++-- objects/obj_pnunit/Alarm_0.gml | 8 ++- objects/obj_popup/Create_0.gml | 18 ------- scripts/scr_UnitGroup/scr_UnitGroup.gml | 51 ++++++++++--------- .../scr_company_order/scr_company_order.gml | 11 +++- scripts/scr_flavor/scr_flavor.gml | 2 +- .../scr_initialize_custom.gml | 20 ++++++-- scripts/scr_management/scr_management.gml | 19 +++++++ scripts/scr_powers/scr_powers.gml | 3 +- scripts/scr_shoot/scr_shoot.gml | 17 ++++++- scripts/scr_squads/scr_squads.gml | 42 ++++++++++----- 16 files changed, 150 insertions(+), 76 deletions(-) diff --git a/datafiles/main/chapters/template.JSON b/datafiles/main/chapters/template.JSON index 291e21eb95..501d2fca10 100644 --- a/datafiles/main/chapters/template.JSON +++ b/datafiles/main/chapters/template.JSON @@ -16,7 +16,7 @@ "purity": 0, //1-10 "stability": 0, //1-99 "cooperation": 0, //1-10 - "homeworld_exists:": 1, //only is there for an obscure bit of code, but to avoid bugs and crashes keep it as is until we fix it + "homeworld_exists": 1, //only is there for an obscure bit of code, but to avoid bugs and crashes keep it as is until we fix it "recruiting_exists": 1, //same here "home_warp": 1, //[0] low/no connections [1] connected [2] warp hub "home_planets": 1, //how many planets in home system, [0]=1 [3]=4 diff --git a/objects/obj_controller/Create_0.gml b/objects/obj_controller/Create_0.gml index a672c521b9..b71582da40 100644 --- a/objects/obj_controller/Create_0.gml +++ b/objects/obj_controller/Create_0.gml @@ -1635,7 +1635,7 @@ for (var company = 0; company < 10; company++) { } if (com > 0) { - if (veter + termi + stand + dread + tact + assa + deva + rhino + raider + standard + scou + whirl > 0) { + if (veter + termi + stand + dread + tact + assa + deva + rhino + raider + standard + scou + whirl + bikers + attack_bikers > 0) { temp[njm] = $"{integer_to_words(com, true, true)} company made of"; } else { temp[njm] = ""; diff --git a/objects/obj_ncombat/Alarm_3.gml b/objects/obj_ncombat/Alarm_3.gml index cea15d12f0..b0edc21b9f 100644 --- a/objects/obj_ncombat/Alarm_3.gml +++ b/objects/obj_ncombat/Alarm_3.gml @@ -14,7 +14,10 @@ repeat (100) { changed = 0; i = 0; - repeat (COMBAT_LOG_CAPACITY) { + // Scan the whole allocated queue (COMBAT_LOG_CAPACITY + 20 slots, see Create), stopping one + // short of the top so the message[i + 1] lookahead stays in range. Compacting only the first + // COMBAT_LOG_CAPACITY slots would strand any tail entries so `messages` never reaches 0. + repeat (COMBAT_LOG_CAPACITY + 19) { i += 1; // Collide the messages if needed @@ -45,7 +48,8 @@ if (messages > 0) { var that = 0; i = 0; - repeat (COMBAT_LOG_CAPACITY) { + // Scan the whole allocated queue so tail entries past COMBAT_LOG_CAPACITY still drain. + repeat (COMBAT_LOG_CAPACITY + 20) { i += 1; if (message[i] != "") { that = i; diff --git a/objects/obj_ncombat/Create_0.gml b/objects/obj_ncombat/Create_0.gml index 1847fb443b..ee1deca2f7 100644 --- a/objects/obj_ncombat/Create_0.gml +++ b/objects/obj_ncombat/Create_0.gml @@ -68,6 +68,10 @@ defeat_message = 0; red_thirst = 0; fugg = 0; fugg2 = 0; +// Hard-timeout counters for stages 2/4. Unlike fugg/fugg2 these are NOT reset by the 60-frame +// status poll in Step, so they keep accumulating during a stall and the anti-hang cap can fire. +stage_elapsed = 0; +stage_elapsed2 = 0; battle_over = 0; done = 0; diff --git a/objects/obj_ncombat/Draw_0.gml b/objects/obj_ncombat/Draw_0.gml index 4cd4e4f5ae..03a5331fc2 100644 --- a/objects/obj_ncombat/Draw_0.gml +++ b/objects/obj_ncombat/Draw_0.gml @@ -50,12 +50,13 @@ if ((display_p2 > 0) && (enemy_forces > 0)) { draw_set_halign(fa_left); -// When pinned to the bottom (log_scroll == 0) the live 45-row window is drawn exactly as before; -// when scrolled up, page back through the retained log_history instead. +// When pinned to the bottom (log_scroll == 0) the live window is drawn exactly as before; +// when scrolled up, page back through the retained log_history instead. Render the same number of +// rows the scroll/history math uses (log_view_lines) so the two never desync. var _log_total = array_length(log_history); var _log_start = (log_scroll <= 0) ? -1 : max(0, _log_total - log_view_lines - log_scroll); -repeat (45) { +repeat (log_view_lines) { l += 1; var _row_txt, _row_col; diff --git a/objects/obj_ncombat/Step_0.gml b/objects/obj_ncombat/Step_0.gml index 5552091b30..bc6995efe7 100644 --- a/objects/obj_ncombat/Step_0.gml +++ b/objects/obj_ncombat/Step_0.gml @@ -133,24 +133,30 @@ if (((fugg >= 60) || (fugg2 >= 60)) && (messages_shown == 0) && (messages_to_sho if (timer_stage == 2) { fugg += 1; + stage_elapsed += 1; } // Don't time out of stage 2 until the combat log has finished displaying - otherwise on a long turn // the stage advances before `messages` drains and the "Enemy Forces at X%" status line is skipped. -// The large hard cap is anti-hang insurance in case the queue ever fails to drain. -if ((timer_stage == 2) && (((fugg > 60) && (messages == 0)) || (fugg > COMBAT_STAGE_TIMEOUT_FRAMES))) { +// The large hard cap is anti-hang insurance in case the queue ever fails to drain. It uses +// stage_elapsed (not fugg) because the 60-frame status poll above resets fugg every time it fires, +// so fugg can never reach the cap during a stall - stage_elapsed keeps counting regardless. +if ((timer_stage == 2) && (((fugg > 60) && (messages == 0)) || (stage_elapsed > COMBAT_STAGE_TIMEOUT_FRAMES))) { timer_stage = 3; } if (timer_stage != 2) { fugg = 0; + stage_elapsed = 0; } if (timer_stage == 4) { fugg2 += 1; + stage_elapsed2 += 1; } -if ((timer_stage == 4) && (((fugg2 > 60) && (messages == 0)) || (fugg2 > COMBAT_STAGE_TIMEOUT_FRAMES))) { +if ((timer_stage == 4) && (((fugg2 > 60) && (messages == 0)) || (stage_elapsed2 > COMBAT_STAGE_TIMEOUT_FRAMES))) { timer_stage = 5; } if (timer_stage != 4) { fugg2 = 0; + stage_elapsed2 = 0; } diff --git a/objects/obj_pnunit/Alarm_0.gml b/objects/obj_pnunit/Alarm_0.gml index fffae790bd..e35944a134 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -61,7 +61,9 @@ try { var _held_fire = []; for (var hf = i; hf < array_length(wep); hf++) { // Only ranged weapons "hold fire"; melee (range 1) never shoots, so skip it. - if (wep[hf] != "" && wep_num[hf] > 0 && range[hf] > 1) { + // Mirror the firing ammo gate (ammo != 0) so out-of-ammo weapons that could not + // have fired aren't reported as having held fire. + if (wep[hf] != "" && wep_num[hf] > 0 && range[hf] > 1 && ammo[hf] != 0) { array_push(_held_fire, wep[hf]); } } @@ -271,7 +273,9 @@ try { var _skipped_fire = []; for (var s = 0; s < array_length(wep); s++) { // Only ranged weapons "hold fire"; melee (range 1) never shoots, so skip it. - if (wep[s] != "" && wep_num[s] > 0 && range[s] > 1) { + // Mirror the firing ammo gate (ammo != 0) so out-of-ammo weapons that could not have + // fired aren't reported as having held fire. + if (wep[s] != "" && wep_num[s] > 0 && range[s] > 1 && ammo[s] != 0) { array_push(_skipped_fire, wep[s]); } } diff --git a/objects/obj_popup/Create_0.gml b/objects/obj_popup/Create_0.gml index c8d78e0391..72e4b3db88 100644 --- a/objects/obj_popup/Create_0.gml +++ b/objects/obj_popup/Create_0.gml @@ -191,45 +191,30 @@ get_unit_promotion_options = function() { i += 1; role_name[i] = obj_ini.role[100][8]; //tacts role_exp[i] = company_promote_data[target_comp].exp; - if (obj_controller.command_set[2] == 0) { - role_exp[i] = 0; - } } if (array_contains([2, 3, 4, 5, 8], target_comp)) { i += 1; role_name[i] = obj_ini.role[100][10]; //assualts role_exp[i] = company_promote_data[target_comp].exp; - if (obj_controller.command_set[2] == 0) { - role_exp[i] = 0; - } } if (array_contains([2, 3, 4, 5, 9], target_comp)) { i += 1; role_name[i] = obj_ini.role[100][9]; //devs role_exp[i] = company_promote_data[target_comp].exp; - if (obj_controller.command_set[2] == 0) { - role_exp[i] = 0; - } } if (array_contains([2, 3, 4, 5], target_comp)) { i += 1; role_name[i] = obj_ini.role[100][13]; //bikers role_exp[i] = company_promote_data[target_comp].exp; - if (obj_controller.command_set[2] == 0) { - role_exp[i] = 0; - } } if (array_contains([2, 3, 4, 5], target_comp)) { i += 1; role_name[i] = obj_ini.role[100][20]; //attack bikers role_exp[i] = company_promote_data[target_comp].exp; - if (obj_controller.command_set[2] == 0) { - role_exp[i] = 0; - } } if (target_comp == 1) { @@ -242,9 +227,6 @@ get_unit_promotion_options = function() { i += 1; role_name[i] = obj_ini.role[100][12]; //scouts role_exp[i] = company_promote_data[target_comp].exp; - if (obj_controller.command_set[2] == 0) { - role_exp[i] = 0; - } } if (target_comp == 1) { diff --git a/scripts/scr_UnitGroup/scr_UnitGroup.gml b/scripts/scr_UnitGroup/scr_UnitGroup.gml index 492ab5a3e0..afb288809f 100644 --- a/scripts/scr_UnitGroup/scr_UnitGroup.gml +++ b/scripts/scr_UnitGroup/scr_UnitGroup.gml @@ -267,7 +267,9 @@ function UnitGroup(units) constructor { } var _sgt = _available_sgt.units[0]; - squad.add_member(_sgt.company, _sgt.marine_number); + // Confirm this squad actually has a slot for this sergeant role BEFORE adding the + // candidate or marking sergeant_found. Otherwise a sergeant whose role the squad has + // no slot for would be added anyway and incorrectly suppress the promotion flow. var _sgt_group = ""; for (var r = 0; r < array_length(squad_unit_types); r++) { var _role_name = squad_unit_types[r]; @@ -278,14 +280,16 @@ function UnitGroup(units) constructor { break; } } - if (_sgt_group != "") { - squad_fulfilment[$ _sgt_group]++; - // Rename pre-existing sergeant to squad-specific role if needed - var _sgt_slot_def = _fill_squad[$ _sgt_group]; - var _target_sgt_role = struct_exists(_sgt_slot_def, "role") ? _sgt_slot_def.role : _sgt_type; - if (_target_sgt_role != _sgt.role()) { - _sgt.update_role(_target_sgt_role); - } + if (_sgt_group == "") { + continue; + } + squad.add_member(_sgt.company, _sgt.marine_number); + squad_fulfilment[$ _sgt_group]++; + // Rename pre-existing sergeant to squad-specific role if needed + var _sgt_slot_def = _fill_squad[$ _sgt_group]; + var _target_sgt_role = struct_exists(_sgt_slot_def, "role") ? _sgt_slot_def.role : _sgt_type; + if (_target_sgt_role != _sgt.role()) { + _sgt.update_role(_target_sgt_role); } sergeant_found = true; } @@ -385,14 +389,18 @@ function UnitGroup(units) constructor { /*and ((squad_fulfilment[$ obj_ini.role[100][8]] > 4)or (squad_fulfilment[$ obj_ini.role[100][10]] > 4) or (squad_fulfilment[$ obj_ini.role[100][9]] > 4)or (squad_fulfilment[$ obj_ini.role[100][3]] > 4) )*/ var _members = squad.get_members(true); - var _exp_unit = 0; if (!bool(_members.number())) { return [false, squad.uid]; } + // Select would-be sergeant candidates and reserve their fulfilment slot, but defer the + // actual role mutation (update_role / add_trait) until squad viability is confirmed below. + // update_role permanently rewrites obj_ini.role and applies stat/command changes, so + // promoting here — before _fulfilled is known — would leave marines mutated even when the + // squad creation attempt ultimately fails, causing persistent state drift. + var _pending_promotions = []; for (var s = 0; s < 2; s++) { var _sgt_type = sgt_types[s]; var _sgt_group = ""; - var _exp_unit = undefined; for (var r = 0; r < array_length(squad_unit_types); r++) { var _role_name = squad_unit_types[r]; var _role_def = _fill_squad[$ _role_name]; @@ -401,16 +409,13 @@ function UnitGroup(units) constructor { _sgt_group = _role_name; break; } - } + } if (_sgt_group != "" && struct_exists(squad_fulfilment, _sgt_group) && (!sergeant_found)) { - _exp_unit = _members.highest_exp(); + var _candidate = _members.highest_exp(); var _sgt_role_def = _fill_squad[$ _sgt_group]; var _actual_sgt_role = struct_exists(_sgt_role_def, "role") ? _sgt_role_def.role : _sgt_type; - _exp_unit.update_role(_actual_sgt_role); squad_fulfilment[$ _sgt_group]++; - if (game_start && irandom(1) == 0) { - _exp_unit.add_trait("lead_example"); - } + array_push(_pending_promotions, { unit: _candidate, role: _actual_sgt_role }); } } @@ -425,12 +430,12 @@ function UnitGroup(units) constructor { } } if (_fulfilled) { - for (var s = 0; s < 2; s++) { - if (struct_exists(squad_fulfilment, sgt_types[s]) && (sergeant_found == false) && (_exp_unit != undefined)) { - _exp_unit.update_role(sgt_types[s]); //if squad is viable promote marine to sergeant - if (game_start && irandom(1) == 0) { - _exp_unit.add_trait("lead_example"); - } + // Squad is viable — now apply the deferred sergeant promotions. + for (var p = 0; p < array_length(_pending_promotions); p++) { + var _promo = _pending_promotions[p]; + _promo.unit.update_role(_promo.role); //if squad is viable promote marine to sergeant + if (game_start && irandom(1) == 0) { + _promo.unit.add_trait("lead_example"); } } //update units squad marker diff --git a/scripts/scr_company_order/scr_company_order.gml b/scripts/scr_company_order/scr_company_order.gml index 1d2e386bc8..93b62e164a 100644 --- a/scripts/scr_company_order/scr_company_order.gml +++ b/scripts/scr_company_order/scr_company_order.gml @@ -199,11 +199,18 @@ function role_hierarchy() { if (!struct_exists(_role_def, "role")) continue; var _specific_role = _role_def.role; if (!array_contains(hierarchy, _specific_role)) { - if (string_count(_vsgt_base, _specific_role) > 0) { + // Classify by the slot's JSON key (_k), not the renamed role string. Veteran-sergeant + // variants are keyed "Veteran Sergeant" but get renamed to names like "Deathwing + // Sergeant" / "Proteus Watch Sergeant" that contain "Sergeant" but NOT the exact + // substring "Veteran Sergeant" — so a role-string match would mis-rank them as + // regular sergeants. Fall back to string matching only when the key isn't a sergeant. + var _is_vsgt = (_k == _vsgt_base) || (string_count(_vsgt_base, _specific_role) > 0); + var _is_sgt = (_k == _sgt_base) || (string_count(_sgt_base, _specific_role) > 0); + if (_is_vsgt) { // Veteran-sergeant variant — insert just before _vsgt_base position var _vpos = array_get_index(hierarchy, _vsgt_base); array_insert(hierarchy, max(0, _vpos), _specific_role); - } else if (string_count(_sgt_base, _specific_role) > 0) { + } else if (_is_sgt) { // Regular sergeant variant — insert just after _sgt_base position var _spos = array_get_index(hierarchy, _sgt_base); array_insert(hierarchy, _spos + 1, _specific_role); diff --git a/scripts/scr_flavor/scr_flavor.gml b/scripts/scr_flavor/scr_flavor.gml index 79500630d6..c27cbf3cda 100644 --- a/scripts/scr_flavor/scr_flavor.gml +++ b/scripts/scr_flavor/scr_flavor.gml @@ -920,7 +920,7 @@ function combat_emit_enemy_status() { var yoo = instance_nearest(0, 0, obj_enunit); newline += string(round(yoo.dudes_hp[1])) + "HP remaining"; } - if ((enemy_forces <= 0) || (!instance_exists(obj_enunit)) && (defeat_message == 0)) { + if (((enemy_forces <= 0) || (!instance_exists(obj_enunit))) && (defeat_message == 0)) { defeat_message = 1; newline = "Enemy Forces Defeated"; timer_maxspeed = 0; diff --git a/scripts/scr_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 9c3ba60e89..303784081c 100644 --- a/scripts/scr_initialize_custom/scr_initialize_custom.gml +++ b/scripts/scr_initialize_custom/scr_initialize_custom.gml @@ -1545,6 +1545,14 @@ function scr_initialize_custom() { "scout", eROLE.SCOUT ], + [ + "biker", + eROLE.BIKER + ], + [ + "attack_biker", + eROLE.ATTACK_BIKER + ], [ "chaplain", eROLE.CHAPLAIN @@ -1801,7 +1809,7 @@ function scr_initialize_custom() { // LOGGER.debug($"squads object for chapter {chapter_name}"); // LOGGER.debug($"{custom_squads}"); - if (struct_exists(obj_creation, "squad_builder")) { + if (variable_instance_exists(obj_creation, "squad_builder")) { if (!struct_exists(obj_ini.chapter_squad_arrangement, "companies")) { obj_ini.chapter_squad_arrangement.companies = []; } @@ -2399,10 +2407,12 @@ function scr_initialize_custom() { /// and the assaults go into the 8th and devastators into the 9th if (_coy.coy >= 2 && _coy.coy <= 5) { if (equal_scouts) { - if (companies.tenth.scouts > 10) { - // LW needs 20 scouts per company to fill proportion:2 scout squads. - // Non-LW equal_scouts uses 10 (proportion:1). - _coy.scouts = _lw ? 20 : 10; + // LW needs 20 scouts per company to fill proportion:2 scout squads. + // Non-LW equal_scouts uses 10 (proportion:1). Guard against the amount + // actually subtracted so the 10th's bank never goes negative. + var _coy_scout_draw = _lw ? 20 : 10; + if (companies.tenth.scouts >= _coy_scout_draw) { + _coy.scouts = _coy_scout_draw; _moved_scouts += _coy.scouts; _coy.tacticals = max(0, (_coy.total - (assault + devastator + _coy.scouts))); companies.tenth.scouts -= _coy.scouts; // fix: subtract this company's amount, not the cumulative total diff --git a/scripts/scr_management/scr_management.gml b/scripts/scr_management/scr_management.gml index eb71629c3b..55c3a20747 100644 --- a/scripts/scr_management/scr_management.gml +++ b/scripts/scr_management/scr_management.gml @@ -94,6 +94,25 @@ function scr_management(argument0) { var _co_units = collect_company(company).index_roles(); pane.line = array_join(pane.line, _co_units.create_plural_strings_array()); + // collect_company() only indexes TTRPG marines, so vehicles must be counted + // separately from obj_ini.veh_role and appended after the role strings. + var _veh_names = ["Land Raider", "Predator", "Rhino", "Land Speeder", "Whirlwind"]; + var _veh_count = array_create(array_length(_veh_names), 0); + for (var i = 0; i < array_length(obj_ini.veh_role[company]); i++) { + for (var s = 0; s < array_length(_veh_names); s++) { + if (obj_ini.veh_role[company][i] == _veh_names[s]) { + _veh_count[s]++; + } + } + } + for (var d = 0; d < array_length(_veh_names); d++) { + if (_veh_count[d] == 1) { + array_push(pane.line, {str1: _veh_names[d], bold: true, italic: false}); + } else if (_veh_count[d] > 1) { + array_push(pane.line, string_plural_count(_veh_names[d], _veh_count[d], false)); + } + } + xx += 156; } } diff --git a/scripts/scr_powers/scr_powers.gml b/scripts/scr_powers/scr_powers.gml index 65268acada..ac5b28bcac 100644 --- a/scripts/scr_powers/scr_powers.gml +++ b/scripts/scr_powers/scr_powers.gml @@ -368,7 +368,8 @@ function flush_psychic_summary(_psy_log) { for (var i = 0; i < array_length(_keys); i++) { var _e = _psy_log[$ _keys[i]]; var _cast_word = (_e.casts == 1) ? "casting" : "castings"; - var _message = $"{_e.casts} {_cast_word} of '{_e.power}'{_e.flavour} {_e.kills} {_e.target} are {_e.verb}."; + var _kills_word = (_e.kills == 1) ? $"a {_e.target} is {_e.verb}" : $"{_e.kills} {_e.target} are {_e.verb}"; + var _message = $"{_e.casts} {_cast_word} of '{_e.power}'{_e.flavour} {_kills_word}."; var _size = _e.vehicle ? (_e.kills * 12) : (_e.kills * 3); add_battle_log_message(_message, _size, 134); } diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index 873745b76b..4a811a7883 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -484,7 +484,9 @@ function scr_shoot_spread(weapon_index_position) { with (obj_enunit) { array_push(_formations, id); for (var r = 1; r <= 30; r++) { - if (dudes[r] != "" && dudes_num[r] > 0) { + // Skip "zombie" ranks (dudes_num > 0 but dudes_hp <= 0): they would dilute _total and + // cause a divide-by-zero in the per-rank damage loop below. + if (dudes[r] != "" && dudes_num[r] > 0 && dudes_hp[r] > 0) { _total += dudes_num[r]; } } @@ -495,13 +497,16 @@ function scr_shoot_spread(weapon_index_position) { // Apply damage proportionally to each rank's share of the field; record every rank that lost models. var _hits = []; // [{ name, kills, bounced }] + var _wounded = undefined; // first rank that took fire but lost no models (wound/bounce fallback) for (var fi = 0; fi < array_length(_formations); fi++) { var _f = _formations[fi]; if (!instance_exists(_f)) { continue; } for (var r = 1; r <= 30; r++) { - if (_f.dudes[r] == "" || _f.dudes_num[r] <= 0) { + // Mirror the _total guard: skip empty/empty-ranked and "zombie" (hp <= 0) ranks so the + // proportional-damage division below can never divide by zero/negative HP. + if (_f.dudes[r] == "" || _f.dudes_num[r] <= 0 || _f.dudes_hp[r] <= 0) { continue; } @@ -522,6 +527,10 @@ function scr_shoot_spread(weapon_index_position) { _f.dudes_num[r] -= _kills; obj_ncombat.enemy_forces -= _kills; array_push(_hits, { name: _f.dudes[r], kills: _kills, bounced: (_final_hit <= 0), block: _f, rank: r }); + } else if (_wounded == undefined) { + // Volley spent ammo but killed no-one here; remember the first such rank so a + // non-killing sweep still reports a wound/bounce instead of going silent. + _wounded = { bounced: (_final_hit <= 0), block: _f, rank: r }; } } } @@ -546,6 +555,10 @@ function scr_shoot_spread(weapon_index_position) { if (instance_exists(_p.block)) { _primary = scr_flavor(weapon_index_position, _p.block, _p.rank, _shots, _p.kills, _p.bounced, true); } + } else if (_wounded != undefined && instance_exists(_wounded.block)) { + // Nothing died but the volley still landed: report a single wounded/bounced target so the + // consolidated flavour log records the shot (casualties = 0 -> injured or bounced). + _primary = scr_flavor(weapon_index_position, _wounded.block, _wounded.rank, _shots, 0, _wounded.bounced, true); } emit_volley_flavour(_primary, _spill); diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 044af294fc..98079c42a0 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -61,10 +61,11 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co target_squad.update_fulfilment(); static sort = function() { - // For each role, clear only the weapon slots that role's loadout actively manages + // For each role, clear every loadout slot that role actively manages // (required + option + random_pick). Slots not mentioned in a role's own loadout // are left untouched — e.g. a role that only defines armour won't have wep1/wep2 cleared. - var _weapon_slots = ["wep1", "wep2"]; + // random_pick can manage mobi/gear/armour as well as weapons, so all managed slots are + // cleared before re-equipping to avoid stale equipment persisting after a reroll. for (var _ri = 0; _ri < array_length(squad_unit_types); _ri++) { var _role_key = squad_unit_types[_ri]; var _role_data = full_squad_data[$ _role_key]; @@ -91,15 +92,14 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co } } + var _managed_slot_names = struct_get_names(_managed_slots); var _role_members = members_UnitGroup.get_from({roles: [_role_key, role_key_to_actual[$ _role_key]]}); while (_role_members.number() > 0) { var _u = _role_members.pop(); - for (var _s = 0; _s < array_length(_weapon_slots); _s++) { - if (struct_exists(_managed_slots, _weapon_slots[_s])) { - var _clear = {}; - _clear[$ _weapon_slots[_s]] = ""; - _u.alter_equipment(_clear, false, false); - } + for (var _s = 0; _s < array_length(_managed_slot_names); _s++) { + var _clear = {}; + _clear[$ _managed_slot_names[_s]] = ""; + _u.alter_equipment(_clear, false, false); } } } @@ -531,13 +531,31 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { var _min_role_allowed = fill_squad[$ _wanted_unit_role][$ "min"]; if (fill_from != undefined) { - var _fill_role = struct_exists(fill_squad[$ _wanted_unit_role], "role") - ? fill_squad[$ _wanted_unit_role].role : _wanted_unit_role; + var _role_def = fill_squad[$ _wanted_unit_role]; + var _fill_role = struct_exists(_role_def, "role") + ? _role_def.role : _wanted_unit_role; // Also try the JSON key itself as a source role (base role before squad rename) var _fill_role_base = _wanted_unit_role; + // Build the ordered list of acceptable source roles: the mapped role, the JSON + // key, then any alternative_roles. create_squad considers alternative_roles when + // fetching/matching marines, so refill must too — otherwise valid replacement + // marines (e.g. bikers for a bike_squad) are ignored when scr_company_order + // updates existing squads. + var _fill_roles = [_fill_role, _fill_role_base]; + if (struct_exists(_role_def, "alternative_roles")) { + var _alts = _role_def.alternative_roles; + for (var _ai = 0; _ai < array_length(_alts); _ai++) { + array_push(_fill_roles, _alts[_ai]); + } + } while (_squad_role_current < _max_role_count) { - var _pick_role = fill_from.has_role(_fill_role) ? _fill_role - : (fill_from.has_role(_fill_role_base) ? _fill_role_base : ""); + var _pick_role = ""; + for (var _fri = 0; _fri < array_length(_fill_roles); _fri++) { + if (fill_from.has_role(_fill_roles[_fri])) { + _pick_role = _fill_roles[_fri]; + break; + } + } if (_pick_role == "") break; var _new_member = fill_from.pop_role_member(_pick_role); add_member(_new_member.company, _new_member.marine_number); From f6c5244a82f2d95b1ad9c363bf4654532ce675d0 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:34:45 +0200 Subject: [PATCH 50/55] fix: cubic again --- scripts/scr_UnitGroup/scr_UnitGroup.gml | 22 ++++++++++++++++++++-- scripts/scr_squads/scr_squads.gml | 3 ++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/scripts/scr_UnitGroup/scr_UnitGroup.gml b/scripts/scr_UnitGroup/scr_UnitGroup.gml index afb288809f..076b1b6f3f 100644 --- a/scripts/scr_UnitGroup/scr_UnitGroup.gml +++ b/scripts/scr_UnitGroup/scr_UnitGroup.gml @@ -398,6 +398,7 @@ function UnitGroup(units) constructor { // promoting here — before _fulfilled is known — would leave marines mutated even when the // squad creation attempt ultimately fails, causing persistent state drift. var _pending_promotions = []; + var _promoted_units = []; // marines already queued for a leader slot, so a 2nd slot picks a distinct one for (var s = 0; s < 2; s++) { var _sgt_type = sgt_types[s]; var _sgt_group = ""; @@ -410,8 +411,25 @@ function UnitGroup(units) constructor { break; } } - if (_sgt_group != "" && struct_exists(squad_fulfilment, _sgt_group) && (!sergeant_found)) { - var _candidate = _members.highest_exp(); + if (_sgt_group != "" && struct_exists(squad_fulfilment, _sgt_group) && (!sergeant_found)) { + // Highest-experience member not already queued for another leader slot, so two + // leader slots (e.g. Sergeant + Veteran Sergeant) never promote the same marine. + var _candidate = undefined; + var _candidate_exp = -1; + for (var _mi = 0; _mi < _members.number(); _mi++) { + var _m = _members.units[_mi]; + if (array_contains(_promoted_units, _m)) { + continue; + } + if (_candidate == undefined || _m.experience > _candidate_exp) { + _candidate = _m; + _candidate_exp = _m.experience; + } + } + if (_candidate == undefined) { + continue; // no distinct member left for this leader slot + } + array_push(_promoted_units, _candidate); var _sgt_role_def = _fill_squad[$ _sgt_group]; var _actual_sgt_role = struct_exists(_sgt_role_def, "role") ? _sgt_role_def.role : _sgt_type; squad_fulfilment[$ _sgt_group]++; diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 98079c42a0..75852289ac 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -99,7 +99,8 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co for (var _s = 0; _s < array_length(_managed_slot_names); _s++) { var _clear = {}; _clear[$ _managed_slot_names[_s]] = ""; - _u.alter_equipment(_clear, false, false); + // Clear via the squad's own from/to_armoury so items don't get destroyed + _u.alter_equipment(_clear, from_armoury, to_armoury); } } } From cb70d61c6c462e369dd132200794361963df4171 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:23:10 +0200 Subject: [PATCH 51/55] fix: cubic's issues --- objects/obj_pnunit/Alarm_0.gml | 13 +++---------- scripts/scr_shoot/scr_shoot.gml | 16 ++++++++++------ scripts/scr_squads/scr_squads.gml | 29 ++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/objects/obj_pnunit/Alarm_0.gml b/objects/obj_pnunit/Alarm_0.gml index e35944a134..ec06854b38 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -287,17 +287,10 @@ try { instance_activate_object(obj_enunit); // Safety net: drop empty/zombie formations the firing loop never reached, so a lingering corpse - // can't keep the battle alive. + // can't keep the battle alive. Reuse destroy_empty_column so the scan covers every rank (not just + // 1-30) and the owner guard/cleanup exactly match the rest of combat. with (obj_enunit) { - var _alive = 0; - for (var _rr = 1; _rr <= 30; _rr++) { - if (dudes_num[_rr] > 0 && dudes_hp[_rr] > 0) { - _alive += dudes_num[_rr]; - } - } - if ((_alive == 0) && (owner != 1)) { - instance_destroy(); - } + destroy_empty_column(id); } if (instance_exists(obj_enunit)) { diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index 4a811a7883..540cb47071 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -279,11 +279,13 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_dat // Never open fire on a dead rank/formation. Stale men/veh/medi (only refreshed on // the enemy's own alarm) and scr_target's rank-1 fallback can aim us at corpses; - // snap to a living rank instead, or clean up the empty formation and bail. + // snap to a living rank instead, or clean up the empty formation and bail. A zombie + // rank (models remain but hp <= 0) counts as dead too, matching find_next_alive_rank, + // so the spill loop below never divides by zero/negative hp. if (!instance_exists(target_object)) { exit; } - if (target_object.dudes_num[target_type] <= 0) { + if (target_object.dudes_num[target_type] <= 0 || target_object.dudes_hp[target_type] <= 0) { var _alive_rank = find_next_alive_rank(target_object, -1); if (_alive_rank == -1) { destroy_empty_column(target_object); @@ -469,10 +471,6 @@ function scr_shoot_spread(weapon_index_position) { var _ap = apa[weapon_index_position]; var _dpw = att[weapon_index_position] / _shots; // per-bike damage var _mod = max(1, splash[weapon_index_position]); - if (ammo[weapon_index_position] > 0) { - ammo[weapon_index_position] -= 1; - } - // Armour multiplier indexed by AP rating (1..4), matching scr_shoot's normal path. var _inf_ap = [1, 3, 2, 1.5, 0]; var _veh_ap = [1, 6, 4, 2, 0]; @@ -495,6 +493,12 @@ function scr_shoot_spread(weapon_index_position) { exit; } + // Consume ammo only after confirming there's something on the field, so a volley into an + // empty battlefield doesn't burn a Speed Force charge for nothing. + if (ammo[weapon_index_position] > 0) { + ammo[weapon_index_position] -= 1; + } + // Apply damage proportionally to each rank's share of the field; record every rank that lost models. var _hits = []; // [{ name, kills, bounced }] var _wounded = undefined; // first rank that took fire but lost no models (wound/bounce fallback) diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 75852289ac..e123626417 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -378,10 +378,13 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { static change_type = function(new_type) { type = new_type; - if (is_array(type)) { - show_debug_message($"[PROBE] change_type got ARRAY type (len {array_length(type)}): {type}"); - } else if (!struct_exists(obj_ini.squad_types, type)) { - show_debug_message($"[PROBE] change_type unknown squad type: \"{type}\""); + // Guard an invalid squad type (a non-string slipped through, e.g. an array, or a key that + // isn't defined in squad_types). Dereferencing squad_types[$ type].type_data on those crashes + // ("I32 argument is array" / undefined). is_string is checked first so struct_exists never + // receives a non-string. Bail out so one bad arrangement entry can't crash squad generation. + if (!is_string(type) || !struct_exists(obj_ini.squad_types, type)) { + show_debug_message($"change_type: invalid squad type '{string(type)}' — skipping type data"); + return; } add_type_data(obj_ini.squad_types[$ type].type_data); }; @@ -869,6 +872,11 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { /// @return {Struct|Undefined} A company template struct with fields {Real} company and {Array} squads, /// or undefined if no template can be resolved. function resolve_company_arrangement(arrangement, company_number) { + // Arrangements are 1-based (companies 1-10); company 0 is the HQ/chapter tier and must never be + // reorganised into a battle template, so never match an entry or fall through to default_squads. + if (company_number < 1) { + return undefined; + } if (struct_exists(arrangement, "companies")) { var _companies = arrangement.companies; for (var i = 0; i < array_length(_companies); i++) { @@ -919,16 +927,23 @@ function apply_squad_distribution_override(arrangement, override) { var _ovr_companies = override.companies; for (var oi = 0; oi < array_length(_ovr_companies); oi++) { var _ovr = _ovr_companies[oi]; + // Deep-clone the override entry (mirroring the default_squads path above) so the live + // arrangement never aliases the override template — otherwise editing the arrangement + // later (squad editor / promote-to-explicit) would mutate distribution_overrides in place. + var _ovr_copy = { company: _ovr.company, squads: array_create(array_length(_ovr.squads)) }; + for (var _si = 0; _si < array_length(_ovr.squads); _si++) { + _ovr_copy.squads[_si] = variable_clone(_ovr.squads[_si]); + } var _found = false; for (var ai = 0; ai < array_length(arrangement.companies); ai++) { - if (arrangement.companies[ai].company == _ovr.company) { - arrangement.companies[ai] = _ovr; + if (arrangement.companies[ai].company == _ovr_copy.company) { + arrangement.companies[ai] = _ovr_copy; _found = true; break; } } if (!_found) { - array_push(arrangement.companies, _ovr); + array_push(arrangement.companies, _ovr_copy); } } } From f51c04a51562478a052beec8e045599c2eca4f75 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:37:58 +0200 Subject: [PATCH 52/55] fix: bike formations reset --- scripts/scr_civil_roster/scr_civil_roster.gml | 1 + scripts/scr_ui_settings/scr_ui_settings.gml | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/scr_civil_roster/scr_civil_roster.gml b/scripts/scr_civil_roster/scr_civil_roster.gml index 9c3e538896..967cda5a6b 100644 --- a/scripts/scr_civil_roster/scr_civil_roster.gml +++ b/scripts/scr_civil_roster/scr_civil_roster.gml @@ -27,6 +27,7 @@ function scr_civil_roster(_unit_location, _target_location, _is_planet) { obj_controller.bat_devastator_column = obj_controller.bat_deva_for[new_combat.formation_set]; obj_controller.bat_assault_column = obj_controller.bat_assa_for[new_combat.formation_set]; + obj_controller.bat_bike_column = obj_controller.bat_bike_for[new_combat.formation_set]; obj_controller.bat_tactical_column = obj_controller.bat_tact_for[new_combat.formation_set]; obj_controller.bat_veteran_column = obj_controller.bat_vete_for[new_combat.formation_set]; obj_controller.bat_hire_column = obj_controller.bat_hire_for[new_combat.formation_set]; diff --git a/scripts/scr_ui_settings/scr_ui_settings.gml b/scripts/scr_ui_settings/scr_ui_settings.gml index cd36d393b7..83032629bc 100644 --- a/scripts/scr_ui_settings/scr_ui_settings.gml +++ b/scripts/scr_ui_settings/scr_ui_settings.gml @@ -530,6 +530,7 @@ function scr_ui_settings() { bat_formation_type[formating] = 1; bat_deva_for[formating] = 1; bat_assa_for[formating] = 4; + bat_bike_for[formating] = 4; bat_tact_for[formating] = 2; bat_vete_for[formating] = 2; bat_hire_for[formating] = 3; From 6f96c6d6a16339f0a8d51669365d1a0fc8fb46eb Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:02:44 +0200 Subject: [PATCH 53/55] fix: broken armour setup --- datafiles/main/squads/base_squads.json | 1 - 1 file changed, 1 deletion(-) diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index a8c6442dee..cb6be7eed9 100644 --- a/datafiles/main/squads/base_squads.json +++ b/datafiles/main/squads/base_squads.json @@ -474,7 +474,6 @@ "alternative_roles": ["Assault","Devastator","Tactical"], "loadout": { "required": { - "armour": ["", 0], "wep1": ["", 0], "wep2": ["", 0], "mobi": ["Bike", 7] From 2d84de0a07afe49e510e47a09bd144ab66551789 Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:03:29 +0200 Subject: [PATCH 54/55] fix: encumbrance issues --- datafiles/main/chapters/1.JSON | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datafiles/main/chapters/1.JSON b/datafiles/main/chapters/1.JSON index 5774322a66..36b0f7f096 100644 --- a/datafiles/main/chapters/1.JSON +++ b/datafiles/main/chapters/1.JSON @@ -1705,7 +1705,7 @@ }, "option": { "wep2": [ - [["Combiplasma", "Plasma Gun", "Meltagun"], 1] + [["Hand Flamer", "Grav-Pistol", "Infernus Pistol", "Plasma Pistol", "Volkite Serpenta"], 1] ] } } @@ -1802,7 +1802,7 @@ "loadout": { "required": { "wep1": ["Relic Blade", 1], - "wep2": ["Plasma Pistol", 1], + "wep2": ["", 1], "gear": ["Iron Halo", 1], "armour": ["Artificer Armour", 1], "mobi": ["Bike", 1] From 32bcbbe2410ea1fc8edf0efec4ef1a845bc50b6d Mon Sep 17 00:00:00 2001 From: CptMacTavish2224 <95313759+CptMacTavish2224@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:05:27 +0200 Subject: [PATCH 55/55] fix: another set of cubic-found issues fixed --- objects/obj_pnunit/Alarm_0.gml | 12 ---- scripts/scr_flavor/scr_flavor.gml | 3 + .../scr_marine_struct/scr_marine_struct.gml | 8 ++- scripts/scr_promote/scr_promote.gml | 7 +- scripts/scr_shoot/scr_shoot.gml | 16 ++++- scripts/scr_squads/scr_squads.gml | 68 ++++++++++++------- 6 files changed, 72 insertions(+), 42 deletions(-) diff --git a/objects/obj_pnunit/Alarm_0.gml b/objects/obj_pnunit/Alarm_0.gml index ec06854b38..0e885fd02d 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -268,18 +268,6 @@ try { } } } - } else { - // The field was already clear when this block's turn came up - its whole arsenal holds fire. - var _skipped_fire = []; - for (var s = 0; s < array_length(wep); s++) { - // Only ranged weapons "hold fire"; melee (range 1) never shoots, so skip it. - // Mirror the firing ammo gate (ammo != 0) so out-of-ammo weapons that could not have - // fired aren't reported as having held fire. - if (wep[s] != "" && wep_num[s] > 0 && range[s] > 1 && ammo[s] != 0) { - array_push(_skipped_fire, wep[s]); - } - } - report_held_fire(_skipped_fire); } combat_tally_flush(); diff --git a/scripts/scr_flavor/scr_flavor.gml b/scripts/scr_flavor/scr_flavor.gml index c27cbf3cda..a5528e0c95 100644 --- a/scripts/scr_flavor/scr_flavor.gml +++ b/scripts/scr_flavor/scr_flavor.gml @@ -33,6 +33,7 @@ function weapon_name_plural(_name) { /// @desc Logs one "held fire" line for weapons that had no live target left to shoot at, e.g. /// when an earlier volley wiped the enemy before the rest of the squad fired. /// @param {Array} _weapon_names Raw weapon names (duplicates allowed) that never fired. +/// @returns {Undefined} function report_held_fire(_weapon_names) { // Dedupe and pluralise. var _unique = []; @@ -854,6 +855,7 @@ function emit_volley_flavour(_primary, _spill_kills) { /// @desc Buffers a non-killing volley (wound or armour-bounce) against a target. Consecutive volleys /// on the same target merge; switching target flushes the previous one, keeping the log /// chronological. _injured true = penetrated but no kill; false = bounced off armour. +/// @returns {Undefined} function combat_tally_add(_target, _subject, _injured) { if (!variable_global_exists("ctally_target")) { global.ctally_target = undefined; @@ -872,6 +874,7 @@ function combat_tally_add(_target, _subject, _injured) { } /// @desc Posts the buffered wound/bounce lines for the current target (one each), then clears them. +/// @returns {Undefined} function combat_tally_flush() { if (!variable_global_exists("ctally_target") || global.ctally_target == undefined) { return; diff --git a/scripts/scr_marine_struct/scr_marine_struct.gml b/scripts/scr_marine_struct/scr_marine_struct.gml index ec9f9253ee..b53d2f6733 100644 --- a/scripts/scr_marine_struct/scr_marine_struct.gml +++ b/scripts/scr_marine_struct/scr_marine_struct.gml @@ -1665,17 +1665,21 @@ function TTRPG_stats(faction, comp, mar, class = "marine", other_spawn_data = {} return wrath; }; static speed_force = function(_ranged = false) { + // Speed Force weapon-profile constants, shared by the ranged and standard-bike variants. + var _SF_ATTACK_MULT = 2; // damage scales at 2x the bike's base weapon + var _SF_RANGE = 14; + var _SF_AMMO = 12; if (_ranged) { // Attack Bike: scales off the "sidecar's" ranged weapon - single firepower profile, no melee option. var _attack = ranged_damage_data[0]; var _weapon = ranged_damage_data[3]; - return new EquipmentStruct({attack: _attack * 2, name: "Speed Force (Ranged)", range: 14, ammo: 12, spli: _weapon.spli, arp: _weapon.arp}, "weapon"); + return new EquipmentStruct({attack: _attack * _SF_ATTACK_MULT, name: "Speed Force (Ranged)", range: _SF_RANGE, ammo: _SF_AMMO, spli: _weapon.spli, arp: _weapon.arp}, "weapon"); } // Standard Bike: scales off melee, dominant melee (M) profile while engaged in front. var _melee_attack = melee_damage_data[0]; var _melee_weapon = melee_damage_data[3]; - var speedf = new EquipmentStruct({attack: _melee_attack * 2, name: "Speed Force", range: 14, ammo: 12, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); + var speedf = new EquipmentStruct({attack: _melee_attack * _SF_ATTACK_MULT, name: "Speed Force", range: _SF_RANGE, ammo: _SF_AMMO, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); var speedf_melee = new EquipmentStruct({attack: _melee_attack * 4, name: "Speed Force(M)", range: 1, ammo: 16, spli: _melee_weapon.spli, arp: _melee_weapon.arp}, "weapon"); diff --git a/scripts/scr_promote/scr_promote.gml b/scripts/scr_promote/scr_promote.gml index 45d05372db..f120ae0460 100644 --- a/scripts/scr_promote/scr_promote.gml +++ b/scripts/scr_promote/scr_promote.gml @@ -125,8 +125,13 @@ function setup_promotion_popup() { if (struct_exists(role_squad_equivilances, role_name[target_role])) { var _grp = collect_company(target_comp); var _result = [true]; - while (_result[0]) { + // Guard against an infinite loop / UI hang if create_squad ever keeps returning + // success without exhausting members. No company holds anywhere near 100 squads + // of one type, so this cap only trips on a genuine bug rather than normal play. + var _squad_guard = 0; + while (_result[0] && _squad_guard < 100) { _result = _grp.create_squad(role_squad_equivilances[$ role_name[target_role]]); + _squad_guard++; } } diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index 540cb47071..42037aae42 100644 --- a/scripts/scr_shoot/scr_shoot.gml +++ b/scripts/scr_shoot/scr_shoot.gml @@ -400,6 +400,16 @@ function scr_shoot(weapon_index_position, target_object, target_type, damage_dat } } +/// @desc Whether a formation rank has living models. A rank chipped to 0 HP but still showing +/// dudes_num is a dead "zombie" and counts as not alive (callers divide by dudes_hp, so this +/// is the single source of truth for the "alive" test). +/// @param {Id.Instance} _block The obj_enunit formation. +/// @param {Real} _rank The rank index. +/// @returns {Bool} +function is_rank_alive(_block, _rank) { + return _block.dudes_num[_rank] > 0 && _block.dudes_hp[_rank] > 0; +} + /// @function find_next_alive_rank /// @description Returns the index of the next living rank (dudes_num > 0 and dudes_hp > 0) in a /// formation, preferring ranks that match the requested vehicle flag. Returns -1 if @@ -413,7 +423,7 @@ function find_next_alive_rank(_block, _prefer_vehicle) { } var _fallback = -1; for (var f = 1; f <= 30; f++) { - if (_block.dudes_num[f] <= 0 || _block.dudes_hp[f] <= 0) { + if (!is_rank_alive(_block, f)) { continue; } if (_prefer_vehicle == -1 || _block.dudes_vehicle[f] == _prefer_vehicle) { @@ -484,7 +494,7 @@ function scr_shoot_spread(weapon_index_position) { for (var r = 1; r <= 30; r++) { // Skip "zombie" ranks (dudes_num > 0 but dudes_hp <= 0): they would dilute _total and // cause a divide-by-zero in the per-rank damage loop below. - if (dudes[r] != "" && dudes_num[r] > 0 && dudes_hp[r] > 0) { + if (dudes[r] != "" && is_rank_alive(id, r)) { _total += dudes_num[r]; } } @@ -510,7 +520,7 @@ function scr_shoot_spread(weapon_index_position) { for (var r = 1; r <= 30; r++) { // Mirror the _total guard: skip empty/empty-ranked and "zombie" (hp <= 0) ranks so the // proportional-damage division below can never divide by zero/negative HP. - if (_f.dudes[r] == "" || _f.dudes_num[r] <= 0 || _f.dudes_hp[r] <= 0) { + if (_f.dudes[r] == "" || !is_rank_alive(_f, r)) { continue; } diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index e123626417..9576fb7e43 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -259,16 +259,23 @@ function SquadEquipmentSorting(squad, from_armoury = true, to_armoury = true) co } }; - // Picks ONE entry (loadout category) at random, then resolves each slot: - // string values are used directly; array values get one item picked at random. - // Any slot omitted from an entry is left unchanged on the unit. - // - // JSON example: - // "random_pick": [ - // { "wep1": ["Sword","Axe","Mace"], "wep2": ["Pistol","Plasma","Volkite"] }, - // { "wep1": "Lightning Claw", "wep2": "Lightning Claw" } - // ] + /// @desc Picks ONE entry (loadout category) at random for the current unit_role's members, then + /// resolves each slot: string values are used directly; array values get one item picked at + /// random. Any slot omitted from an entry is left unchanged on the unit. + /// JSON example: + /// "random_pick": [ + /// { "wep1": ["Sword","Axe","Mace"], "wep2": ["Pistol","Plasma","Volkite"] }, + /// { "wep1": "Lightning Claw", "wep2": "Lightning Claw" } + /// ] + /// @self Struct.SquadEquipmentSorting + /// @param {Array} pick_options Array of loadout-category structs to pick from. + /// @returns {Undefined} static equip_random_pick_for_role = function(pick_options) { + // Guard: an empty or non-array pick list makes (array_length - 1) = -1 below, so irandom(-1) + // yields a negative index and crashes. With nothing to pick, there's nothing to do. + if (!is_array(pick_options) || array_length(pick_options) == 0) { + return; + } var _actual_role = role_key_to_actual[$ unit_role]; var _members_with_role = members_UnitGroup.get_from({roles: [unit_role, _actual_role]}); while (_members_with_role.number() > 0) { @@ -598,6 +605,18 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { required[$ _req_key]--; } } + + // The promotions above filled missing leader slots from existing members but didn't clear + // the `fulfilled = false` set during the deficit pass. Re-derive fulfilment from the updated + // deficits so a squad that only lacked a sergeant isn't left unfulfilled and emptied on return. + fulfilled = true; + var _deficit_keys = struct_get_names(required); + for (var _dk = 0; _dk < array_length(_deficit_keys); _dk++) { + if (required[$ _deficit_keys[_dk]] > 0) { + fulfilled = false; + break; + } + } }; static empty_squad = function() { @@ -909,16 +928,24 @@ function resolve_company_arrangement(arrangement, company_number) { /// (e.g. arrangement.distribution_overrides.equal_specialists). /// Expected optional fields: {Array} default_squads, {Array} companies. /// @return {Undefined} +/// @desc Deep-clones an array of squad-definition structs (variable_clone per element) so the copy +/// is fully independent of the source — used when materialising arrangement/override squad +/// lists so editing one can never mutate the other. +/// @param {Array} _src Array of squad-definition structs. +/// @returns {Array} +function clone_squad_defs(_src) { + var _clone = array_create(array_length(_src)); + for (var _i = 0; _i < array_length(_src); _i++) { + _clone[_i] = variable_clone(_src[_i]); + } + return _clone; +} + function apply_squad_distribution_override(arrangement, override) { if (struct_exists(override, "default_squads")) { // Deep-clone so arrangement.default_squads is independent of the override sub-struct, // preventing any future in-place mutation of the array from corrupting both references. - var _src = override.default_squads; - var _clone = array_create(array_length(_src)); - for (var _i = 0; _i < array_length(_src); _i++) { - _clone[_i] = variable_clone(_src[_i]); - } - arrangement.default_squads = _clone; + arrangement.default_squads = clone_squad_defs(override.default_squads); } if (struct_exists(override, "companies")) { if (!struct_exists(arrangement, "companies")) { @@ -930,10 +957,7 @@ function apply_squad_distribution_override(arrangement, override) { // Deep-clone the override entry (mirroring the default_squads path above) so the live // arrangement never aliases the override template — otherwise editing the arrangement // later (squad editor / promote-to-explicit) would mutate distribution_overrides in place. - var _ovr_copy = { company: _ovr.company, squads: array_create(array_length(_ovr.squads)) }; - for (var _si = 0; _si < array_length(_ovr.squads); _si++) { - _ovr_copy.squads[_si] = variable_clone(_ovr.squads[_si]); - } + var _ovr_copy = { company: _ovr.company, squads: clone_squad_defs(_ovr.squads) }; var _found = false; for (var ai = 0; ai < array_length(arrangement.companies); ai++) { if (arrangement.companies[ai].company == _ovr_copy.company) { @@ -975,11 +999,7 @@ function get_compay_squad_arrangement(company){ // shared array every other defaulted company also points at. Registering it persists the edits // and lets resolve_company_arrangement pick this company up by its own entry from now on. var _src = struct_exists(_arrangement, "default_squads") ? _arrangement.default_squads : []; - var _squads = array_create(array_length(_src)); - for (var _i = 0; _i < array_length(_src); _i++) { - _squads[_i] = variable_clone(_src[_i]); - } - var _entry = { company: company, squads: _squads }; + var _entry = { company: company, squads: clone_squad_defs(_src) }; array_push(_comp_datas, _entry); return _entry; }