diff --git a/ChapterMaster.yyp b/ChapterMaster.yyp index ae73d2af0f..4ca4b505d3 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",}, @@ -462,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",}, @@ -578,6 +580,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 +607,7 @@ "isEcma":false, "LibraryEmitters":[], "MetaData":{ - "IDEVersion":"2024.1400.5.1065", + "IDEVersion":"2024.1400.5.1055", }, "name":"ChapterMaster", "resources":[ 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, diff --git a/datafiles/data/mobility.json b/datafiles/data/mobility.json index 2bfe3862cc..4b6f2b5887 100644 --- a/datafiles/data/mobility.json +++ b/datafiles/data/mobility.json @@ -2,22 +2,65 @@ "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": { "artifact": 35, - "master_crafted": 25, + "master_crafted": 30, "standard": 25 }, + "ranged_hands": -0.5, + "melee_mod":{ + "artifact": 30, + "master_crafted": 25, + "standard": 20 + }, + "special_properties":[ + "Speed Force" + ], + "tags":[ + "bike" + ], "second_profiles": [ "Twin Linked Bolters" ], "value": 35, "requires_to_forge": ["combi_1"] }, + "Attack Bike": { + "abbreviation": "At Bike", + "damage_resistance_mod": { + "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": { + "artifact": 150, + "master_crafted": 120, + "standard": 100 + }, + "ranged_hands": 1, + "melee_mod":{ + "artifact": 20, + "master_crafted": 15, + "standard": 10 + }, + "special_properties": [ + "Speed Force (Ranged)" + ], + "second_profiles": [ + "Twin Linked Bolters" + ], + "tags":[ + "bike", "sf_ranged" + ], + "value": 95, + "requires_to_forge": ["combi_1"] + }, "Conversion Beamer Pack": { "abbreviation": "CnvBmr", "buyable": false, diff --git a/datafiles/images/ui/formation18.png b/datafiles/images/ui/formation18.png new file mode 100644 index 0000000000..d1a2d2d1f8 Binary files /dev/null and b/datafiles/images/ui/formation18.png differ diff --git a/datafiles/main/chapters/1.JSON b/datafiles/main/chapters/1.JSON index 96f484826a..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] ] } } @@ -1722,7 +1722,7 @@ }, "option": { "wep2": [ - [["Flamer", "Grav-Gun", "Meltagun", "Plasma Gun", "Plasma Pistol"], 2] + [["Hand Flamer", "Grav-Pistol", "Infernus Pistol", "Plasma Pistol"], 2] ] } } @@ -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] @@ -1815,13 +1815,13 @@ "role": "Black Knight", "loadout": { "required": { - "wep1": ["Chainsword", 4], + "wep1": ["Power Sword", 4], "wep2": ["Bolt Pistol", 2], "mobi": ["Bike", 4] }, "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/chapters/34.json b/datafiles/main/chapters/34.json index 76d8c9a535..b8d075f747 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, @@ -38,7 +39,6 @@ "", "" ], - "id": 34, "chapter_master": { "ranged": 1.0, "specialty": 2.0, @@ -139,7 +139,7 @@ "culture_styles": [], "home_planets": 1.0, "flagship_name": "Victus", - "monastary_name": "", + "monastery_name": "", "advantages": [ "", "Assault Doctrine", diff --git a/datafiles/main/chapters/template.JSON b/datafiles/main/chapters/template.JSON index 7c64b377f4..501d2fca10 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 diff --git a/datafiles/main/squads/base_squads.json b/datafiles/main/squads/base_squads.json index 7163ec86e5..cb6be7eed9 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}", @@ -86,7 +93,7 @@ } }, "Terminator": { - "max": 4, + "max": 9, "min": 2, "loadout": { "required": { @@ -95,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": { @@ -131,19 +145,24 @@ } }, "Terminator": { - "max": 4, + "max": 9, "min": 2, "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": { @@ -210,14 +229,14 @@ }, "devastator_squad": { - "Devastator": { + "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": [ @@ -255,9 +274,10 @@ }, "tactical_squad": { - "Tactical": { + "Tactical Marine": { "max": 9, "min": 4, + "role": "Tactical", "loadout": { "required": { "wep1": ["wep1[8]", 7], @@ -304,9 +324,10 @@ }, "assault_squad": { - "Assault": { + "Assault Marine": { "max": 9, "min": 4, + "role": "Assault", "loadout": { "required": { "wep1": ["wep1[10]", 5], @@ -358,6 +379,7 @@ "Scout": { "max": 9, "min": 4, + "role": "Scout", "loadout": { "required": { "wep1": ["", 0], @@ -390,6 +412,7 @@ "type_data": { "display_data": "Scout {squad_name}", "class": ["scout"], + "base": "scout", "formation_options": [ "scout", "tactical", @@ -398,39 +421,9 @@ ] } }, - "bike_squad": { - "Assault": { - "max": 9, - "min": 4, - "role": "Biker", - "loadout": { - "required": { - "wep1": ["", "max"], - "wep2": ["Chainsword", "max"], - "mobi": ["Bike", "max"] - } - } - }, - "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": { + "Tactical Marine": { "max": 9, "min": 4, "role": "Breacher", @@ -471,5 +464,89 @@ "display_data": "Breacher {squad_name}", "formation_options": ["tactical", "assault", "devastator", "scout"] } - } + }, + + "bike_squad": { + "Tactical": { + "max": 7, + "min": 1, + "role": "Biker", + "alternative_roles": ["Assault","Devastator","Tactical"], + "loadout": { + "required": { + "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]], + "wep2": [[["Volkite Serpenta", "Plasma Pistol", "Bolt Pistol", "Phobos Bolt Pistol", "Grav-Pistol", "Hand Flamer", "Infernus Pistol", "Ryza Plasma Pistol"],1]] + } + } + }, + "type_data": { + "display_data": "Bike {squad_name}", + "class": ["bike"], + "formation_options": ["biker","assault", "tactical","devastator","scout"], + "base": "biker" + } + }, + + + "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", "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": ["biker","assault", "tactical", "devastator", "scout"], + "base": "biker" + } +} } \ No newline at end of file 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 new file mode 100644 index 0000000000..41f6993946 --- /dev/null +++ b/datafiles/main/squads/lightning_warriors.json @@ -0,0 +1,229 @@ +{ + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "squad": "tactical_squad", "proportion": 0 }, + { "squad": "bike_squad", "proportion": 9 }, + { "squad": "attack_bike_squad", "proportion": 6 }, + { "squad": "devastator_squad", "proportion": 0 }, + { "squad": "assault_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": 6, + "squads": [ + { "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 } + ] + }, + { + "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 } + ] + } + ], + "distribution_overrides": { + "equal_specialists": { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "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 } + ], + "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": "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": "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": "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": 9, + "squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "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 } + ] + } + ] + }, + "equal_scouts": { + "default_squads": [ + { "squad": "command_squad", "max_count": 1, "min_count": 1, "require": true }, + { "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": "tactical_squad", "proportion": 6 } + ] + }, + { + "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 }, + { "squad": "bike_squad", "proportion": 4 } + ] + } + ] + }, + "equal_spescout": { + "default_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 } + ], + "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": "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": 8, + "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": 9, + "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": 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/objects/obj_controller/Create_0.gml b/objects/obj_controller/Create_0.gml index 42e9be7f2d..b71582da40 100644 --- a/objects/obj_controller/Create_0.gml +++ b/objects/obj_controller/Create_0.gml @@ -827,6 +827,7 @@ bat_formation = array_create(_count, ""); bat_formation_type = array_create(_count, 0); bat_deva_for = array_create(_count, 1); bat_assa_for = array_create(_count, 4); +bat_bike_for = array_create(_count, 4); bat_tact_for = array_create(_count, 2); bat_vete_for = array_create(_count, 2); bat_hire_for = array_create(_count, 3); @@ -852,6 +853,7 @@ default_bat_formation(); bat_devastator_column = 1; bat_assault_column = 4; +bat_bike_column = 4; bat_tactical_column = 2; bat_veteran_column = 2; bat_hire_column = 3; @@ -1432,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; @@ -1543,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; } @@ -1568,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; @@ -1625,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] = ""; @@ -1678,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)}"; } 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_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/objects/obj_ncombat/Alarm_3.gml b/objects/obj_ncombat/Alarm_3.gml index 6bee766eaf..b0edc21b9f 100644 --- a/objects/obj_ncombat/Alarm_3.gml +++ b/objects/obj_ncombat/Alarm_3.gml @@ -12,8 +12,13 @@ var changed = 0; repeat (100) { if (good == 0) { changed = 0; + i = 0; - for (var i = 1; i <= 55; i++) { + // 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 if ((message[i] == "") && (message[i + 1] != "")) { @@ -27,37 +32,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 +42,21 @@ 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; + // 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; + break; } } - if ((that != 0) && (that_sz > 0)) { + if (that != 0) { newline = message[that]; if (message_priority[that] > 0) { newline_color = "bright"; @@ -98,6 +78,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 +97,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 +114,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..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; @@ -151,6 +155,16 @@ dead_ene_n = array_create(70, 0); crunch = array_create(70, 0); mucra = array_create(11, 0); +// 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; + message_priority[_m] = 0; +} + post_equipment_lost = new EquipmentTracker(); post_equipment_recovered = new EquipmentTracker(); @@ -178,6 +192,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 2d50f1a285..03a5331fc2 100644 --- a/objects/obj_ncombat/Draw_0.gml +++ b/objects/obj_ncombat/Draw_0.gml @@ -50,30 +50,76 @@ if ((display_p2 > 0) && (enemy_forces > 0)) { draw_set_halign(fa_left); -repeat (45) { +// 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 (log_view_lines) { 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); } - draw_text(x + 6, y - 10 + (l * 18), string_hash_to_newline(string(lines[l]))); + 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(_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 3830813142..bc6995efe7 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; } @@ -42,14 +77,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) { @@ -105,21 +133,30 @@ if (((fugg >= 60) || (fugg2 >= 60)) && (messages_shown == 0) && (messages_to_sho if (timer_stage == 2) { fugg += 1; + stage_elapsed += 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. 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)) { +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 2ec58abead..0e885fd02d 100644 --- a/objects/obj_pnunit/Alarm_0.gml +++ b/objects/obj_pnunit/Alarm_0.gml @@ -51,7 +51,25 @@ 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. + // 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]); + } + } + report_held_fire(_held_fire); + break; + } if (wep[i] == "") { continue; } @@ -66,6 +84,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"; @@ -246,15 +270,28 @@ try { } } + 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. 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) { + destroy_empty_column(id); + } + 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/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/objects/obj_popup/Create_0.gml b/objects/obj_popup/Create_0.gml index 633e6523b4..72e4b3db88 100644 --- a/objects/obj_popup/Create_0.gml +++ b/objects/obj_popup/Create_0.gml @@ -191,27 +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 (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 (target_comp == 1) { @@ -224,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) { @@ -255,6 +255,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..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" @@ -70,12 +78,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_UnitGroup/scr_UnitGroup.gml b/scripts/scr_UnitGroup/scr_UnitGroup.gml index 35e8259574..076b1b6f3f 100644 --- a/scripts/scr_UnitGroup/scr_UnitGroup.gml +++ b/scripts/scr_UnitGroup/scr_UnitGroup.gml @@ -199,10 +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}"); + //LOGGER.info($"sgts : ${sgt_types}"); + sgt_types = role_groups(SPECIALISTS_SQUAD_LEADERS); var roles = active_roles(); @@ -222,20 +223,74 @@ 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; + 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); + } - var _squadless = get_from({squadless: true, roles: squad_unit_types}); + //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]); + } + } + } + } + //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}); 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]++; + // 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]; + 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 || (_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 == "") { + 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; } @@ -252,8 +307,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 (_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; + } + } + if (_has_sgt_requirements) { + break; } } @@ -262,15 +326,61 @@ 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 (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; + 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); + // 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); + } } } @@ -279,16 +389,51 @@ 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 = []; + 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]; - if (struct_exists(squad_fulfilment, _sgt_type) && (!sergeant_found)) { - _exp_unit = _members.highest_exp(); - - squad_fulfilment[$ _sgt_type]++; + 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 || (_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)) { + // 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]++; + array_push(_pending_promotions, { unit: _candidate, role: _actual_sgt_role }); } } @@ -303,12 +448,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.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_chapter_new/scr_chapter_new.gml b/scripts/scr_chapter_new/scr_chapter_new.gml index 8b5c7af2af..faef18b5d0 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 @@ -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_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_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_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_company_order/scr_company_order.gml b/scripts/scr_company_order/scr_company_order.gml index b7996fc3cc..93b62e164a 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); } @@ -194,5 +184,79 @@ function role_hierarchy() { "Ork Sniper" ]; + // 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)) { + // 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); + } + } + } + return hierarchy; } 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_flavor/scr_flavor.gml b/scripts/scr_flavor/scr_flavor.gml index b26bd358e5..a5528e0c95 100644 --- a/scripts/scr_flavor/scr_flavor.gml +++ b/scripts/scr_flavor/scr_flavor.gml @@ -22,8 +22,60 @@ 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. +/// @returns {Undefined} +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 +83,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 +104,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 +129,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 +157,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 +184,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}."; } } } @@ -181,7 +261,87 @@ 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."; + } + } + } + } 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, roaring 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 += $"crushing {casulties} beneath his wheels."; + } + } + } + } 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."; } } } @@ -190,15 +350,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 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 { @@ -221,15 +381,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 { @@ -391,10 +551,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; @@ -435,18 +595,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."; } } } @@ -461,9 +621,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) { @@ -471,28 +631,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. @@ -545,26 +724,211 @@ 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 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) { + 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)) { + // 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(); + } + 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(); } +} + +/// @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; + 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); + } +} - if (leader_message != "") { - add_battle_log_message(leader_message, message_size, message_priority); +/// @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; + } + 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_initialize_custom/scr_initialize_custom.gml b/scripts/scr_initialize_custom/scr_initialize_custom.gml index 7bee2adb51..303784081c 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; @@ -1543,6 +1545,14 @@ function scr_initialize_custom() { "scout", eROLE.SCOUT ], + [ + "biker", + eROLE.BIKER + ], + [ + "attack_biker", + eROLE.ATTACK_BIKER + ], [ "chaplain", eROLE.CHAPLAIN @@ -1639,23 +1649,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"; @@ -1749,7 +1777,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; } @@ -1763,7 +1792,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]; @@ -1777,15 +1806,26 @@ function scr_initialize_custom() { array_push(_swaps, _set); } + // LOGGER.debug($"squads object for chapter {chapter_name}"); + // LOGGER.debug($"{custom_squads}"); + if (variable_instance_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); + } } } @@ -1830,40 +1870,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, @@ -2303,6 +2310,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); @@ -2361,6 +2369,15 @@ 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_spescout (sd==3). + // + // 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) { if (companies.tenth.scouts > 10) { //theoretically this keeps track of moving scouts from the bank of them in 10th @@ -2378,6 +2395,9 @@ function scr_initialize_custom() { _coy.assaults = assault; _coy.devastators = devastator; } + // 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; @@ -2387,11 +2407,15 @@ 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) { - _coy.scouts = 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 -= _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))); @@ -2403,23 +2427,27 @@ function scr_initialize_custom() { _coy.devastators = devastator; } + // 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) { - 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; } + // 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) { _coy.tacticals = 0; _coy.assaults = _coy.total; @@ -2432,7 +2460,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. } } diff --git a/scripts/scr_management/scr_management.gml b/scripts/scr_management/scr_management.gml index 4a5befcff6..55c3a20747 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,29 @@ 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()); + // 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()); - var num = array_create(5, 0); - var nam = [ - "Land Raider", - "Predator", - "Rhino", - "Land Speeder", - "Whirlwind" - ]; - // Vehicles + // 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(nam); s++) { - if (obj_ini.veh_role[company][i] == nam[s]) { - num[s]++; + 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 < 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)); - } + 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_marine_struct/scr_marine_struct.gml b/scripts/scr_marine_struct/scr_marine_struct.gml index df88687d24..b53d2f6733 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; + } } } } @@ -1662,6 +1664,29 @@ 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 * _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 * _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"); + + speedf.second_profiles = [speedf_melee]; + + return speedf; + }; static armour_calc = function() { armour_rating = 0; 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) { 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_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..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 @@ -182,6 +182,16 @@ function scr_player_combat_weapon_stacks() { } } } + if (is_struct(mobi_item) && mobi_item.has_tag("bike")) { + 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, _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_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_powers/scr_powers.gml b/scripts/scr_powers/scr_powers.gml index 99ebf1a53e..ac5b28bcac 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,48 @@ 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 _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); + } + 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 diff --git a/scripts/scr_promote/scr_promote.gml b/scripts/scr_promote/scr_promote.gml index 5c050be414..f120ae0460 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,19 @@ 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]; + // 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++; + } + } + with (obj_controller) { scr_management(1); } diff --git a/scripts/scr_roster/scr_roster.gml b/scripts/scr_roster/scr_roster.gml index 990002aa56..26bc7c36bf 100644 --- a/scripts/scr_roster/scr_roster.gml +++ b/scripts/scr_roster/scr_roster.gml @@ -475,11 +475,15 @@ 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; 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]; @@ -497,6 +501,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; @@ -682,6 +697,12 @@ 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; + case "biker": + col = obj_controller.bat_bike_column; + break; } } if (col == 0) { 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, diff --git a/scripts/scr_shoot/scr_shoot.gml b/scripts/scr_shoot/scr_shoot.gml index e392c7ef80..42037aae42 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,341 @@ 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. 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 || 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); + 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 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; + } } - 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 + // Post the single consolidated line for the whole volley. + emit_volley_flavour(_primary_flavour, _spill_kills); - final_hit_damage_value *= target_object.dudes_dr[target_type]; //damage_resistance mod + // 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]); + } + } + } + } - if (final_hit_damage_value <= 0) { - final_hit_damage_value = 0; - } // Average after armour + if (stop == 0) { + compress_enemy_array(target_object); + destroy_empty_column(target_object); + } + } + } catch (_exception) { + ERROR_HANDLER.handle_exception(_exception); + } +} - c = shots_fired * final_hit_damage_value; // New damage +/// @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; +} - 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 (!is_rank_alive(_block, f)) { + 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]); + // 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++) { + // 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] != "" && is_rank_alive(id, r)) { + _total += dudes_num[r]; + } + } + } + if (_total <= 0) { + exit; + } - shots_remaining = round(damage_remaining / damage_per_weapon); - } + // 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; + } - 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 }] + 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++) { + // 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] == "" || !is_rank_alive(_f, r)) { + 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 }); + } 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 }; + } + } + } - 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); + } + } 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); - 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) { diff --git a/scripts/scr_squads/scr_squads.gml b/scripts/scr_squads/scr_squads.gml index 7cabad2454..9576fb7e43 100644 --- a/scripts/scr_squads/scr_squads.gml +++ b/scripts/scr_squads/scr_squads.gml @@ -45,15 +45,66 @@ 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]; + 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]; + role_key_to_actual[$ _key] = struct_exists(_def, "role") ? _def.role : _key; + } 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(); static sort = function() { + // 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. + // 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]; + 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); + 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; + } + } + + 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(_managed_slot_names); _s++) { + var _clear = {}; + _clear[$ _managed_slot_names[_s]] = ""; + // Clear via the squad's own from/to_armoury so items don't get destroyed + _u.alter_equipment(_clear, from_armoury, to_armoury); + } + } + } + for (var i = 0; i < array_length(squad_unit_types); i++) { unit_role = squad_unit_types[i]; role_squad_loadout(); @@ -69,18 +120,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 +174,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 +212,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 +226,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({roles: [unit_role, _actual_role]}); if (!struct_exists(current_unit_squad_data, "loadout")) { return; } @@ -177,13 +236,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() != unit_role && _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,12 +248,56 @@ 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); } } }; + /// @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) { + var _unit = _members_with_role.pop(); + if (array_contains(ignore_units, _unit.uid)) 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)]; + + // 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; @@ -222,6 +323,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"]); + } }; } @@ -279,6 +385,14 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { static change_type = function(new_type) { type = new_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); }; @@ -302,6 +416,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; }; @@ -320,10 +444,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]); @@ -333,7 +457,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; } @@ -341,7 +465,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]; @@ -374,6 +500,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") { @@ -381,6 +509,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--) { @@ -390,10 +521,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; @@ -408,8 +542,33 @@ 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 _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 = ""; + 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); squad_fulfilment[$ _wanted_unit_role]++; _squad_role_current = squad_fulfilment[$ _wanted_unit_role]; @@ -427,19 +586,35 @@ 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]--; + 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]--; } } - //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]--; + + // 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; } } }; @@ -705,17 +880,128 @@ function UnitSquad(squad_type = undefined, company = 0) constructor { }; } -// creates the origional distribution of squads accross the chapter -// lots of room for customisation of different chapters here +/// @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) { + // 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++) { + 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; +} + +/// @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} +/// @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. + arrangement.default_squads = clone_squad_defs(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]; + // 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: 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) { + arrangement.companies[ai] = _ovr_copy; + _found = true; + break; + } + } + if (!_found) { + array_push(arrangement.companies, _ovr_copy); + } + } + } +} +/// @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; + 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 _entry = { company: company, squads: clone_squad_defs(_src) }; + array_push(_comp_datas, _entry); + return _entry; } function ProportionalSquadEditor(data) constructor { @@ -1124,11 +1410,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); } } } 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..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,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: 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." }, @@ -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]; @@ -74,6 +75,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; diff --git a/scripts/scr_ui_settings/scr_ui_settings.gml b/scripts/scr_ui_settings/scr_ui_settings.gml index 3239d36915..83032629bc 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, ]; @@ -528,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;