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 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 14:36:45 +01:00
1 parent dd16c684e5
commit ea0623fbe3
11 files changed
+413 -85

No files matched your search

+33 -8
View File
@@ -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.
+33 -8
View File
@@ -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.
+243 -54
View File
@@ -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<River>,
pub forts: Vec<FortDef>,
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<IslandDef>,
pub caves: Vec<CaveDef>,
}
// 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<Goal>) ->
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<MissionDef> {
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<MissionDef> {
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<MissionDef> {
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<MissionDef> {
.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;
+57 -12
View File
@@ -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<u8>, // 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<crate::rival::Rival>, // 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;
}
+1
View File
@@ -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,
}
}
+1 -1
View File
@@ -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());
+4 -1
View File
@@ -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)
+3
View File
@@ -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()]),
}
}
+33
View File
@@ -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) {
+1
View File
@@ -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],
}
}
+4 -1
View File
@@ -452,7 +452,7 @@ impl Ui {
}
let mut segs: Vec<Vec<Part>> = 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) {