Skip to content

feat: Lightning Warriors with a small extra#1300

Open
CptMacTavish2224 wants to merge 9 commits into
Adeptus-Dominus:mainfrom
CptMacTavish2224:LW-pure
Open

feat: Lightning Warriors with a small extra#1300
CptMacTavish2224 wants to merge 9 commits into
Adeptus-Dominus:mainfrom
CptMacTavish2224:LW-pure

Conversation

@CptMacTavish2224

@CptMacTavish2224 CptMacTavish2224 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

New squad types, formation bit, template redone, company composition changes, cleaning repetitions in formation bar drag and in the welcome message
Refactors some combat-relevant stuff into more accurate for bikers, adds placeholder Speed Force
Accounts for new mobility hands with small squad setup redones
ID for flesh tearers moved for visibility

Testing

shit works


Summary by cubic

Adds the Lightning Warriors preset with bike‑heavy companies and full biker support (roles, stats, squads, formations, UI). Also streamlines the formation bars and welcome message, and fixes formation persistence, equipment mapping, encumbrance, and promotion/roster edge cases.

  • New Features

    • New Lightning Warriors company preset; adds formation18.png for biker formations and tweaks Black Templars pistol loadouts.
    • Added Biker and Attack Biker roles with default gear, promotion targets, UI/settings, and battle/formation bar support.
    • Rebalanced Bike mobility (higher DR/HP, melee bonus, ranged penalty) and added placeholder Speed Force and bike tags.
    • Company layouts resolved at runtime; squad templates now support alternative roles and multi‑role fills.
  • Bug Fixes

    • Bike formation column now persists across formation UI, roster, and battle setup.
    • Fixed armour/equipment resolution and equipment‑need loops; role lists now include Biker and Attack Biker.
    • Corrected promotion XP display and biker squad mapping; avoided controller null refs on specialist status changes; ensured management panels refresh sorted company data.

Written for commit b4ef9e3. Summary will update on new commits.

Review in cubic

@github-actions github-actions Bot added Area: JSON Changes to external JSON files or their under-the-hood functionality Area: Sprites Changes to sprites/images or their under-the-hood functionality Size: Warning Type: Feature Adds something new labels Jul 4, 2026
@CptMacTavish2224 CptMacTavish2224 marked this pull request as ready for review July 4, 2026 23:44

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 issues found across 33 files

Confidence score: 2/5

  • datafiles/main/squads/base_squads.json now contains a // inline comment, which makes the JSON invalid and can cause squad data loading to fail at runtime. Remove/move that comment to docs or code before merging.
  • scripts/scr_squads/scr_squads.gml (equip_random_pick_for_role) can crash when random_pick is empty, and datafiles/main/chapters/1.JSON currently defines a required wep2 with an empty name, which can cascade into broken equipment resolution. Add a zero-length guard in the picker and fix/validate that loadout entry before merging.
  • scripts/scr_roster/scr_roster.gml (add_unit_to_battle) has no explicit branch for eROLE.BIKER / eROLE.ATTACK_BIKER, so those units can fall through role resolution and be assigned incorrectly or inconsistently. Add explicit handling (and a targeted test or validation run) before merge.
  • datafiles/main/squads/lightning_warriors.json plus override merge behavior in scripts/scr_squads/scr_squads.gml increases configuration risk (missing company 1 in overrides, shared-reference mutation, heavy duplication), making future tweaks error-prone even if current runs pass. De-risk by cloning override entries and aligning company coverage now, or gate merge on a quick regression pass of override-based squad generation.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread datafiles/main/squads/base_squads.json
Comment thread scripts/scr_squads/scr_squads.gml
Comment thread datafiles/main/chapters/1.JSON Outdated
Comment thread scripts/scr_roster/scr_roster.gml
Comment thread datafiles/main/squads/lightning_warriors.json
Comment thread scripts/scr_company_order/scr_company_order.gml
Comment thread scripts/scr_company_order/scr_company_order.gml
Comment thread datafiles/main/chapters/template.JSON Outdated
Comment thread datafiles/main/chapters/template.JSON
Comment thread scripts/scr_marine_struct/scr_marine_struct.gml

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread datafiles/main/chapters/1.JSON
Comment thread scripts/scr_roster/scr_roster.gml
Comment thread datafiles/main/squads/base_squads.json Outdated
Comment thread datafiles/main/squads/base_squads.json Outdated
Comment thread datafiles/main/squads/equal_scouts.json
Comment on lines +906 to +913
// 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

variable_clone by default is a deep clone so most of this is unneeded

Suggested change
// 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 = variable_clone(override.default_squads)

Comment on lines +962 to +966
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]);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same ass above variable_clone natrually clones depth so no need for loops

Comment thread scripts/scr_company_order/scr_company_order.gml
@@ -194,5 +184,79 @@ function role_hierarchy() {
"Ork Sniper"
];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole logic just repeats a logic already covered in the company sort but adds a lot of complexity, to fix sgts displaying above other squad members just ensure the sgt is placed in front of other roles inn the company sort otherwise this dynamic logic needs too be repeated everytime the hierarchy us required for any reason

Comment on lines +1652 to 1688
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;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double slashes are obsolete now, broken on Linux. We use / for paths now.

Comment thread scripts/macros/macros.gml
LIBRARIAN = 17,
SERGEANT = 18,
VETERANSERGEANT = 19,
ATTACK_BIKER = 20,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if ATTACK_BIKER role is needed. Following the established convention, we don't separate roles like that. Assault Terminator is just a Terminator. I'm also not sure as to benefits of this separation in this exact case.

