Atlas of Bretonnia

Workshop journal · · 11 min read

Making the Red Duke playable: from lore to game tables

In Warhammer I's mini-campaign, only Orion and Durthu were playable. In our port, the Red Duke has been playable since 23 September, and the 9.0 vampire rework let us give him mechanics of his own. Here they are, where they stand, and the technical side: tables, Lua, text, pack and tests.

A dark, bluish forest where armoured undead warriors advance; on the right, a livid creature with a red eye.
The Vampire Counts, the Red Duke's own kind. Illustration from the game (Creative Assembly, Games Workshop).

The starting point: a duke without his dukedom

The Red Duke, le Duc écarlate in French, comes from Games Workshop's lore. Once Duke of Aquitaine, turned vampire, he was defeated at the First Battle of Ceren Field and walled up in a marble tower; set free, he returned for the second, where the tomb of his son Galand put him to flight.

Sources: the Vampire Counts army book (7th edition), C. L. Werner's novel The Red Duke, Circle of Blood and Knights of the Grail.

The Red Duke, in red armour, standing on ruins above a valley and a river.
“Crac de Sang”, the illustration for the first mission of his chronicle: the Duke on the ruins overlooking his old dukedom. One of our mod's event images, in the game's format (800 × 450).

According to Warhammer I's starting data, he is already in the Season, leading Mousillon, an AI faction at war with Bastonne; you couldn't play him. Yet in the lore, his land is Aquitaine: Mousillon is CA's choice, and CA doesn't make that faction playable.

Playable on 23 September, rethought with 9.0

On 23 September at 5:15 am, I asked for four more lords on the selection screen, the Duke among them; by 5:47 am, they were there. His selection record is a copy of Mannfred's, with its columns replaced. His scene is his own, built on Vlad's (sword and shield, like the Duke), with the Duke's model swapped in.

The Red Duke's selection record in RPFM
In RPFM: our pack open, the tree unfolded down to db → frontend_faction_leaders_tables, and the table shown on the right. One row per playable lord; the Red Duke's is selected. The Uniform column points to the 3D scene of the selection screen.

But playable doesn't mean gripping. That same day, I asked for a Claude session to take him on after 9.0, so “the Red Duke fits in properly with all of it, with his own mechanics, his own quests, his own goals”. The method: sourced lore first, proposals next, and I choose. (My quotes are translated from French.)

He starts at Mousillon with Château Rachard, Martel and Yremy, at war with Bastonne and, since 24 September, with Aquitaine: Richemont of Aquitaine ordered that he be hunted across all of Bretonnia, an order still in force (The Red Duke; Knights of the Grail).

With Bastonne, peace is blocked for fifteen turns then reopened, as between Kemmler and Parravon: the AI signed peace around turn 6 and stopped fighting (coded but not yet seen in game).

Since 9.0, he has the Vampire Counts rework: lairs, bloodlines, corpses and Blood Decrees. His first lair is at Derrevin Libre, in his old dukedom, below the ruins of Crac de Sang (a place from the novel, put on our map by the project).

His technologies are those of CA's vampire lords, with corpse thresholds halved, since our map is much smaller than Immortal Empires. And Abhorash's Kiss (The Red Duke): his faction starts with one level of the Blood Dragon bloodline.

All of this groundwork is being tested: coded and seen in short automated test runs (2 to 5 turns). New, coded but not yet seen in game: two more lairs, drawn at random, with the Duke served first.

His mechanics, one by one

The Duke's mechanics come down to Lua scripts, effect bundles, dilemmas, incidents and missions. Their numbers are design choices, to be tuned in game: no long game has been played, and balance hasn't been measured.

His chronicle and his victories

The chronicle: five missions, from Crac de Sang to The Grail Crusade, by way of Alberic and the sealed palace of Mousillon. First step being tested; the next ones, tested outside the game.

