Skip to content

feat: Lightning Warriors with a small extra#1300

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

feat: Lightning Warriors with a small extra#1300
CptMacTavish2224 wants to merge 6 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/Attack Biker support across roles, stats, squads, formations, and UI. Also streamlines formation bars/intro and fixes formation persistence, equipment and promotions, and roster display.

  • New Features

    • New Lightning Warriors preset; adds formation18.png for biker formations; updated pistol loadouts in chapter entries.
    • Added Biker and Attack Biker roles with default gear, promotion targets (bike_squad, attack_bike_squad), UI/settings, and formation bar support.
    • Rebalanced Bike mobility (higher DR/HP, melee bonus, ranged penalty) with placeholder Speed Force and bike tags.
    • Company layouts resolved at runtime; squad templates support alternative_roles and multi‑role fills, and equipment sorting respects those slots.
  • Bug Fixes

    • Bike formation column now persists across formation UI, roster, and battle setup.
    • Fixed armour/equipment resolution and equipment‑need loops; role lists 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 ca1ab91. 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.

All reported issues were addressed across 33 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.

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.

All reported issues were addressed 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 thread scripts/scr_squads/scr_squads.gml Outdated
Comment thread scripts/scr_squads/scr_squads.gml Outdated
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

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.

done

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

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.

This isn't just a display fix, determine_leader and hierarchy_keys also eat this, so this is a critical bit of code for all renamed sergeant roles. I will make it less shit, but just pulling the load of this to the defaults will break all custom sergeant types

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.

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.

distinct promotion entries, squad-type fetch pools and roster display is what the benefits are.
To expand:

  • I can easily expand on their different combat role in the future PRs and add missions that are antirely AB related cause they get a separate erole
  • clarity of display, chapter management would only display X Bikers if the AB were not a separate erole entry since both this and the welcome message just builds off strings
  • they have their own squad sort and promotion pipeline so you can actually create an AB squad in the current promotion screen, otherwise it'd be impossible to create any AB squad midgame
  • on squad teardowns if he's tracked as an ex-ABiker he won't get folded into Biker squads first, so less reequipment in the background
    There is an argument that the combat logic doesn't need them, I think I can get away with making the fetch pools and the roster display work without the erole, but the promotion entry [which is a core feature for these base squads imo] would get killed, as well as their default gear entry

Comment thread objects/obj_formation_bar/Create_0.gml Outdated
Comment thread objects/obj_controller/Create_0.gml Outdated
Comment thread datafiles/main/squads/lightning_warriors.json
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);

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.

actually no, this bit checks whether a _specific_role (so the renamed to flavour role string on leader slot like "Biker Sergeant", which update_role writes as a string in his role, so role_hierarchy() would just have the role fall to the bottom of the sort and not be matched as Sergeant-tier) contains a sgt or vsgt base, without this a non-basic "Sergeant" would no longer be highest in the troop list from his squad, just wherever really.
so this is intended behaviour

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.

Though I will give this a second look cause Nelson pointed to a weak link somewhere else that would change this

Comment thread scripts/scr_squads/scr_squads.gml Outdated
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.

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.

It is, my bad. Dead code from my initial code for the alternative roles in jsons

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.

killed

// 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.

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.

that's on me being lazy, I'll collapse it

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 static reinitialization is done.

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.

So long story short I had issues with sergeants not being generated when I save loaded or started different chapters without closing the game. Turns out statis sgt_types = role_groups sampled only once, on the first UnitGroup construct in session. And it never gets updated afterwards cause it's just a static, despite the fact role_groups is very much actively being changed.
This reinit makes it so that each create_squad reads the most recent sergeant role list

Comment thread scripts/scr_squads/scr_squads.gml Outdated
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.

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.

leftover from that time I asked copilot to help I think, I'll give it a look momentarily

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.

addressed

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.

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.

damn you right. Probably an oversight when I was changing approaches, will amend

Comment thread scripts/scr_squads/scr_squads.gml Outdated
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.

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.

It is, I'll give it a once-over

Comment thread objects/obj_controller/Create_0.gml Outdated
@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 thread objects/obj_controller/Create_0.gml Outdated
Comment thread objects/obj_controller/Create_0.gml Outdated
cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes Jul 8, 2026

@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.

0 issues found across 1 file (changes from recent commits).

Re-trigger cubic

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