From 03685f3e1281c8e2c85f023b5f18871ab0a0255b Mon Sep 17 00:00:00 2001 From: gereon77 Date: Wed, 12 Jun 2024 15:18:39 +0200 Subject: [PATCH] Add custom option "Dragon revenge" and refactor handling after unit loss --- .../src/client/GameLogListComponent.tsx | 9 +++ .../src/client/GameSettingsComponent.tsx | 15 ++++ .../src/client/IngameComponent.tsx | 2 +- .../src/client/utils/SfxManager.ts | 52 ++++++++------ agot-bg-game-server/src/common/EntireGame.ts | 3 +- .../ingame-game-state/IngameGameState.ts | 72 +++++++++++++++++++ .../ExecuteLoanGameState.ts | 25 +++---- .../ResolveMarchOrderGameState.ts | 41 +++-------- .../ResolveSingleMarchOrderGameState.ts | 4 +- .../game-data-structure/GameLog.ts | 9 ++- .../game-data-structure/World.ts | 2 +- .../pay-debts-game-state/PayDebtsGameState.ts | 12 ++-- .../port-helper/PortHelper.ts | 4 +- .../WildlingCardEffectInTurnOrderGameState.ts | 16 ++--- .../WildlingsAttackGameState.ts | 20 +++--- .../CrowKillersWildlingVictoryGameState.ts | 6 +- .../MammothRidersWildlingVictoryGameState.ts | 6 +- ...heHordeDescendsWildlingVictoryGameState.ts | 6 +- .../common/lobby-game-state/LobbyGameState.ts | 4 ++ 19 files changed, 198 insertions(+), 110 deletions(-) diff --git a/agot-bg-game-server/src/client/GameLogListComponent.tsx b/agot-bg-game-server/src/client/GameLogListComponent.tsx index 75b13add8..6c235e00d 100644 --- a/agot-bg-game-server/src/client/GameLogListComponent.tsx +++ b/agot-bg-game-server/src/client/GameLogListComponent.tsx @@ -2072,6 +2072,15 @@ export default class GameLogListComponent extends Component{this.fogOfWar ? fogOfWarPlaceholder : region.name}.

; } + case "last-land-unit-transformed-to-dragon": { + const house = this.game.houses.get(data.house); + const unitType = unitTypes.get(data.transformedUnitType); + const region = this.world.regions.get(data.region); + + return <> + Dragon revenge: The last {unitType.name} of House {house.name} has been transformed into a Dragon in {region.name}. + ; + } } } diff --git a/agot-bg-game-server/src/client/GameSettingsComponent.tsx b/agot-bg-game-server/src/client/GameSettingsComponent.tsx index 89c9aae3b..be1bda23d 100644 --- a/agot-bg-game-server/src/client/GameSettingsComponent.tsx +++ b/agot-bg-game-server/src/client/GameSettingsComponent.tsx @@ -827,6 +827,21 @@ export default class GameSettingsComponent extends Component this.changeGameSettings(() => this.gameSettings.dragonWar = !this.gameSettings.dragonWar)} /> + + + If a player has only one remaining non-dragon land unit and no more castles, it will turn into a dragon. + }> + + } + checked={this.gameSettings.dragonRevenge} + onChange={() => this.changeGameSettings(() => this.gameSettings.dragonRevenge = !this.gameSettings.dragonRevenge)} + /> + {this.gameSettings.draftHouseCards && diff --git a/agot-bg-game-server/src/client/IngameComponent.tsx b/agot-bg-game-server/src/client/IngameComponent.tsx index fd358dd90..b380b03f0 100644 --- a/agot-bg-game-server/src/client/IngameComponent.tsx +++ b/agot-bg-game-server/src/client/IngameComponent.tsx @@ -978,7 +978,7 @@ export default class IngameComponent extends Component { - {(this.gameSettings.playerCount >= 8 || this.gameSettings.dragonWar) && + {this.ingame.isDragonGame &&
diff --git a/agot-bg-game-server/src/client/utils/SfxManager.ts b/agot-bg-game-server/src/client/utils/SfxManager.ts index 2c8784b56..512f4aadf 100644 --- a/agot-bg-game-server/src/client/utils/SfxManager.ts +++ b/agot-bg-game-server/src/client/utils/SfxManager.ts @@ -232,7 +232,7 @@ class SfxManager { return Promise.resolve(); } - return this.playNotification(notificationSound, this.gameClient.notificationsVolume); + return this.playNotification(notificationSound); } playVoteNotificationSound(): Promise { @@ -240,7 +240,7 @@ class SfxManager { return Promise.resolve(); } - return this.playNotification(voteSound, this.gameClient.notificationsVolume); + return this.playNotification(voteSound); } playNewMessageReceivedSound(): Promise { @@ -248,7 +248,7 @@ class SfxManager { return Promise.resolve(); } - return this.playNotification(ravenCallSound, this.gameClient.notificationsVolume); + return this.playNotification(ravenCallSound); } playGotTheme(): Promise { @@ -256,7 +256,7 @@ class SfxManager { return Promise.resolve(); } - return this.playMusic(introSound, this.gameClient.musicVolume); + return this.playMusic(introSound); } playCombatSound(attackerId?: string): Promise { @@ -265,7 +265,11 @@ class SfxManager { } const sound = attackerId ? houseThemes.tryGet(attackerId, combatSound) : combatSound; - return this.playMusic(sound, this.gameClient.musicVolume); + return this.playMusic(sound); + } + + get currentDragonStrengthIsLessOrEqualThanTwo(): boolean { + return (this.gameClient.entireGame?.ingameGameState?.game.currentDragonStrength ?? -1) <= 2; } playSoundWhenClickingMarchOrder(region: Region): Promise { @@ -280,7 +284,7 @@ class SfxManager { const hasSiegeEngines = army.some(u => u.type == siegeEngine); const hasDragons = army.some(u => u.type == dragon); - const files = hasDragons && (this.gameClient.entireGame?.ingameGameState?.game.currentDragonStrength ?? -1) <= 2 + const files = hasDragons && this.currentDragonStrengthIsLessOrEqualThanTwo ? soundsForSmallDragons : hasDragons ? soundsForBigDragons @@ -292,7 +296,7 @@ class SfxManager { ? soundsForShips : soundsForFootmenOnly; - return this.playRandomEffect(files, this.gameClient.sfxVolume, true); + return this.playRandomEffect(files, true); } playSoundForLogEvent(log: GameLogData): Promise { @@ -303,14 +307,14 @@ class SfxManager { switch(log.type) { case "doran-martell-asos-used": case "doran-used": - this.playEffect(hystericalLaughSound, this.gameClient.musicVolume, false); + this.playEffect(hystericalLaughSound, false); break; case "killed-after-combat": { const units = log.killed.map(ut => unitTypes.get(ut)); if (units.includes(dragon)) { - return this.playRandomEffect(soundsWhenDragonsAreDestroyed, this.gameClient.musicVolume, false); + return this.playRandomEffect(soundsWhenDragonsAreDestroyed, false); } else { - return this.playRandomEffect(soundsWhenUnitsAreDestroyedBySwords, this.gameClient.musicVolume, false); + return this.playRandomEffect(soundsWhenUnitsAreDestroyedBySwords, false); } break; } @@ -318,54 +322,62 @@ class SfxManager { const killed = _.concat(log.killedBecauseCantRetreat, log.killedBecauseWounded); const units = killed.map(ut => unitTypes.get(ut)); if (units.includes(dragon)) { - return this.playRandomEffect(soundsWhenDragonsAreDestroyed, this.gameClient.musicVolume, false); + return this.playRandomEffect(soundsWhenDragonsAreDestroyed, false); } break; } case "retreat-casualties-suffered": { const units = log.units.map(ut => unitTypes.get(ut)); if (units.includes(dragon)) { - return this.playRandomEffect(soundsWhenDragonsAreDestroyed, this.gameClient.musicVolume, false); + return this.playRandomEffect(soundsWhenDragonsAreDestroyed, false); } break; } + case "last-land-unit-transformed-to-dragon": { + this.playRandomEffect( + this.currentDragonStrengthIsLessOrEqualThanTwo + ? soundsForSmallDragons + : soundsForBigDragons + , false); + break; + } } return Promise.resolve(); } - private playRandomEffect(files: string[], volume: number, withCurrentPlayingCheck: boolean): Promise { + private playRandomEffect(files: string[], withCurrentPlayingCheck: boolean): Promise { const randomFile = pickRandom(files); if (!randomFile) { return Promise.resolve(); } - return this.playEffect(randomFile, volume, withCurrentPlayingCheck); + return this.playEffect(randomFile, withCurrentPlayingCheck); } - private playEffect(file: string, volume: number, withCurrentPlayingCheck: boolean): Promise { + private playEffect(file: string, withCurrentPlayingCheck: boolean): Promise { if (withCurrentPlayingCheck && this.currentlyPlayingSfx.length > 0) { return Promise.resolve(); } const audio = new Audio(file); this.currentlyPlayingSfx.push(audio); audio.onended = () => _.pull(this.currentlyPlayingSfx, audio); - audio.volume = volume; + audio.volume = this.gameClient.sfxVolume; return audio.play(); } - private playMusic(file: string, volume: number): Promise { + private playMusic(file: string): Promise { const audio = new Audio(file); this.currentlyPlayingMusic.push(audio); audio.onended = () => _.pull(this.currentlyPlayingMusic, audio); - audio.volume = volume; + audio.volume = this.gameClient.musicVolume; return audio.play(); } - private playNotification(file: string, volume: number): Promise { + private playNotification(file: string): Promise { const audio = new Audio(file); this.currentlyPlayingNotifications.push(audio); audio.onended = () => _.pull(this.currentlyPlayingNotifications, audio); - audio.volume = volume; + audio.volume = this.gameClient.notificationsVolume; return audio.play(); } } diff --git a/agot-bg-game-server/src/common/EntireGame.ts b/agot-bg-game-server/src/common/EntireGame.ts index 558be0659..748a576a4 100644 --- a/agot-bg-game-server/src/common/EntireGame.ts +++ b/agot-bg-game-server/src/common/EntireGame.ts @@ -54,7 +54,7 @@ export default class EntireGame extends GameState void; @@ -821,4 +821,5 @@ export interface GameSettings { holdVictoryPointsUntilEndOfRound: boolean; fogOfWar: boolean; dragonWar: boolean; + dragonRevenge: boolean; } diff --git a/agot-bg-game-server/src/common/ingame-game-state/IngameGameState.ts b/agot-bg-game-server/src/common/ingame-game-state/IngameGameState.ts index 0c2a97ea6..f2f80a103 100644 --- a/agot-bg-game-server/src/common/ingame-game-state/IngameGameState.ts +++ b/agot-bg-game-server/src/common/ingame-game-state/IngameGameState.ts @@ -46,6 +46,8 @@ import orders from "./game-data-structure/orders"; import { OrderOnMapProperties, UnitOnMapProperties } from "../../client/MapControls"; import houseCardAbilities from "./game-data-structure/house-card/houseCardAbilities"; import SnrError from "../../utils/snrError"; +import { TakeOverPort, findOrphanedShipsAndDestroyThem, isTakeControlOfEnemyPortRequired } from "./port-helper/PortHelper"; +import { dragon } from "./game-data-structure/unitTypes"; export const NOTE_MAX_LENGTH = 5000; @@ -54,6 +56,11 @@ export const enum ReplacementReason { CLOCK_TIMEOUT } +export interface UnitLossConsequence { + victoryConditionsFulfilled?: true; + takeOverPort?: TakeOverPort; +} + export default class IngameGameState extends GameState< EntireGame, WesterosGameState | PlanningGameState | ActionGameState | CancelledGameState | GameEndedGameState @@ -122,6 +129,12 @@ export default class IngameGameState extends GameState< return this.entireGame.gameSettings.fogOfWar; } + get isDragonGame(): boolean { + return this.entireGame.gameSettings.playerCount == 8 || + this.entireGame.gameSettings.dragonWar || + this.entireGame.gameSettings.dragonRevenge; + } + constructor(entireGame: EntireGame) { super(entireGame); @@ -1090,6 +1103,65 @@ export default class IngameGameState extends GameState< } } + // returns true, if game is over and calling state needs to exit from processing + processPossibleConsequencesOfUnitLoss(): UnitLossConsequence { + // Check for last unit in dragon revenge + if (this.entireGame.gameSettings.dragonRevenge) { + for (const house of this.game.houses.values) { + const noCastles = this.world.regions.values.filter(r => r.castleLevel > 0 && r.getController() == house).length == 0; + + if (noCastles) { + const nonDragonLandUnits = this.world.getUnitsOfHouse(house).filter(u => u.type.id != "ship" && u.type.id != "dragon"); + if (nonDragonLandUnits.length == 1) { + const unit = nonDragonLandUnits[0]; + this.log({ + type: "last-land-unit-transformed-to-dragon", + house: house.id, + transformedUnitType: unit.type.id, + region: unit.region.id + }, true); + this.transformUnits(unit.region, [unit], dragon); + } + } + } + } + + // Restore Pentos garrison + this.world.regionsThatRegainGarrison.forEach(staticRegion => { + const region = this.world.getRegion(staticRegion); + if (region.getController() == region.superControlPowerToken && region.garrison != staticRegion.startingGarrison) { + region.garrison = staticRegion.startingGarrison; + this.sendMessageToUsersWhoCanSeeRegion({ + type: "change-garrison", + region: region.id, + newGarrison: region.garrison + }, region); + this.log({ + type: "garrison-returned", + region: region.id, + strength: region.garrison + }); + } + }); + + // Destroy orphaned ships in ports + findOrphanedShipsAndDestroyThem(this, this.actionState); + + // Check for Port take over + const takeOverRequired = isTakeControlOfEnemyPortRequired(this); + if (takeOverRequired) { + return { takeOverPort: takeOverRequired }; + } + + // A unit loss may result in a win, if the lost unit + // was located in an enemy capital => check winning conditions + if (this.checkVictoryConditions()) { + return { victoryConditionsFulfilled: true }; + } + + return { }; + } + onServerMessage(message: ServerMessage): void { if (message.type == "supply-adjusted") { const supplies: [House, number][] = message.supplies.map(([houseId, supply]) => [this.game.houses.get(houseId), supply]); diff --git a/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-consolidate-power-game-state/execute-loan-game-state/ExecuteLoanGameState.ts b/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-consolidate-power-game-state/execute-loan-game-state/ExecuteLoanGameState.ts index bf6ba99eb..96b98b2c0 100644 --- a/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-consolidate-power-game-state/execute-loan-game-state/ExecuteLoanGameState.ts +++ b/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-consolidate-power-game-state/execute-loan-game-state/ExecuteLoanGameState.ts @@ -17,7 +17,7 @@ import LoyalMaesterGameState, { SerializedLoyalMaesterGameState } from "./loyal- import MasterAtArmsGameState, { SerializedMasterAtArmsGameState } from "./master-at-arms-game-state/MasterAtArmsGameState"; import SavvyStewardGameState, { SerializedSavvyStewardGameState } from "./savvy-steward-game-state/SavvyStewardGameState"; import SpymasterGameState, { SerializedSpymasterGameState } from "./spymaster-game-state/SpymasterGameState"; -import { findOrphanedShipsAndDestroyThem, isTakeControlOfEnemyPortGameStateRequired } from "../../../../../common/ingame-game-state/port-helper/PortHelper"; +import { isTakeControlOfEnemyPortRequired } from "../../../../../common/ingame-game-state/port-helper/PortHelper"; import TakeControlOfEnemyPortGameState, { SerializedTakeControlOfEnemyPortGameState } from "../../../take-control-of-enemy-port-game-state/TakeControlOfEnemyPortGameState"; import ActionGameState from "../../ActionGameState"; @@ -86,19 +86,12 @@ export default class ExecuteLoanGameState extends GameState { - const region = this.world.getRegion(staticRegion); - if (region.getController() == region.superControlPowerToken && region.garrison != staticRegion.startingGarrison) { - region.garrison = staticRegion.startingGarrison; - this.ingame.sendMessageToUsersWhoCanSeeRegion({ - type: "change-garrison", - region: region.id, - newGarrison: region.garrison - }, region); - this.ingameGameState.log({ - type: "garrison-returned", - region: region.id, - strength: region.garrison - }); - } - }) - // Gain Loyalty tokens this.ingameGameState.gainLoyaltyTokens(); - // ... destroy orphaned ships (e.g. caused by Arianne) - findOrphanedShipsAndDestroyThem(this.ingameGameState, this.actionGameState); - // ... check if ships can be converted - const analyzePortResult = isTakeControlOfEnemyPortGameStateRequired(this.ingameGameState); - if (analyzePortResult) { - this.setChildGameState(new TakeControlOfEnemyPortGameState(this)).firstStart(analyzePortResult.port, analyzePortResult.newController, house); + // Handle possible unit loss consequences (checks winning condition) + const consequence = this.ingame.processPossibleConsequencesOfUnitLoss(); + if (consequence.victoryConditionsFulfilled) { return; - } - - // ... check victory conditions - if (this.ingameGameState.checkVictoryConditions()) { + } else if (consequence.takeOverPort) { + this.setChildGameState(new TakeControlOfEnemyPortGameState(this)) + .firstStart(consequence.takeOverPort.port, consequence.takeOverPort.newController, house); return; } - // ... check if an other march order can be resolved + // Find next march order to resolve this.proceedNextResolveSingleMarchOrder(); } diff --git a/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-march-order-game-state/resolve-single-march-order-game-state/ResolveSingleMarchOrderGameState.ts b/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-march-order-game-state/resolve-single-march-order-game-state/ResolveSingleMarchOrderGameState.ts index 09f9f029a..8db363692 100644 --- a/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-march-order-game-state/resolve-single-march-order-game-state/ResolveSingleMarchOrderGameState.ts +++ b/agot-bg-game-server/src/common/ingame-game-state/action-game-state/resolve-march-order-game-state/resolve-single-march-order-game-state/ResolveSingleMarchOrderGameState.ts @@ -144,10 +144,10 @@ export default class ResolveSingleMarchOrderGameState extends GameState region.canRegainGarrison); } diff --git a/agot-bg-game-server/src/common/ingame-game-state/pay-debts-game-state/PayDebtsGameState.ts b/agot-bg-game-server/src/common/ingame-game-state/pay-debts-game-state/PayDebtsGameState.ts index 23f236521..8c7089f91 100644 --- a/agot-bg-game-server/src/common/ingame-game-state/pay-debts-game-state/PayDebtsGameState.ts +++ b/agot-bg-game-server/src/common/ingame-game-state/pay-debts-game-state/PayDebtsGameState.ts @@ -6,7 +6,6 @@ import {ServerMessage} from "../../../messages/ServerMessage"; import IngameGameState from "../IngameGameState"; import BetterMap from "../../../utils/BetterMap"; import ResolveSinglePayDebtGameState, { SerializedResolveSinglePayDebtGameState } from "./resolve-single-pay-debt-game-state/ResolveSinglePayDebtGameState"; -import { findOrphanedShipsAndDestroyThem, isTakeControlOfEnemyPortGameStateRequired } from "../port-helper/PortHelper"; import TakeControlOfEnemyPortGameState, { SerializedTakeControlOfEnemyPortGameState } from "../take-control-of-enemy-port-game-state/TakeControlOfEnemyPortGameState"; import ActionGameState from "../action-game-state/ActionGameState"; @@ -27,11 +26,12 @@ export default class PayDebtsGameState extends GameState r.type == port && r.units.size > 0 @@ -85,7 +85,7 @@ export function isTakeControlOfEnemyPortGameStateRequired(ingame: IngameGameStat throw new Error(`Port with id '${portRegion.id}' contains orphaned ships which should have been removed before!`); } -export interface TakeControlOfEnemyPortResult { +export interface TakeOverPort { port: Region; newController: House; } \ No newline at end of file diff --git a/agot-bg-game-server/src/common/ingame-game-state/westeros-game-state/wildlings-attack-game-state/WildlingCardEffectInTurnOrderGameState.ts b/agot-bg-game-server/src/common/ingame-game-state/westeros-game-state/wildlings-attack-game-state/WildlingCardEffectInTurnOrderGameState.ts index 1c98c194d..2839a0d92 100644 --- a/agot-bg-game-server/src/common/ingame-game-state/westeros-game-state/wildlings-attack-game-state/WildlingCardEffectInTurnOrderGameState.ts +++ b/agot-bg-game-server/src/common/ingame-game-state/westeros-game-state/wildlings-attack-game-state/WildlingCardEffectInTurnOrderGameState.ts @@ -2,7 +2,7 @@ import GameState from "../../../GameState"; import WildlingsAttackGameState from "./WildlingsAttackGameState"; import Game from "../../game-data-structure/Game"; import House from "../../game-data-structure/House"; -import { findOrphanedShipsAndDestroyThem, isTakeControlOfEnemyPortGameStateRequired, TakeControlOfEnemyPortResult } from "../../port-helper/PortHelper"; +import { TakeOverPort } from "../../port-helper/PortHelper"; import IngameGameState from "../../IngameGameState"; /** @@ -40,13 +40,11 @@ export default abstract class WildlingCardEffectInTurnOrderGameState { settings.randomVassalAssignment = false; } + if (settings.dragonWar && !this.settings.dragonWar) { + settings.dragonRevenge = true; + } + const hideOrRevealUserNames = settings.faceless != this.settings.faceless; this.entireGame.gameSettings = settings;