Co-op offline with an AI partner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 21:04:49 +01:00
1 parent e3a9d32d61
commit f87b59390e
22 files changed
+272 -5

No files matched your search

+7
View File
@@ -90,6 +90,13 @@ per turn. Fog of war, particles and the camera are local and not part of the sha
five strongest creatures and 20% of his gold, like the single-player `Carry`. The host stores the progress five strongest creatures and 20% of his gold, like the single-player `Carry`. The host stores the progress
(missions done, chosen branches, run seed) in local storage under `dc.coop_editor_progress` (campaign name -> (missions done, chosen branches, run seed) in local storage under `dc.coop_editor_progress` (campaign name ->
`CampaignProgress`). A run ends when no next mission fits co-op. `CampaignProgress`). A run ends when no next mission fits co-op.
- **Co-op offline with an AI partner** (*AI partner* on the Match page, works without the server): the selected
co-op mission (built-in, the league or the editor campaign) starts like a match with the player on side 0 and
an AI seat on side 2 (`App::start_offline_coop`: `start_match` + `sync_ai_sides`), then `end_match` drops the
turn clock and the world runs on this computer like a single-player game. The partner (`Game::is_partner`) is
an allied rival-style master with his own dungeon: two of three healthy creatures follow the player's attack
flag (and attack the gate next to it), and every minute he sends 400 gold to an ally below 300. An editor
campaign run goes on locally: the next mission (or the same after a defeat) with the run seed and the carry.
- **Teams in versus**: on the Match page every seat has a team A-D (default: a team of its own), set with - **Teams in versus**: on the Match page every seat has a team A-D (default: a team of its own), set with
`{"t":"set_team","team":n}` for the own seat; the host may add `"side"` for a free or AI seat. Only before the `{"t":"set_team","team":n}` for the own seat; the host may add `"side"` for a free or AI seat. Only before the
start. Room updates carry `team` per seat, the room list `teams`, and `start` a `teams` list in the order of start. Room updates carry `team` per seat, the room list `teams`, and `start` a `teams` list in the order of
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Kooperace
net.coop_editor = Kampaň z editoru net.coop_editor = Kampaň z editoru
net.coop_builtin = Vestavěná kampaň net.coop_builtin = Vestavěná kampaň
net.coop_league = Liga proti Světlé stráži net.coop_league = Liga proti Světlé stráži
net.coop_solo = AI partner
net.coop_solo_started = Kooperace bez serveru: tvůj AI partner si staví vlastní kobku, následuje tvou útočnou vlajku a pomáhá zlatem.
net.coop_editor_mission = Kampaň z editoru {0}: mise {1} ({2}) net.coop_editor_mission = Kampaň z editoru {0}: mise {1} ({2})
net.coop_editor_none = Žádná mise kampaně z editoru se nehodí pro kooperaci (mapa aspoň 72x72, jiný cíl než soupeřící mistři) net.coop_editor_none = Žádná mise kampaně z editoru se nehodí pro kooperaci (mapa aspoň 72x72, jiný cíl než soupeřící mistři)
net.join = Připojit net.join = Připojit
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = Vlna {0}: hrdinové ({1}) táhnou na modrou mistryni.
msg.wave_warn_on_master = Zvědové hlásí: vlna hrdinů {0} dorazí za 30 sekund a míří na pána {1}. msg.wave_warn_on_master = Zvědové hlásí: vlna hrdinů {0} dorazí za 30 sekund a míří na pána {1}.
msg.wave_on_master = Vlna {0}: {1} hrdinů táhne na pána {2}. msg.wave_on_master = Vlna {0}: {1} hrdinů táhne na pána {2}.
msg.partner_out = Srdce tvého AI partnera padlo – bojuj dál sám!
hud.rival1 = Zelený hud.rival1 = Zelený
hud.rival2 = Modrá hud.rival2 = Modrá
msg.creature_freed = Tvor přetrhl řetězy a přidal se k tobě: {0}! msg.creature_freed = Tvor přetrhl řetězy a přidal se k tobě: {0}!
+3
View File
@@ -661,6 +661,8 @@ net.coop_create = Co-op-kampagne
net.coop_editor = Editor-kampagne net.coop_editor = Editor-kampagne
net.coop_builtin = Indbygget kampagne net.coop_builtin = Indbygget kampagne
net.coop_league = Forbundet mod Lysvagten net.coop_league = Forbundet mod Lysvagten
net.coop_solo = AI-partner
net.coop_solo_started = Co-op uden server: din AI-partner bygger sit eget fangehul, følger dit angrebsflag og hjælper med guld.
net.coop_editor_mission = Editor-kampagne {0}: mission {1} ({2}) net.coop_editor_mission = Editor-kampagne {0}: mission {1} ({2})
net.coop_editor_none = Ingen mission i editor-kampagnen passer til co-op (kort mindst 72x72, et mål ud over rivaliserende mestre) net.coop_editor_none = Ingen mission i editor-kampagnen passer til co-op (kort mindst 72x72, et mål ud over rivaliserende mestre)
net.join = Deltag net.join = Deltag
@@ -708,6 +710,7 @@ 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. msg.wave_on_rival2 = Bølge {0}: {1} helte marcherer mod den blå mester.
msg.wave_warn_on_master = Spejdere melder: heltebølge {0} kommer om 30 sekunder og går mod mester {1}. msg.wave_warn_on_master = Spejdere melder: heltebølge {0} kommer om 30 sekunder og går mod mester {1}.
msg.wave_on_master = Bølge {0}: {1} helte marcherer mod mester {2}. msg.wave_on_master = Bølge {0}: {1} helte marcherer mod mester {2}.
msg.partner_out = Din AI-partners hjerte er faldet – kæmp videre alene!
hud.rival1 = Grøn hud.rival1 = Grøn
hud.rival2 = Blå hud.rival2 = Blå
msg.creature_freed = En {0} sprænger sine lænker og slutter sig til dig! msg.creature_freed = En {0} sprænger sine lænker og slutter sig til dig!
+3
View File
@@ -661,6 +661,8 @@ net.coop_create = Koop-Kampagne
net.coop_editor = Editor-Kampagne net.coop_editor = Editor-Kampagne
net.coop_builtin = Eingebaute Kampagne net.coop_builtin = Eingebaute Kampagne
net.coop_league = Bund gegen die Lichtwacht net.coop_league = Bund gegen die Lichtwacht
net.coop_solo = KI-Partner
net.coop_solo_started = Koop ohne Server: Dein KI-Partner baut einen eigenen Kerker, folgt deiner Angriffsflagge und hilft mit Gold aus.
net.coop_editor_mission = Editor-Kampagne {0}: Mission {1} ({2}) net.coop_editor_mission = Editor-Kampagne {0}: Mission {1} ({2})
net.coop_editor_none = Keine Mission der Editor-Kampagne passt für Koop (Karte mindestens 72x72, ein Ziel außer Rivalen-Meistern) net.coop_editor_none = Keine Mission der Editor-Kampagne passt für Koop (Karte mindestens 72x72, ein Ziel außer Rivalen-Meistern)
net.join = Beitreten net.join = Beitreten
@@ -708,6 +710,7 @@ 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. msg.wave_on_rival2 = Welle {0}: {1} Helden ziehen gegen den blauen Meister.
msg.wave_warn_on_master = Späher melden: Heldenwelle {0} kommt in 30 Sekunden – sie zieht gegen Meister {1}. msg.wave_warn_on_master = Späher melden: Heldenwelle {0} kommt in 30 Sekunden – sie zieht gegen Meister {1}.
msg.wave_on_master = Welle {0}: {1} Helden ziehen gegen Meister {2}. msg.wave_on_master = Welle {0}: {1} Helden ziehen gegen Meister {2}.
msg.partner_out = Das Herz deines KI-Partners ist gefallen – kämpfe allein weiter!
hud.rival1 = Grün hud.rival1 = Grün
hud.rival2 = Blau hud.rival2 = Blau
msg.creature_freed = Ein {0} sprengt seine Ketten und schließt sich dir an! msg.creature_freed = Ein {0} sprengt seine Ketten und schließt sich dir an!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Συνεργατική εκστρ.
net.coop_editor = Εκστρατεία επεξεργαστή net.coop_editor = Εκστρατεία επεξεργαστή
net.coop_builtin = Ενσωματωμένη εκστρατεία net.coop_builtin = Ενσωματωμένη εκστρατεία
net.coop_league = Συμμαχία κατά της Φωτοφρουράς net.coop_league = Συμμαχία κατά της Φωτοφρουράς
net.coop_solo = Σύμμαχος ΤΝ
net.coop_solo_started = Συνεργασία χωρίς διακομιστή: ο σύμμαχός σου ΤΝ χτίζει το δικό του μπουντρούμι, ακολουθεί τη σημαία επίθεσής σου και βοηθά με χρυσό.
net.coop_editor_mission = Εκστρατεία επεξεργαστή {0}: αποστολή {1} ({2}) net.coop_editor_mission = Εκστρατεία επεξεργαστή {0}: αποστολή {1} ({2})
net.coop_editor_none = Καμία αποστολή της εκστρατείας επεξεργαστή δεν ταιριάζει για συνεργασία (χάρτης τουλάχιστον 72x72, ένας στόχος πέρα από τους αντίπαλους άρχοντες) net.coop_editor_none = Καμία αποστολή της εκστρατείας επεξεργαστή δεν ταιριάζει για συνεργασία (χάρτης τουλάχιστον 72x72, ένας στόχος πέρα από τους αντίπαλους άρχοντες)
net.join = Συμμετοχή net.join = Συμμετοχή
@@ -709,6 +711,7 @@ msg.wave_on_rival1 = Κύμα {0}: {1} ήρωες βαδίζουν κατά το
msg.wave_on_rival2 = Κύμα {0}: {1} ήρωες βαδίζουν κατά της γαλάζιας άρχοντος. msg.wave_on_rival2 = Κύμα {0}: {1} ήρωες βαδίζουν κατά της γαλάζιας άρχοντος.
msg.wave_warn_on_master = Οι ανιχνευτές αναφέρουν: το κύμα ηρώων {0} έρχεται σε 30 δευτερόλεπτα και κατευθύνεται στον άρχοντα {1}. msg.wave_warn_on_master = Οι ανιχνευτές αναφέρουν: το κύμα ηρώων {0} έρχεται σε 30 δευτερόλεπτα και κατευθύνεται στον άρχοντα {1}.
msg.wave_on_master = Κύμα {0}: {1} ήρωες βαδίζουν κατά του άρχοντα {2}. msg.wave_on_master = Κύμα {0}: {1} ήρωες βαδίζουν κατά του άρχοντα {2}.
msg.partner_out = Η καρδιά του συμμάχου σου ΤΝ έπεσε – πολέμησε μόνος!
hud.rival1 = Πράσινος hud.rival1 = Πράσινος
hud.rival2 = Γαλάζια hud.rival2 = Γαλάζια
msg.creature_freed = Σπάει τις αλυσίδες του και έρχεται μαζί σου: {0}! msg.creature_freed = Σπάει τις αλυσίδες του και έρχεται μαζί σου: {0}!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Co-op campaign
net.coop_editor = Editor campaign net.coop_editor = Editor campaign
net.coop_builtin = Built-in campaign net.coop_builtin = Built-in campaign
net.coop_league = League against the Lightwatch net.coop_league = League against the Lightwatch
net.coop_solo = AI partner
net.coop_solo_started = Co-op without the server: your AI partner builds its own dungeon, follows your attack flag and helps out with gold.
net.coop_editor_mission = Editor campaign {0}: mission {1} ({2}) net.coop_editor_mission = Editor campaign {0}: mission {1} ({2})
net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters) net.coop_editor_none = No mission of the editor campaign fits co-op (map at least 72x72, a goal besides rival masters)
net.join = Join net.join = Join
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = Wave {0}: {1} heroes march on the blue master.
msg.wave_warn_on_master = Scouts report: hero wave {0} comes in 30 seconds – it heads for master {1}. msg.wave_warn_on_master = Scouts report: hero wave {0} comes in 30 seconds – it heads for master {1}.
msg.wave_on_master = Wave {0}: {1} heroes march on master {2}. msg.wave_on_master = Wave {0}: {1} heroes march on master {2}.
msg.partner_out = Your AI partner's heart has fallen – fight on alone!
hud.rival1 = Green hud.rival1 = Green
hud.rival2 = Blue hud.rival2 = Blue
msg.creature_freed = A {0} breaks its chains and joins you! msg.creature_freed = A {0} breaks its chains and joins you!
+3
View File
@@ -704,6 +704,7 @@ 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. msg.wave_on_rival2 = Oleada {0}: {1} héroes marchan contra la señora azul.
msg.wave_warn_on_master = Los exploradores informan: la oleada {0} llega en 30 segundos y va a por el maestro {1}. msg.wave_warn_on_master = Los exploradores informan: la oleada {0} llega en 30 segundos y va a por el maestro {1}.
msg.wave_on_master = Oleada {0}: {1} héroes marchan contra el maestro {2}. msg.wave_on_master = Oleada {0}: {1} héroes marchan contra el maestro {2}.
msg.partner_out = ¡El corazón de tu socio IA ha caído: sigue luchando solo!
hud.rival1 = Verde hud.rival1 = Verde
hud.rival2 = Azul hud.rival2 = Azul
msg.creature_freed = ¡Un {0} rompe sus cadenas y se une a ti! msg.creature_freed = ¡Un {0} rompe sus cadenas y se une a ti!
@@ -1333,6 +1334,8 @@ demo.m2.wave = Han pasado dos minutos: llegan los héroes.
net.coop_editor = Campaña del editor net.coop_editor = Campaña del editor
net.coop_builtin = Campaña integrada net.coop_builtin = Campaña integrada
net.coop_league = Liga contra la Guardia Luminosa net.coop_league = Liga contra la Guardia Luminosa
net.coop_solo = Socio IA
net.coop_solo_started = Cooperativo sin servidor: tu socio IA construye su propia mazmorra, sigue tu bandera de ataque y te ayuda con oro.
net.coop_editor_mission = Campaña del editor {0}: misión {1} ({2}) net.coop_editor_mission = Campaña del editor {0}: misión {1} ({2})
net.coop_editor_none = Ninguna misión de la campaña del editor sirve para cooperativo (mapa de al menos 72x72, un objetivo aparte de los maestros rivales) net.coop_editor_none = Ninguna misión de la campaña del editor sirve para cooperativo (mapa de al menos 72x72, un objetivo aparte de los maestros rivales)
blk.until = Repetir hasta blk.until = Repetir hasta
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Yhteiskampanja
net.coop_editor = Editorikampanja net.coop_editor = Editorikampanja
net.coop_builtin = Sisäinen kampanja net.coop_builtin = Sisäinen kampanja
net.coop_league = Liitto Valonvartiota vastaan net.coop_league = Liitto Valonvartiota vastaan
net.coop_solo = Kumppani
net.coop_solo_started = Yhteistyö ilman palvelinta: tekoälykumppanisi rakentaa oman luolastonsa, seuraa hyökkäyslippuasi ja auttaa kullalla.
net.coop_editor_mission = Editorikampanja {0}: tehtävä {1} ({2}) net.coop_editor_mission = Editorikampanja {0}: tehtävä {1} ({2})
net.coop_editor_none = Mikään editorikampanjan tehtävä ei sovi yhteispeliin (kartta vähintään 72x72, muukin tavoite kuin kilpailevat mestarit) net.coop_editor_none = Mikään editorikampanjan tehtävä ei sovi yhteispeliin (kartta vähintään 72x72, muukin tavoite kuin kilpailevat mestarit)
net.join = Liity net.join = Liity
@@ -709,6 +711,7 @@ msg.wave_on_rival1 = Aalto {0}: {1} sankaria marssii vihreää mestaria vastaan.
msg.wave_on_rival2 = Aalto {0}: {1} sankaria marssii sinistä mestaria vastaan. msg.wave_on_rival2 = Aalto {0}: {1} sankaria marssii sinistä mestaria vastaan.
msg.wave_warn_on_master = Tiedustelijat ilmoittavat: sankariaalto {0} saapuu 30 sekunnissa ja hyökkää mestari {1} vastaan. msg.wave_warn_on_master = Tiedustelijat ilmoittavat: sankariaalto {0} saapuu 30 sekunnissa ja hyökkää mestari {1} vastaan.
msg.wave_on_master = Aalto {0}: {1} sankaria marssii mestari {2} vastaan. msg.wave_on_master = Aalto {0}: {1} sankaria marssii mestari {2} vastaan.
msg.partner_out = Tekoälykumppanisi sydän on murtunut – taistele yksin!
hud.rival1 = Vihreä hud.rival1 = Vihreä
hud.rival2 = Sininen hud.rival2 = Sininen
msg.creature_freed = Kahleet katkeavat ja uusi olento liittyy joukkoosi: {0}! msg.creature_freed = Kahleet katkeavat ja uusi olento liittyy joukkoosi: {0}!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Co-op kampanja
net.coop_editor = Kampanja uređivača net.coop_editor = Kampanja uređivača
net.coop_builtin = Ugrađena kampanja net.coop_builtin = Ugrađena kampanja
net.coop_league = Savez protiv Svjetlostraže net.coop_league = Savez protiv Svjetlostraže
net.coop_solo = AI partner
net.coop_solo_started = Suradnja bez poslužitelja: tvoj AI partner gradi vlastitu tamnicu, slijedi tvoju zastavu napada i pomaže zlatom.
net.coop_editor_mission = Kampanja uređivača {0}: misija {1} ({2}) net.coop_editor_mission = Kampanja uređivača {0}: misija {1} ({2})
net.coop_editor_none = Nijedna misija kampanje uređivača ne odgovara co-op igri (karta barem 72x72, cilj osim suparničkih gospodara) net.coop_editor_none = Nijedna misija kampanje uređivača ne odgovara co-op igri (karta barem 72x72, cilj osim suparničkih gospodara)
net.join = Pridruži net.join = Pridruži
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = Val {0}: junaci ({1}) marširaju na plavu gospodaricu.
msg.wave_warn_on_master = Izvidnici javljaju: val junaka {0} stiže za 30 sekundi i kreće na gospodara {1}. msg.wave_warn_on_master = Izvidnici javljaju: val junaka {0} stiže za 30 sekundi i kreće na gospodara {1}.
msg.wave_on_master = Val {0}: {1} junaka maršira na gospodara {2}. msg.wave_on_master = Val {0}: {1} junaka maršira na gospodara {2}.
msg.partner_out = Srce tvog AI partnera je palo – bori se dalje sam!
hud.rival1 = Zeleni hud.rival1 = Zeleni
hud.rival2 = Plava hud.rival2 = Plava
msg.creature_freed = Raskida okove i pridružuje ti se: {0}! msg.creature_freed = Raskida okove i pridružuje ti se: {0}!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Koop hadjárat
net.coop_editor = Saját hadjárat net.coop_editor = Saját hadjárat
net.coop_builtin = Beépített hadjárat net.coop_builtin = Beépített hadjárat
net.coop_league = Szövetség a Fényőrség ellen net.coop_league = Szövetség a Fényőrség ellen
net.coop_solo = MI-társ
net.coop_solo_started = Kooperatív szerver nélkül: MI-társad saját kazamatát épít, követi a támadási zászlódat és arannyal segít.
net.coop_editor_mission = Saját hadjárat ({0}): {1}. küldetés ({2}) net.coop_editor_mission = Saját hadjárat ({0}): {1}. küldetés ({2})
net.coop_editor_none = A saját hadjárat egyik küldetése sem alkalmas kooperatív játékra (térkép legalább 72x72, a rivális mestereken kívül még egy cél) net.coop_editor_none = A saját hadjárat egyik küldetése sem alkalmas kooperatív játékra (térkép legalább 72x72, a rivális mestereken kívül még egy cél)
net.join = Belép net.join = Belép
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = {0}. hullám: {1} hős vonul a kék mester ellen.
msg.wave_warn_on_master = A felderítők jelentik: a(z) {0}. hősroham 30 másodperc múlva érkezik, és {1} mester ellen vonul. msg.wave_warn_on_master = A felderítők jelentik: a(z) {0}. hősroham 30 másodperc múlva érkezik, és {1} mester ellen vonul.
msg.wave_on_master = {0}. roham: {1} hős vonul {2} mester ellen. msg.wave_on_master = {0}. roham: {1} hős vonul {2} mester ellen.
msg.partner_out = MI-társad szíve elesett – harcolj tovább egyedül!
hud.rival1 = Zöld hud.rival1 = Zöld
hud.rival2 = Kék hud.rival2 = Kék
msg.creature_freed = Széttépi a láncait, és hozzád csatlakozik: {0}! msg.creature_freed = Széttépi a láncait, és hozzád csatlakozik: {0}!
+3
View File
@@ -704,6 +704,7 @@ msg.wave_on_rival1 = Ondata {0}: {1} eroi marciano sul maestro verde.
msg.wave_on_rival2 = Ondata {0}: {1} eroi marciano sulla maestra blu. msg.wave_on_rival2 = Ondata {0}: {1} eroi marciano sulla maestra blu.
msg.wave_warn_on_master = Gli esploratori riferiscono: l'ondata {0} arriva tra 30 secondi e punta sul maestro {1}. msg.wave_warn_on_master = Gli esploratori riferiscono: l'ondata {0} arriva tra 30 secondi e punta sul maestro {1}.
msg.wave_on_master = Ondata {0}: {1} eroi marciano contro il maestro {2}. msg.wave_on_master = Ondata {0}: {1} eroi marciano contro il maestro {2}.
msg.partner_out = Il cuore del tuo alleato IA è caduto: combatti da solo!
hud.rival1 = Verde hud.rival1 = Verde
hud.rival2 = Blu hud.rival2 = Blu
msg.creature_freed = Spezza le catene e si unisce a te: {0}! msg.creature_freed = Spezza le catene e si unisce a te: {0}!
@@ -1333,6 +1334,8 @@ demo.m2.wave = Sono passati due minuti: arrivano gli eroi.
net.coop_editor = Campagna dell'editor net.coop_editor = Campagna dell'editor
net.coop_builtin = Campagna integrata net.coop_builtin = Campagna integrata
net.coop_league = Lega contro la Guardia della Luce net.coop_league = Lega contro la Guardia della Luce
net.coop_solo = Alleato IA
net.coop_solo_started = Cooperativa senza server: il tuo alleato IA costruisce il suo dungeon, segue la tua bandiera d'attacco e ti aiuta con l'oro.
net.coop_editor_mission = Campagna dell'editor {0}: missione {1} ({2}) net.coop_editor_mission = Campagna dell'editor {0}: missione {1} ({2})
net.coop_editor_none = Nessuna missione della campagna dell'editor è adatta alla cooperativa (mappa almeno 72x72, un obiettivo oltre ai maestri rivali) net.coop_editor_none = Nessuna missione della campagna dell'editor è adatta alla cooperativa (mappa almeno 72x72, un obiettivo oltre ai maestri rivali)
blk.until = Ripeti finché blk.until = Ripeti finché
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Co-op-veldtocht
net.coop_editor = Editorveldtocht net.coop_editor = Editorveldtocht
net.coop_builtin = Vaste veldtocht net.coop_builtin = Vaste veldtocht
net.coop_league = Liga tegen de Lichtwacht net.coop_league = Liga tegen de Lichtwacht
net.coop_solo = AI-partner
net.coop_solo_started = Co-op zonder server: je AI-partner bouwt een eigen kerker, volgt je aanvalsvlag en helpt met goud.
net.coop_editor_mission = Editorveldtocht {0}: missie {1} ({2}) net.coop_editor_mission = Editorveldtocht {0}: missie {1} ({2})
net.coop_editor_none = Geen missie van de editorveldtocht is geschikt voor co-op (kaart minstens 72x72, een doel naast rivaliserende meesters) net.coop_editor_none = Geen missie van de editorveldtocht is geschikt voor co-op (kaart minstens 72x72, een doel naast rivaliserende meesters)
net.join = Meedoen net.join = Meedoen
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = Golf {0}: {1} helden trekken op tegen de blauwe meester.
msg.wave_warn_on_master = Verkenners melden: heldengolf {0} komt over 30 seconden en trekt naar meester {1}. msg.wave_warn_on_master = Verkenners melden: heldengolf {0} komt over 30 seconden en trekt naar meester {1}.
msg.wave_on_master = Golf {0}: {1} helden trekken op tegen meester {2}. msg.wave_on_master = Golf {0}: {1} helden trekken op tegen meester {2}.
msg.partner_out = Het hart van je AI-partner is gevallen – vecht alleen verder!
hud.rival1 = Groen hud.rival1 = Groen
hud.rival2 = Blauw hud.rival2 = Blauw
msg.creature_freed = Een {0} breekt zijn ketens en sluit zich bij je aan! msg.creature_freed = Een {0} breekt zijn ketens en sluit zich bij je aan!
+3
View File
@@ -661,6 +661,8 @@ net.coop_create = Co-op-kampanje
net.coop_editor = Editorkampanje net.coop_editor = Editorkampanje
net.coop_builtin = Innebygd kampanje net.coop_builtin = Innebygd kampanje
net.coop_league = Forbundet mot Lysvakten net.coop_league = Forbundet mot Lysvakten
net.coop_solo = KI-partner
net.coop_solo_started = Samarbeid uten server: KI-partneren din bygger sitt eget fangehull, følger angrepsflagget ditt og hjelper med gull.
net.coop_editor_mission = Editorkampanje {0}: oppdrag {1} ({2}) net.coop_editor_mission = Editorkampanje {0}: oppdrag {1} ({2})
net.coop_editor_none = Ingen oppdrag i editorkampanjen passer for co-op (kart minst 72x72, et mål i tillegg til rivaliserende mestere) net.coop_editor_none = Ingen oppdrag i editorkampanjen passer for co-op (kart minst 72x72, et mål i tillegg til rivaliserende mestere)
net.join = Bli med net.join = Bli med
@@ -708,6 +710,7 @@ 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. msg.wave_on_rival2 = Bølge {0}: {1} helter marsjerer mot den blå mesteren.
msg.wave_warn_on_master = Speidere melder: heltebølge {0} kommer om 30 sekunder og går mot mester {1}. msg.wave_warn_on_master = Speidere melder: heltebølge {0} kommer om 30 sekunder og går mot mester {1}.
msg.wave_on_master = Bølge {0}: {1} helter marsjerer mot mester {2}. msg.wave_on_master = Bølge {0}: {1} helter marsjerer mot mester {2}.
msg.partner_out = Hjertet til KI-partneren din har falt – kjemp videre alene!
hud.rival1 = Grønn hud.rival1 = Grønn
hud.rival2 = Blå hud.rival2 = Blå
msg.creature_freed = En {0} sprenger lenkene sine og slutter seg til deg! msg.creature_freed = En {0} sprenger lenkene sine og slutter seg til deg!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Campanha coop.
net.coop_editor = Campanha do editor net.coop_editor = Campanha do editor
net.coop_builtin = Campanha incluída net.coop_builtin = Campanha incluída
net.coop_league = Liga contra a Guarda da Luz net.coop_league = Liga contra a Guarda da Luz
net.coop_solo = Parceiro IA
net.coop_solo_started = Cooperativo sem servidor: o teu parceiro IA constrói a sua própria masmorra, segue a tua bandeira de ataque e ajuda com ouro.
net.coop_editor_mission = Campanha do editor {0}: missão {1} ({2}) net.coop_editor_mission = Campanha do editor {0}: missão {1} ({2})
net.coop_editor_none = Nenhuma missão da campanha do editor serve para cooperativo (mapa de pelo menos 72x72, um objetivo além dos mestres rivais) net.coop_editor_none = Nenhuma missão da campanha do editor serve para cooperativo (mapa de pelo menos 72x72, um objetivo além dos mestres rivais)
net.join = Entrar net.join = Entrar
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = Vaga {0}: {1} heróis marcham contra a mestra azul.
msg.wave_warn_on_master = Os batedores informam: a onda {0} chega em 30 segundos e vai contra o mestre {1}. msg.wave_warn_on_master = Os batedores informam: a onda {0} chega em 30 segundos e vai contra o mestre {1}.
msg.wave_on_master = Onda {0}: {1} heróis marcham contra o mestre {2}. msg.wave_on_master = Onda {0}: {1} heróis marcham contra o mestre {2}.
msg.partner_out = O coração do teu parceiro IA caiu – continua a lutar sozinho!
hud.rival1 = Verde hud.rival1 = Verde
hud.rival2 = Azul hud.rival2 = Azul
msg.creature_freed = Um {0} parte as correntes e junta-se a ti! msg.creature_freed = Um {0} parte as correntes e junta-se a ti!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Campanie co-op
net.coop_editor = Campanie din editor net.coop_editor = Campanie din editor
net.coop_builtin = Campanie inclusă net.coop_builtin = Campanie inclusă
net.coop_league = Liga împotriva Străjii Luminii net.coop_league = Liga împotriva Străjii Luminii
net.coop_solo = Partener IA
net.coop_solo_started = Cooperativ fără server: partenerul tău IA își construiește propria temniță, îți urmează steagul de atac și te ajută cu aur.
net.coop_editor_mission = Campania din editor {0}: misiunea {1} ({2}) net.coop_editor_mission = Campania din editor {0}: misiunea {1} ({2})
net.coop_editor_none = Nicio misiune din campania editorului nu se potrivește pentru co-op (hartă de cel puțin 72x72, un obiectiv în afară de stăpânii rivali) net.coop_editor_none = Nicio misiune din campania editorului nu se potrivește pentru co-op (hartă de cel puțin 72x72, un obiectiv în afară de stăpânii rivali)
net.join = Intră net.join = Intră
@@ -709,6 +711,7 @@ msg.wave_on_rival1 = Valul {0}: {1} eroi mărșăluiesc asupra stăpânului verd
msg.wave_on_rival2 = Valul {0}: {1} eroi mărșăluiesc asupra stăpânei albastre. msg.wave_on_rival2 = Valul {0}: {1} eroi mărșăluiesc asupra stăpânei albastre.
msg.wave_warn_on_master = Cercetașii raportează: valul de eroi {0} sosește în 30 de secunde și merge spre stăpânul {1}. msg.wave_warn_on_master = Cercetașii raportează: valul de eroi {0} sosește în 30 de secunde și merge spre stăpânul {1}.
msg.wave_on_master = Valul {0}: {1} eroi mărșăluiesc spre stăpânul {2}. msg.wave_on_master = Valul {0}: {1} eroi mărșăluiesc spre stăpânul {2}.
msg.partner_out = Inima partenerului tău IA a căzut – luptă mai departe singur!
hud.rival1 = Verde hud.rival1 = Verde
hud.rival2 = Albastru hud.rival2 = Albastru
msg.creature_freed = Un {0} își rupe lanțurile și ți se alătură! msg.creature_freed = Un {0} își rupe lanțurile și ți se alătură!
+3
View File
@@ -704,6 +704,7 @@ msg.wave_on_rival1 = Волна {0}: {1} героев идут на зелёно
msg.wave_on_rival2 = Волна {0}: {1} героев идут на синюю хозяйку. msg.wave_on_rival2 = Волна {0}: {1} героев идут на синюю хозяйку.
msg.wave_warn_on_master = Разведчики докладывают: волна {0} будет через 30 секунд и идёт на мастера {1}. msg.wave_warn_on_master = Разведчики докладывают: волна {0} будет через 30 секунд и идёт на мастера {1}.
msg.wave_on_master = Волна {0}: {1} героев идут на мастера {2}. msg.wave_on_master = Волна {0}: {1} героев идут на мастера {2}.
msg.partner_out = Сердце вашего ИИ-союзника пало – сражайтесь в одиночку!
hud.rival1 = Зелёный hud.rival1 = Зелёный
hud.rival2 = Синяя hud.rival2 = Синяя
msg.creature_freed = Существо рвёт цепи и присоединяется к тебе: {0}! msg.creature_freed = Существо рвёт цепи и присоединяется к тебе: {0}!
@@ -1333,6 +1334,8 @@ demo.m2.wave = Две минуты прошли: идут герои.
net.coop_editor = Кампания из редактора net.coop_editor = Кампания из редактора
net.coop_builtin = Встроенная кампания net.coop_builtin = Встроенная кампания
net.coop_league = Союз против Светлого Дозора net.coop_league = Союз против Светлого Дозора
net.coop_solo = ИИ-союзник
net.coop_solo_started = Кооператив без сервера: ИИ-союзник строит своё подземелье, идёт к вашему флагу атаки и помогает золотом.
net.coop_editor_mission = Кампания из редактора {0}: миссия {1} ({2}) net.coop_editor_mission = Кампания из редактора {0}: миссия {1} ({2})
net.coop_editor_none = Ни одна миссия кампании из редактора не подходит для кооператива (карта не меньше 72x72, цель помимо мастеров-соперников) net.coop_editor_none = Ни одна миссия кампании из редактора не подходит для кооператива (карта не меньше 72x72, цель помимо мастеров-соперников)
blk.until = Повторять, пока не blk.until = Повторять, пока не
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Kooperácia
net.coop_editor = Kampaň z editora net.coop_editor = Kampaň z editora
net.coop_builtin = Vstavaná kampaň net.coop_builtin = Vstavaná kampaň
net.coop_league = Liga proti Svetlej stráži net.coop_league = Liga proti Svetlej stráži
net.coop_solo = AI partner
net.coop_solo_started = Kooperácia bez servera: tvoj AI partner si stavia vlastnú kobku, nasleduje tvoju útočnú vlajku a pomáha zlatom.
net.coop_editor_mission = Kampaň z editora {0}: misia {1} ({2}) net.coop_editor_mission = Kampaň z editora {0}: misia {1} ({2})
net.coop_editor_none = Žiadna misia kampane z editora sa nehodí na kooperáciu (mapa aspoň 72x72, iný cieľ než súperiaci majstri) net.coop_editor_none = Žiadna misia kampane z editora sa nehodí na kooperáciu (mapa aspoň 72x72, iný cieľ než súperiaci majstri)
net.join = Pripojiť net.join = Pripojiť
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = Vlna {0}: hrdinovia ({1}) tiahnu na modrú majsterku.
msg.wave_warn_on_master = Prieskumníci hlásia: vlna hrdinov {0} príde o 30 sekúnd a mieri na pána {1}. msg.wave_warn_on_master = Prieskumníci hlásia: vlna hrdinov {0} príde o 30 sekúnd a mieri na pána {1}.
msg.wave_on_master = Vlna {0}: {1} hrdinov pochoduje na pána {2}. msg.wave_on_master = Vlna {0}: {1} hrdinov pochoduje na pána {2}.
msg.partner_out = Srdce tvojho AI partnera padlo – bojuj ďalej sám!
hud.rival1 = Zelený hud.rival1 = Zelený
hud.rival2 = Modrá hud.rival2 = Modrá
msg.creature_freed = Tvor roztrhol reťaze a pridal sa k tebe: {0}! msg.creature_freed = Tvor roztrhol reťaze a pridal sa k tebe: {0}!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Ко-оп кампања
net.coop_editor = Кампања уређивача net.coop_editor = Кампања уређивача
net.coop_builtin = Уграђена кампања net.coop_builtin = Уграђена кампања
net.coop_league = Савез против Светлостраже net.coop_league = Савез против Светлостраже
net.coop_solo = АИ партнер
net.coop_solo_started = Сарадња без сервера: твој АИ партнер гради сопствену тамницу, прати твоју заставу напада и помаже златом.
net.coop_editor_mission = Кампања уређивача {0}: мисија {1} ({2}) net.coop_editor_mission = Кампања уређивача {0}: мисија {1} ({2})
net.coop_editor_none = Ниједна мисија кампање уређивача не одговара ко-оп игри (мапа најмање 72x72, циљ осим супарничких господара) net.coop_editor_none = Ниједна мисија кампање уређивача не одговара ко-оп игри (мапа најмање 72x72, циљ осим супарничких господара)
net.join = Придружи net.join = Придружи
@@ -709,6 +711,7 @@ msg.wave_on_rival1 = Талас {0}: хероји ({1}) марширају на
msg.wave_on_rival2 = Талас {0}: хероји ({1}) марширају на плаву господарицу. msg.wave_on_rival2 = Талас {0}: хероји ({1}) марширају на плаву господарицу.
msg.wave_warn_on_master = Извиђачи јављају: талас јунака {0} стиже за 30 секунди и креће на господара {1}. msg.wave_warn_on_master = Извиђачи јављају: талас јунака {0} стиже за 30 секунди и креће на господара {1}.
msg.wave_on_master = Талас {0}: {1} јунака маршира на господара {2}. msg.wave_on_master = Талас {0}: {1} јунака маршира на господара {2}.
msg.partner_out = Срце твог АИ партнера је пало – бори се даље сам!
hud.rival1 = Зелени hud.rival1 = Зелени
hud.rival2 = Плава hud.rival2 = Плава
msg.creature_freed = Раскида окове и придружује ти се: {0}! msg.creature_freed = Раскида окове и придружује ти се: {0}!
+3
View File
@@ -662,6 +662,8 @@ net.coop_create = Co-op-kampanj
net.coop_editor = Egen kampanj net.coop_editor = Egen kampanj
net.coop_builtin = Inbyggd kampanj net.coop_builtin = Inbyggd kampanj
net.coop_league = Förbundet mot Ljusvakten net.coop_league = Förbundet mot Ljusvakten
net.coop_solo = AI-partner
net.coop_solo_started = Samarbete utan server: din AI-partner bygger en egen fängelsehåla, följer din attackflagga och hjälper till med guld.
net.coop_editor_mission = Egen kampanj {0}: uppdrag {1} ({2}) net.coop_editor_mission = Egen kampanj {0}: uppdrag {1} ({2})
net.coop_editor_none = Inget uppdrag i redigerarens kampanj passar för co-op (karta minst 72x72, ett mål utöver rivaliserande mästare) net.coop_editor_none = Inget uppdrag i redigerarens kampanj passar för co-op (karta minst 72x72, ett mål utöver rivaliserande mästare)
net.join = Gå med net.join = Gå med
@@ -709,6 +711,7 @@ 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. msg.wave_on_rival2 = Våg {0}: {1} hjältar marscherar mot den blå mästaren.
msg.wave_warn_on_master = Spejare rapporterar: hjältevåg {0} kommer om 30 sekunder och går mot mästare {1}. msg.wave_warn_on_master = Spejare rapporterar: hjältevåg {0} kommer om 30 sekunder och går mot mästare {1}.
msg.wave_on_master = Våg {0}: {1} hjältar marscherar mot mästare {2}. msg.wave_on_master = Våg {0}: {1} hjältar marscherar mot mästare {2}.
msg.partner_out = Din AI-partners hjärta har fallit – kämpa vidare ensam!
hud.rival1 = Grön hud.rival1 = Grön
hud.rival2 = Blå hud.rival2 = Blå
msg.creature_freed = En {0} bryter sina kedjor och ansluter sig till dig! msg.creature_freed = En {0} bryter sina kedjor och ansluter sig till dig!
+67 -2
View File
@@ -343,6 +343,16 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
menu.error = Some(i18n::tr("net.coop_editor_none")); menu.error = Some(i18n::tr("net.coop_editor_none"));
} }
} }
MenuAction::MatchCoopSolo(doc, mission) => {
// the open lobby connection is not needed: the game runs on this computer alone
if app.start_offline_coop(doc, mission, None, Vec::new()) {
menu.page = None;
menu.error = None;
app.set_status(i18n::tr("net.coop_solo_started"), now);
} else {
menu.error = Some(i18n::tr("net.coop_editor_none"));
}
}
MenuAction::MatchTakeSide(side) => app.client.take_side(side), MenuAction::MatchTakeSide(side) => app.client.take_side(side),
MenuAction::MatchSetAi(side, ai) => app.client.set_ai(side, ai), MenuAction::MatchSetAi(side, ai) => app.client.set_ai(side, ai),
MenuAction::MatchSetTeam(side, team, own) => app.client.set_team(if own { None } else { Some(side) }, team), MenuAction::MatchSetTeam(side, team, own) => app.client.set_team(if own { None } else { Some(side) }, team),
@@ -500,6 +510,7 @@ struct App {
coop: bool, coop: bool,
coop_editor: bool, // the co-op match plays a mission of an editor campaign coop_editor: bool, // the co-op match plays a mission of an editor campaign
coop_run: Option<CoopRun>, // ... and this is where the run through that campaign stands coop_run: Option<CoopRun>, // ... and this is where the run through that campaign stands
offline: bool, // a co-op game played without the server: the other seat is an AI partner
fps: f32, fps: f32,
renderer: String, renderer: String,
frame_ms: f32, frame_ms: f32,
@@ -624,9 +635,32 @@ impl App {
self.match_mode = false; self.match_mode = false;
self.coop = false; self.coop = false;
self.coop_editor = false; self.coop_editor = false;
self.offline = false;
self.client.on_disconnect(); self.client.on_disconnect();
} }
// A co-op mission without the server: built like a match start with the player on side 0 and an
// AI partner on side 2, then the world runs on this computer alone (no turns, no relay).
// `doc` is an editor campaign (None: a built-in co-op mission); `run_seed`/`carry` continue a run.
fn start_offline_coop(&mut self, doc: Option<campaign_doc::CampaignDoc>, mission: usize, run_seed: Option<u32>, carry: Vec<(u8, campaign::Carry)>) -> bool {
let size = match &doc {
Some(d) => d.missions.get(mission).map(|m| m.size),
None => campaign::missions().get(mission).map(|m| m.size),
};
let Some((map_w, map_h)) = size else { return false };
let next = run_seed.is_some();
let seed = run_seed.unwrap_or_else(|| next_seed(self.seed));
let editor = doc.is_some();
let info = StartInfo { seed, map_w, map_h, difficulty: self.settings.difficulty, sides: vec![0, 2], local_side: Some(0), mission: Some(mission), ai: vec![2], doc, editor, teams: Vec::new(), next, carry };
if !self.start_match_game(info) {
return false;
}
self.game.end_match();
self.offline = true;
self.recorded = false;
true
}
// the shared part of a co-op start (built-in or editor mission): the masters, AI seats, settings // the shared part of a co-op start (built-in or editor mission): the masters, AI seats, settings
fn start_coop_game(&mut self, mut game: Game, info: &StartInfo) -> bool { fn start_coop_game(&mut self, mut game: Game, info: &StartInfo) -> bool {
let local = info.local_side.unwrap_or(0); let local = info.local_side.unwrap_or(0);
@@ -758,7 +792,7 @@ async fn main() {
let seed = (miniquad::date::now() * 1000.0) as u32 | 1; let seed = (miniquad::date::now() * 1000.0) as u32 | 1;
// behind the title menu: the endless bridge battle // behind the title menu: the endless bridge battle
let (game, cam) = Game::new_demo(seed); let (game, cam) = Game::new_demo(seed);
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false, coop_editor: false, coop_run: None, fps: 0.0, renderer: platform::renderer(), frame_ms: 0.0, frame_hist: [0.0; FRAME_HIST], frame_hist_pos: 0, custom_campaign: None, custom_progress: None, custom_mission_index: None, custom_next_index: None, editor: None, editor_testing: false, editor_test_only: false, editor_public_pending: false, editor_loading: None, pending_briefing: None }; let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false, coop_editor: false, coop_run: None, offline: false, fps: 0.0, renderer: platform::renderer(), frame_ms: 0.0, frame_hist: [0.0; FRAME_HIST], frame_hist_pos: 0, custom_campaign: None, custom_progress: None, custom_mission_index: None, custom_next_index: None, editor: None, editor_testing: false, editor_test_only: false, editor_public_pending: false, editor_loading: None, pending_briefing: None };
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(); menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
apply_game_settings(&mut app.game, &app.settings); apply_game_settings(&mut app.game, &app.settings);
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None }; let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
@@ -1471,6 +1505,7 @@ app.pending_briefing = briefing_of(&menu.campaign_doc, i);
| MenuAction::MatchAbort | MenuAction::MatchAbort
| MenuAction::MatchLeave | MenuAction::MatchLeave
| MenuAction::MatchCoopEditorCreate(..) | MenuAction::MatchCoopEditorCreate(..)
| MenuAction::MatchCoopSolo(..)
| MenuAction::MatchChat(..) | MenuAction::MatchChat(..)
| MenuAction::MatchEditField(..)) => account_action(a, &mut cloud, &mut menu, &mut app, now), | MenuAction::MatchEditField(..)) => account_action(a, &mut cloud, &mut menu, &mut app, now),
MenuAction::EditName => { MenuAction::EditName => {
@@ -1517,13 +1552,41 @@ app.pending_briefing = briefing_of(&menu.campaign_doc, i);
// a co-op mission of an editor campaign is over: the host starts the next one (or the // a co-op mission of an editor campaign is over: the host starts the next one (or the
// same one again) for everybody; the others wait for it // same one again) for everybody; the others wait for it
UiAction::EndNext if app.coop_editor && app.game.local_side == 0 && app.coop_run.as_ref().map_or(false, |r| r.next.is_some() && !r.sent) => { UiAction::EndNext if app.coop_editor && app.game.local_side == 0 && app.coop_run.as_ref().map_or(false, |r| r.next.is_some() && !r.sent) => {
if let Some(run) = app.coop_run.as_mut() { if app.offline {
// no relay: the next mission of the run starts here at once
let run = app.coop_run.take().expect("a co-op run");
let next = run.next.unwrap_or(run.mission);
if app.start_offline_coop(Some(run.doc), next, Some(run.run_seed), run.carry_out) {
ui.log.clear();
autosave_t = 0.0;
}
} else if let Some(run) = app.coop_run.as_mut() {
if let Some(next) = run.next { if let Some(next) = run.next {
app.client.next_mission(next, run.run_seed, &run.carry_out); app.client.next_mission(next, run.run_seed, &run.carry_out);
run.sent = true; run.sent = true;
} }
} }
} }
// a built-in co-op mission without the server: the next one after a victory, else again
UiAction::EndNext if app.offline && !app.coop_editor => {
let i = app.game.mission.as_ref().map_or(0, |m| m.index);
let next = if app.game.state == 1 { campaign::next_coop_mission(i, 1) } else { i };
if next <= i && app.game.state == 1 {
app.leave_match();
menu.page = Some(Page::Title);
app.seed = next_seed(app.seed);
(app.game, app.cam) = Game::new_demo(app.seed);
} else if app.start_offline_coop(None, next, None, Vec::new()) {
ui.log.clear();
autosave_t = 0.0;
}
}
UiAction::EndCampaign if app.offline && !app.coop_editor => {
app.leave_match();
menu.page = Some(Page::Title);
app.seed = next_seed(app.seed);
(app.game, app.cam) = Game::new_demo(app.seed);
}
UiAction::EndNext if app.coop_editor && app.coop_run.as_ref().map_or(false, |r| r.next.is_some()) => {} UiAction::EndNext if app.coop_editor && app.coop_run.as_ref().map_or(false, |r| r.next.is_some()) => {}
// ... or the run is over (or the player leaves): the match ends, back to the lobby // ... or the run is over (or the player leaves): the match ends, back to the lobby
UiAction::EndNext | UiAction::EndCampaign if app.coop_editor => { UiAction::EndNext | UiAction::EndCampaign if app.coop_editor => {
@@ -1755,6 +1818,8 @@ app.pending_briefing = briefing_of(&campaign, i);
run.finish(&app.game); run.finish(&app.game);
} }
} }
} else if app.coop {
// a built-in co-op mission is no step of the single-player campaign: no progress
} else if app.custom_campaign.is_none() { } else if app.custom_campaign.is_none() {
if let (Some(i), false) = (app.game.mission.as_ref().map(|m| m.index), app.recorded) { if let (Some(i), false) = (app.game.mission.as_ref().map(|m| m.index), app.recorded) {
if app.game.state != 0 { if app.game.state != 0 {
+5
View File
@@ -279,6 +279,7 @@ enum Btn {
MatchCoopNext, MatchCoopNext,
MatchCoopCreate, MatchCoopCreate,
MatchCoopSource, // switch between the built-in campaign and the editor campaign MatchCoopSource, // switch between the built-in campaign and the editor campaign
MatchCoopSolo, // the selected co-op mission without the server, with an AI partner
MatchTake(usize), MatchTake(usize),
MatchAi(usize), MatchAi(usize),
MatchTeam(usize), // next team for that seat MatchTeam(usize), // next team for that seat
@@ -397,6 +398,7 @@ pub enum MenuAction {
MatchCreate, MatchCreate,
MatchCoopCreate(usize), MatchCoopCreate(usize),
MatchCoopEditorCreate(usize), // co-op match of a mission of `campaign_doc` MatchCoopEditorCreate(usize), // co-op match of a mission of `campaign_doc`
MatchCoopSolo(Option<crate::campaign_doc::CampaignDoc>, usize), // offline with an AI partner: editor campaign or built-in, mission
MatchTakeSide(u8), MatchTakeSide(u8),
MatchSetAi(u8, bool), MatchSetAi(u8, bool),
MatchSetTeam(u8, u8, bool), // side, team, the own seat MatchSetTeam(u8, u8, bool), // side, team, the own seat
@@ -798,6 +800,8 @@ impl Menu {
self.confirm = None; self.confirm = None;
MenuAction::Consumed MenuAction::Consumed
} }
Some(Btn::MatchCoopSolo) if self.coop_editor => MenuAction::MatchCoopSolo(Some(self.coop_doc().clone()), self.coop_editor_mission),
Some(Btn::MatchCoopSolo) => MenuAction::MatchCoopSolo(None, self.coop_mission),
Some(Btn::MatchCoopCreate) if self.coop_editor => MenuAction::MatchCoopEditorCreate(self.coop_editor_mission), Some(Btn::MatchCoopCreate) if self.coop_editor => MenuAction::MatchCoopEditorCreate(self.coop_editor_mission),
Some(Btn::MatchCoopCreate) => MenuAction::MatchCoopCreate(self.coop_mission), Some(Btn::MatchCoopCreate) => MenuAction::MatchCoopCreate(self.coop_mission),
Some(Btn::MatchReady) => MenuAction::MatchReady(!self.match_ui.ready), Some(Btn::MatchReady) => MenuAction::MatchReady(!self.match_ui.ready),
@@ -1992,6 +1996,7 @@ impl Menu {
items.push((Btn::MatchCoopPrev, "<".to_string())); items.push((Btn::MatchCoopPrev, "<".to_string()));
items.push((Btn::MatchCoopCreate, tr("net.coop_create"))); items.push((Btn::MatchCoopCreate, tr("net.coop_create")));
items.push((Btn::MatchCoopNext, ">".to_string())); items.push((Btn::MatchCoopNext, ">".to_string()));
items.push((Btn::MatchCoopSolo, tr("net.coop_solo")));
} }
// the button names what comes next // the button names what comes next
let next = match (editor_coop, self.coop_league) { let next = match (editor_coop, self.coop_league) {
+139 -3
View File
@@ -7,6 +7,10 @@ use crate::map::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub const RIVAL_CORE_HP: f32 = 2000.0; pub const RIVAL_CORE_HP: f32 = 2000.0;
// a co-op AI partner helps masters short of gold: every minute it may send some
pub const PARTNER_GIFT_EVERY: f32 = 60.0;
pub const PARTNER_GIFT_BELOW: f32 = 300.0;
pub const PARTNER_GIFT: f32 = 400.0;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Rival { pub struct Rival {
@@ -20,6 +24,8 @@ pub struct Rival {
pub attack_t: f32, pub attack_t: f32,
pub difficulty: u8, // 0 easy, 1 medium, 2 hard pub difficulty: u8, // 0 easy, 1 medium, 2 hard
pub power: f32, // grows with the time: level of the new creatures pub power: f32, // grows with the time: level of the new creatures
#[serde(default)]
pub gift_t: f32, // co-op AI partner: seconds until it looks whether its allies need gold
} }
struct Pace { struct Pace {
@@ -135,7 +141,7 @@ impl Game {
t.gold = 300; t.gold = 300;
t.res = 0; t.res = 0;
} }
self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: r, grow_t: 20.0, spawn_t: 5.0, attack_t: pace(difficulty).attack, difficulty, power: 0.0 }); self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: r, grow_t: 20.0, spawn_t: 5.0, attack_t: pace(difficulty).attack, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY });
for k in 0..3 { for k in 0..3 {
self.spawn_rival_unit(side, EK::Kobold, 1, k as f32); self.spawn_rival_unit(side, EK::Kobold, 1, k as f32);
} }
@@ -167,6 +173,14 @@ impl Game {
r.spawn_t -= dt; r.spawn_t -= dt;
r.attack_t -= dt; r.attack_t -= dt;
} }
// a co-op partner sends gold to allied masters who run short
if self.is_partner(side) {
self.rivals[k].gift_t -= dt;
if self.rivals[k].gift_t <= 0.0 {
self.rivals[k].gift_t = PARTNER_GIFT_EVERY;
self.partner_gift(side);
}
}
// growth: one more ring of ground (no imps needed) // growth: one more ring of ground (no imps needed)
if self.rivals[k].grow_t <= 0.0 { if self.rivals[k].grow_t <= 0.0 {
self.rivals[k].grow_t = p.grow * (0.8 + self.rng.f() * 0.4); self.rivals[k].grow_t = p.grow * (0.8 + self.rng.f() * 0.4);
@@ -294,7 +308,14 @@ impl Game {
let (x, y) = self.rivals[k].centre(); let (x, y) = self.rivals[k].centre();
self.fx(1, x, y, 0.0, 0.0, 1.0, 0xffffffff); self.fx(1, x, y, 0.0, 0.0, 1.0, 0xffffffff);
self.sfx(Sfx::Victory, x, y); self.sfx(Sfx::Victory, x, y);
self.msg(if side == 2 { "msg.rival1_out" } else { "msg.rival2_out" }, vec![]); let key = if self.is_partner(side) {
"msg.partner_out"
} else if side == 2 {
"msg.rival1_out"
} else {
"msg.rival2_out"
};
self.msg(key, vec![]);
// its creatures scatter and vanish, its ground is free // its creatures scatter and vanish, its ground is free
for e in self.ents.iter_mut() { for e in self.ents.iter_mut() {
if e.alive && !e.hero && e.side == side { if e.alive && !e.hero && e.side == side {
@@ -385,6 +406,41 @@ impl Game {
} }
// units of a rival master: fight enemies nearby, march when sent, otherwise guard home // units of a rival master: fight enemies nearby, march when sent, otherwise guard home
// an AI seat of a co-op game: an allied master who builds his own dungeon
pub fn is_partner(&self, side: u8) -> bool {
self.coop && self.ai_sides.contains(&side)
}
// the partner gives gold to every allied master below the limit
fn partner_gift(&mut self, side: u8) {
let me = self.me_side;
for s in self.master_sides() {
if self.is_out(s) || self.gold_of(s) >= PARTNER_GIFT_BELOW {
continue;
}
self.switch_to(s);
let got = self.add_gold(PARTNER_GIFT).floor();
if got >= 1.0 {
self.msg("msg.gold_received", vec![(got as i32).to_string(), format!("@net.side{side}")]);
self.sfx_ui(Sfx::Payday);
}
}
self.switch_to(me);
}
// a co-op partner's creatures follow the attack flag of the first master still standing
fn partner_flag(&self, side: u8) -> Option<usize> {
if !self.is_partner(side) {
return None;
}
let lead = self.master_sides().into_iter().find(|s| !self.is_out(*s))?;
if lead == self.me_side {
self.flag
} else {
self.masters.iter().find(|m| m.side == lead)?.flag
}
}
pub fn rival_ai(&mut self, e: &mut Ent, dt: f32) { pub fn rival_ai(&mut self, e: &mut Ent, dt: f32) {
if e.think <= 0.0 { if e.think <= 0.0 {
e.think = 0.5; e.think = 0.5;
@@ -397,18 +453,41 @@ impl Game {
} }
} }
} }
// two of three healthy creatures of a co-op partner join the attack at the allies' flag
let assist = if e.id % 3 != 0 && e.hp > e.max_hp * 0.4 { self.partner_flag(e.side) } else { None };
if let Job::Attack(id) = e.job { if let Job::Attack(id) = e.job {
let (hx, hy) = e.home.unwrap_or((e.x, e.y)); let (hx, hy) = e.home.unwrap_or((e.x, e.y));
let far = e.target.is_none() && (e.x - hx).powi(2) + (e.y - hy).powi(2) > 15.0 * 15.0; let far = e.target.is_none() && assist.is_none() && (e.x - hx).powi(2) + (e.y - hy).powi(2) > 15.0 * 15.0;
if !far && self.attack_job(e, id, dt) { if !far && self.attack_job(e, id, dt) {
return; return;
} }
e.job = Job::None; e.job = Job::None;
e.path.clear(); e.path.clear();
} }
if let Some(f) = assist {
// the flag at the humans' gate: attack the gate
let (fx, fy) = xy(f);
let (fx, fy) = (fx as f32 + 0.5, fy as f32 + 0.5);
let (gx, gy) = self.gate_center();
if e.target.is_none() && self.has_gate() && self.gate_hp > 0.0 && (fx - gx).powi(2) + (fy - gy).powi(2) <= 8.0 * 8.0 {
e.target = Some(1);
}
}
if self.march_on(e, dt) { if self.march_on(e, dt) {
return; return;
} }
if let Some(f) = assist {
let (fx, fy) = xy(f);
let d2 = (e.x - fx as f32 - 0.5).powi(2) + (e.y - fy as f32 - 0.5).powi(2);
if d2 > 2.5 * 2.5 && e.path.is_empty() && e.repath <= 0.0 {
e.repath = 1.5;
if let Some(p) = self.map.creature_path(flies(e.kind), e.tile(), f) {
e.path = p;
}
}
self.step_path(e, dt);
return;
}
// guard the own dungeon // guard the own dungeon
let (hx, hy) = e.home.unwrap_or((e.x, e.y)); let (hx, hy) = e.home.unwrap_or((e.x, e.y));
let d2 = (e.x - hx).powi(2) + (e.y - hy).powi(2); let d2 = (e.x - hx).powi(2) + (e.y - hy).powi(2);
@@ -538,4 +617,61 @@ mod tests {
g.spawn_wave(); g.spawn_wave();
assert!(g.ents[before..].iter().filter(|e| e.hero).all(|e| e.target.is_none()), "the wave comes for the player"); assert!(g.ents[before..].iter().filter(|e| e.hero).all(|e| e.target.is_none()), "the wave comes for the player");
} }
// the first mission of the co-op league, set up like an offline co-op game: player on side 0,
// an AI partner on side 2, no server
fn offline_league_game() -> Game {
let doc = crate::editor::coop_campaign::coop_campaign();
let mut g = crate::campaign::start_editor_coop_mission(&doc.missions[0], 0, 77);
g.coop = true;
assert!(g.start_match(&[0, 2], 0));
g.sync_ai_sides(&[2], 1);
g.end_match();
g
}
#[test]
fn an_offline_coop_partner_is_an_allied_ai_master() {
let mut g = offline_league_game();
assert!(g.netplay.is_none() && g.masters.is_empty());
assert!(g.rival_alive(2) && g.is_partner(2));
assert!(g.allied(0, 2) && g.allied(2, 0));
// short of gold: the partner helps out within a minute
g.gold = 0.0;
for _ in 0..(65 * 20) {
g.update(0.05);
}
assert!(g.msgs.iter().any(|m| m.key == "msg.gold_received"), "the partner sent gold");
assert_eq!(g.state, 0);
// the partner's creatures never fight the player's
assert!(!g.ents.iter().any(|e| e.alive && !e.hero && e.side == 0 && e.hp < e.max_hp && matches!(e.job, Job::Attack(_))));
}
#[test]
fn the_partner_follows_the_attack_flag() {
let mut g = offline_league_game();
for _ in 0..(90 * 20) {
g.update(0.05);
}
let (rx, ry) = g.rivals.iter().find(|r| r.side == 2).unwrap().centre();
// a flag on the partner's own ground a little away from its core
let f = (0..g.map.t.len())
.find(|&i| {
let (x, y) = xy(i);
let d2 = (x as f32 - rx).powi(2) + (y as f32 - ry).powi(2);
g.map.t[i].owner == Owner::of_side(2) && g.map.walk(x, y) && d2 > 16.0 && d2 < 36.0
})
.expect("partner ground");
g.flag = Some(f);
let (fx, fy) = xy(f);
let mut near = false;
for _ in 0..(30 * 20) {
g.update(0.05);
near |= g.ents.iter().any(|e| e.alive && e.side == 2 && !e.hero && e.id % 3 != 0 && (e.x - fx as f32 - 0.5).powi(2) + (e.y - fy as f32 - 0.5).powi(2) < 3.0 * 3.0);
if near {
break;
}
}
assert!(near, "partner creatures gather at the flag");
}
} }