The Duke's chroniclefive missions, from turn 2Crac de SangTake Derrevin Libre, within 25 turns. Reward: 1,000 gold,Blood Knights and the blade he once carried.1The Blood of AquitaineAn Aquitaine army lies in wait. Break two Bretonnian armiesand take Chateau d'Epee, within 35 turns. 2,500 gold, BloodKnights.2Douse the LighthouseTake Turris Vigilans and slay Alberic in battle, within40 turns. 3,000 gold.3The Sealed PalaceRaise one of his cities to the highest tier, within50 turns. 3,000 gold.4The Grail CrusadeThe finale: a Bretonnian crusade gathers at TurrisVigilans; crush it. 6,000 gold, two Blood Knights units, aTerrorgheist.5?Step 1: being tested. Steps 2 to 5: tested outside the game. A failedstep also moves on to the next one.
His chronicle, from Crac de Sang to the Grail Crusade: each mission's objective, time limit and reward. Objectives and rewards are set by the script; the missions table only gives the title, the text and the image.

The victories: the short one, retake the province of Aquitaine, build the Tomb of the Red Duke there, at Gien, and use Blood Decrees five times. The long one: ten Blood Decrees, two bloodline lords awakened in lairs, Aquitaine and Quenelles destroyed, the sealed palace of Mousillon reopened and the finale of his chronicle.

They reward you without ending the game: only domination (27 of the 57 settlements) ends it. Being tested.

His dukedom and his favour

