From 4c9fbe47e49d5f65c5c9731f0a371e14438308f5 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Fri, 6 Nov 2026 00:18:47 +0100 Subject: [PATCH] Traders come through the portal and sell rune plans, rare creatures, wood and mana Co-Authored-By: Claude Opus 5.5 --- docs/ROADMAP.md | 4 +- lang/cs.lang | 11 ++++ lang/da.lang | 11 ++++ lang/de.lang | 11 ++++ lang/el.lang | 11 ++++ lang/en.lang | 11 ++++ lang/es.lang | 11 ++++ lang/fi.lang | 11 ++++ lang/hr.lang | 11 ++++ lang/hu.lang | 11 ++++ lang/it.lang | 11 ++++ lang/nl.lang | 11 ++++ lang/no.lang | 11 ++++ lang/pt.lang | 11 ++++ lang/ro.lang | 11 ++++ lang/ru.lang | 11 ++++ lang/sk.lang | 11 ++++ lang/sr.lang | 11 ++++ lang/sv.lang | 11 ++++ src/game.rs | 13 +++- src/gfx.rs | 16 ++++- src/main.rs | 1 + src/traders.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++++++ src/ui.rs | 7 ++ 24 files changed, 406 insertions(+), 3 deletions(-) create mode 100644 src/traders.rs diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7b418c2..87b6370 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -78,7 +78,9 @@ login; the menu page *Achievements*), room levels 2 and 3 (researched in the lib banks, a new gold vein; every 8-12 minutes in free games and versus matches, switch in the game settings; the host's choice goes to every client with `"events"` in the create message) and wild beasts (`src/beasts.rs`: dens of cave bears and giant beetles in the caverns of bigger random maps; neutral like the dwarves; beaten while -a prison has room, a beast gives in and joins). +a prison has room, a beast gives in and joins), traders (`src/traders.rs`: every ~10 minutes in single player a +merchant waits a minute at an own portal; a click with the hand buys rune plans, an ember wyrm, a chainbreaker, +wood or a mana potion) and seasons (snow above ground in winter, pumpkins at Halloween, from the real date). 1. **Random events** – earthquake (tunnels cave in), lava eruption, flood, gold vein found. Rare (every 8–12 minutes), free play and multiplayer only, can be switched off in the menu; in diff --git a/lang/cs.lang b/lang/cs.lang index d275695..58497f4 100644 --- a/lang/cs.lang +++ b/lang/cs.lang @@ -332,6 +332,16 @@ unit.priestess = Plamenná kněžka msg.hero_captured = Omráčen a zajat: {0}! Kořist: {1} zlata. msg.defected = Přeběhl na tvou stranu: {0}! msg.beast_tamed = Divoký {0} se vzdává a přidává se k tvé kobce! +msg.trader_arrives = Tvým portálem přišel obchodník: {0} za {1} zlata. Klikni na něj rukou a kup – čeká minutu. +msg.trader_sold = Koupeno: {0}. Bylo mi potěšením, říká obchodník. +msg.trader_left = Obchodník pokrčí rameny a odejde portálem. +trader.info = Nabízí {0} za {1} zlata (klikni rukou) +trader.left = Odejde za {0} s +trader.rune = plány věže runové kovárny +trader.wyrm = žhavého draka +trader.chainbreaker = lamače řetězů +trader.wood = 30 dřeva +trader.mana = lektvar, který doplní tvou manu msg.prisoner_died = Zemřel na mučidlech: {0}. job.carry = Nese zajatce job.pray = Modlí se @@ -1040,6 +1050,7 @@ tool.altar.desc = Polož tvora na oltář a obětuj ho. Určité dvojice obětí unit.revenant = Navrátilec unit.cavebear = Jeskynní medvěd unit.beetle = Obří brouk +unit.trader = Obchodník job.guard_post = Na stráži job.spar = Zápasí msg.revenant_rises = Ze hřbitova vstává navrátilec a slouží ti! diff --git a/lang/da.lang b/lang/da.lang index 4cba200..583bd08 100644 --- a/lang/da.lang +++ b/lang/da.lang @@ -332,6 +332,16 @@ unit.priestess = Flammepræstinde msg.hero_captured = {0} slået bevidstløs og taget til fange! Bytte: {1} guld. msg.defected = {0} er gået over til din side! msg.beast_tamed = En vild {0} overgiver sig og slutter sig til dit fangehul! +msg.trader_arrives = En handelsmand kom gennem din portal: {0} for {1} guld. Klik på ham med hånden for at købe – han venter et minut. +msg.trader_sold = Købt: {0}. En fornøjelse, siger handelsmanden. +msg.trader_left = Handelsmanden trækker på skuldrene og går gennem portalen. +trader.info = Tilbyder {0} for {1} guld (klik med hånden) +trader.left = Går om {0} s +trader.rune = tegningerne til et runesmedjetårn +trader.wyrm = en glødlindorm +trader.chainbreaker = en kædebryder +trader.wood = 30 træ +trader.mana = en drik, der fylder din mana msg.prisoner_died = {0} døde under torturen. job.carry = Bærer en fange job.pray = Beder @@ -1039,6 +1049,7 @@ tool.altar.desc = Slip et væsen på alteret for at ofre det. Visse par af ofre unit.revenant = Genfærd unit.cavebear = Hulebjørn unit.beetle = Kæmpebille +unit.trader = Handelsmand job.guard_post = På vagt job.spar = Sparrer msg.revenant_rises = Et genfærd rejser sig fra kirkegården og tjener dig! diff --git a/lang/de.lang b/lang/de.lang index 98d72ee..574df00 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -331,6 +331,16 @@ unit.priestess = Flammenpriesterin msg.hero_captured = {0} bewusstlos geschlagen und gefangen genommen! Beute: {1} Gold. msg.defected = {0} ist zu dir übergelaufen! msg.beast_tamed = Ein wilder {0} ergibt sich und schließt sich deinem Kerker an! +msg.trader_arrives = Ein Händler kam durch dein Portal: {0} für {1} Gold. Klick ihn mit der Hand an, um zu kaufen – er wartet eine Minute. +msg.trader_sold = Gekauft: {0}. Ein Vergnügen, sagt der Händler. +msg.trader_left = Der Händler zuckt mit den Schultern und geht durchs Portal. +trader.info = Bietet {0} für {1} Gold (mit der Hand anklicken) +trader.left = Geht in {0} s +trader.rune = die Pläne eines Runenschmiede-Turms +trader.wyrm = einen Glutwurm +trader.chainbreaker = einen Kettenbrecher +trader.wood = 30 Holz +trader.mana = einen Trank, der dein Mana füllt msg.prisoner_died = {0} ist unter der Folter gestorben. job.carry = Trägt einen Gefangenen job.pray = Betet @@ -1039,6 +1049,7 @@ tool.altar.desc = Lass eine Kreatur auf den Altar fallen, um sie zu opfern. Best unit.revenant = Wiedergänger unit.cavebear = Höhlenbär unit.beetle = Riesenkäfer +unit.trader = Händler job.guard_post = Hält Wache job.spar = Übungskampf msg.revenant_rises = Ein Wiedergänger erhebt sich vom Friedhof und dient dir! diff --git a/lang/el.lang b/lang/el.lang index 0eb4b91..afc8aab 100644 --- a/lang/el.lang +++ b/lang/el.lang @@ -332,6 +332,16 @@ unit.priestess = Ιέρεια της Φλόγας msg.hero_captured = Αναίσθητος και αιχμάλωτος: {0}! Λάφυρα: {1} χρυσός. msg.defected = Αυτομόλησε στο πλευρό σου: {0}! msg.beast_tamed = Ένα άγριο ον ({0}) παραδίνεται και μπαίνει στο μπουντρούμι σου! +msg.trader_arrives = Ένας έμπορος ήρθε από την πύλη σου: {0} για {1} χρυσό. Κάνε κλικ πάνω του με το χέρι για να αγοράσεις – περιμένει ένα λεπτό. +msg.trader_sold = Αγοράστηκε: {0}. Ήταν χαρά μου, λέει ο έμπορος. +msg.trader_left = Ο έμπορος σηκώνει τους ώμους και φεύγει από την πύλη. +trader.info = Προσφέρει {0} για {1} χρυσό (κλικ με το χέρι) +trader.left = Φεύγει σε {0} δ +trader.rune = τα σχέδια ενός πύργου ρουνικού σιδηρουργείου +trader.wyrm = έναν δράκο της θράκας +trader.chainbreaker = έναν σπάστη αλυσίδων +trader.wood = 30 ξύλο +trader.mana = ένα φίλτρο που γεμίζει το μάνα σου msg.prisoner_died = Πέθανε στα βασανιστήρια: {0}. job.carry = Μεταφέρει αιχμάλωτο job.pray = Προσεύχεται @@ -1040,6 +1050,7 @@ tool.altar.desc = Άφησε ένα πλάσμα στον βωμό για να unit.revenant = Βρικόλακας unit.cavebear = Αρκούδα σπηλαίων unit.beetle = Γιγάντιο σκαθάρι +unit.trader = Έμπορος job.guard_post = Σε φρουρά job.spar = Προπονείται msg.revenant_rises = Ένας βρικόλακας σηκώνεται από το νεκροταφείο και σε υπηρετεί! diff --git a/lang/en.lang b/lang/en.lang index af97191..3d934e6 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -332,6 +332,16 @@ unit.priestess = Flame Priestess msg.hero_captured = {0} knocked out and taken prisoner! Loot: {1} gold. msg.defected = {0} has defected to your side! msg.beast_tamed = A wild {0} gives in and joins your dungeon! +msg.trader_arrives = A trader came through your portal: {0} for {1} gold. Click him with the hand to buy - he waits a minute. +msg.trader_sold = Bought: {0}. Pleasure doing business, says the trader. +msg.trader_left = The trader shrugs and leaves through the portal. +trader.info = Offers {0} for {1} gold (click with the hand) +trader.left = Leaves in {0} s +trader.rune = the plans of a rune forge tower +trader.wyrm = an ember wyrm +trader.chainbreaker = a chainbreaker +trader.wood = 30 wood +trader.mana = a potion that fills your mana msg.prisoner_died = {0} died under torture. job.carry = Carrying a prisoner job.pray = Praying @@ -1040,6 +1050,7 @@ tool.altar.desc = Drop a creature on the altar to sacrifice it. Certain pairs of unit.revenant = Revenant unit.cavebear = Cave bear unit.beetle = Giant beetle +unit.trader = Trader job.guard_post = On guard job.spar = Sparring msg.revenant_rises = A revenant rises from the graveyard and serves you! diff --git a/lang/es.lang b/lang/es.lang index e88dac0..481fd59 100644 --- a/lang/es.lang +++ b/lang/es.lang @@ -332,6 +332,16 @@ unit.priestess = Sacerdotisa de llamas msg.hero_captured = ¡{0} noqueado y hecho prisionero! Botín: {1} de oro. msg.defected = ¡{0} se ha pasado a tu bando! msg.beast_tamed = ¡Un {0} salvaje se rinde y se une a tu mazmorra! +msg.trader_arrives = Un mercader ha cruzado tu portal: {0} por {1} de oro. Haz clic en él con la mano para comprar; espera un minuto. +msg.trader_sold = Comprado: {0}. Un placer, dice el mercader. +msg.trader_left = El mercader se encoge de hombros y se marcha por el portal. +trader.info = Ofrece {0} por {1} de oro (clic con la mano) +trader.left = Se va en {0} s +trader.rune = los planos de una torre de forja rúnica +trader.wyrm = una sierpe de brasas +trader.chainbreaker = un rompecadenas +trader.wood = 30 de madera +trader.mana = una poción que llena tu maná msg.prisoner_died = {0} ha muerto bajo tortura. job.carry = Llevando a un prisionero job.pray = Rezando @@ -1032,6 +1042,7 @@ tool.altar.desc = Suelta una criatura sobre el altar para sacrificarla. Ciertas unit.revenant = Aparecido unit.cavebear = Oso cavernario unit.beetle = Escarabajo gigante +unit.trader = Mercader job.guard_post = De guardia job.spar = Combatiendo msg.revenant_rises = ¡Un aparecido se levanta del cementerio y te sirve! diff --git a/lang/fi.lang b/lang/fi.lang index a549c7c..876ca56 100644 --- a/lang/fi.lang +++ b/lang/fi.lang @@ -332,6 +332,16 @@ unit.priestess = Liekkipapitar msg.hero_captured = {0} tyrmätty ja otettu vangiksi! Saalis: {1} kultaa. msg.defected = {0} on loikannut puolellesi! msg.beast_tamed = Villi {0} antautuu ja liittyy luolastoosi! +msg.trader_arrives = Kauppias tuli portaalistasi: {0} hintaan {1} kultaa. Napsauta häntä kädellä ostaaksesi – hän odottaa minuutin. +msg.trader_sold = Ostettu: {0}. Ilo tehdä kauppaa, sanoo kauppias. +msg.trader_left = Kauppias kohauttaa olkapäitään ja lähtee portaalin kautta. +trader.info = Tarjoaa: {0} hintaan {1} kultaa (napsauta kädellä) +trader.left = Lähtee {0} s kuluttua +trader.rune = riimupajatornin piirustukset +trader.wyrm = hehkulohikäärme +trader.chainbreaker = ketjunkatkoja +trader.wood = 30 puuta +trader.mana = juoma, joka täyttää manasi msg.prisoner_died = {0} kuoli kidutuksessa. job.carry = Kantaa vankia job.pray = Rukoilee @@ -1040,6 +1050,7 @@ tool.altar.desc = Pudota olento alttarille uhrataksesi sen. Tietyt uhriparit tuo unit.revenant = Haamu unit.cavebear = Luolakarhu unit.beetle = Jättikuoriainen +unit.trader = Kauppias job.guard_post = Vartiossa job.spar = Ottelee msg.revenant_rises = Haamu nousee hautausmaalta ja palvelee sinua! diff --git a/lang/hr.lang b/lang/hr.lang index 1e8973a..d28ddcd 100644 --- a/lang/hr.lang +++ b/lang/hr.lang @@ -332,6 +332,16 @@ unit.priestess = Svećenica plamena msg.hero_captured = Onesviješten i zarobljen: {0}! Plijen: {1} zlata. msg.defected = Prebjegao je na tvoju stranu: {0}! msg.beast_tamed = Divlji {0} se predaje i pridružuje tvojoj tamnici! +msg.trader_arrives = Kroz tvoj portal došao je trgovac: {0} za {1} zlata. Klikni ga rukom da kupiš – čeka minutu. +msg.trader_sold = Kupljeno: {0}. Zadovoljstvo, kaže trgovac. +msg.trader_left = Trgovac sliježe ramenima i odlazi kroz portal. +trader.info = Nudi {0} za {1} zlata (klikni rukom) +trader.left = Odlazi za {0} s +trader.rune = nacrte tornja runske kovačnice +trader.wyrm = žarnog zmaja +trader.chainbreaker = lomitelja lanaca +trader.wood = 30 drva +trader.mana = napitak koji puni tvoju manu msg.prisoner_died = Umro je pod mučenjem: {0}. job.carry = Nosi zarobljenika job.pray = Moli se @@ -1040,6 +1050,7 @@ tool.altar.desc = Spusti stvorenje na žrtvenik da ga žrtvuješ. Određeni paro unit.revenant = Povratnik unit.cavebear = Špiljski medvjed unit.beetle = Divovski kukac +unit.trader = Trgovac job.guard_post = Na straži job.spar = Spara se msg.revenant_rises = Povratnik ustaje iz groblja i služi ti! diff --git a/lang/hu.lang b/lang/hu.lang index bf9ea21..9153e7d 100644 --- a/lang/hu.lang +++ b/lang/hu.lang @@ -332,6 +332,16 @@ unit.priestess = Lángpapnő msg.hero_captured = Kiütve és foglyul ejtve: {0}! Zsákmány: {1} arany. msg.defected = Átállt hozzád: {0}! msg.beast_tamed = Egy vad {0} megadja magát, és csatlakozik a kazamatádhoz! +msg.trader_arrives = Kereskedő érkezett a portálodon át: {0} {1} aranyért. Kattints rá a kézzel a vásárláshoz – egy percig vár. +msg.trader_sold = Megvetted: {0}. Öröm volt, mondja a kereskedő. +msg.trader_left = A kereskedő vállat von, és távozik a portálon át. +trader.info = Kínál: {0} {1} aranyért (kattints a kézzel) +trader.left = {0} mp múlva távozik +trader.rune = egy rúnakovács-torony tervrajzát +trader.wyrm = egy parázssárkányt +trader.chainbreaker = egy láncszaggatót +trader.wood = 30 fát +trader.mana = egy italt, amely feltölti a manádat msg.prisoner_died = Belehalt a kínzásba: {0}. job.carry = Foglyot cipel job.pray = Imádkozik @@ -1040,6 +1050,7 @@ tool.altar.desc = Dobj egy lényt az oltárra, hogy feláldozd. Bizonyos áldoza unit.revenant = Visszajáró unit.cavebear = Barlangi medve unit.beetle = Óriásbogár +unit.trader = Kereskedő job.guard_post = Őrt áll job.spar = Edz msg.revenant_rises = Egy visszajáró kel ki a temetőből, és téged szolgál! diff --git a/lang/it.lang b/lang/it.lang index d70c2df..1fe90b2 100644 --- a/lang/it.lang +++ b/lang/it.lang @@ -332,6 +332,16 @@ unit.priestess = Sacerdotessa della Fiamma msg.hero_captured = {0} stordito e fatto prigioniero! Bottino: {1} oro. msg.defected = {0} è passato dalla tua parte! msg.beast_tamed = Un {0} selvatico si arrende e si unisce al tuo dungeon! +msg.trader_arrives = Un mercante è arrivato dal tuo portale: {0} per {1} oro. Cliccalo con la mano per comprare: aspetta un minuto. +msg.trader_sold = Comprato: {0}. Un piacere, dice il mercante. +msg.trader_left = Il mercante alza le spalle e se ne va dal portale. +trader.info = Offre {0} per {1} oro (clic con la mano) +trader.left = Se ne va tra {0} s +trader.rune = i piani di una torre della forgia runica +trader.wyrm = un drago di brace +trader.chainbreaker = uno spezzacatene +trader.wood = 30 legno +trader.mana = una pozione che riempie il mana msg.prisoner_died = {0} è morto sotto tortura. job.carry = Trasporta un prigioniero job.pray = Prega @@ -1032,6 +1042,7 @@ tool.altar.desc = Posa una creatura sull'altare per sacrificarla. Certe coppie d unit.revenant = Redivivo unit.cavebear = Orso delle caverne unit.beetle = Scarabeo gigante +unit.trader = Mercante job.guard_post = Di guardia job.spar = Si allena in coppia msg.revenant_rises = Un redivivo risorge dal cimitero e ti serve! diff --git a/lang/nl.lang b/lang/nl.lang index f856ed9..2e5fad6 100644 --- a/lang/nl.lang +++ b/lang/nl.lang @@ -332,6 +332,16 @@ unit.priestess = Vlampriesteres msg.hero_captured = {0} knock-out en gevangengenomen! Buit: {1} goud. msg.defected = {0} is naar jouw kant overgelopen! msg.beast_tamed = Een wilde {0} geeft zich over en sluit zich aan bij je kerker! +msg.trader_arrives = Er kwam een handelaar door je portaal: {0} voor {1} goud. Klik hem aan met de hand om te kopen – hij wacht een minuut. +msg.trader_sold = Gekocht: {0}. Prettig zakendoen, zegt de handelaar. +msg.trader_left = De handelaar haalt zijn schouders op en vertrekt door het portaal. +trader.info = Biedt {0} voor {1} goud (klik met de hand) +trader.left = Vertrekt over {0} s +trader.rune = de plannen van een runensmidstoren +trader.wyrm = een gloeddraak +trader.chainbreaker = een kettingbreker +trader.wood = 30 hout +trader.mana = een drankje dat je mana vult msg.prisoner_died = {0} is bezweken onder de marteling. job.carry = Draagt een gevangene job.pray = Bidt @@ -1040,6 +1050,7 @@ tool.altar.desc = Zet een wezen op het altaar om het te offeren. Bepaalde paren unit.revenant = Wederkeerder unit.cavebear = Holenbeer unit.beetle = Reuzenkever +unit.trader = Handelaar job.guard_post = Op wacht job.spar = Spart msg.revenant_rises = Een wederkeerder rijst op uit het kerkhof en dient jou! diff --git a/lang/no.lang b/lang/no.lang index c91cdb6..50d2607 100644 --- a/lang/no.lang +++ b/lang/no.lang @@ -332,6 +332,16 @@ unit.priestess = Flammeprestinne msg.hero_captured = {0} slått bevisstløs og tatt til fange! Bytte: {1} gull. msg.defected = {0} har gått over til din side! msg.beast_tamed = En vill {0} overgir seg og slutter seg til fangehullet ditt! +msg.trader_arrives = En handelsmann kom gjennom portalen din: {0} for {1} gull. Klikk på ham med hånden for å kjøpe – han venter et minutt. +msg.trader_sold = Kjøpt: {0}. En fornøyelse, sier handelsmannen. +msg.trader_left = Handelsmannen trekker på skuldrene og går gjennom portalen. +trader.info = Tilbyr {0} for {1} gull (klikk med hånden) +trader.left = Går om {0} s +trader.rune = tegningene til et runesmietårn +trader.wyrm = en glødlindorm +trader.chainbreaker = en lenkebryter +trader.wood = 30 tre +trader.mana = en drikk som fyller manaen din msg.prisoner_died = {0} døde under torturen. job.carry = Bærer en fange job.pray = Ber @@ -1039,6 +1049,7 @@ tool.altar.desc = Slipp en skapning på alteret for å ofre den. Visse par av of unit.revenant = Gjenganger unit.cavebear = Hulebjørn unit.beetle = Kjempebille +unit.trader = Handelsmann job.guard_post = På vakt job.spar = Sparrer msg.revenant_rises = En gjenganger reiser seg fra kirkegården og tjener deg! diff --git a/lang/pt.lang b/lang/pt.lang index 520b31e..c752955 100644 --- a/lang/pt.lang +++ b/lang/pt.lang @@ -332,6 +332,16 @@ unit.priestess = Sacerdotisa das Chamas msg.hero_captured = {0} ficou inconsciente e foi feito prisioneiro! Saque: {1} de ouro. msg.defected = {0} desertou para o teu lado! msg.beast_tamed = Um {0} selvagem rende-se e junta-se à tua masmorra! +msg.trader_arrives = Um mercador chegou pelo teu portal: {0} por {1} de ouro. Clica nele com a mão para comprar – espera um minuto. +msg.trader_sold = Comprado: {0}. Foi um prazer, diz o mercador. +msg.trader_left = O mercador encolhe os ombros e sai pelo portal. +trader.info = Oferece {0} por {1} de ouro (clica com a mão) +trader.left = Sai em {0} s +trader.rune = os planos de uma torre da forja rúnica +trader.wyrm = uma serpe de brasa +trader.chainbreaker = um quebra-correntes +trader.wood = 30 de madeira +trader.mana = uma poção que enche o teu mana msg.prisoner_died = {0} morreu sob tortura. job.carry = A carregar um prisioneiro job.pray = A rezar @@ -1040,6 +1050,7 @@ tool.altar.desc = Larga uma criatura no altar para a sacrificar. Certos pares de unit.revenant = Espectro unit.cavebear = Urso das cavernas unit.beetle = Escaravelho gigante +unit.trader = Mercador job.guard_post = De guarda job.spar = A treinar combate msg.revenant_rises = Um espectro ergue-se do cemitério e serve-te! diff --git a/lang/ro.lang b/lang/ro.lang index ead6eda..ed58773 100644 --- a/lang/ro.lang +++ b/lang/ro.lang @@ -332,6 +332,16 @@ unit.priestess = Preoteasa Flăcării msg.hero_captured = {0} doborât și luat prizonier! Pradă: {1} aur. msg.defected = {0} a trecut de partea ta! msg.beast_tamed = Un {0} sălbatic se predă și se alătură temniței tale! +msg.trader_arrives = Un negustor a venit prin portalul tău: {0} pentru {1} aur. Dă clic pe el cu mâna ca să cumperi – așteaptă un minut. +msg.trader_sold = Cumpărat: {0}. A fost o plăcere, spune negustorul. +msg.trader_left = Negustorul ridică din umeri și pleacă prin portal. +trader.info = Oferă {0} pentru {1} aur (clic cu mâna) +trader.left = Pleacă în {0} s +trader.rune = planurile unui turn al forjei runice +trader.wyrm = un balaur de jar +trader.chainbreaker = un rupător de lanțuri +trader.wood = 30 de lemn +trader.mana = o poțiune care îți umple mana msg.prisoner_died = {0} a murit sub tortură. job.carry = Duce un prizonier job.pray = Se roagă @@ -1040,6 +1050,7 @@ tool.altar.desc = Lasă o creatură pe altar ca s-o sacrifici. Anumite perechi d unit.revenant = Strigoi unit.cavebear = Urs de peșteră unit.beetle = Gândac uriaș +unit.trader = Negustor job.guard_post = De pază job.spar = Se duelează msg.revenant_rises = Un strigoi se ridică din cimitir și te servește! diff --git a/lang/ru.lang b/lang/ru.lang index 5d7afd6..7794f3b 100644 --- a/lang/ru.lang +++ b/lang/ru.lang @@ -332,6 +332,16 @@ unit.priestess = Жрица пламени msg.hero_captured = {0} оглушён и взят в плен! Добыча: {1} золота. msg.defected = {0} переходит на твою сторону! msg.beast_tamed = Дикий {0} сдаётся и присоединяется к твоему подземелью! +msg.trader_arrives = Через твой портал пришёл торговец: {0} за {1} золота. Щёлкни по нему рукой, чтобы купить, — он ждёт минуту. +msg.trader_sold = Куплено: {0}. Приятно иметь дело, говорит торговец. +msg.trader_left = Торговец пожимает плечами и уходит через портал. +trader.info = Предлагает {0} за {1} золота (щёлкни рукой) +trader.left = Уйдёт через {0} с +trader.rune = чертежи башни рунной кузницы +trader.wyrm = огненного змея +trader.chainbreaker = разрушителя цепей +trader.wood = 30 дерева +trader.mana = зелье, восполняющее ману msg.prisoner_died = {0} не пережил пыток. job.carry = Несёт пленника job.pray = Молится @@ -1032,6 +1042,7 @@ tool.altar.desc = Опусти существо на алтарь, чтобы п unit.revenant = Восставший unit.cavebear = Пещерный медведь unit.beetle = Гигантский жук +unit.trader = Торговец job.guard_post = На страже job.spar = Спаррингует msg.revenant_rises = С кладбища встаёт восставший и служит тебе! diff --git a/lang/sk.lang b/lang/sk.lang index 5ba9931..8fea215 100644 --- a/lang/sk.lang +++ b/lang/sk.lang @@ -332,6 +332,16 @@ unit.priestess = Plamenná kňažka msg.hero_captured = Omráčený a zajatý: {0}! Korisť: {1} zlata. msg.defected = Prebehol na tvoju stranu: {0}! msg.beast_tamed = Divoký {0} sa vzdáva a pridáva sa k tvojej kobke! +msg.trader_arrives = Cez tvoj portál prišiel obchodník: {0} za {1} zlata. Klikni naň rukou a kúp – čaká minútu. +msg.trader_sold = Kúpené: {0}. Bolo mi potešením, hovorí obchodník. +msg.trader_left = Obchodník pokrčí plecami a odíde cez portál. +trader.info = Ponúka {0} za {1} zlata (klikni rukou) +trader.left = Odíde o {0} s +trader.rune = plány veže runovej vyhne +trader.wyrm = žeravého šarkana +trader.chainbreaker = lámača reťazí +trader.wood = 30 dreva +trader.mana = elixír, ktorý doplní tvoju manu msg.prisoner_died = Zomrel pri mučení: {0}. job.carry = Nesie zajatca job.pray = Modlí sa @@ -1040,6 +1050,7 @@ tool.altar.desc = Polož tvora na oltár a obetuj ho. Určité dvojice obetí pr unit.revenant = Navrátilec unit.cavebear = Jaskynný medveď unit.beetle = Obrovský chrobák +unit.trader = Obchodník job.guard_post = Na stráži job.spar = Zápasí msg.revenant_rises = Z cintorína vstáva navrátilec a slúži ti! diff --git a/lang/sr.lang b/lang/sr.lang index c678ec3..62e6738 100644 --- a/lang/sr.lang +++ b/lang/sr.lang @@ -332,6 +332,16 @@ unit.priestess = Свештеница пламена msg.hero_captured = Онесвешћен и заробљен: {0}! Плен: {1} злата. msg.defected = Пребегао је на твоју страну: {0}! msg.beast_tamed = Дивљи {0} се предаје и придружује твојој тамници! +msg.trader_arrives = Кроз твој портал дошао је трговац: {0} за {1} злата. Кликни га руком да купиш – чека минут. +msg.trader_sold = Купљено: {0}. Задовољство, каже трговац. +msg.trader_left = Трговац слеже раменима и одлази кроз портал. +trader.info = Нуди {0} за {1} злата (кликни руком) +trader.left = Одлази за {0} s +trader.rune = нацрте куле рунске ковачнице +trader.wyrm = жарког змаја +trader.chainbreaker = ломитеља ланаца +trader.wood = 30 дрва +trader.mana = напитак који пуни твоју ману msg.prisoner_died = Умро је под мучењем: {0}. job.carry = Носи заробљеника job.pray = Моли се @@ -1040,6 +1050,7 @@ tool.altar.desc = Спусти створење на жртвеник да га unit.revenant = Повратник unit.cavebear = Пећински медвед unit.beetle = Џиновски инсект +unit.trader = Трговац job.guard_post = На стражи job.spar = Спаринг msg.revenant_rises = Повратник устаје из гробља и служи ти! diff --git a/lang/sv.lang b/lang/sv.lang index 70433cf..e4c207c 100644 --- a/lang/sv.lang +++ b/lang/sv.lang @@ -332,6 +332,16 @@ unit.priestess = Flamprästinna msg.hero_captured = {0} utslagen och tagen till fånga! Byte: {1} guld. msg.defected = {0} har gått över till din sida! msg.beast_tamed = En vild {0} ger sig och ansluter sig till din fängelsehåla! +msg.trader_arrives = En handlare kom genom din portal: {0} för {1} guld. Klicka på honom med handen för att köpa – han väntar en minut. +msg.trader_sold = Köpt: {0}. Ett nöje, säger handlaren. +msg.trader_left = Handlaren rycker på axlarna och går genom portalen. +trader.info = Erbjuder {0} för {1} guld (klicka med handen) +trader.left = Går om {0} s +trader.rune = ritningarna till ett runsmedjetorn +trader.wyrm = en glödlindorm +trader.chainbreaker = en kedjebrytare +trader.wood = 30 trä +trader.mana = en dryck som fyller din mana msg.prisoner_died = {0} dog under tortyren. job.carry = Bär en fånge job.pray = Ber @@ -1040,6 +1050,7 @@ tool.altar.desc = Släpp en varelse på altaret för att offra den. Vissa par av unit.revenant = Gengångare unit.cavebear = Grottbjörn unit.beetle = Jätteskalbagge +unit.trader = Handlare job.guard_post = På vakt job.spar = Sparrar msg.revenant_rises = En gengångare reser sig från kyrkogården och tjänar dig! diff --git a/src/game.rs b/src/game.rs index 60aa572..f322a80 100644 --- a/src/game.rs +++ b/src/game.rs @@ -130,6 +130,7 @@ pub enum EK { // wild beasts of the caverns (beasts.rs): neutral until tamed CaveBear, // strong and shaggy, likes the mushroom garden Beetle, // a giant armoured beetle, likes the lair + Trader, // a merchant visiting through the portal (traders.rs) } pub struct St { @@ -176,6 +177,7 @@ pub fn st(k: EK) -> St { EK::Catapult => s("unit.catapult", 320.0, 40.0, 7.5, 0.9, 3.5, 0, 200), EK::CaveBear => s("unit.cavebear", 420.0, 24.0, 1.0, 1.9, 1.4, 110, 0), EK::Beetle => s("unit.beetle", 300.0, 13.0, 1.0, 1.7, 1.1, 70, 0), + EK::Trader => s("unit.trader", 200.0, 1.0, 1.0, 1.5, 2.0, 0, 0), } } @@ -391,7 +393,7 @@ impl Ent { } // the player's creatures (not imps, heroes or the rivals' units) pub fn creature(&self) -> bool { - !self.hero && self.side == me_side() && self.kind != EK::Worker && !self.chained + !self.hero && self.side == me_side() && self.kind != EK::Worker && self.kind != EK::Trader && !self.chained } // 0 player, 1 humans, 2 and 3 rival masters pub fn side(&self) -> u8 { @@ -609,6 +611,8 @@ pub struct Game { #[serde(default)] pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges #[serde(default)] + pub trading: crate::traders::Trading, // the visiting trader (traders.rs) + #[serde(default)] pub happenings: Option, // random events (free games and versus), see events.rs #[serde(default)] pub stats: crate::stats::GameStats, // statistics of the master at this computer (see stats.rs) @@ -950,6 +954,7 @@ impl Game { vrng: crate::particles::vrng(), dwarf_caves: Vec::new(), bridgework: Default::default(), + trading: Default::default(), happenings: None, stats: Default::default(), wall_plans: Vec::new(), @@ -1724,6 +1729,7 @@ impl Game { self.update_bridges(dt); self.update_wood(dt); self.update_events(dt); + self.update_traders(dt); // fallen hearts of a match are shared state: every client marks them in the same step self.update_fallen_masters(); // the end of the game and the fog are about the master at this computer @@ -2400,6 +2406,8 @@ impl Game { self.captive_update(e, dt); } else if self.is_rival_side(e.side) || (e.rival() && !self.is_master(e.side)) { self.rival_ai(e, dt); + } else if e.kind == EK::Trader { + self.trader_ai(e, dt); } else if e.dwarf() { self.dwarf_ai(e, dt); } else if e.hero { @@ -5412,6 +5420,9 @@ impl Game { return false; } let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return false }; + if self.buy_from_trader(j) { + return false; + } let e = &self.ents[j]; if e.rival() || e.dwarf() { return false; // the rivals' creatures and the dwarves cannot be picked up diff --git a/src/gfx.rs b/src/gfx.rs index 463fd99..7f22cb1 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -1574,6 +1574,19 @@ fn sprite_rows(k: EK) -> [&'static str; 10] { "...y..y...", "..........", ], + // trader: a hooded merchant with a big pack and a lantern + EK::Trader => [ + "...kkk....", + "..kPPPk.BB", + ".kPfefPkBb", + ".kPffffkBb", + "..kPPPPkBB", + ".kPPyPPPk.", + "ykPPPPPPk.", + "o.kPPPPk..", + "..kPk.kPk.", + "..kk...kk.", + ], // cave bear: shaggy brown fur, small yellow eyes EK::CaveBear => [ ".kk....kk.", @@ -1708,7 +1721,7 @@ fn pal(ch: char) -> Option { }) } -pub const SPRITE_KINDS: [EK; 26] = [ +pub const SPRITE_KINDS: [EK; 27] = [ EK::Worker, EK::Kobold, EK::Spider, @@ -1735,6 +1748,7 @@ pub const SPRITE_KINDS: [EK; 26] = [ EK::Catapult, EK::CaveBear, EK::Beetle, + EK::Trader, ]; // width of the sprite texture in pixels pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32; diff --git a/src/main.rs b/src/main.rs index 1f90f04..a9288b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ mod settings; mod siege; mod sfx; mod tracks; +mod traders; mod traps; mod sound; mod surface; diff --git a/src/traders.rs b/src/traders.rs new file mode 100644 index 0000000..8a1919c --- /dev/null +++ b/src/traders.rs @@ -0,0 +1,170 @@ +// Traders: now and then a merchant comes through one of the master's portals and waits there +// for a minute. A click with the hand buys what he offers - the plans of a rune forge tower, a +// rare creature, a bundle of wood or a potion that fills the mana. Single player only (in a +// match the masters would have to agree on who buys). + +use crate::game::*; +use crate::map::*; +use serde::{Deserialize, Serialize}; + +// seconds until the first trader, between two traders, and how long one waits +pub const TRADER_FIRST: f32 = 420.0; +pub const TRADER_GAP: f32 = 540.0; +pub const TRADER_WAIT: f32 = 60.0; + +// what a trader may offer: (language key, price) +pub const OFFERS: [(&str, f32); 5] = [("trader.rune", 450.0), ("trader.wyrm", 1200.0), ("trader.chainbreaker", 900.0), ("trader.wood", 250.0), ("trader.mana", 200.0)]; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct Trading { + pub t: f32, // seconds until the next trader comes + pub here: Option<(u32, u8, f32)>, // the trader on the map: unit id, offer, seconds left +} + +impl Game { + pub(crate) fn update_traders(&mut self, dt: f32) { + if self.netplay.is_some() || self.demo.is_some() || self.state != 0 { + return; + } + if self.trading.t == 0.0 && self.trading.here.is_none() { + self.trading.t = TRADER_FIRST; + } + if let Some((id, offer, left)) = self.trading.here { + let left = left - dt; + if left <= 0.0 || self.find(id).is_none() { + self.trader_leaves(true); + } else { + self.trading.here = Some((id, offer, left)); + } + return; + } + self.trading.t -= dt; + if self.trading.t > 0.0 { + return; + } + self.trading.t = TRADER_GAP + self.rng.f() * 180.0; + // he comes through an own portal + let portals: Vec<(i32, i32)> = (0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).map(|k| self.map.portals[k]).collect(); + let Some(&(px, py)) = portals.get((self.rng.next() as usize) % portals.len().max(1)) else { return }; + let spot = DIR8.iter().map(|&(dx, dy)| (px + 1 + dx * 2, py + 1 + dy * 2)).find(|&(x, y)| self.map.walk(x, y)); + let Some((x, y)) = spot else { return }; + let offer = (self.rng.next() % OFFERS.len() as u32) as u8; + let id = self.spawn(EK::Trader, false, x as f32 + 0.5, y as f32 + 0.5, 1); + if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) { + e.side = self.me_side; + e.home = Some((x as f32 + 0.5, y as f32 + 0.5)); + e.settled = true; + e.happy = 1.0; + } + self.trading.here = Some((id, offer, TRADER_WAIT)); + let (key, price) = OFFERS[offer as usize]; + self.msg("msg.trader_arrives", vec![format!("@{}", key), (price as i32).to_string()]); + self.sfx_ui(Sfx::Deposit); + } + + // the trader goes back through the portal + fn trader_leaves(&mut self, unsold: bool) { + if let Some((id, _, _)) = self.trading.here.take() { + if let Some(j) = self.find(id) { + let (x, y) = (self.ents[j].x, self.ents[j].y); + self.ents[j].alive = false; + self.fx(7, x, y, 0.0, 0.0, 0.8, 0xffff60c0); + } + if unsold { + self.msg("msg.trader_left", vec![]); + } + } + } + + // the hand on the trader: buy his offer (true: it was the trader) + pub(crate) fn buy_from_trader(&mut self, j: usize) -> bool { + let Some((id, offer, _)) = self.trading.here else { return false }; + if self.ents[j].id != id { + return false; + } + let (key, price) = OFFERS[offer as usize]; + if self.gold < price { + self.msg("msg.no_gold", vec![]); + self.sfx_ui(Sfx::Error); + return true; + } + self.gold -= price; + let (x, y) = (self.ents[j].x, self.ents[j].y); + match offer { + 0 => self.rune_free += 1, + 1 | 2 => { + let (kind, level) = if offer == 1 { (EK::Wyrm, 3) } else { (EK::Chainbreaker, 4) }; + let nid = self.spawn(kind, false, x, y, level); + if let Some(e) = self.ents.iter_mut().find(|e| e.id == nid) { + e.side = self.me_side; + e.settled = false; + e.job = Job::Register; + e.timer = 1.0; + e.happy = 1.0; + } + } + 3 => self.wood += 30.0, + _ => self.mana = self.mana_max(), + } + self.msg("msg.trader_sold", vec![format!("@{}", key)]); + self.sfx_ui(Sfx::Payday); + self.trader_leaves(false); + true + } + + // a trader just stands by the portal and waits + pub(crate) fn trader_ai(&mut self, e: &mut Ent, dt: f32) { + e.hunger = 0.0; + e.energy = 1.0; + e.happy = 1.0; + if let Some((hx, hy)) = e.home { + if (e.x - hx).abs() + (e.y - hy).abs() > 0.3 && e.path.is_empty() { + e.x = hx; + e.y = hy; + } + } + let _ = dt; + } + + // what the trader offers (hover information): (offer key, price, seconds left) + pub fn trader_offer(&self, id: u32) -> Option<(&'static str, f32, f32)> { + self.trading.here.filter(|h| h.0 == id).map(|(_, o, left)| (OFFERS[o as usize].0, OFFERS[o as usize].1, left)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_trader_comes_and_sells() { + let mut g = Game::new(7, 64, 64); + // own the portal + let (px, py) = g.map.portals[0]; + for y in py - 2..py + 5 { + for x in px - 2..px + 5 { + if inb(x, y) && g.map.t[idx(x, y)].kind != Kind::Portal { + g.map.t[idx(x, y)].kind = Kind::Floor; + } + if inb(x, y) { + g.map.t[idx(x, y)].owner = Owner::Player; + } + } + } + g.trading.t = 0.01; + g.update_traders(0.05); + let (id, offer, _) = g.trading.here.expect("a trader came"); + let j = g.find(id).unwrap(); + assert!(!g.ents[j].creature(), "not one of the household"); + g.gold = 5000.0; + let (gold, runes, wood) = (g.gold, g.rune_free, g.wood); + assert!(g.buy_from_trader(j)); + assert!(g.gold < gold); + match offer { + 0 => assert_eq!(g.rune_free, runes + 1), + 3 => assert!(g.wood > wood), + _ => {} + } + assert!(g.trading.here.is_none() && !g.ents[j].alive, "he leaves"); + } +} diff --git a/src/ui.rs b/src/ui.rs index be2b9ba..24a2f8a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1508,6 +1508,13 @@ pub fn hover_info(g: &Game, ent: Option, tile: Option<(i32, i32)>) -> Vec if let Some(j) = ent { let e = &g.ents[j]; let s = st(e.kind); + // the trader: his offer, the price and how long he waits + if let Some((key, price, left)) = g.trader_offer(e.id) { + out.push((tr(s.name), GOLD)); + out.push((trf("trader.info", &[tr(key), (price as i32).to_string()]), FRAME_HI)); + out.push((trf("trader.left", &[(left.ceil() as i32).to_string()]), DIM)); + return out; + } out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI })); out.push((trf("info.rank", &[tr(&format!("rank.{}", e.level.clamp(1, 10)))]), GOLD)); out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));