Comment thread objects/obj_formation_bar/Create_0.gml
Comment thread objects/obj_controller/Create_0.gml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure about having so many bikers with this adv. But literally can't be bothered to discuss or test this right now. Expect changes to this if I decide later.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

¯_(ツ)_/¯

Comment on lines +207 to +208
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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure if I'm reading this correctly, but string_count() takes "what" to search for as the first argument, and "where" to search as the second. It seems the intent here is reversed, isn't it?

Shouldn't it be:

var _is_vsgt = (_k == _vsgt_base) || (string_count(_specific_role, _vsgt_base) > 0);
var _is_sgt  = (_k == _sgt_base)  || (string_count(_specific_role, _sgt_base) > 0);

Comment on lines +409 to +418
//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;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this dead code? Don't see any uses of squad_role_alternatives at glance.

// 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++) {

@EttyKitty EttyKitty Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern of:

  • iterating squad_unit_types
  • reading _fill_squad[$ _role_name]
  • extracting _primary_role_name = struct_exists(_role_def, "role") ? _role_def.role : _role_name
  • and matching against sgt_types

Is copy-pasted in the new code 5(!!!!) times.

This exact sergeant block is copy-pasted lower, in _has_sgt_requirements check and then again in promotion _sgt_group resolution.

Comment on lines +202 to +206
static sgt_types = [];

static create_squad = function(squad_type, squad_loadout = true, squad_uid = "", game_start = false) {
// LOGGER.info($"sgts : ${sgt_types}");
//LOGGER.info($"sgts : ${sgt_types}");
sgt_types = role_groups(SPECIALISTS_SQUAD_LEADERS);

@EttyKitty EttyKitty Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't get why this reinitialization was done.

Comment on lines +126 to +127
// (all mutable state lives in optional_fill_counts). variable_clone can incorrectly
// flatten doubly-nested arrays in this GML version, corrupting the group structure.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is plain AI slop and is wrong.

The bug in the original code was using wrong array index (_optional_load_data[1]++ vs needed _optional_load_data[2]++) from what I read (unless I'm blind). But the AI slop decided to fix it by using structs instead and write a misleading comment.

Is using structs here bad per se? Probably not. Explicit keys are better than unnamed indices, I guess.

Comment on lines +291 to +293
if (_target_sgt_role != _sgt.role()) {
_sgt.update_role(_target_sgt_role);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this renames a pre-existing sergeant before the squad viability check later down. If the squad creation fails, the sergeant has been permanently renamed for nothing. I thought logic outlined in deferred-promotion wanted to avoid such cases.

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}\"");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A weird debug leftover. Or not. If it can get an array, return early? Or remove the lines.

Comment on lines +191 to +259
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)) {
// 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 (_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);
}
}
}
}

// 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);
}
}
}

@EttyKitty EttyKitty Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure, but I think the addition of this code block started to freeze the game on each opening of chapter view, because sort_all_companies()>scr_company_order() are called every time the window is opened.
I'm not sure if it's this exact path that is bad, but I suspect it is.
No caching. The hierarchy is rebuilt from scratch every call.

Comment on lines +1398 to +1405
var _role_aliases = {
"Tactical Sergeant": _canon[eROLE.TACTICAL],
"Devastator Sergeant": _canon[eROLE.DEVASTATOR],
"Assault Sergeant": _canon[eROLE.ASSAULT],
"Scout Sergeant": _canon[eROLE.SCOUT],
"Biker Sergeant": _canon[eROLE.BIKER],
"Attack Bike Sergeant": _canon[eROLE.ATTACK_BIKER],
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the entire setup of the roles system is that aliases are always by default by-passable meaning this struct should be redundant, all sgts are sgts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore this i shall re-comment more precisely in a separate commit in

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it's more immersive and cooler to have the sergeants display what type of squad they're leading instead of just being "Sergeants"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I got what the original comment meant. And got it properly refactored. Thanks for noting that, I missed that.

@EttyKitty

Copy link
Copy Markdown
Collaborator

And of course: ugly AI slop with 99999999 lines of verbose comments I don't need.

Aside from everything stated, some of the changes, new functionality and fixes are cool.

Comment on lines +1407 to +1414

var _count_roles = function(_com, _last_slot, _aliases) {
var _counts = {};
for (var _mm = 0; _mm <= _last_slot; _mm++) {
var _r = obj_ini.role[_com][_mm];
_r = _aliases[$ _r] ?? _r; //aliases are replaced with canon for clarity
if (_r != "") {
_counts [$ _r] = (_counts[$ _r] ?? 0) + 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this duplicates the work of the UnitIndex refactor insstead to be in terms of the UnitIndex to avoid dead and repetitive code

Comment on lines +1462 to +1468
var _role_display_order = [
eROLE.CAPTAIN, eROLE.CHAPLAIN, eROLE.APOTHECARY, eROLE.TECHMARINE,
eROLE.ANCIENT, eROLE.CHAMPION, eROLE.LIBRARIAN, eROLE.SERGEANT,
eROLE.VETERANSERGEANT, eROLE.TERMINATOR, eROLE.VETERAN, eROLE.TACTICAL,
eROLE.ASSAULT, eROLE.DEVASTATOR, eROLE.SCOUT, eROLE.BIKER,
eROLE.ATTACK_BIKER, eROLE.DREADNOUGHT
];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the already existing hierarchy structure to prevent duplication

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: JSON Changes to external JSON files or their under-the-hood functionality Area: Sprites Changes to sprites/images or their under-the-hood functionality Size: Warning Type: Feature Adds something new

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants