From ea0623fbe3652325cdafb96a401a394ecb0edce3 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 14:36:45 +0100 Subject: [PATCH] Campaign of 30 missions: six against the rival masters Mossgrim and Lady Nightwell, islands, mana springs, sealed caves with chained creatures - missions interleaved into one story (EN/DE), new epilogue, map sizes grow to 176x176 - islands in water or lava with treasure and/or a mana spring; rivers at least four tiles wide - sealed caves with treasure and a chained creature that joins once the cave is claimed - new goals: rival cores, mana springs, freed creatures - rival messages name the masters; new progress key for the new mission order Co-Authored-By: Claude Opus 5.5 --- lang/de.lang | 41 +++++-- lang/en.lang | 41 +++++-- src/campaign.rs | 297 +++++++++++++++++++++++++++++++++++++++--------- src/game.rs | 69 +++++++++-- src/gfx.rs | 1 + src/main.rs | 2 +- src/map.rs | 5 +- src/menu.rs | 3 + src/props.rs | 33 ++++++ src/render3d.rs | 1 + src/ui.rs | 5 +- 11 files changed, 413 insertions(+), 85 deletions(-) diff --git a/lang/de.lang b/lang/de.lang index 06633e5..f6384eb 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -451,6 +451,9 @@ goal.loot = Erbeute {0} aus den Ritterschätzen goal.towers = Reiße alle Rittertürme nieder goal.survive = Schlage {0} Wellen zurück goal.left = (noch {0}) +goal.rivals = Zerstöre die Kerne der rivalisierenden Meister +goal.springs = Nimm {0} Manaquellen ein +goal.freed = Befreie {0} angekettete Kreaturen end.mission_won = Mission erfüllt! end.mission_won_sub = Krummfinger nickt anerkennend. Deine stärksten Kreaturen und ein Fünftel des Goldes ziehen mit in die nächste Mission. end.mission_lost = Mission gescheitert @@ -499,9 +502,29 @@ camp.m18.title = Die Lavazitadelle des Marschalls camp.m18.story = Marschall Gerold der Unbeugsame thront in seiner Lavazitadelle, ein Paladin, der sich noch nie gebeugt hat - weder sein Knie noch seine Meinung. | Sein Tor bewacht den Weg zum Großmeister. Brich es. Gerold wird versuchen, dich zuerst zu brechen. camp.m19.title = Die letzte Wacht camp.m19.story = Drei Festen hüten die letzten Splitter vor der Zitadelle. Der Orden hat alles hineingeworfen, was ihm geblieben ist, einschließlich seiner letzten guten Laune. | Hol dir alle drei Splitter. Danach steht nur noch ein alter Mann in glänzender Rüstung zwischen dir und deinem Kern. +camp.m21.title = Der Nachbar +camp.m21.story = Schlechte Nachrichten, Meister: Wir sind nicht die einzige Finsternis hier unten. Mossgrim der Modrige hat sich im Osten eingegraben - ein Meister, der nach feuchtem Keller riecht und das Ambiente nennt. Er hat gehört, ein gebrochener Kern sei billig zu haben. | Seine Kreaturen tragen Grün, auf seinen Böden wächst Pilz, und sein Kern leuchtet wie alter Käse. Zerstöre ihn, bevor er deinen zerstört. Und im Norden liegt eine kleine Insel mit einem Schatz - Brücken, Meister, Brücken. +camp.m22.title = Der Angekettete +camp.m22.story = Die Ritter haben im Norden eine Höhle zugemauert und darin einen Oger angekettet, einen bösen, starken, wunderbaren Oger. Sie haben vergessen, ihm zu sagen, warum. | Grab dich in die Höhle und nimm sie ein - sobald unser Boden ihn berührt, sprengt er seine Ketten und schuldet dir alles. Dann hol dir den Splitter im Osten. In einer anderen versiegelten Höhle im Süden soll Gold liegen. +camp.m23.title = Lady Nightwell +camp.m23.story = Ein Brief, Meister, auf schwarzem Papier, mit Lilienduft: "Liebe Zerbrochene, deine Splitter würden sich wunderbar auf meinem Kaminsims machen. Deine Lady Nightwell." | Sie wohnt jenseits des Flusses in blauen Hallen und ist doppelt so klug wie Mossgrim, was nicht schwer ist. Überquere das Wasser, zerbrich ihren Kern und nimm unterwegs die Schatzinsel mit. +camp.m24.title = Die Manaquellen +camp.m24.story = Auf drei Inseln quillt hier altes Mana hervor - rohe Kraft der Tiefe, verschwendet an Fische. | Bau Brücken hinüber und lass die Imps jede Quelle einnehmen: Jede gibt dir für immer Mana und lässt dich mehr davon speichern. Die Ritter im Osten haben es auch bemerkt, also beeil dich. +camp.m25.title = Mossgrims Rache +camp.m25.story = Mossgrim ist zurück. Er hat seinen Kern neu gebaut, sich einen neuen Bart aus Schimmel wachsen lassen und - das ist das Schlimmste - einen Pakt mit dem Orden geschlossen: Ihr Tor öffnet sich neben seinen Hallen. | Zwei Feinde, die sich auch gegenseitig hassen. Lass sie einander beißen, dann zerbrich Mossgrims Kern endgültig. +camp.m26.title = Nightwells Inselfestung +camp.m26.story = Lady Nightwell hat sich hinter einen Lavastrom zurückgezogen und schickt ihre besten Kreaturen, um sich um uns zu kümmern. Ihre Briefe sind nicht mehr höflich. | Auf einer Insel in der Lava gibt es eine Manaquelle - nimm sie, sie wird helfen. Dann setz über und beende ihre Ambitionen. Mitsamt ihrem Kern. +camp.m27.title = Die versiegelten Tiefen +camp.m27.story = Der Orden hat hier unten seine gefährlichsten Gefangenen begraben, in versiegelten Höhlen: einen Kettenbrecher und einen Gargoyle, dazu einen Tresor voll mit eigenem beschlagnahmtem Gold. | Befreie beide Gefangenen und nimm die zwei Manaquellen auf den Inseln ein. Irgendwer musste sie ja finden. Dann am besten jemand, der sie zu schätzen weiß. +camp.m28.title = Der Pakt der Meister +camp.m28.story = Mossgrim und Lady Nightwell hassen sich nicht mehr. Na gut, ein bisschen - aber nur so lange, bis sie gemeinsam über dich herfallen. Grün im Nordosten, Blau im Südosten. | Zwischen uns liegt eine Insel mit einer Manaquelle. Halte sie, dann zerbrich beide Kerne. Lange bleiben die beiden keine Verbündeten, aber so lange sollten sie auch nicht mehr leben. +camp.m29.title = Die Glutinseln +camp.m29.story = Lavainseln, Meister, mit kochenden Manaquellen in der Mitte, und eine versiegelte Höhle, in der die Ritter einen jungen Lindwurm angekettet haben. Junge Lindwürmer sind jähzornig. Herrlich. | Befreie den Lindwurm, nimm beide Quellen ein und den Splitter in der Burg jenseits des Feuers. Dann ist der Weg zum Großmeister frei. camp.m20.title = Der Großmeister -camp.m20.story = Großmeister Valerian Hellschild erwartet dich in seiner Zitadelle jenseits des großen Flusses, mit dem letzten Splitter und dem Tor des Ordens. Hundert Jahre lang hat er gebetet, poliert und sich vorbereitet. | Zerstöre sein Tor, hol dir den Splitter und mach deinen Kern wieder ganz. Ich bin direkt hinter dir. Ein gutes Stück hinter dir. -camp.epilogue = Der letzte Splitter versinkt im Kern, und die Dunkelheit atmet wieder. Irgendwo dort oben schreibt der Orden der Lichtwacht einen sehr langen Bericht. | Krummfinger hat Tee gekocht. Du bist wieder ganz, Meister. Das Unterreich gehört dir. +camp.m20.story = Großmeister Valerian Hellschild erwartet dich in seiner Zitadelle jenseits des großen Flusses, mit dem letzten Splitter und dem Tor des Ordens. Hundert Jahre lang hat er gebetet, poliert und sich vorbereitet. | Zerstöre sein Tor und hol dir den Splitter. Dann ist dein Kern ganz - und jeder Meister, der ihn je haben wollte, wird auf einmal danach greifen. Eins nach dem anderen. +camp.m30.title = Der Thron der Tiefe +camp.m30.story = Es kam, wie ich gesagt habe: Dein Kern ist ganz, und jetzt wollen ihn alle. Mossgrim und Lady Nightwell marschieren gemeinsam, und was vom Orden übrig ist, öffnet ein letztes Mal sein Tor. | Zerstöre beide Meisterkerne und das Tor des Ordens. Danach hat die Tiefe genau einen Meister. Rate, wen. Kleiner Tipp: nicht mich, ich bin der Teemann. +camp.epilogue = Der grüne Kern zerbröselt, der blaue erlischt, und das Tor des Ordens verstummt. Irgendwo dort oben schreibt die Lichtwacht einen sehr langen Bericht, und Lady Nightwells Briefe bleiben aus. | Krummfinger hat Tee gekocht. Du bist wieder ganz, Meister. Das Unterreich gehört dir. lair.arrivals = {0} freie Plätze: Neue Kreaturen kommen mit {1}% Tempo job.help = Hilft mit @@ -576,8 +599,8 @@ job.make_bed = Richtet sein Bett ein job.fetch_wage = Holt sich seinen Lohn msg.payday_walk = Zahltag! {0} Kreaturen holen sich ihren Lohn aus der Schatzkammer ({1} Gold inkl. Training). msg.wage_unpaid = Dein {0} hat kein Gold für seinen Lohn gefunden und ist wütend! -owner.rival1 = gehört dem grünen Meister -owner.rival2 = gehört dem blauen Meister +owner.rival1 = gehört Mossgrim, dem grünen Meister +owner.rival2 = gehört Lady Nightwell, der blauen Meisterin # rival masters (free game) newgame.difficulty = Gegner @@ -589,12 +612,14 @@ newgame.opponents2 = Zwei Gegner: zwei rivalisierende Meister oder - zufällig - msg.welcome_rivals = Ein rivalisierender Dungeon-Meister ist nah ({0} insgesamt): Zerstöre jeden gegnerischen Seelenkern. msg.welcome_both = Ein Meister und das Tor der Menschen stellen sich dir entgegen: Zerstöre den gegnerischen Kern und das Tor. msg.welcome_humans = Das Tor der Menschen stellt sich dir entgegen: Zerstöre es. -msg.rival1_attack = Der grüne Meister schickt {0} Kreaturen gegen dich! -msg.rival2_attack = Der blaue Meister schickt {0} Kreaturen gegen dich! -msg.rival1_out = Der grüne Meister ist besiegt - sein Seelenkern ist zersprungen! -msg.rival2_out = Der blaue Meister ist besiegt - sein Seelenkern ist zersprungen! +msg.rival1_attack = Mossgrim, der grüne Meister, schickt {0} Kreaturen gegen dich! +msg.rival2_attack = Lady Nightwell, die blaue Meisterin, schickt {0} Kreaturen gegen dich! +msg.rival1_out = Mossgrim ist besiegt - sein Seelenkern ist zersplittert! +msg.rival2_out = Lady Nightwell ist besiegt - ihr Seelenkern ist zersplittert! msg.rival_unit_slain = Ein {0} eines Rivalen fällt! +{1} Gold. msg.wave_on_rival1 = Welle {0}: {1} Helden ziehen gegen den grünen Meister. msg.wave_on_rival2 = Welle {0}: {1} Helden ziehen gegen den blauen Meister. hud.rival1 = Grün hud.rival2 = Blau +msg.creature_freed = Ein {0} sprengt seine Ketten und schließt sich dir an! +msg.spring_taken = Eine Manaquelle gehört dir: mehr Mana und ein höheres Maximum. diff --git a/lang/en.lang b/lang/en.lang index 3676757..06fd7cc 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -452,6 +452,9 @@ goal.loot = Take {0} from the knight vaults goal.towers = Raze all knight towers goal.survive = Beat {0} waves goal.left = ({0} left) +goal.rivals = Destroy the cores of the rival masters +goal.springs = Claim {0} mana springs +goal.freed = Free {0} creatures in chains end.mission_won = Mission accomplished! end.mission_won_sub = Crookfinger nods approvingly. Your strongest creatures and a fifth of the gold come along into the next mission. end.mission_lost = Mission failed @@ -500,9 +503,29 @@ camp.m18.title = The Marshal's Lava Citadel camp.m18.story = Marshal Gerold the Unbending sits in his lava citadel, a paladin who has never bent anything - not his knee, not his opinion. | His gate guards the road to the Grand Master. Break it. Gerold will try to break you first. camp.m19.title = The Last Watch camp.m19.story = Three keeps hold the last shards before the citadel. The Order has thrown everything it has left into them, including its last good mood. | Take all three shards. After this, only one old man in shiny armour stands between you and your core. +camp.m21.title = The Neighbour +camp.m21.story = Bad news, master: we are not the only darkness down here. Mossgrim the Mouldy has dug in to the east - a master who smells of wet cellar and calls it ambience. He has heard that a broken core is cheap to take. | His creatures wear green, his floors grow fungus, and his core glows like old cheese. Destroy it before he destroys yours. And somewhere north lies a little island with treasure - bridges, master, bridges. +camp.m22.title = The Chained One +camp.m22.story = The knights have walled up a cave to the north and chained an ogre inside, a nasty, strong, wonderful ogre. They forgot to tell him why. | Dig into the cave and claim it - the moment our ground touches him, he will break his chains and owe you everything. Then take the shard in the east. Another sealed cave to the south is said to hold gold. +camp.m23.title = Lady Nightwell +camp.m23.story = A letter, master, on black paper, scented with lilies: "Dear broken one, your shards would look lovely on my mantelpiece. Yours, Lady Nightwell." | She lives beyond the river in blue halls and is twice as clever as Mossgrim, which is not hard. Cross the water, break her core and take the treasure island on the way. +camp.m24.title = The Mana Springs +camp.m24.story = Old mana wells up on three islands here - raw power of the deep, wasted on fish. | Bridge over to them and let the imps claim every spring: each one gives you mana for ever and lets you store more of it. The knights in the east have noticed too, so be quick. +camp.m25.title = Mossgrim's Revenge +camp.m25.story = Mossgrim is back. He has rebuilt his core, grown a new beard of mould, and - this is the worst part - made a deal with the Order: their gate opens beside his halls. | Two enemies who also hate each other. Let them bite one another, then break Mossgrim's core for good. +camp.m26.title = Nightwell's Island Fortress +camp.m26.story = Lady Nightwell has withdrawn behind a lava stream and sends her finest creatures out to deal with us. Her letters have stopped being polite. | There is a mana spring on an island in the lava - take it, it will help. Then cross over and end her ambitions. With her core. +camp.m27.title = The Sealed Depths +camp.m27.story = The Order buried its most dangerous prisoners down here, in sealed caves: a chainbreaker and a gargoyle, and a vault full of their own confiscated gold. | Free both prisoners and claim the two mana springs on the islands. Somebody had to find them. It might as well be someone who appreciates them. +camp.m28.title = The Pact of the Masters +camp.m28.story = Mossgrim and Lady Nightwell have stopped hating each other. Well, a little - just long enough to come for you together. Green to the north-east, blue to the south-east. | An island with a mana spring lies between us. Hold it, then break both cores. They will not stay allies for long, but neither should they stay alive that long. +camp.m29.title = The Ember Islands +camp.m29.story = Lava islands, master, with springs of mana boiling in the middle, and a sealed cave where the knights chained a young wyrm. Young wyrms have short tempers. Lovely. | Free the wyrm, take both springs and the shard in the keep beyond the fire. Then the road to the Grand Master is open. camp.m20.title = The Grand Master -camp.m20.story = Grand Master Valerian Brightshield waits in his citadel beyond the great river, with the last shard and the gate of the order. He has prayed, polished and prepared for a hundred years. | Destroy his gate, take back the shard and make your core whole again. I will be right behind you. Well behind you. -camp.epilogue = The last shard sinks into the core, and the darkness breathes again. Somewhere above, the Order of the Lightwatch is writing a very long report. | Crookfinger has made tea. You are whole, master. The underworld is yours. +camp.m20.story = Grand Master Valerian Brightshield waits in his citadel beyond the great river, with the last shard and the gate of the order. He has prayed, polished and prepared for a hundred years. | Destroy his gate and take back the shard. Then your core is whole - and every master who ever wanted it will come for it at once. One thing at a time. +camp.m30.title = The Throne of the Deep +camp.m30.story = It happened as I said: your core is whole, and now everybody wants it. Mossgrim and Lady Nightwell march together, and what is left of the Order opens its gate one last time. | Destroy both master cores and the gate of the order. After this the deep has exactly one master. Guess who. Hint: not me, I am the tea man. +camp.epilogue = The green core crumbles, the blue one goes dark, and the gate of the order falls silent. Somewhere above, the Lightwatch is writing a very long report, and Lady Nightwell's letters have stopped coming. | Crookfinger has made tea. You are whole, master. The underworld is yours. lair.arrivals = {0} free beds: new creatures arrive at {1}% speed job.help = Helping @@ -577,8 +600,8 @@ job.make_bed = Setting up its bed job.fetch_wage = Fetching its wage msg.payday_walk = Payday! {0} creatures fetch their wages from the treasury ({1} gold incl. training). msg.wage_unpaid = Your {0} found no gold for its wage and is furious! -owner.rival1 = held by the green master -owner.rival2 = held by the blue master +owner.rival1 = held by Mossgrim, the green master +owner.rival2 = held by Lady Nightwell, the blue master # rival masters (free game) newgame.difficulty = Opponents @@ -590,12 +613,14 @@ newgame.opponents2 = Two opponents: two rival masters, or - by chance - one mast msg.welcome_rivals = A rival dungeon master is near ({0} in all): destroy every rival soul core to win. msg.welcome_both = A rival master and the humans' gate oppose you: destroy the rival soul core and the gate. msg.welcome_humans = The humans' gate opposes you: destroy it. -msg.rival1_attack = The green master sends {0} creatures against you! -msg.rival2_attack = The blue master sends {0} creatures against you! -msg.rival1_out = The green master is defeated - his soul core has shattered! -msg.rival2_out = The blue master is defeated - his soul core has shattered! +msg.rival1_attack = Mossgrim, the green master, sends {0} creatures against you! +msg.rival2_attack = Lady Nightwell, the blue master, sends {0} creatures against you! +msg.rival1_out = Mossgrim is defeated - his soul core has shattered! +msg.rival2_out = Lady Nightwell is defeated - her soul core has shattered! msg.rival_unit_slain = A {0} of a rival master falls! +{1} gold. msg.wave_on_rival1 = Wave {0}: {1} heroes march on the green master. msg.wave_on_rival2 = Wave {0}: {1} heroes march on the blue master. hud.rival1 = Green hud.rival2 = Blue +msg.creature_freed = A {0} breaks its chains and joins you! +msg.spring_taken = A mana spring is yours: more mana and a higher maximum. diff --git a/src/campaign.rs b/src/campaign.rs index 0158803..6a913bd 100644 --- a/src/campaign.rs +++ b/src/campaign.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; // From which mission (0-based) on each tool is available (same order as ui::TOOLS): // dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp, // torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie, steel -pub const TOOL_UNLOCK: [usize; 20] = [0, 0, 0, 0, 0, 2, 4, 0, 0, 3, 4, 0, 6, 9, 3, 5, 9, 1, 6, 10]; +pub const TOOL_UNLOCK: [usize; 20] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17]; pub struct FortDef { pub at: (f32, f32), // centre as a fraction of the map size @@ -45,6 +45,33 @@ pub struct MissionDef { pub rivers: Vec, pub forts: Vec, pub gift: Vec<(EK, u32)>, // creatures that join at the start + pub rivals: Vec<(f32, f32, u8, u8)>, // rival masters: position (fraction of the map), difficulty, side (2 Mossgrim, 3 Lady Nightwell) + pub islands: Vec, + pub caves: Vec, +} + +// an island in a lake of water or lava, with treasure and/or a mana spring +pub struct IslandDef { + pub at: (f32, f32), + pub r: i32, + pub lava: bool, + pub spring: bool, + pub treasure: i32, +} + +// a sealed cave: treasure and/or a lone creature in chains that joins when the cave is claimed +pub struct CaveDef { + pub at: (f32, f32), + pub chained: Option<(EK, u32)>, + pub treasure: i32, +} + +fn island(at: (f32, f32), lava: bool, spring: bool, treasure: i32) -> IslandDef { + IslandDef { at, r: 2, lava, spring, treasure } +} + +fn cave(at: (f32, f32), chained: Option<(EK, u32)>, treasure: i32) -> CaveDef { + CaveDef { at, chained, treasure } } // carried over from the last won mission: the strongest creatures and part of the gold @@ -132,45 +159,58 @@ fn base(key: &'static str, size: (usize, usize), seed: u32, goals: Vec) -> rivers: Vec::new(), forts: Vec::new(), gift: Vec::new(), + rivals: Vec::new(), + islands: Vec::new(), + caves: Vec::new(), } } +// Rivers are at least four tiles wide: a ranged attack just reaches across. pub fn missions() -> Vec { let mut v = Vec::new(); // ---- Act I: awakening v.push(MissionDef { gold: 2500.0, forts: vec![fort((0.74, 0.5), (15, 13), &[3], &[], 1, 4, 0, 1, 1, 1200).shard()], ..base("m01", (64, 64), 101, vec![Goal::Shards]) }); v.push(MissionDef { gold: 2500.0, - rivers: vec![river(false, true, 0.47, 3, 0)], - forts: vec![fort((0.8, 0.5), (15, 15), &[3], &[Arrow], 1, 4, 1, 1, 1, 1500).shard()], - ..base("m02", (80, 64), 202, vec![Goal::Shards]) + rivers: vec![river(false, true, 0.47, 4, 0)], + forts: vec![fort((0.82, 0.5), (15, 15), &[3], &[Arrow], 1, 4, 1, 1, 1, 1500).shard()], + ..base("m02", (72, 64), 202, vec![Goal::Shards]) }); - v.push(MissionDef { forts: vec![fort((0.74, 0.5), (19, 17), &[3, 0], &[Arrow, Arrow], 1, 6, 2, 1, 1, 1500)], ..base("m03", (80, 80), 303, vec![Goal::KillAll]) }); + v.push(MissionDef { forts: vec![fort((0.74, 0.5), (19, 17), &[3, 0], &[Arrow, Arrow], 1, 6, 2, 1, 1, 1500)], ..base("m03", (80, 72), 303, vec![Goal::KillAll]) }); + v.push(MissionDef { portals: 2, treasures: 1, waves: true, wave_limit: 4, first_wave: 270.0, gate: true, ..base("m04", (80, 72), 404, vec![Goal::Survive(4)]) }); v.push(MissionDef { - portals: 2, - treasures: 1, - waves: true, - wave_limit: 4, - first_wave: 270.0, - gate: true, - ..base("m04", (80, 64), 404, vec![Goal::Survive(4)]) + rivals: vec![(0.78, 0.5, 0, 2)], + islands: vec![island((0.5, 0.2), false, false, 1200)], + ..base("m21", (80, 80), 2121, vec![Goal::Rivals]) }); v.push(MissionDef { - rivers: vec![river(false, true, 0.42, 3, 1)], - forts: vec![fort((0.76, 0.5), (21, 21), &[3, 2], &[Arrow, Arrow, Arrow], 1, 6, 2, 2, 4, 2000)], - ..base("m05", (96, 80), 505, vec![Goal::Loot(6000)]) + rivers: vec![river(false, true, 0.42, 4, 1)], + islands: vec![island((0.45, 0.8), false, false, 1500)], + forts: vec![fort((0.78, 0.5), (21, 21), &[3, 2], &[Arrow, Arrow, Arrow], 1, 6, 2, 2, 4, 2000)], + ..base("m05", (88, 80), 505, vec![Goal::Loot(6000)]) }); - // ---- Act II: the rivers + v.push(MissionDef { + caves: vec![cave((0.5, 0.22), Some((Ogre, 6)), 800), cave((0.56, 0.8), None, 1500)], + forts: vec![fort((0.82, 0.5), (17, 17), &[3], &[Arrow, Arrow], 1, 5, 1, 2, 1, 1200).shard()], + ..base("m22", (88, 88), 2222, vec![Goal::Freed(1), Goal::Shards]) + }); + // ---- Act II: the rivers and the other masters v.push(MissionDef { rivers: vec![river(false, true, 0.52, 5, 1)], - forts: vec![fort((0.82, 0.5), (21, 19), &[3], &[Arrow, Cannon, Arrow, Cannon], 1, 8, 2, 2, 2, 1500).heart()], - ..base("m06", (96, 80), 606, vec![Goal::Heart]) + forts: vec![fort((0.84, 0.5), (21, 19), &[3], &[Arrow, Cannon, Arrow, Cannon], 1, 8, 2, 2, 2, 1500).heart()], + ..base("m06", (96, 88), 606, vec![Goal::Heart]) }); v.push(MissionDef { - rivers: vec![river(true, true, 0.45, 3, 0)], - forts: vec![fort((0.79, 0.45), (19, 19), &[3], &[Arrow, Cannon], 2, 7, 2, 2, 2, 1800).shard()], + rivers: vec![river(true, true, 0.45, 4, 0)], + forts: vec![fort((0.8, 0.45), (19, 19), &[3], &[Arrow, Cannon], 2, 7, 2, 2, 2, 1800).shard()], ..base("m07", (96, 96), 707, vec![Goal::Shards]) }); + v.push(MissionDef { + rivers: vec![river(false, true, 0.55, 4, 1)], + islands: vec![island((0.4, 0.2), false, false, 2000)], + rivals: vec![(0.8, 0.5, 1, 3)], + ..base("m23", (104, 96), 2323, vec![Goal::Rivals]) + }); v.push(MissionDef { time_limit: 900.0, gift: vec![(Ogre, 3), (Kobold, 3), (Kobold, 3), (Spider, 2)], @@ -178,44 +218,48 @@ pub fn missions() -> Vec { fort((0.6, 0.27), (17, 15), &[3, 2], &[Arrow], 1, 4, 1, 2, 3, 1500), fort((0.78, 0.72), (17, 15), &[3, 0], &[Arrow, Cannon], 1, 4, 1, 2, 3, 1500), ], - ..base("m08", (96, 80), 808, vec![Goal::Loot(8000)]) + ..base("m08", (96, 96), 808, vec![Goal::Loot(8000)]) }); v.push(MissionDef { - rivers: vec![river(false, false, 0.5, 3, 2)], + rivers: vec![river(false, false, 0.5, 4, 2)], forts: vec![ fort((0.76, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon], 2, 7, 2, 2, 1, 1500).shard(), fort((0.76, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon], 2, 7, 2, 2, 1, 1500).shard(), ], ..base("m09", (112, 96), 909, vec![Goal::Shards]) }); + v.push(MissionDef { gold: 4000.0, portals: 2, treasures: 1, waves: true, wave_limit: 6, first_wave: 240.0, gate: true, ..base("m10", (104, 96), 1010, vec![Goal::Survive(6)]) }); + v.push(MissionDef { + islands: vec![island((0.42, 0.25), false, true, 0), island((0.6, 0.75), true, true, 800), island((0.74, 0.35), false, true, 1200)], + forts: vec![fort((0.86, 0.6), (15, 15), &[3], &[Arrow, Cannon], 2, 6, 2, 2, 1, 1200)], + ..base("m24", (112, 104), 2424, vec![Goal::Springs(3)]) + }); + v.push(MissionDef { forts: vec![fort((0.72, 0.5), (25, 25), &[3, 0, 2], &[Arrow, Cannon, Magic, Arrow, Cannon], 2, 8, 4, 3, 2, 2000)], ..base("m11", (112, 104), 1111, vec![Goal::KillAll]) }); v.push(MissionDef { - gold: 4000.0, - portals: 2, - treasures: 1, - waves: true, - wave_limit: 6, - first_wave: 240.0, gate: true, - ..base("m10", (96, 80), 1010, vec![Goal::Survive(6)]) + waves: true, + first_wave: 330.0, + rivers: vec![river(false, false, 0.62, 4, 2)], + rivals: vec![(0.8, 0.3, 1, 2)], + ..base("m25", (120, 104), 2525, vec![Goal::Rivals]) }); v.push(MissionDef { - forts: vec![fort((0.72, 0.5), (25, 25), &[3, 0, 2], &[Arrow, Cannon, Magic, Arrow, Cannon], 2, 8, 4, 3, 2, 2000)], - ..base("m11", (112, 96), 1111, vec![Goal::KillAll]) + rivers: vec![river(false, true, 0.4, 4, 1), river(true, true, 0.62, 4, 1)], + forts: vec![fort((0.86, 0.5), (21, 25), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart().shard()], + ..base("m12", (120, 112), 1212, vec![Goal::Heart, Goal::Shards]) }); + // ---- Act III: the citadel and the masters v.push(MissionDef { - rivers: vec![river(false, true, 0.4, 3, 1), river(true, true, 0.62, 3, 1)], - forts: vec![fort((0.85, 0.5), (21, 25), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart().shard()], - ..base("m12", (112, 96), 1212, vec![Goal::Heart, Goal::Shards]) - }); - // ---- Act III: the citadel - v.push(MissionDef { - rivers: vec![river(false, false, 0.22, 3, 1)], + rivers: vec![river(false, false, 0.22, 4, 1)], forts: vec![fort((0.75, 0.55), (27, 23), &[3, 0], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 2, 12, 3, 3, 4, 3500)], - ..base("m13", (112, 96), 1313, vec![Goal::Loot(13000)]) + ..base("m13", (120, 112), 1313, vec![Goal::Loot(13000)]) }); + v.push(MissionDef { forts: vec![fort((0.7, 0.5), (25, 25), &[3, 1], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic, Arrow, Cannon], 3, 8, 2, 3, 2, 2000)], ..base("m14", (128, 112), 1414, vec![Goal::Towers]) }); v.push(MissionDef { - forts: vec![fort((0.7, 0.5), (25, 25), &[3, 1], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic, Arrow, Cannon], 3, 8, 2, 3, 2, 2000)], - ..base("m14", (112, 112), 1414, vec![Goal::Towers]) + rivers: vec![river(true, true, 0.62, 4, 1)], + islands: vec![island((0.45, 0.78), true, true, 2500)], + rivals: vec![(0.84, 0.5, 2, 3)], + ..base("m26", (128, 120), 2626, vec![Goal::Rivals]) }); v.push(MissionDef { portals: 0, @@ -223,40 +267,61 @@ pub fn missions() -> Vec { gift: vec![(Ogre, 5), (Ogre, 4), (Mage, 5), (Mage, 4), (Spider, 4), (Kobold, 4), (Kobold, 4), (Mage, 3)], rivers: vec![river(false, true, 0.42, 4, 1)], forts: vec![fort((0.74, 0.5), (21, 21), &[3, 0, 2], &[Arrow, Cannon, Magic], 2, 10, 2, 3, 2, 2000)], - ..base("m15", (112, 96), 1515, vec![Goal::KillAll]) + ..base("m15", (128, 120), 1515, vec![Goal::KillAll]) }); v.push(MissionDef { waves: true, first_wave: 360.0, gate_defenders: true, - rivers: vec![river(true, true, 0.55, 3, 2)], - forts: vec![fort((0.82, 0.5), (23, 23), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart()], - ..base("m16", (112, 96), 1616, vec![Goal::Heart]) + rivers: vec![river(true, true, 0.55, 4, 2)], + forts: vec![fort((0.84, 0.5), (23, 23), &[3], &[Arrow, Cannon, Magic, Arrow], 2, 10, 2, 3, 2, 2000).heart()], + ..base("m16", (128, 128), 1616, vec![Goal::Heart]) + }); + v.push(MissionDef { + caves: vec![cave((0.42, 0.2), Some((Chainbreaker, 4)), 1000), cave((0.45, 0.82), Some((Gargoyle, 6)), 1000), cave((0.66, 0.5), None, 3000)], + islands: vec![island((0.62, 0.2), false, true, 0), island((0.66, 0.82), true, true, 0)], + forts: vec![fort((0.86, 0.5), (17, 17), &[3], &[Arrow, Cannon, Magic], 3, 8, 2, 3, 1, 1500)], + ..base("m27", (136, 128), 2727, vec![Goal::Freed(2), Goal::Springs(2)]) }); v.push(MissionDef { portals: 2, - rivers: vec![river(false, true, 0.36, 3, 2), river(false, true, 0.56, 3, 2), river(false, false, 0.5, 3, 2)], + rivers: vec![river(false, true, 0.36, 4, 2), river(false, true, 0.56, 4, 2), river(false, false, 0.5, 4, 2)], forts: vec![ - fort((0.8, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(), - fort((0.8, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(), + fort((0.82, 0.24), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(), + fort((0.82, 0.76), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 2, 8, 2, 3, 2, 2000).shard(), ], - ..base("m17", (128, 112), 1717, vec![Goal::Shards, Goal::Loot(6000)]) + ..base("m17", (136, 136), 1717, vec![Goal::Shards, Goal::Loot(6000)]) + }); + v.push(MissionDef { + portals: 2, + rivers: vec![river(false, true, 0.6, 4, 2)], + islands: vec![island((0.45, 0.5), false, true, 2000)], + rivals: vec![(0.84, 0.25, 1, 2), (0.84, 0.75, 1, 3)], + ..base("m28", (144, 136), 2828, vec![Goal::Rivals]) }); v.push(MissionDef { portals: 2, rivers: vec![river(true, true, 0.5, 4, 1)], - forts: vec![fort((0.82, 0.5), (25, 25), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 3, 14, 3, 4, 3, 2500).heart().boss(Paladin, 5)], - ..base("m18", (128, 112), 1818, vec![Goal::Heart]) + forts: vec![fort((0.84, 0.5), (25, 25), &[3], &[Arrow, Cannon, Magic, Arrow, Cannon, Magic], 3, 14, 3, 4, 3, 2500).heart().boss(Paladin, 5)], + ..base("m18", (144, 144), 1818, vec![Goal::Heart]) }); v.push(MissionDef { portals: 2, - rivers: vec![river(false, true, 0.42, 3, 2), river(true, false, 0.5, 3, 2)], + rivers: vec![river(false, true, 0.42, 4, 2), river(true, false, 0.5, 4, 2)], forts: vec![ fort((0.62, 0.22), (19, 17), &[3, 2], &[Arrow, Cannon, Magic], 3, 8, 2, 4, 1, 2500).shard(), fort((0.86, 0.5), (19, 19), &[3], &[Arrow, Cannon, Magic, Arrow], 3, 10, 2, 4, 1, 2500).shard(), fort((0.62, 0.8), (19, 17), &[3, 0], &[Arrow, Cannon, Magic], 3, 8, 2, 4, 1, 2500).shard(), ], - ..base("m19", (144, 128), 1919, vec![Goal::Shards]) + ..base("m19", (152, 144), 1919, vec![Goal::Shards]) + }); + v.push(MissionDef { + portals: 2, + rivers: vec![river(true, true, 0.62, 4, 1)], + islands: vec![island((0.42, 0.22), true, true, 2500), island((0.45, 0.78), true, true, 2500)], + caves: vec![cave((0.5, 0.5), Some((Wyrm, 5)), 2000)], + forts: vec![fort((0.85, 0.5), (21, 21), &[3], &[Arrow, Cannon, Magic, Magic], 3, 12, 2, 4, 2, 2500).shard()], + ..base("m29", (152, 152), 2929, vec![Goal::Shards, Goal::Springs(2)]) }); v.push(MissionDef { portals: 3, @@ -269,7 +334,19 @@ pub fn missions() -> Vec { .heart() .shard() .boss(Paladin, 10)], - ..base("m20", (144, 144), 2020, vec![Goal::Heart, Goal::Shards]) + ..base("m20", (160, 160), 2020, vec![Goal::Heart, Goal::Shards]) + }); + v.push(MissionDef { + portals: 3, + gold: 6000.0, + gate: true, + waves: true, + first_wave: 420.0, + gate_defenders: true, + rivers: vec![river(false, true, 0.45, 4, 2), river(true, false, 0.5, 4, 2)], + islands: vec![island((0.3, 0.2), false, true, 3000)], + rivals: vec![(0.85, 0.22, 2, 2), (0.85, 0.78, 2, 3)], + ..base("m30", (176, 176), 3030, vec![Goal::Rivals, Goal::Heart]) }); v } @@ -308,6 +385,91 @@ impl FortDef { } } +// keeps the core area, portals, gates and fortresses (rock is carved away) +fn free_for_terrain(t: &Tile) -> bool { + t.owner == Owner::None && !matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate | Kind::Tower | Kind::Shard) +} + +// an island ringed by four tiles of water or lava +fn place_island(g: &mut Game, d: &IslandDef, w: usize, h: usize, seed: u32) { + let (cx, cy) = ((d.at.0 * w as f32) as i32, (d.at.1 * h as f32) as i32); + let outer = d.r + 4; + let mut rng = Rng(seed | 1); + for y in cy - outer..=cy + outer { + for x in cx - outer..=cx + outer { + if x < 2 || y < 2 || x >= w as i32 - 2 || y >= h as i32 - 2 { + continue; + } + let dist = (((x - cx).pow(2) + (y - cy).pow(2)) as f32).sqrt(); + let t = &mut g.map.t[idx(x, y)]; + if !free_for_terrain(t) || dist > outer as f32 + 0.4 { + continue; + } + t.gold = 0; + t.marked = false; + t.reinforced = false; + t.kind = if dist <= d.r as f32 + 0.4 { + Kind::Floor + } else if d.lava { + Kind::Lava + } else { + Kind::Water + }; + } + } + if d.spring { + g.map.t[idx(cx, cy)].kind = Kind::ManaSpring; + } + if d.treasure > 0 { + let spots = [(cx - 1, cy), (cx + 1, cy), (cx, cy - 1), (cx, cy + 1)]; + let per = d.treasure / spots.len() as i32; + for &(x, y) in &spots { + let t = &mut g.map.t[idx(x, y)]; + t.kind = Kind::Treasury; + t.gold = per; + let r = rng.f(); + t.res = if r < 0.15 { 3 } else if r < 0.4 { 2 } else if r < 0.6 { 1 } else { 0 }; + } + } +} + +// a small cave sealed by earth all around, with treasure and/or a creature in chains +fn place_cave(g: &mut Game, d: &CaveDef, w: usize, h: usize) { + let (cx, cy) = ((d.at.0 * w as f32) as i32, (d.at.1 * h as f32) as i32); + for y in cy - 3..=cy + 3 { + for x in cx - 3..=cx + 3 { + if x < 2 || y < 2 || x >= w as i32 - 2 || y >= h as i32 - 2 { + continue; + } + let t = &mut g.map.t[idx(x, y)]; + if !free_for_terrain(t) { + continue; + } + let inside = (x - cx).abs() <= 2 && (y - cy).abs() <= 2; + t.kind = if inside { Kind::Floor } else { Kind::Earth }; + t.gold = 0; + t.marked = false; + t.reinforced = false; + t.hp = 6.0; + } + } + if d.treasure > 0 { + let spots = [(cx - 2, cy - 2), (cx + 2, cy - 2), (cx - 2, cy + 2), (cx + 2, cy + 2)]; + for &(x, y) in &spots { + let t = &mut g.map.t[idx(x, y)]; + t.kind = Kind::Treasury; + t.gold = d.treasure / 4; + t.res = 2; + } + } + if let Some((kind, level)) = d.chained { + let id = g.spawn(kind, false, cx as f32 + 0.5, cy as f32 + 0.5, level); + if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) { + e.chained = true; + } + } +} + // Builds the game of mission i (carry: what the last mission handed over) pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game { let all = missions(); @@ -326,10 +488,22 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game { g.build_fortress(&f.spec(w, h)); shards += f.shard as u32; } + for (k, isl) in d.islands.iter().enumerate() { + place_island(&mut g, isl, w, h, d.seed.wrapping_add(k as u32 * 131)); + } + for c in &d.caves { + place_cave(&mut g, c, w, h); + } + for &(rx, ry, diff, side) in &d.rivals { + let x = ((rx * w as f32) as i32).clamp(10, w as i32 - 10); + let y = ((ry * h as f32) as i32).clamp(10, h as i32 - 10); + g.add_rival(side, x, y, diff); + } let heart = d.gate || d.forts.iter().any(|f| f.heart); if !heart { g.gate_hp = 0.0; } + g.gate_on = heart; g.guard_treasures(); g.place_all_torches(); g.mission = Some(MissionState { @@ -401,8 +575,23 @@ mod tests { assert!(vaults as u32 >= *n, "{}: vaults hold {}", d.key, vaults); } Goal::Survive(_) => assert!(m.waves, "{}", d.key), + Goal::Rivals => assert!(!g.rivals.is_empty(), "{}", d.key), + Goal::Springs(n) => { + let springs = g.map.t.iter().filter(|t| t.kind == Kind::ManaSpring).count(); + assert!(springs as u32 >= *n, "{}: {} springs", d.key, springs); + } + Goal::Freed(n) => { + let chained = g.ents.iter().filter(|e| e.alive && e.chained).count(); + assert!(chained as u32 >= *n, "{}: {} chained", d.key, chained); + } } } + // rival masters stand with an intact core + for r in &g.rivals { + let t = &g.map.t[idx(r.core.0 + 1, r.core.1 + 1)]; + assert!(t.kind == Kind::Core && t.owner == Owner::of_side(r.side), "{}: rival core broken", d.key); + println!(" rival {} at {:?}", r.side, r.core); + } // wave missions: the heroes can reach the core if m.waves { let (gx, gy) = g.map.gate; diff --git a/src/game.rs b/src/game.rs index c36623f..afc2565 100644 --- a/src/game.rs +++ b/src/game.rs @@ -83,6 +83,8 @@ pub struct SfxEvent { pub pos: Option<(f32, f32)>, // world position (for distance-based volume), None = UI } pub const MANA_MAX: f32 = 400.0; +pub const SPRING_MANA: f32 = 0.6; // mana per second of each claimed mana spring +pub const SPRING_MAX: f32 = 150.0; // more mana maximum per claimed spring pub const CARRY_MAX: i32 = 300; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] @@ -241,6 +243,8 @@ pub struct Ent { pub side: u8, // 0 player, 2 and 3 rival masters (heroes: see side()) #[serde(default)] pub target: Option, // attackers: side whose core (or gate) they march on + #[serde(default)] + pub chained: bool, // a lone creature chained in a sealed cave: joins when the cave is claimed } fn yes() -> bool { @@ -295,6 +299,7 @@ impl Ent { pay_t: 0.0, side: 0, target: None, + chained: false, } } pub fn tile(&self) -> usize { @@ -305,7 +310,7 @@ impl Ent { } // the player's creatures (not imps, heroes or the rivals' units) pub fn creature(&self) -> bool { - !self.hero && self.side == 0 && self.kind != EK::Worker + !self.hero && self.side == 0 && self.kind != EK::Worker && !self.chained } // 0 player, 1 humans, 2 and 3 rival masters pub fn side(&self) -> u8 { @@ -334,6 +339,9 @@ pub enum Goal { Loot(u32), // take treasure worth this much from knight vaults Towers, // raze all knight towers Survive(u32), // beat this many waves + Rivals, // destroy the soul cores of all rival masters + Springs(u32), // claim this many mana springs + Freed(u32), // free this many chained creatures } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -424,7 +432,11 @@ pub struct Game { #[serde(default)] pub rivals: Vec, // AI dungeon masters #[serde(default = "yes")] - pub gate_on: bool, // free game: the humans' gate exists // tiles next to fighting heroes: knight ground there cannot be claimed + pub gate_on: bool, // free game: the humans' gate exists + #[serde(skip)] + pub springs: usize, // claimed mana springs (recounted) + #[serde(default)] + pub freed: u32, // chained creatures freed // tiles next to fighting heroes: knight ground there cannot be claimed #[serde(skip)] pub hints: bool, // show hint messages (setting) #[serde(skip, default = "hint_times")] @@ -568,6 +580,8 @@ impl Game { mission: None, rivals: Vec::new(), gate_on: true, + springs: 0, + freed: 0, }; let (cx, cy) = g.map.core; // the soul core starts surrounded by a one tile wide ring of treasury @@ -613,6 +627,9 @@ impl Game { Goal::Loot(n) => self.loot >= n as f32, Goal::Towers => !self.towers.iter().any(|t| t.hero), Goal::Survive(n) => self.wave >= n && !self.ents.iter().any(|e| e.alive && e.active_hero() && !e.guard && e.patrol.is_empty()), + Goal::Rivals => self.rivals.iter().all(|r| !r.alive), + Goal::Springs(n) => self.springs >= n as usize, + Goal::Freed(n) => self.freed >= n, } } @@ -694,6 +711,7 @@ impl Game { pub fn recount(&mut self) { let mut c = [0usize; 8]; + self.springs = self.map.t.iter().filter(|t| t.kind == Kind::ManaSpring && t.owner == Owner::Player).count(); for t in &self.map.t { if t.owner == Owner::Player { if let Some(r) = room_index(t.kind) { @@ -704,6 +722,10 @@ impl Game { self.counts = c; } + pub fn mana_max(&self) -> f32 { + MANA_MAX + SPRING_MAX * self.springs as f32 + } + pub fn gold_cap(&self) -> f32 { CORE_STORE + TREASURY_TILE * self.counts[0] as f32 } @@ -907,7 +929,7 @@ impl Game { fn step(&mut self, dt: f32) { self.time += dt; self.recount(); - self.mana = (self.mana + 1.0 * dt).min(MANA_MAX); + self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max()); self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP); for t in self.map.t.iter_mut() { if t.kind == Kind::Farm { @@ -1532,7 +1554,23 @@ impl Game { self.msg("msg.level_up", vec![format!("@{}", s.name), e.level.to_string()]); } } - if e.hero && e.captive != 0 { + if e.chained { + // waits in chains until the player's imps claim its cave + e.moving = false; + let (x, y) = xy(e.tile()); + let reached = std::iter::once((0, 0)).chain(DIR8.iter().copied()).any(|(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |t| t.owner == Owner::Player)); + if reached { + e.chained = false; + e.settled = false; + e.job = Job::Register; + e.timer = 1.0; + e.happy = 1.0; + self.freed += 1; + self.fx(7, e.x, e.y, 0.0, 0.0, 1.2, 0xff44ddff); + self.sfx(Sfx::Convert, e.x, e.y); + self.msg("msg.creature_freed", vec![format!("@{}", st(e.kind).name)]); + } + } else if e.hero && e.captive != 0 { self.captive_update(e, dt); } else if e.rival() { self.rival_ai(e, dt); @@ -1667,7 +1705,7 @@ impl Game { let mut bd = radius * radius; for o in &self.ents { // every other side is an enemy - if !o.alive || o.held || o.side() == e.side() || o.captive != 0 { + if !o.alive || o.held || o.side() == e.side() || o.captive != 0 || o.chained { continue; } let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2); @@ -2118,6 +2156,11 @@ impl Game { self.sfx(Sfx::Payday, x, y); self.msg("msg.vault_taken", vec![t.gold.to_string()]); } + if t.kind == Kind::ManaSpring { + self.fx(7, x, y, 0.0, 0.0, 1.2, 0xffffe060); + self.sfx(Sfx::Magic, x, y); + self.msg("msg.spring_taken", vec![]); + } if t.kind == Kind::Shard { self.shards_taken += 1; self.fx(7, x, y, 0.0, 0.0, 1.2, 0xff6060ff); @@ -2251,7 +2294,7 @@ impl Game { fn claimable(&self, i: usize, me: u32) -> bool { let t = &self.map.t[i]; let ok = match t.owner { - Owner::None => matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard), + Owner::None => matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring), // knight ground (also vaults and shard chambers), but not while heroes fight there // knight and rival ground (also vaults and shard chambers), but not while enemies fight there Owner::Hero | Owner::Rival1 | Owner::Rival2 => { @@ -2459,15 +2502,17 @@ impl Game { e.working = true; e.timer += dt * self.help_speed(t); // knight ground takes longer - let knight = self.map.t[t].owner == Owner::Hero; - if e.timer >= if knight { 3.0 } else { 1.0 } { + let prev = self.map.t[t].owner; + let knight = prev == Owner::Hero; + if e.timer >= if prev != Owner::None { 3.0 } else { 1.0 } { self.unreserve(me); e.job = Job::None; let (tx, ty) = xy(t); if knight { - self.take_knight_tile(t); self.alarm(tx as f32 + 0.5, ty as f32 + 0.5, e.id); } + // vaults, shards and springs (of the knights, rivals or nobody) + self.take_knight_tile(t); if let (Kind::Portal, Some(pk)) = (self.map.t[t].kind, self.map.portal_at(tx, ty)) { let (px, py) = self.map.portals[pk]; for y in 0..3 { @@ -2827,7 +2872,7 @@ impl Game { } e.working = true; let rate = if e.kind == EK::Mage { 1.2 } else { 0.5 }; - self.mana = (self.mana + rate * dt).min(MANA_MAX); + self.mana = (self.mana + rate * dt).min(self.mana_max()); e.xp += 3.0 * dt; e.timer -= dt; if e.timer <= 0.0 { @@ -2843,7 +2888,7 @@ impl Game { } e.working = true; let rate = if e.kind == EK::Priestess { 1.6 } else { 0.9 }; - self.mana = (self.mana + rate * dt).min(MANA_MAX); + self.mana = (self.mana + rate * dt).min(self.mana_max()); e.happy = (e.happy + 0.01 * dt).min(1.0); e.xp += 1.0 * dt; e.timer -= dt; @@ -3781,7 +3826,7 @@ impl Game { for (j, e) in self.ents.iter().enumerate() { // knight towers shoot the player's units, the player's towers shoot heroes // knight towers shoot everybody but the humans, the player's towers everybody else - let enemy = e.captive == 0 && if t.hero { e.side() != 1 } else { e.side() != 0 }; + let enemy = e.captive == 0 && !e.chained && if t.hero { e.side() != 1 } else { e.side() != 0 }; if !e.alive || e.held || !enemy { continue; } diff --git a/src/gfx.rs b/src/gfx.rs index 86268ea..7a473ca 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -1382,6 +1382,7 @@ impl Gfx { Kind::Shard => T_HEROFLOOR + v, Kind::Aerie => T_AERIE + v, Kind::Steel => T_STEEL + v, + Kind::ManaSpring => T_PORTALFLOOR + v, } } diff --git a/src/main.rs b/src/main.rs index 61ba4b9..1a45448 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,7 +148,7 @@ fn mission_game(i: usize, progress: &campaign::Progress) -> (Game, Cam) { (g, cam) } -const PROGRESS_KEY: &str = "dc.campaign"; +const PROGRESS_KEY: &str = "dc.campaign30"; // new key: the 30-mission order differs from the old one fn store_progress(p: &campaign::Progress) { platform::storage_set(PROGRESS_KEY, &serde_json::to_string(p).unwrap_or_default()); diff --git a/src/map.rs b/src/map.rs index 677f312..0067e11 100644 --- a/src/map.rs +++ b/src/map.rs @@ -59,6 +59,7 @@ pub enum Kind { Diamond, // diamond vein: very rare, very valuable Aerie, // room for flying creatures (their beds) Steel, // steel wall: hardly anyone gets through + ManaSpring, // crystal spring: once claimed, more mana and a higher mana maximum } // options for the map generator (campaign missions) @@ -87,7 +88,7 @@ pub struct River { } // order matters for the packed savegame codec: only append new kinds -const KINDS: [Kind; 25] = [ +const KINDS: [Kind; 26] = [ Kind::Rock, Kind::Earth, Kind::Gold, @@ -113,6 +114,7 @@ const KINDS: [Kind; 25] = [ Kind::Diamond, Kind::Aerie, Kind::Steel, + Kind::ManaSpring, ]; // treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond @@ -207,6 +209,7 @@ impl Tile { | Kind::LavaBridge | Kind::Shard | Kind::Aerie + | Kind::ManaSpring ) } // river tiles (open, but nobody can walk there) diff --git a/src/menu.rs b/src/menu.rs index ea1641a..2625bf4 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1174,5 +1174,8 @@ pub fn goal_text(g: Goal) -> String { Goal::Loot(n) => trf("goal.loot", &[n.to_string()]), Goal::Towers => tr("goal.towers"), Goal::Survive(n) => trf("goal.survive", &[n.to_string()]), + Goal::Rivals => tr("goal.rivals"), + Goal::Springs(n) => trf("goal.springs", &[n.to_string()]), + Goal::Freed(n) => trf("goal.freed", &[n.to_string()]), } } diff --git a/src/props.rs b/src/props.rs index 874f68c..7e20e62 100644 --- a/src/props.rs +++ b/src/props.rs @@ -97,10 +97,32 @@ pub fn draw_props_solid(g: &Game, cam: &Cam, t: f32) { Kind::Temple if (x + z) % 2 == 0 && h % 3 == 0 => candle_stand(c + vec3(((h >> 4) % 5) as f32 * 0.1 - 0.2, 0.0, ((h >> 8) % 5) as f32 * 0.1 - 0.2), l), Kind::Library if z % 2 == 0 => shelf(c, h, l), Kind::Library if h % 3 == 0 => lectern(c, l), + Kind::ManaSpring => spring_basin(c, l), _ => {} } } } + // creatures in chains: an iron post with chains to the creature + for e in g.ents.iter().filter(|e| e.alive && e.chained) { + if (e.x as i32) < x0 || (e.x as i32) > x1 || (e.y as i32) < z0 || (e.y as i32) > z1 { + continue; + } + let post = vec3(e.x.floor() + 0.5, 0.0, e.y.floor() + 0.15); + cube(post + vec3(0.0, 0.35, 0.0), vec3(0.08, 0.7, 0.08), rgb(70, 70, 78, 0.6)); + for n in 0..6 { + let f = n as f32 / 6.0; + let side = if n % 2 == 0 { 0.08 } else { -0.08 }; + let p = post.lerp(vec3(e.x + side, 0.25, e.y), f) + vec3(0.0, 0.3 * (1.0 - f) + 0.05 * (f * 3.14).sin(), 0.0); + cube(p, vec3(0.05, 0.05, 0.05), rgb(120, 120, 130, 0.6)); + } + } +} + +// a stone basin around a mana spring +fn spring_basin(c: Vec3, l: f32) { + for (dx, dz, sx, sz) in [(0.0, -0.38, 0.8, 0.1), (0.0, 0.38, 0.8, 0.1), (-0.38, 0.0, 0.1, 0.8), (0.38, 0.0, 0.1, 0.8)] { + cube(c + vec3(dx, 0.08, dz), vec3(sx, 0.16, sz), rgb(90, 95, 110, l)); + } } pub fn draw_props_glow(g: &Game, cam: &Cam, t: f32) { @@ -161,6 +183,17 @@ pub fn draw_props_glow(g: &Game, cam: &Cam, t: f32) { } } } + Kind::ManaSpring => { + // rising mana: a pulsing pool and motes that drift up + let f = 0.8 + 0.2 * (t * 2.0 + k).sin(); + draw_cube(c + vec3(0.0, 0.1, 0.0), vec3(0.66, 0.04, 0.66), None, Color::new(0.3, 0.8, 1.0, 0.85 * f)); + for n in 0..5 { + let a = n as f32 * 1.26 + k; + let y = ((t * 0.5 + n as f32 * 0.2) % 1.0) * 1.2; + draw_sphere(c + vec3(a.cos() * 0.2, 0.15 + y, a.sin() * 0.2), 0.05 * (1.2 - y), None, Color::new(0.5, 0.9, 1.0, 0.9)); + } + draw_sphere(c + vec3(0.0, 0.4, 0.0), 0.3 * f, None, Color::new(0.3, 0.7, 1.0, 0.15)); + } Kind::Lair if tile.bed != 0 => { // the flame priestess sleeps on embers if let Some(EK::Priestess) = u.beds.get(&tile.bed) { diff --git a/src/render3d.rs b/src/render3d.rs index 972ae9b..af418ed 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1805,6 +1805,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] { Kind::Shard => [255, 60, 120, 255], Kind::Aerie => [170, 140, 90, 255], Kind::Steel => [150, 160, 180, 255], + Kind::ManaSpring => [90, 230, 255, 255], } } diff --git a/src/ui.rs b/src/ui.rs index 2422324..4dbe537 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -452,7 +452,7 @@ impl Ui { } let mut segs: Vec> = vec![ gold_seg, - vec![txt(tr("hud.mana"), MANA), Part::Gap, Part::Bar(g.mana / MANA_MAX, MANA), Part::Gap, txt(format!("{:<3}", g.mana as i32), TXT)], + vec![txt(tr("hud.mana"), MANA), Part::Gap, Part::Bar(g.mana / g.mana_max(), MANA), Part::Gap, txt(format!("{:<3}", g.mana as i32), TXT)], vec![txt(tr("hud.core"), RED), Part::Gap, Part::Bar(g.core_hp / CORE_HP, RED)], vec![txt(tr("hud.creatures"), TXT), Part::Gap, txt(format!("{}/{}", g.creature_count(), cap), TXT)], vec![txt(tr("hud.workers"), TXT), Part::Gap, txt(g.worker_count().to_string(), TXT)], @@ -699,6 +699,9 @@ impl Ui { Goal::Loot(n) => format!(" {}/{}", (g.loot as u32).min(n), n), Goal::Towers => trf("goal.left", &[g.towers.iter().filter(|t| t.hero).count().to_string()]), Goal::Survive(n) => format!(" {}/{}", g.wave.min(n), n), + Goal::Rivals => trf("goal.left", &[g.rivals.iter().filter(|r| r.alive).count().to_string()]), + Goal::Springs(n) => format!(" {}/{}", g.springs.min(n as usize), n), + Goal::Freed(n) => format!(" {}/{}", g.freed.min(n), n), }; let text = format!("{} {}{}", if done { "\u{221a}" } else { "\u{25ba}" }, crate::menu::goal_text(goal), progress); for l in wrap(&text, gcols as usize - 4) {