The lost dukedom: an effect bundle depending on the domains of Aquitaine he holds (Chateau d'Epee, Derrevin Libre, Gien), from “The Dispossessed Duke” with none to “The Duke Restored” with all three. Lore: he takes his dukedom back from those he calls usurpers (The Red Duke). In testing: the “Dispossessed Duke” level applies in the automated tests of 25 September; reconquest and the restored dukedom are not yet seen.

Abhorash's favour: CA's Blessing of the Lady, turned inside out. When the Duke wins a battle he leads, especially a heroic victory, his army gains melee attack and leadership, in three degrees; the favour is lost if he withdraws or is beaten, and wears off without battle. Lore: the Blood Dragons' code of honour (Vampire Counts, 6th edition). Coded but not yet seen in game.

His dukedom, his favourThe lost dukedomChateau d'Epee, Derrevin Libre, GienThe Dispossessed Dukeleadership −4, public order −2The Reconquest of Aquitaineleadership −2no bundleno more penaltyThe Duke Restoredleadership +6, income +10%, replenishment +10%his whole faction, whether played or notAbhorash's favourthe played Duke's army, when he leads it3+10 attack, +10 leadershipanother heroic victory against a lord; an incident announces it2+8 attack, +8 leadershipa heroic victory against an army led by a lord1+6 attack, +6 leadershipa victory he leads: heroic, certain; decisive, 20%; close, 10%It is lostAll of it if he withdraws, is beaten, stands on Ceren Fieldor dodges a challenge.One degree after 5 turns without a battle he leads.Dukedom: in testing ("dispossessed" seen); favour: coded, not yet seen.
Top, the lost dukedom: an effect bundle on his whole faction according to the domains of Aquitaine he holds. Bottom, Abhorash's favour: three degrees on the army he leads, won by victory, lost by retreat.

What hunts him

Richemont's Decree: Aquitaine wars on him, with no peace possible. If he takes a domain of Aquitaine, Questing Knights arrive three turns later; then, from turn 8, every ten turns as long as he holds one, 10 to 18 units depending on the turn. Coded but not yet seen in game.

Richemont's huntthe played Duke, if he holds a domain of Aquitaine10 units14 units18 unitsarmy:05101520253035turn?818286an army of Questing Knights, Grail Knights and otherknights, with pilgrims: from turn 8, then every 10 turns?the challenge: from turn 6, a nearby Bretonnian lordchallenges him (coded, not yet seen in game)What moves itHe takes a domain of Aquitaine→ the knights arrive 3 turns laterHe levies the blood tax→ the next hunt comes 3 turns soonerHe spares the peasants→ it comes 2 turns laterA hunt goes by→ each blood vassal may break his oath (30%)With Aquitaine, no peace is possible.
The hunt, in a game with nothing else happening: Questing Knights from turn 8, then every 10 turns, in ever larger armies. His choices hasten or delay it. Coded, not yet seen in game; numbers to be tuned in game.

The Tomb of Galand: each turn spent on Ceren Field, below Gien (an estimated location, for lack of a source), his units lose 15% of their strength, and an illustrated incident announces it. Coded but not yet seen in game.

The Red Duke recoils, one arm raised, before the light of a chapel; around him, his skeletons crumble to dust.
The illustration for the “Tomb of Galand” incident: before the Grail chapel, his dead crumble to dust; in the distance, on the hill, the tower where he was walled up.

The challenge: from turn 6, a nearby Bretonnian lord challenges him; the Duke has eight turns to defeat him in person, and dodging it breaks his favour. It is coded, not yet seen in game: the priority goes first to a playable version. It is planned, and will only stay if it proves useful in play.

His choices and his servants

The blood tax and the blood oath: two dilemmas. For each Bretonnian region taken, levy the tax (Blood and corpses, but the hunt draws closer) or spare the peasants. A Bretonnian lord reduced to a single holding can become his vassal by blood, and pay him a tithe every turn.

Safeguards, so the Duke isn't “overpowered”: two vassals at most (three once the dukedom is restored), never Aquitaine, oaths that can break, less public order per vassal. The tax comes from the lore; the vassals are inspired by it. Coded but not yet seen in game.

Items and heroes: his two 9.0 CA items, the Armour of Blood and the Jewel of Unlife, as he ranks up; three legendary heroes allowed, Walach Harkon, a Blood Dragon like him, Krell and Dieter Helsnicht. Coded but not yet seen in game.

Tech 1: from Python batches to game tables

Everything the game reads from its database goes, for us, through batches: replayable Python functions that write into the XML tables of the Assembly Kit, CA's official kit. They copy a CA row under our keys, or build a new row on its model. Nothing is rewritten, everything is backed up before writing.

From batch to gameThe Python batcha replayable function: it copies a CA row under a key ofour own, saison_duc_…donnees_campagne.pytables_gameplay.pyThe Assembly KitCA's official kit: its tables in XML; a backup beforeevery writeraw_data/dbthe modthe startThe mod's tablesbuild_pack.py drivesrpfm_server: our rows, in theorder of the game's schemaeffect_bundlesdilemmasincidentsmissionsThe start tablesstart_pos_*: in a separatepack, never in the modzz_startpos_db.packThe game builds thestartpossave counter at 1; structurecompared with the previousonestartpos.esfThe mod's packthe tables, the startpos, the Lua scripts, the text, theimagessaison_des_revelations.packThe in-game testa new table in a played pack: a startup test before anyannouncementOne table too many once closed the game without a message.
A table row, from Python batch to game. The batch writes into the kit; then two paths: the mod's tables go into the pack through RPFM, the start tables let the game generate the startpos, which then joins the pack.

Golden rule: keys of our own, never a CA table replaced, so that campaigns coexist with the mod active. On 23 September, two replaced CA tables crashed a tooltip. Everything that belongs to the Duke starts with saison_duc_. His batches:

  • 13: faction_to_mercenary_set_junctions, his regiments of renown.
  • 17: starting armies, treasury, second lord (start_pos_* tables).
  • 31: the two support factions of the 9.0 vampires and our campaign's unique heroes (campaign_to_agent_subtypes).
  • 33: faction_agent_permitted_subtypes, which allows Walach Harkon, Krell and Dieter Helsnicht for Mousillon; without these rows, the technologies that grant them gave the played Duke nothing.
  • 34: effect_bundles, seven effect bundles under our keys; their effects, CA's, are applied by the script.
  • 35: dilemmas and its choice and option tables.
  • 32 and 37: incidents, the Duke's nine announcements, with no target or payload: the effects stay in the script.

A second file tells the pack which rows to take (kit table, game table, column, prefix):

TABLES_LOT34 = [
    ('effect_bundles', 'effect_bundles_tables', 'key', 'saison_duc_'),
]

Tech 2: RPFM and the pack

RPFM, Frodo45127's Rusted PackFile Manager, is the community tool for packs; since version 5, its windowless server, rpfm_server, exposes its functions to an AI.

Our build_pack.py drives it: it reads our rows from the kit and copies them in the order of the game's schema, which is what counts (missing column: empty value; extra column: dropped). One trap: RPFM wants rows in “processed” format, a colour as a single value.

It also checks each Lua script's syntax, since one error makes the whole campaign load fail, and refuses any file that would take the place of a Warhammer III file.

The Duke's effect bundles in the effect_bundles table
The effect_bundles table: the Duke's bundles (saison_duc_… keys), with the description the player reads. The table says what is displayed; the script decides when to apply it and which effects it carries.

Tech 3: start tables and the startpos

A lord becomes playable mostly in the startpos, the start position. For the Duke: start_pos_factions (playable and major faction), start_pos_characters, start_pos_starting_general_options (the link to his record) and start_pos_land_units.

The Red Duke: startpos or not?Needs the startposPlayable and major factionstart_pos_factionsThe Duke, garrisoned factionleaderstart_pos_charactersThe link to his selectionrecordstart_pos_starting_general_optionsHis starting armiesstart_pos_land_unitsStarting wars and treatiesstart_pos_diplomacyRegiments of renownfaction_to_mercenary_set_junctionsDatabase or script, nostartposLord declared for thecampaigncampaign_to_agent_subtypesHeroes allowedfaction_agent_permitted_subtypesChroniclemissionsAnnouncements and choicesincidentsdilemmasMechanicseffect_bundlesDukedom, favour, tax, hunt,victory: Lua scriptsHis record (frontend_faction_leaders) has no startposflag; in our workshop, the startpos is regeneratedanyway after every change to the record.
Left column: the tables where the Assembly Kit's schema flags a field requires_startpos_reprocess; they only count once the startpos is regenerated. Right column: rebuilding the pack is enough.

These tables never ship in the mod: a separate pack is used for generation, and the game itself builds the startpos. The recipe:

  1. The batch writes into the kit; the start tables go into the generation pack.
  2. Validation: no cell to fix; pack rebuilt.
  3. The game generates the startpos.
  4. Checks: save counter at 1 (otherwise every new game thinks it's a saved one), structure compared with the previous one.
  5. The startpos goes into the pack; then the test.

Details and traps: the tools guide. The Duke, brick by brick in RPFM: the full example in the RPFM guide.

Tech 4: the Lua scripts

The Duke has his own file, saison_duc.lua, loaded by required.lua. On every load, saison_start.lua starts the 9.0 vampire systems in CA's order, then the Duke's, whose Kiss of Abhorash borrows the bloodline ritual. The rest is event listeners:

  • RegionFactionChangeEvent: a domain of Aquitaine changes hands, or a Bretonnian region falls to the played Duke.
  • DilemmaChoiceMadeEvent: a dilemma's choice.
  • BattleCompleted and CharacterWithdrewFromBattle: the favour, won or broken.
  • FactionBeginTurnPhaseNormal: the tomb, the vassals, the favour wearing off, the challenge, the hunt.
  • MissionSucceeded and MissionFailed: the challenge, met or dodged.
  • ScriptEventVampireLairAwakened, a CA event: the awakenings for the long victory.

The turn listener, shortened:

core:add_listener(
    "saison_duc_tour",
    "FactionBeginTurnPhaseNormal",
    function(context) return context:faction():name() == "wh_main_vmp_mousillon" end,
    function(context)
        local faction = context:faction();
        saison_sur("tombeau de Galand", tombeau_de_galand, faction);
        if faction:is_human() then
            saison_sur("vassaux du sang", tour_des_vassaux, faction);
            saison_sur("usure de la faveur", user_la_faveur, faction);
            saison_sur("defi", tour_du_defi, faction);
            -- then Richemont's hunt, if its turn has come
        end;
    end,
    true
);

The Duke's data goes into CA's Lua tables on load (saison_donnees_ca.lua). For victories, CA's engine is loaded from its own folder, never copied, and its Immortal Empires list replaced by our ten lists, written with its own objective generators; CA gives the Duke no list.

What 9.0 changed

  • The old blood kiss system is gone, and our tuning with it.
  • FactionTurnStart reached no mod listener on our map; ours moved to FactionBeginTurnPhaseNormal. Cause found and fixed in the automated tests, to be confirmed in a played game: three CA listener conditions, for Mother Ostankya and Yuan Bo, read systems absent from our map, and their error cut the event off.
  • CA's technology that confederates the Red Duke would, if researched by the played Duke, absorb his own faction: it is locked for him.
A listener that failsUnprotectedFactionTurnStartCA listener (Mother Ostankya)error: a system missing from ourmapOther CA listenersnever calledOur listeners (the Duke…)never calledProtectedFactionTurnStartCA listener (Mother Ostankya)error: a system missing from ourmapscript_log: "ecouteur [key]en erreur sur <event>"Other CA listenersOur listeners (the Duke…)9.0, on our map: the error in three CA conditions cut the eventoff for everyone. Our turn starts go throughFactionBeginTurnPhaseNormal, and every listener is protected.
Left, the released game: one CA listener's error stops the event, and the next ones, ours included, are never called. Right, with required.lua: the error is named once in the log, and the event goes on.

In the released game, an error in one listener stops the event for all, CA's included. Ours are protected (saison_ecouteur, saison_sur): the error goes to the log, and the rest of the turn plays on.

Tech 5: text, in French and English

Our text lives in textes_gameplay.json, with French and English for each key. A game text key is <table>_<column>_<row key>: dilemmas_localised_title_saison_duc_impot_du_sang gives “L'impôt du sang” and “The Blood Tax”.

A check first looks for defects. French goes into the main pack, English into a small pack that loads first, under the same path. Golden rule: never overwrite a game text, which Warhammer III would load whatever the player's language; a key the game knows is removed, and the game supplies it itself, up to date.

Names follow the game's text: in 9.0, CA's French calls the Duke “Duc écarlate” in 21 texts, including his own name, against 3 “Duc Rouge”; we fixed 22 texts. With no voiced line for him, I chose a silent flyover with its text for his prologue (coded but not yet seen in game).

Tests: what they see, what they don't

Any pack that adds a table goes through a startup test. Then come the automated games: a separate test pack launches the game through the interface, like a player, and plays turn after turn.

Their limit: when the AI plays everything, the player's faction doesn't play. The lost dukedom and the tomb also apply to an AI Duke, but the hunt, the favour, the challenge and the dilemmas only target the played Duke. So, for the most part, the Duke can only be seen by playing him.

What the tests seeAn automated test cansee itStartup: errors and hangs,turn after turnThe 9.0 vampire systems,Abhorash's KissThe lost dukedom and Galand'stomb: they also apply to anAI DukeWars between AI factionsThe AI plays every faction: theplayer's faction doesn't play.Only by playing himThe illustrated announcements(incidents)Abhorash's favourThe blood tax and the bloodoath (dilemmas)Richemont's huntThe challengeThe real test: a game, and eleventhings to watch.
Left, what an automated test can show: the game just has to run. Right, what only targets the played Duke: in a test where the AI plays everything, his faction doesn't play.

A Duke test on 23 September showed that on turn 1 nobody had an enemy, hence the starting wars. A 29-turn game with Drycha saw Grom and the AI Duke wipe out Bordeleaux before turn 20; hence, between AI factions, no war on Bordeleaux before turn 15 (coded but not yet seen in game).

For the chronicle, a test bench runs our scripts in a Lua interpreter, with an imitation of CA's interface: that's “tested outside the game”. The real test will be my own game, with eleven things to watch.

Next

Tune these mechanics' timings and strength in game. Other mechanics are in preparation.

The Duke's card and the mechanics are on the home page; the porting method is in the tutorial.

Suggest an ideaSee the map