From b29e312e6b037be30ebce68dc908871b0a740851 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 09:01:57 +0100 Subject: [PATCH] Fourth master: dwarves get their own side so the yellow AI works; yellow colours and texts; AI cores use the player's throne and heart in their colour Co-Authored-By: Claude Opus 5.5 --- lang/cs.lang | 2 + lang/da.lang | 2 + lang/de.lang | 2 + lang/el.lang | 2 + lang/en.lang | 2 + lang/es.lang | 2 + lang/fi.lang | 2 + lang/hr.lang | 2 + lang/hu.lang | 2 + lang/it.lang | 2 + lang/nl.lang | 2 + lang/no.lang | 2 + lang/pt.lang | 2 + lang/ro.lang | 2 + lang/ru.lang | 2 + lang/sk.lang | 2 + lang/sr.lang | 2 + lang/sv.lang | 2 + src/ai.rs | 6 ++- src/dwarves.rs | 2 +- src/game.rs | 4 +- src/render3d.rs | 98 +++++++++++++++++++++++++++---------------------- src/rival.rs | 23 +++++++++++- src/save.rs | 6 +++ src/ui.rs | 16 ++++---- 25 files changed, 136 insertions(+), 55 deletions(-) diff --git a/lang/cs.lang b/lang/cs.lang index 58e2c5d..46cbf9f 100644 --- a/lang/cs.lang +++ b/lang/cs.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, zelený mistr, na tebe posílá tvory ({0})! msg.rival2_attack = Lady Nightwell, modrá mistryně, na tebe posílá tvory ({0})! msg.rival1_out = Mossgrim je poražen - jeho jádro duše se roztříštilo! msg.rival2_out = Lady Nightwell je poražena - její jádro duše se roztříštilo! +msg.rival3_attack = Saffrax, žlutý pán, posílá proti tobě {0} tvorů! +msg.rival3_out = Saffrax je poražen - jeho jádro duše se roztříštilo! msg.rival_unit_slain = Padl tvor soupeřícího mistra: {0}! +{1} zlata. msg.wave_on_rival1 = Vlna {0}: hrdinové ({1}) táhnou na zeleného mistra. msg.wave_on_rival2 = Vlna {0}: hrdinové ({1}) táhnou na modrou mistryni. diff --git a/lang/da.lang b/lang/da.lang index 10e493f..d34481f 100644 --- a/lang/da.lang +++ b/lang/da.lang @@ -776,6 +776,8 @@ msg.rival1_attack = Mossgrim, den grønne mester, sender {0} væsner mod dig! msg.rival2_attack = Lady Nightwell, den blå mester, sender {0} væsner mod dig! msg.rival1_out = Mossgrim er besejret - hans sjælekerne er splintret! msg.rival2_out = Lady Nightwell er besejret - hendes sjælekerne er splintret! +msg.rival3_attack = Saffrax, den gule mester, sender {0} væsner mod dig! +msg.rival3_out = Saffrax er besejret - hans sjælekerne er knust! msg.rival_unit_slain = En {0} fra en rivaliserende mester falder! +{1} guld. msg.wave_on_rival1 = Bølge {0}: {1} helte marcherer mod den grønne mester. msg.wave_on_rival2 = Bølge {0}: {1} helte marcherer mod den blå mester. diff --git a/lang/de.lang b/lang/de.lang index 6dfb414..7be0f8b 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -776,6 +776,8 @@ msg.rival1_attack = Mossgrim, der grüne Meister, schickt {0} Kreaturen gegen di 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.rival3_attack = Saffrax, der gelbe Meister, schickt {0} Kreaturen gegen dich! +msg.rival3_out = Saffrax ist besiegt - sein 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. diff --git a/lang/el.lang b/lang/el.lang index 4c794dd..4c4b2d4 100644 --- a/lang/el.lang +++ b/lang/el.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Ο Μόσγκριμ, ο πράσινος άρχοντας, msg.rival2_attack = Η Λαίδη Νάιτγουελ, η γαλάζια άρχοντας, στέλνει {0} πλάσματα εναντίον σου! msg.rival1_out = Ο Μόσγκριμ νικήθηκε - ο πυρήνας ψυχής του θρυμματίστηκε! msg.rival2_out = Η Λαίδη Νάιτγουελ νικήθηκε - ο πυρήνας ψυχής της θρυμματίστηκε! +msg.rival3_attack = Ο Σάφραξ, ο κίτρινος άρχοντας, στέλνει {0} πλάσματα εναντίον σου! +msg.rival3_out = Ο Σάφραξ νικήθηκε - ο πυρήνας ψυχής του θρυμματίστηκε! msg.rival_unit_slain = Πέφτει πλάσμα αντίπαλου άρχοντα: {0}! +{1} χρυσός. msg.wave_on_rival1 = Κύμα {0}: {1} ήρωες βαδίζουν κατά του πράσινου άρχοντα. msg.wave_on_rival2 = Κύμα {0}: {1} ήρωες βαδίζουν κατά της γαλάζιας άρχοντος. diff --git a/lang/en.lang b/lang/en.lang index a1c6e57..b4d2d12 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -777,6 +777,8 @@ 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.rival3_attack = Saffrax, the yellow master, sends {0} creatures against you! +msg.rival3_out = Saffrax is defeated - his 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. diff --git a/lang/es.lang b/lang/es.lang index 18e4d9a..2665b51 100644 --- a/lang/es.lang +++ b/lang/es.lang @@ -769,6 +769,8 @@ msg.rival1_attack = ¡Mossgrim, el señor verde, envía {0} criaturas contra ti! msg.rival2_attack = ¡Lady Nightwell, la señora azul, envía {0} criaturas contra ti! msg.rival1_out = Mossgrim ha sido derrotado: ¡su núcleo de alma se ha hecho añicos! msg.rival2_out = Lady Nightwell ha sido derrotada: ¡su núcleo de alma se ha hecho añicos! +msg.rival3_attack = ¡Saffrax, el amo amarillo, envía {0} criaturas contra ti! +msg.rival3_out = Saffrax ha sido derrotado: ¡su núcleo de alma se ha hecho añicos! msg.rival_unit_slain = ¡Cae un {0} de un señor rival! +{1} de oro. msg.wave_on_rival1 = Oleada {0}: {1} héroes marchan contra el señor verde. msg.wave_on_rival2 = Oleada {0}: {1} héroes marchan contra la señora azul. diff --git a/lang/fi.lang b/lang/fi.lang index 0d9879e..db3de6a 100644 --- a/lang/fi.lang +++ b/lang/fi.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, vihreä mestari, lähettää kimppuusi {0} olentoa msg.rival2_attack = Lady Nightwell, sininen mestari, lähettää kimppuusi {0} olentoa! msg.rival1_out = Mossgrim on voitettu - hänen sieluytimensä on murskattu! msg.rival2_out = Lady Nightwell on voitettu - hänen sieluytimensä on murskattu! +msg.rival3_attack = Saffrax, keltainen mestari, lähettää {0} olentoa sinua vastaan! +msg.rival3_out = Saffrax on lyöty - hänen sieluydin on murskattu! msg.rival_unit_slain = Kilpailevan mestarin olento kaatuu: {0}! +{1} kultaa. msg.wave_on_rival1 = Aalto {0}: {1} sankaria marssii vihreää mestaria vastaan. msg.wave_on_rival2 = Aalto {0}: {1} sankaria marssii sinistä mestaria vastaan. diff --git a/lang/hr.lang b/lang/hr.lang index 832412a..decefd7 100644 --- a/lang/hr.lang +++ b/lang/hr.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, zeleni gospodar, šalje stvorenja na tebe: {0}! msg.rival2_attack = Lady Nightwell, plava gospodarica, šalje stvorenja na tebe: {0}! msg.rival1_out = Mossgrim je poražen - njegova jezgra duše se razbila! msg.rival2_out = Lady Nightwell je poražena - njezina jezgra duše se razbila! +msg.rival3_attack = Saffrax, žuti gospodar, šalje {0} stvorenja na tebe! +msg.rival3_out = Saffrax je poražen - njegova jezgra duše je razbijena! msg.rival_unit_slain = Palo je stvorenje suparničkog gospodara: {0}! +{1} zlata. msg.wave_on_rival1 = Val {0}: junaci ({1}) marširaju na zelenog gospodara. msg.wave_on_rival2 = Val {0}: junaci ({1}) marširaju na plavu gospodaricu. diff --git a/lang/hu.lang b/lang/hu.lang index 01b14bf..0efa6be 100644 --- a/lang/hu.lang +++ b/lang/hu.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, a zöld mester, {0} lényt küld ellened! msg.rival2_attack = Lady Nightwell, a kék mester, {0} lényt küld ellened! msg.rival1_out = Mossgrim legyőzve - a lélekmagja darabokra tört! msg.rival2_out = Lady Nightwell legyőzve - a lélekmagja darabokra tört! +msg.rival3_attack = Saffrax, a sárga mester, {0} lényt küld ellened! +msg.rival3_out = Saffrax legyőzve - a lélekmagja darabokra tört! msg.rival_unit_slain = Elesett egy rivális mester lénye: {0}! +{1} arany. msg.wave_on_rival1 = {0}. hullám: {1} hős vonul a zöld mester ellen. msg.wave_on_rival2 = {0}. hullám: {1} hős vonul a kék mester ellen. diff --git a/lang/it.lang b/lang/it.lang index df8d5de..311d493 100644 --- a/lang/it.lang +++ b/lang/it.lang @@ -769,6 +769,8 @@ msg.rival1_attack = Mossgrim, il maestro verde, ti manda contro {0} creature! msg.rival2_attack = Lady Nightwell, la maestra blu, ti manda contro {0} creature! msg.rival1_out = Mossgrim è sconfitto - il suo nucleo dell'anima è in frantumi! msg.rival2_out = Lady Nightwell è sconfitta - il suo nucleo dell'anima è in frantumi! +msg.rival3_attack = Saffrax, il signore giallo, manda {0} creature contro di te! +msg.rival3_out = Saffrax è sconfitto: il suo nucleo dell'anima è andato in frantumi! msg.rival_unit_slain = Cade una creatura di un maestro rivale: {0}! +{1} oro. msg.wave_on_rival1 = Ondata {0}: {1} eroi marciano sul maestro verde. msg.wave_on_rival2 = Ondata {0}: {1} eroi marciano sulla maestra blu. diff --git a/lang/nl.lang b/lang/nl.lang index 2fac40e..f6b8871 100644 --- a/lang/nl.lang +++ b/lang/nl.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, de groene meester, stuurt {0} wezens op je af! msg.rival2_attack = Lady Nightwell, de blauwe meester, stuurt {0} wezens op je af! msg.rival1_out = Mossgrim is verslagen - zijn zielenkern is verbrijzeld! msg.rival2_out = Lady Nightwell is verslagen - haar zielenkern is verbrijzeld! +msg.rival3_attack = Saffrax, de gele meester, stuurt {0} wezens op je af! +msg.rival3_out = Saffrax is verslagen - zijn zielskern is verbrijzeld! msg.rival_unit_slain = Een {0} van een rivaliserende meester valt! +{1} goud. msg.wave_on_rival1 = Golf {0}: {1} helden trekken op tegen de groene meester. msg.wave_on_rival2 = Golf {0}: {1} helden trekken op tegen de blauwe meester. diff --git a/lang/no.lang b/lang/no.lang index 4958190..965e014 100644 --- a/lang/no.lang +++ b/lang/no.lang @@ -776,6 +776,8 @@ msg.rival1_attack = Mossgrim, den grønne mesteren, sender {0} skapninger mot de msg.rival2_attack = Lady Nightwell, den blå mesteren, sender {0} skapninger mot deg! msg.rival1_out = Mossgrim er beseiret - sjelekjernen hans er knust! msg.rival2_out = Lady Nightwell er beseiret - sjelekjernen hennes er knust! +msg.rival3_attack = Saffrax, den gule mesteren, sender {0} vesener mot deg! +msg.rival3_out = Saffrax er beseiret - sjelekjernen hans er knust! msg.rival_unit_slain = En {0} fra en rivaliserende mester faller! +{1} gull. msg.wave_on_rival1 = Bølge {0}: {1} helter marsjerer mot den grønne mesteren. msg.wave_on_rival2 = Bølge {0}: {1} helter marsjerer mot den blå mesteren. diff --git a/lang/pt.lang b/lang/pt.lang index 3168e9c..ebd8735 100644 --- a/lang/pt.lang +++ b/lang/pt.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, o mestre verde, manda {0} criaturas contra ti! msg.rival2_attack = Lady Nightwell, a mestra azul, manda {0} criaturas contra ti! msg.rival1_out = Mossgrim foi derrotado - o núcleo de alma dele despedaçou-se! msg.rival2_out = Lady Nightwell foi derrotada - o núcleo de alma dela despedaçou-se! +msg.rival3_attack = Saffrax, o mestre amarelo, envia {0} criaturas contra ti! +msg.rival3_out = Saffrax foi derrotado - o seu núcleo de alma estilhaçou-se! msg.rival_unit_slain = Cai um {0} de um mestre rival! +{1} de ouro. msg.wave_on_rival1 = Vaga {0}: {1} heróis marcham contra o mestre verde. msg.wave_on_rival2 = Vaga {0}: {1} heróis marcham contra a mestra azul. diff --git a/lang/ro.lang b/lang/ro.lang index 6a97e1a..f310322 100644 --- a/lang/ro.lang +++ b/lang/ro.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, stăpânul verde, trimite {0} creaturi împotriva msg.rival2_attack = Lady Nightwell, stăpâna albastră, trimite {0} creaturi împotriva ta! msg.rival1_out = Mossgrim e învins - miezul sufletului său s-a sfărâmat! msg.rival2_out = Lady Nightwell e învinsă - miezul sufletului ei s-a sfărâmat! +msg.rival3_attack = Saffrax, stăpânul galben, trimite {0} creaturi împotriva ta! +msg.rival3_out = Saffrax este învins - nucleul său de suflet s-a sfărâmat! msg.rival_unit_slain = Un {0} al unui stăpân rival cade! +{1} aur. msg.wave_on_rival1 = Valul {0}: {1} eroi mărșăluiesc asupra stăpânului verde. msg.wave_on_rival2 = Valul {0}: {1} eroi mărșăluiesc asupra stăpânei albastre. diff --git a/lang/ru.lang b/lang/ru.lang index 1673fea..b0ce3b7 100644 --- a/lang/ru.lang +++ b/lang/ru.lang @@ -769,6 +769,8 @@ msg.rival1_attack = Моссгрим, зелёный хозяин, шлёт на msg.rival2_attack = Леди Найтвелл, синяя хозяйка, шлёт на тебя {0} существ! msg.rival1_out = Моссгрим повержен - его ядро души разбито! msg.rival2_out = Леди Найтвелл повержена - её ядро души разбито! +msg.rival3_attack = Саффракс, жёлтый хозяин, шлёт на тебя {0} существ! +msg.rival3_out = Саффракс повержен - его ядро души разбито! msg.rival_unit_slain = Пал боец соперника: {0}! +{1} золота. msg.wave_on_rival1 = Волна {0}: {1} героев идут на зелёного хозяина. msg.wave_on_rival2 = Волна {0}: {1} героев идут на синюю хозяйку. diff --git a/lang/sk.lang b/lang/sk.lang index fe9f8be..7ec4e7e 100644 --- a/lang/sk.lang +++ b/lang/sk.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, zelený majster, na teba posiela tvory ({0})! msg.rival2_attack = Lady Nightwell, modrá majsterka, na teba posiela tvory ({0})! msg.rival1_out = Mossgrim je porazený - jeho jadro duše sa roztrieštilo! msg.rival2_out = Lady Nightwell je porazená - jej jadro duše sa roztrieštilo! +msg.rival3_attack = Saffrax, žltý pán, posiela proti tebe {0} tvorov! +msg.rival3_out = Saffrax je porazený - jeho jadro duše sa roztrieštilo! msg.rival_unit_slain = Padol tvor súperiaceho majstra: {0}! +{1} zlata. msg.wave_on_rival1 = Vlna {0}: hrdinovia ({1}) tiahnu na zeleného majstra. msg.wave_on_rival2 = Vlna {0}: hrdinovia ({1}) tiahnu na modrú majsterku. diff --git a/lang/sr.lang b/lang/sr.lang index e08b6b9..89b385d 100644 --- a/lang/sr.lang +++ b/lang/sr.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Мосгрим, зелени господар, шаље ст msg.rival2_attack = Лејди Најтвел, плава господарица, шаље створења на тебе: {0}! msg.rival1_out = Мосгрим је поражен - његово језгро душе се разбило! msg.rival2_out = Лејди Најтвел је поражена - њено језгро душе се разбило! +msg.rival3_attack = Сафракс, жути господар, шаље {0} створења на тебе! +msg.rival3_out = Сафракс је поражен - његово језгро душе је разбијено! msg.rival_unit_slain = Пало је створење супарничког господара: {0}! +{1} злата. msg.wave_on_rival1 = Талас {0}: хероји ({1}) марширају на зеленог господара. msg.wave_on_rival2 = Талас {0}: хероји ({1}) марширају на плаву господарицу. diff --git a/lang/sv.lang b/lang/sv.lang index 1516286..2aab736 100644 --- a/lang/sv.lang +++ b/lang/sv.lang @@ -777,6 +777,8 @@ msg.rival1_attack = Mossgrim, den gröna mästaren, skickar {0} varelser mot dig msg.rival2_attack = Lady Nightwell, den blå mästaren, skickar {0} varelser mot dig! msg.rival1_out = Mossgrim är besegrad - hans själskärna har krossats! msg.rival2_out = Lady Nightwell är besegrad - hennes själskärna har krossats! +msg.rival3_attack = Saffrax, den gula mästaren, skickar {0} varelser mot dig! +msg.rival3_out = Saffrax är besegrad - hans själskärna har krossats! msg.rival_unit_slain = En {0} hos en rivaliserande mästare faller! +{1} guld. msg.wave_on_rival1 = Våg {0}: {1} hjältar marscherar mot den gröna mästaren. msg.wave_on_rival2 = Våg {0}: {1} hjältar marscherar mot den blå mästaren. diff --git a/src/ai.rs b/src/ai.rs index e2a9583..99d2938 100644 --- a/src/ai.rs +++ b/src/ai.rs @@ -296,7 +296,11 @@ impl Game { self.rivals[k].attacking = true; self.rivals[k].army0 = army; if t == self.local_side { - let key = if side == 2 { "msg.rival1_attack" } else { "msg.rival2_attack" }; + let key = match side { + 2 => "msg.rival1_attack", + 3 => "msg.rival2_attack", + _ => "msg.rival3_attack", + }; let n = self.creature_count().to_string(); self.tell_local(key, vec![n], Some(Sfx::WaveAlarm)); } diff --git a/src/dwarves.rs b/src/dwarves.rs index 3ad8c5a..d636c5b 100644 --- a/src/dwarves.rs +++ b/src/dwarves.rs @@ -7,7 +7,7 @@ use crate::game::*; use crate::map::*; use serde::{Deserialize, Serialize}; -pub const DWARF_SIDE: u8 = 4; +pub const DWARF_SIDE: u8 = 6; // its own side (4 is the fourth master) const LEASH: f32 = 7.0; // dwarves do not chase enemies further from the cave centre const CHEST_CHANCE: f32 = 0.3; diff --git a/src/game.rs b/src/game.rs index 5e5d79b..0062127 100644 --- a/src/game.rs +++ b/src/game.rs @@ -299,7 +299,7 @@ pub struct Ent { #[serde(default)] pub pay_t: f32, // seconds until the creature goes for its wage #[serde(default)] - pub side: u8, // 0 player, 2 and 3 rival masters (heroes: see side()) + pub side: u8, // 0 player, 2..=4 the other masters, 6 dwarves (heroes: see side()) #[serde(default)] pub target: Option, // attackers: side whose core (or gate) they march on #[serde(default)] @@ -409,7 +409,7 @@ impl Ent { } // a unit of a rival master pub fn rival(&self) -> bool { - !self.hero && (self.side == 2 || self.side == 3) + !self.hero && (2..=4).contains(&self.side) } // a dwarf of a dwarf cave (neutral, never an opponent for victory) pub fn dwarf(&self) -> bool { diff --git a/src/render3d.rs b/src/render3d.rs index 5ea5794..35f08b8 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1336,7 +1336,7 @@ impl R3d { } for rv in g.rivals.iter().filter(|r| r.alive) { let (rx, rz) = rv.centre(); - let c = if rv.side == 2 { vec3(0.3, 1.0, 0.4) } else { vec3(0.35, 0.55, 1.0) }; + let c = master_rgb(rv.side); push(vec3(rx, 1.4, rz), 7.0, c, 1.1, 50.0); } // glowing lava near the camera (every third tile) @@ -2319,10 +2319,8 @@ fn ring(c: Vec3, r: f32, color: Color) { } } -fn draw_landmarks_solid(g: &Game, t: f32) { - // soul core: an obsidian throne whose four curved spikes hold the crystal heart in chains - let (cx, cy) = g.map.core; - let c = vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5); +// the obsidian throne of a soul core (centre c on the floor) +fn draw_throne(c: Vec3, t: f32) { let obs = |k: u8| Color::from_rgba(28 + k, 14 + k / 2, 20 + k / 2, 255); draw_cube(c + vec3(0.0, 0.08, 0.0), vec3(2.9, 0.16, 2.9), None, obs(6)); draw_cube(c + vec3(0.0, 0.22, 0.0), vec3(2.3, 0.14, 2.3), None, obs(14)); @@ -2352,15 +2350,16 @@ fn draw_landmarks_solid(g: &Game, t: f32) { line3(p0, p1, if q % 2 == 0 { 0.06 } else { 0.035 }, iron); } } - // rival masters' cores: obsidian pedestal (the orb glows in the glow pass) +} + +fn draw_landmarks_solid(g: &Game, t: f32) { + // soul core: an obsidian throne whose four curved spikes hold the crystal heart in chains + let (cx, cy) = g.map.core; + draw_throne(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), t); + // the other masters' cores: the same throne (their hearts glow in their colours) for rv in g.rivals.iter().filter(|r| r.alive && g.map.shown(idx(r.core.0 + 1, r.core.1 + 1))) { let (rx, rz) = rv.centre(); - let c = vec3(rx, 0.0, rz); - draw_cube(c + vec3(0.0, 0.08, 0.0), vec3(2.6, 0.16, 2.6), None, Color::from_rgba(24, 22, 30, 255)); - draw_cube(c + vec3(0.0, 0.22, 0.0), vec3(1.8, 0.14, 1.8), None, Color::from_rgba(34, 32, 42, 255)); - for (dx, dz) in [(-1.1f32, -1.1f32), (1.1, -1.1), (1.1, 1.1), (-1.1, 1.1)] { - draw_cube(c + vec3(dx, 0.5, dz), vec3(0.22, 1.0, 0.22), None, Color::from_rgba(30, 28, 38, 255)); - } + draw_throne(vec3(rx, 0.0, rz), t + rv.side as f32); } // portals: a demon gate - an arch of stone blocks with bone ribs and horns on a stepped // platform (the vortex glows in the glow pass); grey and dead until claimed @@ -2447,46 +2446,64 @@ fn draw_landmarks_solid(g: &Game, t: f32) { let _ = t; } -fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) { - // soul core: a faceted crystal heart beating lub-dub (faster when hurt, cracked when badly - // hurt), glowing veins across the floor, rising embers - let (cx, cy) = g.map.core; - let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0); +// the colour of a master side (the first master red, rivals green, blue and yellow) +pub fn master_rgb(side: u8) -> Vec3 { + match side { + 2 => vec3(0.3, 0.95, 0.4), + 3 => vec3(0.35, 0.55, 1.0), + 4 => vec3(1.0, 0.85, 0.25), + _ => vec3(1.0, 0.2, 0.2), + } +} + +// recolours a red-tinted colour into `tint`: keeps its brightness and whiteness, swaps the hue +fn hue_to(tint: Option, c: Color) -> Color { + let Some(t) = tint else { return c }; + let hi = c.r.max(c.g).max(c.b); + let lo = c.r.min(c.g).min(c.b); + let m = t.x.max(t.y).max(t.z).max(0.001); + let k = (hi - lo) / m; + Color::new(lo + t.x * k, lo + t.y * k, lo + t.z * k, c.a) +} + +// a soul core's crystal heart beating lub-dub (faster when hurt, cracked when badly hurt), +// glowing veins across the floor, rising embers; `tint`: another master's colour for the red +fn draw_heart(base: Vec3, hp: f32, t: f32, cam_r: Vec3, tint: Option) { + let tc = |r: f32, g: f32, b: f32, a: f32| hue_to(tint, Color::new(r, g, b, a)); let rate = 0.9 + (1.0 - hp) * 1.6; let ph = (t * rate).fract(); let beat = (-((ph - 0.02) / 0.06).powi(2)).exp() + 0.65 * (-((ph - 0.2) / 0.06).powi(2)).exp(); let pulse = 1.0 + beat * 0.09; - let base = vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5); let hc = base + vec3(0.0, 1.55 + (t * 1.1).sin() * 0.03, 0.0); let right = vec3(cam_r.x, 0.0, cam_r.z).normalize_or_zero(); let up = Vec3::Y; let eye = EYE.with(|e| e.get()); let front = vec3(eye.x, 0.0, eye.z).normalize_or_zero(); // towards the viewer - let red = Color::new(0.38 + 0.38 * hp + 0.2 * beat, 0.02 + 0.04 * beat, 0.07, 1.0); - let hot = Color::new(1.0, 0.35 + 0.25 * beat, 0.3, 0.7); + let red = tc(0.38 + 0.38 * hp + 0.2 * beat, 0.02 + 0.04 * beat, 0.07, 1.0); + let hot = tc(1.0, 0.35 + 0.25 * beat, 0.3, 0.7); // two lobes and the point below, then the facets as bright chips let s0 = 0.36 * pulse; for sgn in [-1.0f32, 1.0] { draw_sphere(hc + right * (sgn * 0.21 * pulse) + up * (0.12 * pulse), s0 * 0.85, None, red); } for (k, (y, r)) in [(-0.05f32, 0.36f32), (-0.22, 0.28), (-0.36, 0.18), (-0.46, 0.09)].iter().enumerate() { - draw_sphere(hc + up * (y * pulse), r * pulse, None, if k == 0 { red } else { Color::new(red.r * 0.9, red.g, red.b, 1.0) }); + draw_sphere(hc + up * (y * pulse), r * pulse, None, if k == 0 { red } else { tc(red.r * 0.9, red.g, red.b, 1.0) }); } - draw_sphere(hc + right * (-0.12 * pulse) + up * (0.2 * pulse), 0.1 * pulse, None, Color::new(1.0, 0.75, 0.7, 0.75)); + draw_sphere(hc + right * (-0.12 * pulse) + up * (0.2 * pulse), 0.1 * pulse, None, tc(1.0, 0.75, 0.7, 0.75)); for k in 0..7 { let a = k as f32 * 0.9 + 0.4; let p = hc + right * (a.cos() * 0.22 * pulse) + up * (a.sin() * 0.2 * pulse - 0.05) + front * 0.3; - draw_cube(p, Vec3::splat(0.05), None, Color::new(1.0, 0.5 + 0.3 * ((t * 3.0 + k as f32).sin() * 0.5 + 0.5), 0.45, 0.8)); + draw_cube(p, Vec3::splat(0.05), None, tc(1.0, 0.5 + 0.3 * ((t * 3.0 + k as f32).sin() * 0.5 + 0.5), 0.45, 0.8)); } - draw_sphere(hc, 0.75 * pulse, None, Color::new(1.0, 0.15, 0.15, 0.16 + 0.12 * beat)); - draw_sphere(hc, 1.1 * pulse, None, Color::new(1.0, 0.1, 0.1, 0.06 + 0.05 * beat)); + draw_sphere(hc, 0.75 * pulse, None, tc(1.0, 0.15, 0.15, 0.16 + 0.12 * beat)); + draw_sphere(hc, 1.1 * pulse, None, tc(1.0, 0.1, 0.1, 0.06 + 0.05 * beat)); // cracks when hurt let cracks = ((1.0 - hp) * 7.0) as usize; for k in 0..cracks.min(6) { let a = k as f32 * 2.3; let p0 = hc + right * (a.cos() * 0.08) + up * (a.sin() * 0.1); let p1 = p0 + right * (a.cos() * 0.25) + up * ((a * 1.7).sin() * 0.22); - line3(p0 + front * 0.37, p1 + front * 0.33, 0.025, Color::new(0.08, 0.0, 0.02, 1.0)); + line3(p0 + front * 0.37, p1 + front * 0.33, 0.025, tc(0.08, 0.0, 0.02, 1.0)); } // veins across the floor, a pulse running outwards with every beat for k in 0..8 { @@ -2497,7 +2514,7 @@ fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) { let wig = ((k * 7 + q * 3) as f32).sin() * 0.18; let next = base + vec3(a.cos() * (1.3 + f * 2.2) - a.sin() * wig, 0.02, a.sin() * (1.3 + f * 2.2) + a.cos() * wig); let wave = (-((f - ph * 1.6) / 0.18).powi(2)).exp(); - line3(prev, next, 0.05, Color::new(1.0, 0.15 + 0.3 * wave, 0.1, (0.25 + 0.7 * wave) * (1.0 - f * 0.5) * (0.4 + 0.6 * hp))); + line3(prev, next, 0.05, tc(1.0, 0.15 + 0.3 * wave, 0.1, (0.25 + 0.7 * wave) * (1.0 - f * 0.5) * (0.4 + 0.6 * hp))); prev = next; } } @@ -2506,22 +2523,20 @@ fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) { let e = (t * 0.35 + k as f32 * 0.137).fract(); let a = k as f32 * 2.4; let p = hc + vec3(a.cos() * (0.2 + e * 0.5), -0.2 + e * 2.0, a.sin() * (0.2 + e * 0.5)); - draw_cube(p, Vec3::splat(0.04 * (1.0 - e) + 0.01), None, Color::new(1.0, 0.45 + 0.4 * (1.0 - e), 0.15, 1.0 - e)); + draw_cube(p, Vec3::splat(0.04 * (1.0 - e) + 0.01), None, tc(1.0, 0.45 + 0.4 * (1.0 - e), 0.15, 1.0 - e)); } let _ = hot; - // rival masters' cores in their colours +} + +fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) { + let (cx, cy) = g.map.core; + let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0); + draw_heart(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), hp, t, cam_r, None); + // the other masters' hearts: the same heart in their colours for rv in g.rivals.iter().filter(|r| r.alive && g.map.shown(idx(r.core.0 + 1, r.core.1 + 1))) { let (rx, rz) = rv.centre(); let hp = (rv.hp / crate::rival::RIVAL_CORE_HP).clamp(0.0, 1.0); - let col = if rv.side == 2 { vec3(0.3, 0.95, 0.4) } else { vec3(0.35, 0.55, 1.0) }; - let pulse = 1.0 + (t * (2.0 + (1.0 - hp) * 6.0) + rv.side as f32).sin() * 0.06; - let c = vec3(rx, 1.3 + (t * 1.2).sin() * 0.05, rz); - draw_sphere(c, 0.55 * pulse, None, Color::new(col.x * (0.4 + 0.6 * hp), col.y * (0.4 + 0.6 * hp), col.z * (0.4 + 0.6 * hp), 1.0)); - draw_sphere(c, 0.85 * pulse, None, Color::new(col.x, col.y, col.z, 0.22)); - for k in 0..4 { - let a = t * 0.8 + k as f32 * 1.57; - draw_cube(c + vec3(a.cos() * 0.9, (t + k as f32).sin() * 0.2, a.sin() * 0.9), vec3(0.08, 0.14, 0.04), None, Color::new(col.x, col.y, col.z, 0.8)); - } + draw_heart(vec3(rx, 0.0, rz), hp, t + rv.side as f32 * 0.37, cam_r, Some(master_rgb(rv.side))); } // portals: runes on the arch and a vertical vortex inside it, sparks pouring out and falling for (pk, &(px, py)) in g.map.portals.iter().enumerate() { @@ -2604,11 +2619,8 @@ fn draw_shadows(g: &Game) { } else if e.dwarf() { Color::new(0.85, 0.55, 0.25, 0.85) } else if e.rival() { - if e.side == 2 { - Color::new(0.35, 0.95, 0.4, 0.85) - } else { - Color::new(0.4, 0.6, 1.0, 0.85) - } + let c = master_rgb(e.side); + Color::new(c.x, c.y, c.z, 0.85) } else if e.guard { Color::new(1.0, 0.85, 0.3, 0.9) } else if e.hero { diff --git a/src/rival.rs b/src/rival.rs index 0489a05..4784a7b 100644 --- a/src/rival.rs +++ b/src/rival.rs @@ -206,8 +206,10 @@ impl Game { "msg.partner_out" } else if side == 2 { "msg.rival1_out" - } else { + } else if side == 3 { "msg.rival2_out" + } else { + "msg.rival3_out" }; self.msg(key, vec![]); // its creatures scatter and vanish, its ground is free @@ -447,6 +449,25 @@ mod tests { assert!(g.torches.iter().any(|t| g.map.t[t.wall].owner == owner || g.map.t[t.wall].solid()), "torches"); } + #[test] + fn the_fourth_master_works_too_and_is_no_dwarf() { + let mut g = (1..200u32).map(|s| Game::new_free(s, 96, 96, 1)).find(|g| g.rivals.len() == 1 && !g.gate_on).unwrap(); + let far = |g: &Game, (x, y): (i32, i32)| { + let d = |(a, b): (i32, i32)| (a - x).pow(2) + (b - y).pow(2); + d(g.map.core).min(d(g.rivals[0].core)) + }; + let spot = [(14, 14), (80, 14), (14, 80), (80, 80)].into_iter().max_by_key(|&p| far(&g, p)).unwrap(); + g.add_rival(4, spot.0, spot.1, 1); + let owner = Owner::of_side(4); + let ground0 = g.map.t.iter().filter(|t| t.owner == owner).count(); + for _ in 0..(6 * 60 * 20) { + g.update(0.05); + } + assert!(g.ents.iter().any(|e| e.alive && e.side == 4 && e.rival() && !e.dwarf()), "its units belong to a master"); + let ground = g.map.t.iter().filter(|t| t.owner == owner).count(); + assert!(ground > ground0 + 20, "the yellow master digs and claims: {} -> {}", ground0, ground); + } + #[test] fn ai_dig_marks_carry_the_masters_side() { let mut g = rival_game(1); diff --git a/src/save.rs b/src/save.rs index 282c22f..2674434 100644 --- a/src/save.rs +++ b/src/save.rs @@ -52,6 +52,12 @@ pub fn from_bytes(data: &[u8]) -> Result { return Err("save.err_version"); } let mut game = s.game; + // older saves kept the dwarves on side 4, which is now the fourth master + if !game.is_master(4) && !game.rivals.iter().any(|r| r.side == 4) { + for e in game.ents.iter_mut().filter(|e| !e.hero && e.side == 4) { + e.side = crate::dwarves::DWARF_SIDE; + } + } game.map.activate(); game.recount(); Ok(Loaded { game, camera: s.camera }) diff --git a/src/ui.rs b/src/ui.rs index 14fac58..54d1f34 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -765,8 +765,13 @@ impl Ui { } // the soul cores of the rival masters for rv in g.rivals.iter().filter(|r| r.alive) { - let col = if rv.side == 2 { Color::new(0.4, 0.95, 0.45, 1.0) } else { Color::new(0.45, 0.65, 1.0, 1.0) }; - let key = if rv.side == 2 { "hud.rival1" } else { "hud.rival2" }; + let c = crate::render3d::master_rgb(rv.side); + let col = Color::new(c.x, c.y, c.z, 1.0); + let key = match rv.side { + 2 => "hud.rival1", + 3 => "hud.rival2", + _ => "net.side4", + }; segs.insert(3, vec![txt(tr(key), col), Part::Gap, Part::Bar(rv.hp / crate::rival::RIVAL_CORE_HP, col)]); } if g.waves_on() { @@ -995,11 +1000,8 @@ impl Ui { } else if e.dwarf() { Color::new(0.85, 0.55, 0.25, 1.0) } else if e.rival() { - if e.side == 2 { - Color::new(0.3, 0.95, 0.4, 1.0) - } else { - Color::new(0.4, 0.6, 1.0, 1.0) - } + let c = crate::render3d::master_rgb(e.side); + Color::new(c.x, c.y, c.z, 1.0) } else if e.kind == EK::Worker { Color::new(1.0, 0.6, 0.3, 1.0) } else { Color::new(1.0, 0.25, 0.25, 1.0) }; draw_rectangle((mrx + e.disp_x.floor() * ms).floor(), (mry + e.disp_y.floor() * ms).floor(), dot, dot, c); }