Skip to content

Commit

Permalink
Add custom option "Dragon revenge" and refactor handling after unit loss
Browse files Browse the repository at this point in the history
  • Loading branch information
gereon77 committed Jun 12, 2024
1 parent 45679a9 commit 03685f3
Show file tree
Hide file tree
Showing 19 changed files with 198 additions and 110 deletions.
9 changes: 9 additions & 0 deletions agot-bg-game-server/src/client/GameLogListComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,15 @@ export default class GameLogListComponent extends Component<GameLogListComponent
There was no further loyalty token available that could have been placed in <b>{this.fogOfWar ? fogOfWarPlaceholder : region.name}</b>.
</p>;
}
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 <>
<b>Dragon revenge</b>: The last <b>{unitType.name}</b> of House <b>{house.name}</b> has been transformed into a <b>Dragon</b> in <b>{region.name}</b>.
</>;
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions agot-bg-game-server/src/client/GameSettingsComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,21 @@ export default class GameSettingsComponent extends Component<GameSettingsCompone
onChange={() => this.changeGameSettings(() => this.gameSettings.dragonWar = !this.gameSettings.dragonWar)}
/>
</Col>
<Col xs="12">
<FormCheck
id="dragon-revenge-setting"
type="switch"
label={
<OverlayTrigger overlay={
<Tooltip id="dragon-revenge-tooltip">
If a player has only one remaining non-dragon land unit and no more castles, it will turn into a dragon.
</Tooltip>}>
<label htmlFor="dragon-revenge-setting">Dragon revenge</label>
</OverlayTrigger>}
checked={this.gameSettings.dragonRevenge}
onChange={() => this.changeGameSettings(() => this.gameSettings.dragonRevenge = !this.gameSettings.dragonRevenge)}
/>
</Col>
</Col>
{this.gameSettings.draftHouseCards && <Col xs="6" lg="auto" id="draft-decks-settings-col" className="no-gutters">
<Col xs="12">
Expand Down
2 changes: 1 addition & 1 deletion agot-bg-game-server/src/client/IngameComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ export default class IngameComponent extends Component<IngameComponentProps> {
</div>
</div>
</Row>
{(this.gameSettings.playerCount >= 8 || this.gameSettings.dragonWar) && <Row className="mx-0 mt-3">
{this.ingame.isDragonGame && <Row className="mx-0 mt-3">
<OverlayTrigger overlay={this.renderDragonStrengthTooltip()}
placement="auto">
<div>
Expand Down
52 changes: 32 additions & 20 deletions agot-bg-game-server/src/client/utils/SfxManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,31 +232,31 @@ class SfxManager {
return Promise.resolve();
}

return this.playNotification(notificationSound, this.gameClient.notificationsVolume);
return this.playNotification(notificationSound);
}

playVoteNotificationSound(): Promise<void> {
if (this.gameClient.muted) {
return Promise.resolve();
}

return this.playNotification(voteSound, this.gameClient.notificationsVolume);
return this.playNotification(voteSound);
}

playNewMessageReceivedSound(): Promise<void> {
if (this.gameClient.muted) {
return Promise.resolve();
}

return this.playNotification(ravenCallSound, this.gameClient.notificationsVolume);
return this.playNotification(ravenCallSound);
}

playGotTheme(): Promise<void> {
if (this.gameClient.muted) {
return Promise.resolve();
}

return this.playMusic(introSound, this.gameClient.musicVolume);
return this.playMusic(introSound);
}

playCombatSound(attackerId?: string): Promise<void> {
Expand All @@ -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<void> {
Expand All @@ -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
Expand All @@ -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<void> {
Expand All @@ -303,69 +307,77 @@ 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;
}
case "immediatly-killed-after-combat": {
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<void> {
private playRandomEffect(files: string[], withCurrentPlayingCheck: boolean): Promise<void> {
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<void> {
private playEffect(file: string, withCurrentPlayingCheck: boolean): Promise<void> {
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<void> {
private playMusic(file: string): Promise<void> {
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<void> {
private playNotification(file: string): Promise<void> {
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();
}
}
Expand Down
3 changes: 2 additions & 1 deletion agot-bg-game-server/src/common/EntireGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
draftHouseCards: false, thematicDraft: false, limitedDraft: false, randomDraft: false, blindDraft: false, selectedDraftDecks: HouseCardDecks.All,
mixedWesterosDeck1: false, cokWesterosPhase: false, fogOfWar: false, victoryPointsCountNeededToWin: 7, loyaltyTokenCountNeededToWin: 7, endless: false, faceless: false,
useVassalPositions: false, precedingMustering: false, randomStartPositions: false, initialLiveClock: 60,
noPrivateChats: false, tournamentMode: false, fixedClock: false, holdVictoryPointsUntilEndOfRound: false, dragonWar: false
noPrivateChats: false, tournamentMode: false, fixedClock: false, holdVictoryPointsUntilEndOfRound: false, dragonWar: false, dragonRevenge: false
};

onSendClientMessage?: (message: ClientMessage) => void;
Expand Down Expand Up @@ -821,4 +821,5 @@ export interface GameSettings {
holdVictoryPointsUntilEndOfRound: boolean;
fogOfWar: boolean;
dragonWar: boolean;
dragonRevenge: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -86,19 +86,12 @@ export default class ExecuteLoanGameState extends GameState<ResolveConsolidatePo
}

onExecuteLoanFinish(house: House): void {
// There might be orphaned CP* orders now so we remove them.
this.parentGameState.actionGameState.findOrphanedOrdersAndRemoveThem();
findOrphanedShipsAndDestroyThem(this.ingame, this.parentGameState.actionGameState);
// ... check if ships can be converted
const analyzePortResult = isTakeControlOfEnemyPortGameStateRequired(this.parentGameState.ingame);
if (analyzePortResult) {
this.setChildGameState(new TakeControlOfEnemyPortGameState(this)).firstStart(analyzePortResult.port, analyzePortResult.newController, house);
const consequence = this.ingame.processPossibleConsequencesOfUnitLoss();
if (consequence.victoryConditionsFulfilled) {
return;
}

// Faceless men may remove a unit in an enemy home town.
// If the enemy regains this castle, he might win the game.
if (this.ingame.checkVictoryConditions()) {
} else if (consequence.takeOverPort) {
this.setChildGameState(new TakeControlOfEnemyPortGameState(this))
.firstStart(consequence.takeOverPort.port, consequence.takeOverPort.newController, house);
return;
}

Expand All @@ -109,9 +102,9 @@ export default class ExecuteLoanGameState extends GameState<ResolveConsolidatePo
if (!previousHouse) {
throw new Error("previousHouse must be set here!");
}
const analyzePortResult = isTakeControlOfEnemyPortGameStateRequired(this.parentGameState.ingame);
if (analyzePortResult) {
this.setChildGameState(new TakeControlOfEnemyPortGameState(this)).firstStart(analyzePortResult.port, analyzePortResult.newController, previousHouse);
const takeOverRequired = isTakeControlOfEnemyPortRequired(this.parentGameState.ingame);
if (takeOverRequired) {
this.setChildGameState(new TakeControlOfEnemyPortGameState(this)).firstStart(takeOverRequired.port, takeOverRequired.newController, previousHouse);
return;
}

Expand Down
Loading

0 comments on commit 03685f3

Please sign in to comment.