From bd956a4f57ebe98d840b98738868f0dee8ebb30e Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 11:35:11 +0100 Subject: [PATCH] Master talents: experience, points and four branches (Builder, Warlord, Warlock, Merchant) with a toolbar window Co-Authored-By: Claude Opus 5.5 --- docs/ROADMAP.md | 7 +- lang/cs.lang | 19 +++++ lang/da.lang | 19 +++++ lang/de.lang | 19 +++++ lang/el.lang | 19 +++++ lang/en.lang | 19 +++++ lang/es.lang | 19 +++++ lang/fi.lang | 19 +++++ lang/hr.lang | 19 +++++ lang/hu.lang | 19 +++++ lang/it.lang | 19 +++++ lang/nl.lang | 19 +++++ lang/no.lang | 19 +++++ lang/pt.lang | 19 +++++ lang/ro.lang | 19 +++++ lang/ru.lang | 19 +++++ lang/sk.lang | 19 +++++ lang/sr.lang | 19 +++++ lang/sv.lang | 19 +++++ src/ai.rs | 1 + src/campaign.rs | 8 +- src/commands.rs | 7 +- src/game.rs | 43 +++++++--- src/main.rs | 2 + src/spells.rs | 2 +- src/stats.rs | 2 + src/talents.rs | 220 ++++++++++++++++++++++++++++++++++++++++++++++++ src/ui.rs | 108 +++++++++++++++++++++++- 28 files changed, 720 insertions(+), 22 deletions(-) create mode 100644 src/talents.rs diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d040175..b088ba6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -129,7 +129,10 @@ grid), guard and attack markers (shields and swords), savegames with the game st to the gate; slain, his loot is yours; from wave 3), monk (spells do not touch him, 5), ranger (shoots, lays snares that hurt and stun a creature, 7), inquisitor (fragile, every 20 s turns a nearby kobold or spider to the heroes, 9). In the campaign from missions 5, 9, 12 and 16 on. -4. **Master talents** - Builder, Warlord, Warlock, Merchant (5 levels each), a toolbar button; per game, - carried over in the campaign. +4. **Master talents** (done, v202611-00064, talents.rs) - experience from slain heroes, digging, rooms, walls, + traps, towers, spells and gold; points get dearer (300, 450, 600 ...). Builder (imps dig +10%, rooms -6% per + level), Warlord (creatures +5% damage, -5% damage taken), Warlock (mana +15%, spells -6%), Merchant (wages + -6%, treasure +6%); 5 levels each. The "Master" button (number of free points, pulsing) opens the window. + AI masters spend their points themselves; per game, carried over in the campaign and co-op runs. 5. **Weather above ground** - rain (puts out fire), fog (less sight), storms (lightning in trees and units). 6. **Named creatures** - names per kind and language, epithets at higher levels, memorial stones. diff --git a/lang/cs.lang b/lang/cs.lang index a34d69d..3e346c3 100644 --- a/lang/cs.lang +++ b/lang/cs.lang @@ -1693,3 +1693,22 @@ tut.slap = Křivoprst: Ha! Slyšel jsi to? Něco přichází od východu. Připr tut.obj6 = Poraz dva rytíře. tut.knights = Křivoprst: Dva rytíři! Pošli na ně tvory útočnou vlajkou (Rozkazy) nebo je zasáhni bleskem. tut.win = Křivoprst: Výborně, pane! To je všechno. No - skoro. Zbytek poví kampaň. Přistavím vodu na čaj. + +# master talents +cat.master = Pán +talent.title = Pánovy talenty +talent.points = Volné body talentů: {0} +talent.next = {0}/{1} zkušeností do dalšího bodu +talent.all = Všechny talenty jsou naučené. +talent.builder = Stavitel +talent.warlord = Vojevůdce +talent.warlock = Černokněžník +talent.merchant = Obchodník +talent.builder.desc = Čertíci kopou o {0}% rychleji, místnosti stojí o {1}% méně. +talent.warlord.desc = Tvorové udeří o {0}% silněji a dostávají o {1}% méně poškození. +talent.warlock.desc = Mana proudí o {0}% rychleji, kouzla stojí o {1}% méně. +talent.merchant.desc = Mzdy jsou o {0}% nižší, doručené poklady mají o {1}% vyšší hodnotu. +talent.none = Zatím nenaučeno. +talent.then = Další úroveň: {0} +msg.talent_point = Nový bod talentu! Použij ho v okně pána. +msg.talent_learned = {0}: úroveň {1} naučena. diff --git a/lang/da.lang b/lang/da.lang index 1ea720f..5bd93f7 100644 --- a/lang/da.lang +++ b/lang/da.lang @@ -1692,3 +1692,22 @@ tut.slap = Krumfinger: Ha! Hørte du det? Noget kommer fra øst. Gør dig klar: tut.obj6 = Besejr de to riddere. tut.knights = Krumfinger: To riddere! Send dine væsner af sted med angrebsflaget (Ordrer), eller ram dem med lynet. tut.win = Krumfinger: Godt gået, mester! Det er alt, hvad der skal til. Altså – næsten. Kampagnen fortæller resten. Jeg sætter kedlen over. + +# master talents +cat.master = Mester +talent.title = Mestertalenter +talent.points = Frie talentpoint: {0} +talent.next = {0}/{1} erfaring til næste point +talent.all = Alle talenter er lært. +talent.builder = Bygmester +talent.warlord = Krigsherre +talent.warlock = Heksemester +talent.merchant = Købmand +talent.builder.desc = Imps graver {0}% hurtigere, rum koster {1}% mindre. +talent.warlord.desc = Væsner slår {0}% hårdere og tager {1}% mindre skade. +talent.warlock.desc = Mana flyder {0}% hurtigere, besværgelser koster {1}% mindre. +talent.merchant.desc = Lønninger er {0}% lavere, afleverede skatte er {1}% mere værd. +talent.none = Endnu ikke lært. +talent.then = Næste niveau: {0} +msg.talent_point = Et nyt talentpoint! Brug det i mestervinduet. +msg.talent_learned = {0} niveau {1} lært. diff --git a/lang/de.lang b/lang/de.lang index 123bf1f..30069d1 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -1692,3 +1692,22 @@ tut.slap = Crookfinger: Ha! Hast du das gehört? Aus dem Osten kommt etwas. Mach tut.obj6 = Besiege die beiden Ritter. tut.knights = Crookfinger: Zwei Ritter! Schick deine Kreaturen mit der Angriffsflagge (Befehle) oder triff sie mit dem Blitz. tut.win = Crookfinger: Gut gemacht, Meister! Mehr ist es nicht. Na ja – fast. Den Rest erzählt die Kampagne. Ich setze Wasser auf. + +# master talents +cat.master = Meister +talent.title = Meister-Talente +talent.points = Freie Talentpunkte: {0} +talent.next = {0}/{1} Erfahrung bis zum nächsten Punkt +talent.all = Alle Talente sind gelernt. +talent.builder = Baumeister +talent.warlord = Kriegsherr +talent.warlock = Hexer +talent.merchant = Händler +talent.builder.desc = Imps graben {0}% schneller, Räume kosten {1}% weniger. +talent.warlord.desc = Kreaturen schlagen {0}% härter zu und nehmen {1}% weniger Schaden. +talent.warlock.desc = Mana fließt {0}% schneller, Zauber kosten {1}% weniger. +talent.merchant.desc = Löhne sind {0}% niedriger, gelieferte Schätze sind {1}% mehr wert. +talent.none = Noch nicht gelernt. +talent.then = Nächste Stufe: {0} +msg.talent_point = Ein neuer Talentpunkt! Verteile ihn im Meister-Fenster. +msg.talent_learned = {0} Stufe {1} gelernt. diff --git a/lang/el.lang b/lang/el.lang index 940631a..0cf00c1 100644 --- a/lang/el.lang +++ b/lang/el.lang @@ -1693,3 +1693,22 @@ tut.slap = Στραβοδάχτυλος: Χα! Το άκουσες; Κάτι έ tut.obj6 = Νίκησε τους δύο ιππότες. tut.knights = Στραβοδάχτυλος: Δύο ιππότες! Στείλε τα πλάσματά σου με τη σημαία επίθεσης (Εντολές) ή χτύπησέ τους με κεραυνό. tut.win = Στραβοδάχτυλος: Μπράβο, αφέντη! Αυτό ήταν όλο. Ε - σχεδόν. Τα υπόλοιπα θα τα πει η εκστρατεία. Βάζω νερό για τσάι. + +# master talents +cat.master = Άρχοντας +talent.title = Ταλέντα του άρχοντα +talent.points = Ελεύθεροι πόντοι ταλέντου: {0} +talent.next = {0}/{1} εμπειρία ως τον επόμενο πόντο +talent.all = Όλα τα ταλέντα έχουν μαθευτεί. +talent.builder = Χτίστης +talent.warlord = Πολέμαρχος +talent.warlock = Μαυρομάγος +talent.merchant = Έμπορος +talent.builder.desc = Τα δαιμόνια σκάβουν {0}% πιο γρήγορα, τα δωμάτια κοστίζουν {1}% λιγότερο. +talent.warlord.desc = Τα πλάσματα χτυπούν {0}% πιο δυνατά και δέχονται {1}% λιγότερη ζημιά. +talent.warlock.desc = Η μάνα ρέει {0}% πιο γρήγορα, τα ξόρκια κοστίζουν {1}% λιγότερο. +talent.merchant.desc = Οι μισθοί είναι {0}% χαμηλότεροι, ο παραδομένος θησαυρός αξίζει {1}% περισσότερο. +talent.none = Δεν έχει μαθευτεί ακόμη. +talent.then = Επόμενο επίπεδο: {0} +msg.talent_point = Νέος πόντος ταλέντου! Ξόδεψέ τον στο παράθυρο του άρχοντα. +msg.talent_learned = {0}: μαθεύτηκε το επίπεδο {1}. diff --git a/lang/en.lang b/lang/en.lang index 31cce4b..9c9c616 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -1693,3 +1693,22 @@ tut.slap = Crookfinger: Ha! Did you hear that? Something is coming from the east tut.obj6 = Defeat the two knights. tut.knights = Crookfinger: Two knights! Send your creatures with the attack flag (Orders) or strike them with lightning. tut.win = Crookfinger: Well done, master! That is all there is to it. Well - almost. The campaign tells the rest. I'll put the kettle on. + +# master talents +cat.master = Master +talent.title = Master talents +talent.points = Free talent points: {0} +talent.next = {0}/{1} experience to the next point +talent.all = Every talent is learned. +talent.builder = Builder +talent.warlord = Warlord +talent.warlock = Warlock +talent.merchant = Merchant +talent.builder.desc = Imps dig {0}% faster, rooms cost {1}% less. +talent.warlord.desc = Creatures hit {0}% harder and take {1}% less damage. +talent.warlock.desc = Mana flows {0}% faster, spells cost {1}% less. +talent.merchant.desc = Wages are {0}% lower, delivered treasure is worth {1}% more. +talent.none = Not learned yet. +talent.then = Next level: {0} +msg.talent_point = A new talent point! Spend it in the Master window. +msg.talent_learned = {0} level {1} learned. diff --git a/lang/es.lang b/lang/es.lang index 0d5cf8c..6ff7010 100644 --- a/lang/es.lang +++ b/lang/es.lang @@ -1693,3 +1693,22 @@ tut.slap = Dedotuerto: ¡Ja! ¿Lo has oído? Algo viene del este. Prepárate: el tut.obj6 = Derrota a los dos caballeros. tut.knights = Dedotuerto: ¡Dos caballeros! Envía a tus criaturas con la bandera de ataque (Órdenes) o fulmínalos con el rayo. tut.win = Dedotuerto: ¡Bien hecho, amo! Eso es todo. Bueno, casi. La campaña cuenta el resto. Voy a poner el agua a hervir. + +# master talents +cat.master = Maestro +talent.title = Talentos del maestro +talent.points = Puntos de talento libres: {0} +talent.next = {0}/{1} de experiencia hasta el siguiente punto +talent.all = Todos los talentos están aprendidos. +talent.builder = Constructor +talent.warlord = Caudillo +talent.warlock = Brujo +talent.merchant = Mercader +talent.builder.desc = Los diablillos cavan un {0}% más rápido y las salas cuestan un {1}% menos. +talent.warlord.desc = Las criaturas golpean un {0}% más fuerte y reciben un {1}% menos de daño. +talent.warlock.desc = El maná fluye un {0}% más rápido y los hechizos cuestan un {1}% menos. +talent.merchant.desc = Los sueldos son un {0}% más bajos y el tesoro entregado vale un {1}% más. +talent.none = Aún no aprendido. +talent.then = Siguiente nivel: {0} +msg.talent_point = ¡Un nuevo punto de talento! Gástalo en la ventana del maestro. +msg.talent_learned = {0}: nivel {1} aprendido. diff --git a/lang/fi.lang b/lang/fi.lang index 1de78a6..6825ab9 100644 --- a/lang/fi.lang +++ b/lang/fi.lang @@ -1693,3 +1693,22 @@ tut.slap = Crookfinger: Ha! Kuulitko? Idästä tulee jotain. Valmistaudu: salama tut.obj6 = Kukista kaksi ritaria. tut.knights = Crookfinger: Kaksi ritaria! Lähetä olentosi hyökkäyslipulla (Käskyt) tai iske heitä salamalla. tut.win = Crookfinger: Hyvin tehty, mestari! Siinä kaikki. No - melkein. Kampanja kertoo loput. Panen teeveden kiehumaan. + +# master talents +cat.master = Mestari +talent.title = Mestarin kyvyt +talent.points = Vapaita kykypisteitä: {0} +talent.next = {0}/{1} kokemusta seuraavaan pisteeseen +talent.all = Kaikki kyvyt on opittu. +talent.builder = Rakentaja +talent.warlord = Sotaherra +talent.warlock = Noita +talent.merchant = Kauppias +talent.builder.desc = Pirut kaivavat {0}% nopeammin, huoneet maksavat {1}% vähemmän. +talent.warlord.desc = Olennot lyövät {0}% kovempaa ja ottavat {1}% vähemmän vahinkoa. +talent.warlock.desc = Mana virtaa {0}% nopeammin, loitsut maksavat {1}% vähemmän. +talent.merchant.desc = Palkat ovat {0}% pienemmät, toimitettu aarre on {1}% arvokkaampi. +talent.none = Ei vielä opittu. +talent.then = Seuraava taso: {0} +msg.talent_point = Uusi kykypiste! Käytä se mestarin ikkunassa. +msg.talent_learned = {0}: taso {1} opittu. diff --git a/lang/hr.lang b/lang/hr.lang index cb42757..07cf619 100644 --- a/lang/hr.lang +++ b/lang/hr.lang @@ -1693,3 +1693,22 @@ tut.slap = Krivoprst: Ha! Jesi li čuo? Nešto dolazi s istoka. Pripremi se: mun tut.obj6 = Porazi dva viteza. tut.knights = Krivoprst: Dva viteza! Pošalji stvorenja zastavom napada (Naredbe) ili ih udari munjom. tut.win = Krivoprst: Bravo, gospodaru! To je sve. Pa - skoro. Ostatak će ispričati kampanja. Idem staviti vodu za čaj. + +# master talents +cat.master = Gospodar +talent.title = Gospodarevi talenti +talent.points = Slobodni bodovi talenta: {0} +talent.next = {0}/{1} iskustva do sljedećeg boda +talent.all = Svi talenti su naučeni. +talent.builder = Graditelj +talent.warlord = Vojskovođa +talent.warlock = Vještac +talent.merchant = Trgovac +talent.builder.desc = Vražići kopaju {0}% brže, sobe koštaju {1}% manje. +talent.warlord.desc = Stvorenja udaraju {0}% jače i primaju {1}% manje štete. +talent.warlock.desc = Mana teče {0}% brže, čarolije koštaju {1}% manje. +talent.merchant.desc = Plaće su {0}% niže, dostavljeno blago vrijedi {1}% više. +talent.none = Još nije naučeno. +talent.then = Sljedeća razina: {0} +msg.talent_point = Novi bod talenta! Potroši ga u prozoru gospodara. +msg.talent_learned = {0}: naučena razina {1}. diff --git a/lang/hu.lang b/lang/hu.lang index c988d11..06dbfc7 100644 --- a/lang/hu.lang +++ b/lang/hu.lang @@ -1693,3 +1693,22 @@ tut.slap = Crookfinger: Ha! Hallottad? Valami jön keletről. Készülj: a vill tut.obj6 = Győzd le a két lovagot. tut.knights = Crookfinger: Két lovag! Küldd rájuk a lényeidet a támadózászlóval (Parancs), vagy sújtsd őket villámmal. tut.win = Crookfinger: Szép munka, mester! Ennyi az egész. Nos - majdnem. A többit elmondja a hadjárat. Felteszem a teavizet. + +# master talents +cat.master = Mester +talent.title = Mesteri tehetségek +talent.points = Szabad tehetségpontok: {0} +talent.next = {0}/{1} tapasztalat a következő pontig +talent.all = Minden tehetség megtanulva. +talent.builder = Építész +talent.warlord = Hadúr +talent.warlock = Boszorkánymester +talent.merchant = Kereskedő +talent.builder.desc = A manók {0}%-kal gyorsabban ásnak, a szobák {1}%-kal olcsóbbak. +talent.warlord.desc = A lények {0}%-kal erősebben ütnek, és {1}%-kal kevesebb sebzést kapnak. +talent.warlock.desc = A mana {0}%-kal gyorsabban áramlik, a varázslatok {1}%-kal olcsóbbak. +talent.merchant.desc = A bérek {0}%-kal alacsonyabbak, a leadott kincs {1}%-kal többet ér. +talent.none = Még nincs megtanulva. +talent.then = Következő szint: {0} +msg.talent_point = Új tehetségpont! Költsd el a mester ablakában. +msg.talent_learned = {0}: {1}. szint megtanulva. diff --git a/lang/it.lang b/lang/it.lang index ec9d426..ceba863 100644 --- a/lang/it.lang +++ b/lang/it.lang @@ -1693,3 +1693,22 @@ tut.slap = Ditastorte: Ah! Hai sentito? Qualcosa arriva da est. Preparati: il fu tut.obj6 = Sconfiggi i due cavalieri. tut.knights = Ditastorte: Due cavalieri! Manda le tue creature con la bandiera d'attacco (Ordini) o colpiscili con il fulmine. tut.win = Ditastorte: Ben fatto, padrone! Tutto qui. Be', quasi. Il resto lo racconta la campagna. Metto su il bollitore. + +# master talents +cat.master = Maestro +talent.title = Talenti del maestro +talent.points = Punti talento liberi: {0} +talent.next = {0}/{1} esperienza al prossimo punto +talent.all = Tutti i talenti sono appresi. +talent.builder = Costruttore +talent.warlord = Condottiero +talent.warlock = Stregone +talent.merchant = Mercante +talent.builder.desc = Gli Imp scavano il {0}% più in fretta, le stanze costano il {1}% in meno. +talent.warlord.desc = Le creature colpiscono il {0}% più forte e subiscono il {1}% di danni in meno. +talent.warlock.desc = Il mana scorre il {0}% più in fretta, le magie costano il {1}% in meno. +talent.merchant.desc = I salari sono il {0}% più bassi, il tesoro consegnato vale il {1}% in più. +talent.none = Non ancora appreso. +talent.then = Livello successivo: {0} +msg.talent_point = Un nuovo punto talento! Spendilo nella finestra del maestro. +msg.talent_learned = {0}: livello {1} appreso. diff --git a/lang/nl.lang b/lang/nl.lang index 6aed486..76b312b 100644 --- a/lang/nl.lang +++ b/lang/nl.lang @@ -1693,3 +1693,22 @@ tut.slap = Krommevinger: Ha! Hoorde je dat? Er komt iets uit het oosten. Maak je tut.obj6 = Versla de twee ridders. tut.knights = Krommevinger: Twee ridders! Stuur je wezens met de aanvalsvlag (Bevelen) of tref ze met de bliksem. tut.win = Krommevinger: Goed gedaan, meester! Meer is het niet. Nou ja, bijna. De veldtocht vertelt de rest. Ik zet water op. + +# master talents +cat.master = Meester +talent.title = Meestertalenten +talent.points = Vrije talentpunten: {0} +talent.next = {0}/{1} ervaring tot het volgende punt +talent.all = Alle talenten zijn geleerd. +talent.builder = Bouwmeester +talent.warlord = Krijgsheer +talent.warlock = Heksenmeester +talent.merchant = Koopman +talent.builder.desc = Imps graven {0}% sneller, kamers kosten {1}% minder. +talent.warlord.desc = Wezens slaan {0}% harder en krijgen {1}% minder schade. +talent.warlock.desc = Mana stroomt {0}% sneller, spreuken kosten {1}% minder. +talent.merchant.desc = Lonen zijn {0}% lager, afgeleverde schatten zijn {1}% meer waard. +talent.none = Nog niet geleerd. +talent.then = Volgend niveau: {0} +msg.talent_point = Een nieuw talentpunt! Besteed het in het meestervenster. +msg.talent_learned = {0} niveau {1} geleerd. diff --git a/lang/no.lang b/lang/no.lang index edda26a..e53ac1b 100644 --- a/lang/no.lang +++ b/lang/no.lang @@ -1692,3 +1692,22 @@ tut.slap = Krokfinger: Ha! Hørte du det? Noe kommer fra øst. Gjør deg klar: l tut.obj6 = Beseir de to ridderne. tut.knights = Krokfinger: To riddere! Send skapningene dine med angrepsflagget (Ordrer), eller slå dem med lynet. tut.win = Krokfinger: Godt gjort, mester! Det var det hele. Vel – nesten. Kampanjen forteller resten. Jeg setter på kjelen. + +# master talents +cat.master = Mester +talent.title = Mestertalenter +talent.points = Ledige talentpoeng: {0} +talent.next = {0}/{1} erfaring til neste poeng +talent.all = Alle talenter er lært. +talent.builder = Byggmester +talent.warlord = Krigsherre +talent.warlock = Heksemester +talent.merchant = Kjøpmann +talent.builder.desc = Imper graver {0}% raskere, rom koster {1}% mindre. +talent.warlord.desc = Skapninger slår {0}% hardere og tar {1}% mindre skade. +talent.warlock.desc = Mana flyter {0}% raskere, formler koster {1}% mindre. +talent.merchant.desc = Lønningene er {0}% lavere, levert skatt er verdt {1}% mer. +talent.none = Ikke lært ennå. +talent.then = Neste nivå: {0} +msg.talent_point = Et nytt talentpoeng! Bruk det i mestervinduet. +msg.talent_learned = {0} nivå {1} lært. diff --git a/lang/pt.lang b/lang/pt.lang index aae7188..c56240c 100644 --- a/lang/pt.lang +++ b/lang/pt.lang @@ -1693,3 +1693,22 @@ tut.slap = Dedotorto: Ah! Ouviste aquilo? Vem aí qualquer coisa do leste. Prepa tut.obj6 = Derrota os dois cavaleiros. tut.knights = Dedotorto: Dois cavaleiros! Envia as tuas criaturas com a bandeira de ataque (Ordens) ou atinge-os com o relâmpago. tut.win = Dedotorto: Muito bem, mestre! É tudo. Bem, quase. A campanha conta o resto. Vou pôr a chaleira ao lume. + +# master talents +cat.master = Mestre +talent.title = Talentos do mestre +talent.points = Pontos de talento livres: {0} +talent.next = {0}/{1} de experiência até ao próximo ponto +talent.all = Todos os talentos foram aprendidos. +talent.builder = Construtor +talent.warlord = Senhor da Guerra +talent.warlock = Bruxo +talent.merchant = Mercador +talent.builder.desc = Os diabretes cavam {0}% mais depressa e as salas custam {1}% menos. +talent.warlord.desc = As criaturas batem {0}% mais forte e sofrem {1}% menos dano. +talent.warlock.desc = A mana flui {0}% mais depressa e os feitiços custam {1}% menos. +talent.merchant.desc = Os salários são {0}% mais baixos e o tesouro entregue vale {1}% mais. +talent.none = Ainda não aprendido. +talent.then = Próximo nível: {0} +msg.talent_point = Um novo ponto de talento! Gasta-o na janela do mestre. +msg.talent_learned = {0}: nível {1} aprendido. diff --git a/lang/ro.lang b/lang/ro.lang index d3931d2..7e655ea 100644 --- a/lang/ro.lang +++ b/lang/ro.lang @@ -1693,3 +1693,22 @@ tut.slap = Crookfinger: Ha! Ai auzit? Ceva vine dinspre est. Pregătește-te: fu tut.obj6 = Învinge cei doi cavaleri. tut.knights = Crookfinger: Doi cavaleri! Trimite-ți creaturile cu steagul de atac (Ordine) sau lovește-i cu fulgerul. tut.win = Crookfinger: Bravo, stăpâne! Asta e tot. Ei - aproape. Campania îți spune restul. Pun de un ceai. + +# master talents +cat.master = Stăpân +talent.title = Talentele stăpânului +talent.points = Puncte de talent libere: {0} +talent.next = {0}/{1} experiență până la următorul punct +talent.all = Toate talentele sunt învățate. +talent.builder = Constructor +talent.warlord = Căpetenie +talent.warlock = Vrăjitor +talent.merchant = Negustor +talent.builder.desc = Drăcușorii sapă cu {0}% mai repede, camerele costă cu {1}% mai puțin. +talent.warlord.desc = Creaturile lovesc cu {0}% mai tare și primesc cu {1}% mai puține daune. +talent.warlock.desc = Mana curge cu {0}% mai repede, vrăjile costă cu {1}% mai puțin. +talent.merchant.desc = Simbriile sunt cu {0}% mai mici, comorile livrate valorează cu {1}% mai mult. +talent.none = Încă neînvățat. +talent.then = Nivelul următor: {0} +msg.talent_point = Un nou punct de talent! Folosește-l în fereastra stăpânului. +msg.talent_learned = {0}: nivelul {1} învățat. diff --git a/lang/ru.lang b/lang/ru.lang index 4f684ec..f35c735 100644 --- a/lang/ru.lang +++ b/lang/ru.lang @@ -1693,3 +1693,22 @@ tut.slap = Кривопал: Ха! Слышал? С востока что-то tut.obj6 = Победи двух рыцарей. tut.knights = Кривопал: Два рыцаря! Пошли своих существ флагом атаки (Приказы) или порази их молнией. tut.win = Кривопал: Молодец, хозяин! Вот и всё. Ну — почти. Остальное расскажет кампания. Пойду поставлю чайник. + +# master talents +cat.master = Мастер +talent.title = Таланты мастера +talent.points = Свободные очки талантов: {0} +talent.next = {0}/{1} опыта до следующего очка +talent.all = Все таланты изучены. +talent.builder = Строитель +talent.warlord = Полководец +talent.warlock = Чернокнижник +talent.merchant = Торговец +talent.builder.desc = Бесята копают на {0}% быстрее, комнаты стоят на {1}% меньше. +talent.warlord.desc = Существа бьют на {0}% сильнее и получают на {1}% меньше урона. +talent.warlock.desc = Мана течёт на {0}% быстрее, чары стоят на {1}% меньше. +talent.merchant.desc = Жалованье на {0}% ниже, доставленные сокровища ценятся на {1}% выше. +talent.none = Ещё не изучено. +talent.then = Следующий уровень: {0} +msg.talent_point = Новое очко таланта! Потрать его в окне мастера. +msg.talent_learned = {0}: изучен уровень {1}. diff --git a/lang/sk.lang b/lang/sk.lang index 045f50d..5443e11 100644 --- a/lang/sk.lang +++ b/lang/sk.lang @@ -1693,3 +1693,22 @@ tut.slap = Krivoprst: Ha! Počul si? Niečo prichádza z východu. Priprav sa: b tut.obj6 = Poraz dvoch rytierov. tut.knights = Krivoprst: Dvaja rytieri! Pošli na nich tvory útočnou vlajkou (Rozkazy) alebo ich zasiahni bleskom. tut.win = Krivoprst: Výborne, pane! To je všetko. No - takmer. Zvyšok povie kampaň. Pristavím vodu na čaj. + +# master talents +cat.master = Pán +talent.title = Pánove talenty +talent.points = Voľné body talentov: {0} +talent.next = {0}/{1} skúseností do ďalšieho bodu +talent.all = Všetky talenty sú naučené. +talent.builder = Staviteľ +talent.warlord = Vojvodca +talent.warlock = Černokňažník +talent.merchant = Obchodník +talent.builder.desc = Čertíci kopú o {0}% rýchlejšie, miestnosti stoja o {1}% menej. +talent.warlord.desc = Tvory udierajú o {0}% silnejšie a dostávajú o {1}% menej poškodenia. +talent.warlock.desc = Mana prúdi o {0}% rýchlejšie, kúzla stoja o {1}% menej. +talent.merchant.desc = Mzdy sú o {0}% nižšie, doručené poklady majú o {1}% vyššiu hodnotu. +talent.none = Zatiaľ nenaučené. +talent.then = Ďalšia úroveň: {0} +msg.talent_point = Nový bod talentu! Použi ho v okne pána. +msg.talent_learned = {0}: úroveň {1} naučená. diff --git a/lang/sr.lang b/lang/sr.lang index 6633133..93c18f2 100644 --- a/lang/sr.lang +++ b/lang/sr.lang @@ -1693,3 +1693,22 @@ tut.slap = Кривопрст: Ха! Јеси ли чуо? Нешто долаз tut.obj6 = Порази два витеза. tut.knights = Кривопрст: Два витеза! Пошаљи створења заставом напада (Наредбе) или их удари муњом. tut.win = Кривопрст: Браво, господару! То је све. Па - скоро. Остатак ће испричати кампања. Идем да ставим воду за чај. + +# master talents +cat.master = Господар +talent.title = Господареви таленти +talent.points = Слободни бодови талента: {0} +talent.next = {0}/{1} искуства до следећег бода +talent.all = Сви таленти су научени. +talent.builder = Градитељ +talent.warlord = Војсковођа +talent.warlock = Вештац +talent.merchant = Трговац +talent.builder.desc = Ђаволци копају {0}% брже, собе коштају {1}% мање. +talent.warlord.desc = Створења ударају {0}% јаче и примају {1}% мање штете. +talent.warlock.desc = Мана тече {0}% брже, чини коштају {1}% мање. +talent.merchant.desc = Плате су {0}% ниже, достављено благо вреди {1}% више. +talent.none = Још није научено. +talent.then = Следећи ниво: {0} +msg.talent_point = Нови бод талента! Потроши га у прозору господара. +msg.talent_learned = {0}: научен ниво {1}. diff --git a/lang/sv.lang b/lang/sv.lang index 80aba6c..b4f3bef 100644 --- a/lang/sv.lang +++ b/lang/sv.lang @@ -1693,3 +1693,22 @@ tut.slap = Crookfinger: Ha! Hörde du det? Något kommer från öster. Gör dig tut.obj6 = Besegra de två riddarna. tut.knights = Crookfinger: Två riddare! Skicka dina varelser med anfallsflaggan (Order) eller träffa dem med blixten. tut.win = Crookfinger: Bra gjort, mästare! Det är allt. Nåja – nästan. Kampanjen berättar resten. Jag sätter på tevattnet. + +# master talents +cat.master = Mästare +talent.title = Mästartalanger +talent.points = Lediga talangpoäng: {0} +talent.next = {0}/{1} erfarenhet till nästa poäng +talent.all = Alla talanger är inlärda. +talent.builder = Byggmästare +talent.warlord = Krigsherre +talent.warlock = Häxmästare +talent.merchant = Köpman +talent.builder.desc = Hobar gräver {0}% snabbare, rum kostar {1}% mindre. +talent.warlord.desc = Varelser slår {0}% hårdare och tar {1}% mindre skada. +talent.warlock.desc = Mana flödar {0}% snabbare, besvärjelser kostar {1}% mindre. +talent.merchant.desc = Lönerna är {0}% lägre, levererade skatter är värda {1}% mer. +talent.none = Inte inlärd än. +talent.then = Nästa nivå: {0} +msg.talent_point = En ny talangpoäng! Använd den i mästarfönstret. +msg.talent_learned = {0} nivå {1} inlärd. diff --git a/src/ai.rs b/src/ai.rs index eb094d9..a355b02 100644 --- a/src/ai.rs +++ b/src/ai.rs @@ -54,6 +54,7 @@ impl Game { self.ai_rooms(cx, cy); self.ai_towers(d, cx, cy); self.ai_fight(k, d, side, cx, cy); + self.ai_talents(); } // the tiles an AI master may mark: within MARK_REACH tiles of its own ground diff --git a/src/campaign.rs b/src/campaign.rs index 88bc586..cb6b3af 100644 --- a/src/campaign.rs +++ b/src/campaign.rs @@ -84,6 +84,8 @@ pub struct Carry { pub rune_free: u32, // unbuilt rune forge towers from chests #[serde(default)] pub rune_final: u32, // unbuilt or sold rune forge towers that were already given back once + #[serde(default)] + pub talents: crate::talents::Talents, // the master's talents go along from mission to mission } pub const CARRY_CREATURES: usize = 5; @@ -580,6 +582,7 @@ fn start_mission_def(d: &MissionDef, i: usize, carry: Option<&Carry>, coop: bool g.gold += c.gold; g.rune_free += c.rune_free; g.rune_final += c.rune_final; + g.talents = c.talents.clone(); } for (k, &(kind, level, xp)) in joiners.iter().enumerate() { let a = k as f32 * 0.9; @@ -796,7 +799,7 @@ pub fn carry_from(g: &Game) -> Carry { let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect(); let power = |e: &Ent| e.max_hp * e.atk() / st(e.kind).interval; c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal)); - Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next } + Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next, talents: g.talents.clone() } } // a co-op match of an editor campaign: what every master takes into the next mission - his @@ -810,7 +813,7 @@ pub fn coop_carry_from(g: &Game) -> Vec<(u8, Carry)> { let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && !e.hero && e.side == side && e.kind != EK::Worker && !e.chained && e.captive == 0).collect(); c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal).then(a.id.cmp(&b.id))); let creatures = c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(); - (side, Carry { creatures, gold: (g.gold_of(side).max(0.0) * CARRY_GOLD).floor(), rune_free: 0, rune_final: 0 }) + (side, Carry { creatures, gold: (g.gold_of(side).max(0.0) * CARRY_GOLD).floor(), rune_free: 0, rune_final: 0, talents: g.talents_of(side) }) }) .collect() } @@ -824,6 +827,7 @@ pub fn apply_coop_carry(g: &mut Game, carry: &[(u8, Carry)]) { } g.switch_to(*side); g.gold += c.gold.max(0.0); + g.talents = c.talents.clone(); let (cx, cy) = g.map.core; for (k, &(kind, level, xp)) in c.creatures.iter().take(CARRY_CREATURES).enumerate() { let a = k as f32 * 1.1; diff --git a/src/commands.rs b/src/commands.rs index 6cd3012..2fc645c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -101,6 +101,10 @@ pub enum Act { to: u8, amount: u32, }, + // spend a talent point on a branch (talents.rs) + Talent { + branch: u8, + }, } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] @@ -148,7 +152,7 @@ impl Game { fn mark_command(&mut self, act: Act, side: u8) { let ((x, y), kind, dur) = match act { - Act::SendGold { .. } => return, // nothing to point at on the map + Act::SendGold { .. } | Act::Talent { .. } => return, // nothing to point at on the map Act::Dig(r) => (rect_center(r), MARK_DIG, 4.0), Act::Room { r, .. } => (rect_center(r), MARK_ROOM, 5.0), Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0), @@ -227,6 +231,7 @@ impl Game { Act::CancelOrders => self.cancel_orders(), Act::Unmark { x, y } => self.unmark(x, y), Act::SendGold { to, amount } => self.send_gold(to, amount), + Act::Talent { branch } => self.learn_talent(branch as usize), } } } diff --git a/src/game.rs b/src/game.rs index b395543..6034082 100644 --- a/src/game.rs +++ b/src/game.rs @@ -523,6 +523,8 @@ pub struct MasterState { pub rune_next: u32, pub portal_timer: f32, pub punch_ready: f32, + #[serde(default)] + pub talents: crate::talents::Talents, } #[derive(Serialize, Deserialize)] @@ -640,6 +642,8 @@ pub struct Game { #[serde(default)] pub snares: Vec, // tiles with a ranger's snare (rogues.rs) #[serde(default)] + pub talents: crate::talents::Talents, // of the master being processed (talents.rs) + #[serde(default)] pub guard_marks: Vec, // guard markers (shields) of the master being processed (orders.rs) #[serde(default)] pub attack_marks: Vec, // attack markers (swords) @@ -990,6 +994,7 @@ impl Game { trading: Default::default(), happenings: None, snares: Vec::new(), + talents: Default::default(), guard_marks: Vec::new(), attack_marks: Vec::new(), marks_t: 0.0, @@ -1218,7 +1223,7 @@ impl Game { // what the next payday costs: wages of all creatures plus the training bill pub fn next_payday_cost(&self) -> f32 { - let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum(); + let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum::() * self.talents.wages(); wages + self.training_bill } @@ -1514,6 +1519,7 @@ impl Game { rune_next: self.rune_next, portal_timer: self.portal_timer, punch_ready: self.punch_ready, + talents: self.talents.clone(), } } @@ -1549,6 +1555,7 @@ impl Game { self.rune_next = m.rune_next; self.portal_timer = m.portal_timer; self.punch_ready = m.punch_ready; + self.talents = m.talents; } // make `side` the master being processed (no-op for sides that are no masters) @@ -1734,7 +1741,7 @@ impl Game { // a master's household: mana, core, portals, payday fn economy(&mut self, dt: f32) { self.recount(); - self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max()); + self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * self.talents.mana_rate() * dt).min(self.mana_max()); self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP); let owned = self.portals_owned(); @@ -2081,13 +2088,14 @@ impl Game { self.training_bill = 0.0; let mut n = 0; let mut total = bill; + let merchant = self.talents.wages(); for i in 0..self.ents.len() { if !self.ents[i].alive || !self.ents[i].creature() { continue; } let r = self.rng.f(); let e = &mut self.ents[i]; - let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32); + let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32) * merchant; e.pay_due += w; e.pay_t = r * 55.0; n += 1; @@ -2761,7 +2769,9 @@ impl Game { } fn hit(&mut self, e: &mut Ent, j: usize) { - let dmg = e.atk() * (0.85 + self.rng.f() * 0.3); + // the Warlord talent of the attacker's master + let boost = if !e.hero && Owner::MASTER_SIDES.contains(&e.side) { self.talents_of(e.side).damage() } else { 1.0 }; + let dmg = e.atk() * (0.85 + self.rng.f() * 0.3) * boost; let (tx, ty) = (self.ents[j].x, self.ents[j].y); let s = st(e.kind); // the higher the level, the bigger the attack looks (up to almost twice at Lv 10) @@ -2870,8 +2880,12 @@ impl Game { } let my_id = e.id; e.invis = 0.0; // attacking breaks invisibility + let armour = { + let t = &self.ents[j]; + if !t.hero && Owner::MASTER_SIDES.contains(&t.side) { self.talents_of(t.side).armour() } else { 1.0 } + }; let t = &mut self.ents[j]; - t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg }; + t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg } * armour; t.flash = 0.15; if t.creature() && !matches!(t.job, Job::Attack(_)) { t.job = Job::Attack(my_id); @@ -2946,6 +2960,7 @@ impl Game { } if hero { let b = s.bounty as f32 + if kind == EK::Thief { self.thief_loot(id) } else { 0.0 }; + self.add_master_xp(s.bounty as f32 * crate::talents::HERO_XP); let got = self.add_gold(b); self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff); self.sfx(Sfx::HeroDeath, x, y); @@ -3495,7 +3510,7 @@ impl Game { e.face = sgn(tx as f32 + 0.5 - e.x); e.working = true; let skilled = if crate::personality::knows(e, "dig") { 1.25 } else { 1.0 }; - e.timer += dt * self.help_speed(t) * skilled; + e.timer += dt * self.help_speed(t) * skilled * self.talents.dig_speed(); if e.timer >= 0.45 { e.timer = 0.0; let fxx = (e.x + tx as f32 + 0.5) * 0.5; @@ -3655,7 +3670,7 @@ impl Game { } Job::Deliver => { if self.is_deposit(cur) { - let amount = e.carry as f32; + let amount = e.carry as f32 * self.talents.treasure(); let got = self.add_res_at(amount, e.carry_res, Some(cur)); if got < amount { self.hint(3, "hint.treasury_full"); @@ -4939,7 +4954,7 @@ impl Game { pub fn build_rect(&mut self, room: usize, x0: i32, y0: i32, x1: i32, y1: i32) { let kind = ROOM_KINDS[room]; - let cost = ROOM_COST[room] as f32; + let cost = (ROOM_COST[room] as f32 * self.talents.room_cost()).round(); let mut built = 0; let mut broke = false; for y in y0.min(y1)..=y0.max(y1) { @@ -5693,13 +5708,13 @@ impl Game { } pub fn lightning(&mut self, wx: f32, wy: f32) { - const COST: f32 = 50.0; - if self.mana < COST { + let cost = 50.0 * self.talents.spell_cost(); + if self.mana < cost { self.msg("msg.no_mana_bolt", vec![]); self.sfx_ui(Sfx::Error); return; } - self.mana -= COST; + self.mana -= cost; self.count(crate::stats::SPELLS, 1.0); self.sfx(Sfx::Lightning, wx, wy); self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa); @@ -5720,13 +5735,13 @@ impl Game { } pub fn heal(&mut self, wx: f32, wy: f32) { - const COST: f32 = 60.0; - if self.mana < COST { + let cost = 60.0 * self.talents.spell_cost(); + if self.mana < cost { self.msg("msg.no_mana_heal", vec![]); self.sfx_ui(Sfx::Error); return; } - self.mana -= COST; + self.mana -= cost; self.count(crate::stats::SPELLS, 1.0); self.sfx(Sfx::Heal, wx, wy); let mut fxs = Vec::new(); diff --git a/src/main.rs b/src/main.rs index 33b7e65..220fcff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ mod props; mod research; mod rival; mod rogues; +mod talents; mod render3d; mod rooms; mod save; @@ -1821,6 +1822,7 @@ app.pending_briefing = briefing_of(&campaign, i); } UiAction::OpenMenu => menu.page = Some(Page::Pause), UiAction::SendGold(to, amount) => app.game.queue(Act::SendGold { to, amount }), + UiAction::LearnTalent(branch) => app.game.queue(Act::Talent { branch }), UiAction::Locked => { let locked = ui.last_locked.unwrap_or(usize::MAX); app.game.msg(if app.game.can_research(locked) { "msg.tool_research" } else { "msg.tool_locked" }, vec![]); diff --git a/src/spells.rs b/src/spells.rs index 56f1f49..d9bd28c 100644 --- a/src/spells.rs +++ b/src/spells.rs @@ -92,7 +92,7 @@ impl Game { } pub fn cast(&mut self, spell: Spell, wx: f32, wy: f32) { - let cost = spell.cost(); + let cost = spell.cost() * self.talents.spell_cost(); if self.mana < cost { self.msg("msg.no_mana_cost", vec![(cost as i32).to_string()]); self.sfx_ui(Sfx::Error); diff --git a/src/stats.rs b/src/stats.rs index 5c7f3be..0970e4c 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -48,6 +48,8 @@ impl GameStats { impl Game { // counts for the master at this computer (another master's deeds count on his computer) pub(crate) fn count(&mut self, k: usize, n: f32) { + // every master's deeds bring him experience for his talents + self.add_master_xp(crate::talents::xp_for(k) * n); if self.me_side != self.local_side || self.demo.is_some() { return; } diff --git a/src/talents.rs b/src/talents.rs new file mode 100644 index 0000000..91ae27a --- /dev/null +++ b/src/talents.rs @@ -0,0 +1,220 @@ +// Master talents: a master earns experience (slain heroes, dug tiles, rooms, walls, traps, +// towers, spells, gold) and with it talent points, spent on four branches of five levels each: +// Builder (imps dig faster, rooms cost less), Warlord (creatures hit harder and take less +// damage), Warlock (more mana, cheaper spells), Merchant (lower wages, more gold from treasure). +// Per game in free play and matches; in the campaign they go along from mission to mission. + +use crate::game::*; +use serde::{Deserialize, Serialize}; + +pub const BRANCHES: usize = 4; +pub const BUILDER: usize = 0; +pub const WARLORD: usize = 1; +pub const WARLOCK: usize = 2; +pub const MERCHANT: usize = 3; +pub const MAX_LEVEL: u8 = 5; +pub const NAMES: [&str; BRANCHES] = ["talent.builder", "talent.warlord", "talent.warlock", "talent.merchant"]; + +// experience for the deeds of a master (stats counter -> experience per unit) +pub fn xp_for(counter: usize) -> f32 { + use crate::stats as k; + match counter { + k::DUG => 2.0, + k::ROOMS => 3.0, + k::WALLS => 2.0, + k::TRAPS => 8.0, + k::TOWERS => 25.0, + k::SPELLS => 5.0, + k::SIEGE => 20.0, + k::GOLD => 0.02, + _ => 0.0, + } +} +// experience for a slain hero: a share of his bounty +pub const HERO_XP: f32 = 0.5; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Talents { + pub xp: f32, + pub lv: [u8; BRANCHES], +} + +// experience the k-th talent point needs in all (the points get dearer) +pub fn point_xp(k: u32) -> f32 { + (0..k).map(|i| 300.0 + 150.0 * i as f32).sum() +} + +impl Talents { + pub fn spent(&self) -> u32 { + self.lv.iter().map(|&l| l as u32).sum() + } + // points earned so far (at most enough for every level) + pub fn earned(&self) -> u32 { + let max = BRANCHES as u32 * MAX_LEVEL as u32; + (1..=max).take_while(|&k| self.xp >= point_xp(k)).count() as u32 + } + pub fn free(&self) -> u32 { + self.earned().saturating_sub(self.spent()) + } + // progress towards the next point: (experience into it, experience it needs) + pub fn progress(&self) -> Option<(f32, f32)> { + let k = self.earned(); + if k >= BRANCHES as u32 * MAX_LEVEL as u32 { + return None; + } + let (a, b) = (point_xp(k), point_xp(k + 1)); + Some((self.xp - a, b - a)) + } + pub fn can_learn(&self, branch: usize) -> bool { + branch < BRANCHES && self.lv[branch] < MAX_LEVEL && self.free() > 0 + } + fn level(&self, branch: usize) -> f32 { + self.lv.get(branch).copied().unwrap_or(0) as f32 + } + // Builder: imps dig and claim faster, rooms cost less + pub fn dig_speed(&self) -> f32 { + 1.0 + 0.1 * self.level(BUILDER) + } + pub fn room_cost(&self) -> f32 { + 1.0 - 0.06 * self.level(BUILDER) + } + // Warlord: creatures hit harder and take less damage + pub fn damage(&self) -> f32 { + 1.0 + 0.05 * self.level(WARLORD) + } + pub fn armour(&self) -> f32 { + 1.0 / (1.0 + 0.05 * self.level(WARLORD)) + } + // Warlock: mana flows faster, spells cost less + pub fn mana_rate(&self) -> f32 { + 1.0 + 0.15 * self.level(WARLOCK) + } + pub fn spell_cost(&self) -> f32 { + 1.0 - 0.06 * self.level(WARLOCK) + } + // Merchant: lower wages, more gold from delivered treasure + pub fn wages(&self) -> f32 { + 1.0 - 0.06 * self.level(MERCHANT) + } + pub fn treasure(&self) -> f32 { + 1.0 + 0.06 * self.level(MERCHANT) + } +} + +impl Game { + // the talents of the master on `side` + pub fn talents_of(&self, side: u8) -> Talents { + if side == self.me_side { + self.talents.clone() + } else { + self.masters.iter().find(|m| m.side == side).map(|m| m.talents.clone()).unwrap_or_default() + } + } + + // experience for the master being processed; a new talent point is announced + pub(crate) fn add_master_xp(&mut self, xp: f32) { + if xp <= 0.0 || self.demo.is_some() { + return; + } + let before = self.talents.earned(); + self.talents.xp += xp; + if self.talents.earned() > before && self.me_side == self.local_side { + self.msg("msg.talent_point", vec![]); + self.sfx_ui(Sfx::LevelUp); + } + } + + // the master spends a point on a branch (an order: the same on every computer of a match) + pub fn learn_talent(&mut self, branch: usize) { + if !self.talents.can_learn(branch) { + self.sfx_ui(Sfx::Error); + return; + } + self.talents.lv[branch] += 1; + let lv = self.talents.lv[branch]; + self.msg("msg.talent_learned", vec![format!("@{}", NAMES[branch]), lv.to_string()]); + self.sfx_ui(Sfx::LevelUp); + } + + // an AI master spends his points by himself: the branches in turn, the lowest first + pub(crate) fn ai_talents(&mut self) { + while self.talents.free() > 0 { + let order = [WARLORD, BUILDER, MERCHANT, WARLOCK]; + let Some(&b) = order.iter().filter(|&&b| self.talents.lv[b] < MAX_LEVEL).min_by_key(|&&b| self.talents.lv[b]) else { break }; + self.talents.lv[b] += 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn points_get_dearer_and_levels_are_capped() { + let mut t = Talents::default(); + assert_eq!(t.earned(), 0); + t.xp = point_xp(1); + assert_eq!((t.earned(), t.free()), (1, 1)); + assert!(point_xp(3) - point_xp(2) > point_xp(2) - point_xp(1)); + t.xp = 1e9; + assert_eq!(t.earned(), 20); + t.lv = [5, 5, 5, 5]; + assert_eq!(t.free(), 0); + assert!(!t.can_learn(0) && t.progress().is_none()); + } + + #[test] + fn deeds_bring_points_and_points_bring_talents() { + let mut g = Game::new(9, 64, 64); + let (cx, cy) = g.map.core; + g.mark_rect(cx - 6, cy - 6, cx + 8, cy + 8, true); + for _ in 0..(20 * 60 * 6) { + g.update(0.05); + } + assert!(g.talents.xp > 100.0, "digging gives experience: {}", g.talents.xp); + g.talents.xp = point_xp(2); + let room0 = g.talents.room_cost(); + g.learn_talent(BUILDER); + g.learn_talent(WARLORD); + g.learn_talent(WARLORD); // no third point + assert_eq!(g.talents.lv, [1, 1, 0, 0]); + assert!(g.talents.room_cost() < room0 && g.talents.damage() > 1.0); + } + + #[test] + fn talents_make_rooms_cheaper_and_wages_lower() { + let mut g = Game::new(9, 64, 64); + let (cx, cy) = g.map.core; + for y in cy + 4..cy + 7 { + for x in cx..cx + 3 { + let t = &mut g.map.t[crate::map::idx(x, y)]; + t.kind = crate::map::Kind::Floor; + t.owner = crate::map::Owner::Player; + } + } + g.gold = 10000.0; + let gold0 = g.gold; + g.build_rect(1, cx, cy + 4, cx + 2, cy + 6); + let plain = gold0 - g.gold; + g.sell_rect(cx, cy + 4, cx + 2, cy + 6); + g.talents.lv[BUILDER] = 5; + let gold1 = g.gold; + g.build_rect(1, cx, cy + 4, cx + 2, cy + 6); + let cheap = gold1 - g.gold; + assert!(cheap < plain * 0.8, "{} < {}", cheap, plain); + g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 3); + let wages = g.next_payday_cost(); + g.talents.lv[MERCHANT] = 5; + assert!(g.next_payday_cost() < wages); + } + + #[test] + fn ai_masters_spend_their_points() { + let mut g = Game::new(9, 64, 64); + g.talents.xp = point_xp(6); + g.ai_talents(); + assert_eq!(g.talents.free(), 0); + assert!(g.talents.lv.iter().all(|&l| l >= 1), "{:?}", g.talents.lv); + } +} diff --git a/src/ui.rs b/src/ui.rs index 67c5c37..83be387 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -457,6 +457,8 @@ enum TBtn { Tool(usize), Menu, Overview, + Talents, // the master's talent window + Learn(u8), // spend a point on a branch OvRow(u32), OvScroll(i32), GoldTo, // the next ally to receive gold @@ -478,6 +480,7 @@ pub enum UiAction { ChatSetTeam(bool), ChatSend, SendGold(u8, u32), // gold for an ally of a match (side, amount) + LearnTalent(u8), // spend a talent point on a branch Consumed, } @@ -485,6 +488,7 @@ pub struct Ui { pub tool: Tool, pub show_help: bool, pub show_overview: bool, // list of all creatures + pub show_talents: bool, // the master's talents pub coop_end: Option<(bool, bool, bool)>, // end of an editor co-op mission: (host, a next mission, asked for it) gold_to: usize, // the ally chosen in the creature overview ov_scroll: usize, @@ -547,6 +551,28 @@ pub fn wrap(s: &str, cols: usize) -> Vec { out } +// what a talent branch gives at its level now, and at the next level +fn branch_text(t: &crate::talents::Talents, b: usize) -> String { + use crate::talents::*; + let desc = |lv: u8| { + let l = lv as i32; + let (a, c) = match b { + BUILDER => (10 * l, 6 * l), + WARLORD => (5 * l, 5 * l), + WARLOCK => (15 * l, 6 * l), + _ => (6 * l, 6 * l), + }; + trf(&format!("{}.desc", NAMES[b]), &[a.to_string(), c.to_string()]) + }; + let lv = t.lv[b]; + let now = if lv == 0 { tr("talent.none") } else { desc(lv) }; + if lv < MAX_LEVEL { + format!("{} {}", now, trf("talent.then", &[desc(lv + 1)])) + } else { + now + } +} + #[cfg(test)] mod wrap_tests { #[test] @@ -573,6 +599,7 @@ impl Ui { tool: Tool::Dig, show_help: true, show_overview: false, + show_talents: false, coop_end: None, gold_to: 0, ov_scroll: 0, @@ -664,9 +691,17 @@ impl Ui { TBtn::Overview => { self.open_cat = None; self.show_overview = !self.show_overview; + self.show_talents = false; self.ov_scroll = 0; UiAction::Consumed } + TBtn::Talents => { + self.open_cat = None; + self.show_talents = !self.show_talents; + self.show_overview = false; + UiAction::Consumed + } + TBtn::Learn(b) => UiAction::LearnTalent(b), TBtn::OvScroll(d) => { self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize; UiAction::Consumed @@ -891,7 +926,7 @@ impl Ui { let mut popup_top = tb_y; // categories + menu button; a category with several tools opens a row above // (the same layout for mouse and touch; on the desktop the keys still select tools directly) - let n = CATS.len() + 2; + let n = CATS.len() + 3; let bcols = (((cols - 2) as usize / n) as f32).clamp(4.0, 8.0); let bw = bcols * CW(); let x0 = grid_center(sw, bw * n as f32); @@ -932,10 +967,27 @@ impl Ui { let ol: String = ol.chars().take(maxc).collect(); gfx.text(&ol, bx + (bw - gfx.measure(&ol, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT); self.touch_btns.push((r, TBtn::Overview)); - // menu button + // the master's talents: the frame pulses while points are free let bx = x0 + (CATS.len() + 1) as f32 * bw; let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()); let hovered = !touch_ui() && r.contains(vec2(mx, my)); + let free = g.talents.free(); + let glow = free > 0 && ((now * 3.0) as i64) % 2 == 0; + draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_talents { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) }); + draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if glow { GOLD } else if self.show_talents { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) }); + let isz = 2.0 * CH() - 2.0 * px; + draw_texture_ex(&gfx.atlas, bx + (bw - isz) * 0.5, by + 2.0 * px, WHITE, DrawTextureParams { source: Some(Gfx::cell(I_RUNE)), dest_size: Some(vec2(isz, isz)), ..Default::default() }); + if free > 0 { + gfx.text(&free.to_string(), r.x + 3.0 * px, r.y + 3.0 * px, SMALL(), if glow { WHITE } else { GOLD }); + } + let tl = tr("cat.master"); + let tl: String = tl.chars().take(maxc).collect(); + gfx.text(&tl, bx + (bw - gfx.measure(&tl, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT); + self.touch_btns.push((r, TBtn::Talents)); + // menu button + let bx = x0 + (CATS.len() + 2) as f32 * bw; + let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH()); + let hovered = !touch_ui() && r.contains(vec2(mx, my)); draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) }); draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0)); gfx.glyph('≡', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI); @@ -1204,6 +1256,9 @@ impl Ui { if self.show_overview { self.draw_overview(g, gfx, tb_y); } + if self.show_talents { + self.draw_talents(g, gfx, tb_y); + } // ---------------- Tooltip if let Some(i) = hover_tool { @@ -1232,6 +1287,55 @@ impl Ui { } // all creatures, grouped by kind: level, mood, wage, job, health; a click jumps there + // the master's talents: four branches of five levels, points from experience + fn draw_talents(&mut self, g: &Game, gfx: &Gfx, bottom: f32) { + use crate::talents::*; + let (mx, my) = mouse(); + let t = &g.talents; + let cols = ((sw() / CW()) as i32 - 2).min(48); + let inner = (cols - 4) as usize; + // the lines of each branch: name and level, what it gives now (wrapped) + let rows_of = |b: usize| wrap(&branch_text(t, b), inner.saturating_sub(6)).len() as i32 + 2; + let rows = 5 + (0..BRANCHES).map(rows_of).sum::(); + let x = grid_center(sw(), cols as f32 * CW()).max(0.0); + let y = (bottom - rows as f32 * CH() - CH()).max(CH() * 4.0); + gfx.window_titled(x, y, cols, rows, S(), false, FRAME_HI, BG, &tr("talent.title"), FRAME_HI); + self.rects.push(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH())); + let ix = x + CW() * 2.0; + let mut yy = y + CH(); + // points and the way to the next one + gfx.text(&trf("talent.points", &[t.free().to_string()]), ix, yy, S(), if t.free() > 0 { GOLD } else { TXT }); + yy += CH() * 1.2; + match t.progress() { + Some((have, need)) => { + ascii_bar(gfx, ix, yy, 16, (have / need).clamp(0.0, 1.0), MANA, SMALL()); + gfx.text(&trf("talent.next", &[(have as i32).to_string(), (need as i32).to_string()]), ix + CW() * 17.0, yy, SMALL(), DIM); + } + None => gfx.text(&tr("talent.all"), ix, yy, SMALL(), DIM), + } + yy += CH() * 1.8; + for b in 0..BRANCHES { + let lv = t.lv[b]; + let pips: String = (0..MAX_LEVEL).map(|k| if k < lv { '■' } else { '·' }).collect(); + gfx.text(&format!("{} {}", tr(NAMES[b]), pips), ix, yy, S(), FRAME_HI); + // a button to learn the next level + if t.can_learn(b) { + let r = Rect::new(x + (cols - 7) as f32 * CW(), yy - CH() * 0.2, CW() * 4.0, CH() * 1.6); + let hov = r.contains(vec2(mx, my)); + draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) }); + draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, GOLD); + gfx.text("+", r.x + (r.w - gfx.measure("+", S())) * 0.5, r.y + (r.h - GH * S()) * 0.5, S(), GOLD); + self.touch_btns.push((r, TBtn::Learn(b as u8))); + } + yy += CH() * 1.2; + for l in wrap(&branch_text(t, b), inner.saturating_sub(6)) { + gfx.text(&l, ix + CW() * 2.0, yy, SMALL(), DIM); + yy += CH(); + } + yy += CH() * 0.8; + } + } + fn draw_overview(&mut self, g: &Game, gfx: &Gfx, bottom: f32) { let (sw, _) = (sw(), sh()); let (mx, my) = mouse();