feat: Lightning Warriors with a small extra#1300
Conversation
There was a problem hiding this comment.
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
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| 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]); | ||
| } |
There was a problem hiding this comment.
same ass above variable_clone natrually clones depth so no need for loops
| @@ -194,5 +184,79 @@ function role_hierarchy() { | |||
| "Ork Sniper" | |||
| ]; | |||
|
|
|||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
| 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; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Double slashes are obsolete now, broken on Linux. We use / for paths now.
| LIBRARIAN = 17, | ||
| SERGEANT = 18, | ||
| VETERANSERGEANT = 19, | ||
| ATTACK_BIKER = 20, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| 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); |
There was a problem hiding this comment.
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);There was a problem hiding this comment.
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
There was a problem hiding this comment.
Though I will give this a second look cause Nelson pointed to a weak link somewhere else that would change this
| //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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Isn't this dead code? Don't see any uses of squad_role_alternatives at glance.
There was a problem hiding this comment.
It is, my bad. Dead code from my initial code for the alternative roles in jsons
| // 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++) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
that's on me being lazy, I'll collapse it
| 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); |
There was a problem hiding this comment.
Don't get why this static reinitialization is done.
There was a problem hiding this comment.
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
| // (all mutable state lives in optional_fill_counts). variable_clone can incorrectly | ||
| // flatten doubly-nested arrays in this GML version, corrupting the group structure. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
leftover from that time I asked copilot to help I think, I'll give it a look momentarily
| if (_target_sgt_role != _sgt.role()) { | ||
| _sgt.update_role(_target_sgt_role); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
damn you right. Probably an oversight when I was changing approaches, will amend
| 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
It is, I'll give it a once-over
|
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. |
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
formation18.pngfor biker formations; updated pistol loadouts in chapter entries.bike_squad,attack_bike_squad), UI/settings, and formation bar support.biketags.alternative_rolesand multi‑role fills, and equipment sorting respects those slots.Bug Fixes
Written for commit ca1ab91. Summary will update on new commits.