feat: Lightning Warriors with a small extra#1300
Conversation
There was a problem hiding this comment.
17 issues found across 33 files
Confidence score: 2/5
datafiles/main/squads/base_squads.jsonnow 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 whenrandom_pickis empty, anddatafiles/main/chapters/1.JSONcurrently defines a requiredwep2with 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 foreROLE.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.jsonplus override merge behavior inscripts/scr_squads/scr_squads.gmlincreases 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
There was a problem hiding this comment.
2 issues found across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // 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; |
There was a problem hiding this comment.
variable_clone by default is a deep clone so most of this is unneeded
| // 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) |
| 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
| 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.
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.
| 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);| //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.
| // 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.
| 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 reinitialization was done.
| // (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.
| 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.
| 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}\""); | ||
| } |
There was a problem hiding this comment.
A weird debug leftover. Or not. If it can get an array, return early? Or remove the lines.
| 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.
| 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], | ||
| }; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
ignore this i shall re-comment more precisely in a separate commit in
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
I think I got what the original comment meant. And got it properly refactored. Thanks for noting that, I missed that.
|
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. |
|
|
||
| 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; |
There was a problem hiding this comment.
this duplicates the work of the UnitIndex refactor insstead to be in terms of the UnitIndex to avoid dead and repetitive code
| 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 | ||
| ]; |
There was a problem hiding this comment.
use the already existing hierarchy structure to prevent duplication
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
formation18.pngfor biker formations and tweaks Black Templars pistol loadouts.Bug Fixes
Written for commit b4ef9e3. Summary will update on new commits.