Skip to content

Commit

Permalink
Fog improvement: Introduce unit visibility range
Browse files Browse the repository at this point in the history
  • Loading branch information
gereon77 committed Jul 6, 2024
1 parent 0ec6994 commit 93db562
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 56 deletions.
23 changes: 16 additions & 7 deletions agot-bg-game-server/src/client/GameSettingsComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LobbyGameState from "../common/lobby-game-state/LobbyGameState";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { allGameSetups, getGameSetupContainer } from "../common/ingame-game-state/game-data-structure/createGame";
import IngameGameState from "../common/ingame-game-state/IngameGameState";
import { isMobile } from "react-device-detect";

interface GameSettingsComponentProps {
gameClient: GameClient;
Expand Down Expand Up @@ -394,14 +395,22 @@ export default class GameSettingsComponent extends Component<GameSettingsCompone
label={
<OverlayTrigger overlay={
<Tooltip id="custom-balancing-tooltip">
A community proposal to {this.props.entireGame.isMotherOfDragons ?
"avoid an early gang up against Targaryen" :
"improve Tyrell's starting position"}. For details see<br/>
<a href="https://community.swordsandravens.net/viewtopic.php?t=6" target="_blank" rel="noopener noreferrer">
Tex&apos;s balance proposal
</a>.
{this.props.entireGame.isMotherOfDragons
? <>
A community proposal to avoid an early gang up against Targaryen.<br/>
For details see<br/>
<a href="https://www.boardgamedungeon.net/threads/my-balance-proposition-for-a-game-of-thrones-mother-of-dragons-expansion-targaryen.11/" target="_blank" rel="noopener noreferrer">
Tex&apos;s balance proposal
</a>.
</>
: <>
A community proposal to improve Tyrell&apos;s starting position by
adding a ship to Redwyne Straight&apos;s.
</>}
</Tooltip>}
delay={{show: 0, hide: 1500}}>
delay={{show: 0, hide: 1500}}
placement={isMobile ? "auto" : "bottom"}
>
<label htmlFor="custom-balancing-setting">Custom Balancing</label>
</OverlayTrigger>}
checked={this.gameSettings.customBalancing}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default class WesterosGameStateComponent extends Component<GameStateCompo
[ShiftingAmbitionsGameState, ShiftingAmbitionsComponent],
[NewInformationGameState, NewInformationComponent]
])}
{this.props.gameState.childGameState instanceof DarkWingsDarkWordsGameState && (
{this.props.gameState.childGameState instanceof DarkWingsDarkWordsGameState && !this.props.gameState.ingame.fogOfWar && (
<Row className="mt-3 justify-content-center">
<PossiblePowerTokenGainsComponent ingame={this.props.gameState.ingame} />
</Row>
Expand Down
23 changes: 12 additions & 11 deletions agot-bg-game-server/src/common/EntireGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
onCaptureSentryMessage?: (message: string, severity: "info" | "warning" | "error" | "fatal") => void;
onSaveGame?: (updateLastActive: boolean) => void;
onGetUser?: (userId: string) => Promise<StoredUserData | null>;
onBeforeGameStateChangedTransmitted?: () => void;

// Throttled saveGame so we don't spam the website client
saveGame: (updateLastActive: boolean) => void = _.throttle(this.privateSaveGame, 2000);
Expand Down Expand Up @@ -174,10 +173,10 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
// console.log("===GAME STATE CHANGED===");
// The GameState tree has been changed, broadcast a message to transmit to them
// the new game state.
if (this.ingameGameState) {
this.ingameGameState.updateVisibleRegions();
}
this.broadcastCustomToClients(u => {
if (this.onBeforeGameStateChangedTransmitted != null) {
this.onBeforeGameStateChangedTransmitted();
}
// To serialize the specific game state that has changed, the code serializes the entire
// game state tree and pick the appropriate serializedGameState.
// TODO: Find less wasteful way of doing this
Expand Down Expand Up @@ -222,6 +221,9 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
p.resetWaitedFor();
});

if (this.ingameGameState) {
this.ingameGameState.updateVisibleRegions(true);
}
this.notifyWaitedUsers();
return true;
}
Expand Down Expand Up @@ -642,8 +644,8 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
getPlayersInGame(): {userId: string; data: object}[] {
// eslint-disable-next-line @typescript-eslint/ban-types
const players: {userId: string; data: object}[] = [];
if (this.childGameState instanceof LobbyGameState) {
this.childGameState.players.forEach((user, house) => {
if (this.lobbyGameState) {
this.lobbyGameState.players.forEach((user, house) => {
// If the game is in "randomize house" mode, don't specify any houses in the PlayerInGame data
const playerData: {[key: string]: any} = {};

Expand All @@ -656,11 +658,10 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
data: playerData
});
});
} else if (this.childGameState instanceof IngameGameState) {
const ingame = this.childGameState as IngameGameState;
const waitedForUsers = ingame.getWaitedUsers();
} else if (this.ingameGameState) {
const waitedForUsers = this.ingameGameState.getWaitedUsers();

ingame.players.forEach((player, user) => {
this.ingameGameState.players.forEach((player, user) => {
// "Important chat rooms" are chat rooms where unseen messages will display
// a badge next to the game in the website.
// In this case, it's all private rooms with this player in it. The next line
Expand All @@ -674,7 +675,7 @@ export default class EntireGame extends GameState<null, LobbyGameState | IngameG
"house": player.house.id,
"waited_for": waitedForUsers.includes(user),
"important_chat_rooms": importantChatRooms.map(cr => cr.roomId),
"is_winner": ingame.childGameState instanceof GameEndedGameState ? ingame.childGameState.winner == player.house : false,
"is_winner": this.ingameGameState!.childGameState instanceof GameEndedGameState ? this.ingameGameState!.childGameState.winner == player.house : false,
"needed_for_vote": player.isNeededForVote
}
});
Expand Down
76 changes: 44 additions & 32 deletions agot-bg-game-server/src/common/ingame-game-state/IngameGameState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default class IngameGameState extends GameState<
@observable ordersOnBoard: BetterMap<Region, Order> = new BetterMap();
@observable visibleRegionsPerPlayer: BetterMap<Player, Region[]> = new BetterMap();
@observable publicVisibleRegions: Region[] = [];
unitVisibilityRange = 1;
unitVisibilityRangeModifier = 0;

votes: BetterMap<string, Vote> = new BetterMap();
@observable paused: Date | null = null;
Expand Down Expand Up @@ -132,10 +132,6 @@ export default class IngameGameState extends GameState<

constructor(entireGame: EntireGame) {
super(entireGame);

entireGame.onBeforeGameStateChangedTransmitted = () => {
this.updateVisibleRegions();
}
}

beginGame(housesToCreate: string[], futurePlayers: BetterMap<string, User>): void {
Expand Down Expand Up @@ -248,7 +244,6 @@ export default class IngameGameState extends GameState<
proceedPlanningGameState(planningRestrictions: PlanningRestriction[] = [], revealedWesterosCards: WesterosCard[] = []): void {
this.game.vassalRelations = new BetterMap();
this.broadcastVassalRelations();
this.updateVisibleRegions(true);
this.setChildGameState(new PlanningGameState(this)).firstStart(planningRestrictions, revealedWesterosCards);
}

Expand Down Expand Up @@ -337,7 +332,7 @@ export default class IngameGameState extends GameState<
});

if (this.fogOfWar) {
this.unitVisibilityRange = 1;
this.unitVisibilityRangeModifier = 0;
this.publicVisibleRegions = [];
this.entireGame.users.values.filter(u => u.connected).forEach(u => {
this.entireGame.sendMessageToClients([u], {
Expand All @@ -348,7 +343,7 @@ export default class IngameGameState extends GameState<
applyChangesNow: !this.players.has(u)
});
});
this.updateVisibleRegions(true);
// this.updateVisibleRegions(false) will be called by entire game after game state changed
}

if (this.game.turn > 1) {
Expand Down Expand Up @@ -1660,6 +1655,11 @@ export default class IngameGameState extends GameState<
return this.visibleRegionsPerPlayer.get(player);
}

calculateVisibilityRangeForRegion(region: Region): number {
const baseRange = Math.max(...region.units.values.map(u => u.type.baseVisibilityRange));
return Math.max(0, baseRange + this.unitVisibilityRangeModifier);
}

calculateVisibleRegionsForPlayer(player: Player | null): Region[] {
if (!this.fogOfWar || !player) {
return [];
Expand All @@ -1673,34 +1673,46 @@ export default class IngameGameState extends GameState<
const allRegionsWithControllers = this.world.getAllRegionsWithControllers();

// We begin with all controlled areas of own and vassal units. We definitely always see them
const result: Region[] = allRegionsWithControllers.filter(([_r, h]) => controlledHouses.includes(h)).map(([r, _h]) => r);
let regionsWithUnits = result.filter(r => r.units.size > 0);
const checkedRegions: Region[] = [];
const result: Set<Region> = new Set(allRegionsWithControllers.filter(([_r, h]) => controlledHouses.includes(h)).map(([r, _h]) => r));
const regionsWithUnits = Array.from(result).filter(r => r.units.size > 0);
const checkedRegions = new Set<Region>();

// Additionally we see regions adjacents to our regions with units
for(let i=0; i < this.unitVisibilityRange; i++) {
const additionalRegionsToCheck: Region[] = [];
for (let j=0; j<regionsWithUnits.length; j++) {
const region = regionsWithUnits[j];
let adjacent: Region[] = [];
if (!checkedRegions.includes(region)) {
adjacent = this.world.getNeighbouringRegions(region);
result.push(...adjacent);
additionalRegionsToCheck.push(...adjacent)
checkedRegions.push(region);
}
}
for (let i = 0; i < regionsWithUnits.length; i++) {
const rootRegion = regionsWithUnits[i];

const visibilityRange = this.calculateVisibilityRangeForRegion(rootRegion);
let rootRegions = [rootRegion];
for (let j = 0; j < visibilityRange; j++) {
const allAdjacents = new Set<Region>();
for (let k = 0; k < rootRegions.length; k++) {
const region = rootRegions[k];
if (checkedRegions.has(region)) {
continue;
}

if (this.unitVisibilityRange > 1) {
regionsWithUnits.push(...additionalRegionsToCheck);
regionsWithUnits = _.uniq(regionsWithUnits);
const adjacent = this.world.getNeighbouringRegions(region);
adjacent.forEach(r => {
allAdjacents.add(r);
result.add(r);
});
checkedRegions.add(region);
}
rootRegions = Array.from(allAdjacents);
}
}

result.push(...this.calculateRequiredVisibleRegionsForPlayer(player));
result.push(...this.publicVisibleRegions)
[...this.calculateRequiredVisibleRegionsForPlayer(player), ...this.publicVisibleRegions].forEach(r => result.add(r));

return _.uniq(result);
// Add ports of visible castles:
result.forEach(r => {
const port = this.world.getAdjacentPortOfCastle(r);
if (port) {
result.add(port);
}
});

return Array.from(result);
}

calculateRequiredVisibleRegionsForPlayer(player: Player): Region[] {
Expand Down Expand Up @@ -2308,7 +2320,7 @@ export default class IngameGameState extends GameState<
? this.visibleRegionsPerPlayer.entries.map(([p, regions]) => [p.user.id, regions.map(r => r.id)])
: this.visibleRegionsPerPlayer.entries.filter(([p, _regions]) => p.user == user).map(([p, regions]) => [p.user.id, regions.map(r => r.id)]),
publicVisibleRegions: this.publicVisibleRegions.map(r => r.id),
unitVisibilityRange: this.unitVisibilityRange,
unitVisibilityRangeModifier: this.unitVisibilityRangeModifier,
oldPlayerIds: this.oldPlayerIds,
replacerIds: this.replacerIds,
timeoutPlayerIds: this.timeoutPlayerIds,
Expand All @@ -2335,7 +2347,7 @@ export default class IngameGameState extends GameState<
data.visibleRegionsPerPlayer.map(([uid, rids]) => [ingameGameState.players.get(entireGame.users.get(uid)), rids.map(rid => ingameGameState.world.regions.get(rid))])
);
ingameGameState.publicVisibleRegions = data.publicVisibleRegions.map(rid => ingameGameState.world.regions.get(rid));
ingameGameState.unitVisibilityRange = data.unitVisibilityRange;
ingameGameState.unitVisibilityRangeModifier = data.unitVisibilityRangeModifier;
ingameGameState.oldPlayerIds = data.oldPlayerIds;
ingameGameState.replacerIds = data.replacerIds;
ingameGameState.timeoutPlayerIds = data.timeoutPlayerIds;
Expand Down Expand Up @@ -2382,7 +2394,7 @@ export interface SerializedIngameGameState {
players: SerializedPlayer[];
visibleRegionsPerPlayer: [string, string[]][];
publicVisibleRegions: string[];
unitVisibilityRange: number;
unitVisibilityRangeModifier: number;
oldPlayerIds: string[];
replacerIds: string[];
timeoutPlayerIds: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,6 @@ export default class PostCombatGameState extends GameState<
applyChangesNow: !this.combat.ingameGameState.players.has(u)
});
});
this.combat.ingameGameState.updateVisibleRegions(true);
}

this.combat.resolveMarchOrderGameState.onResolveSingleMarchOrderGameStateFinish(this.attacker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class UnitType {
walksOn: RegionKind;
canTransport: RegionKind | null;
canRetreat: boolean;
baseVisibilityRange: number;

constructor(
id: string,
Expand All @@ -18,7 +19,8 @@ export default class UnitType {
combatStrength: number,
combatStrengthOnAttackStructure: number | null = null,
canTransport: RegionKind | null = null,
canRetreat = true
canRetreat = true,
baseVisibilityRange = 1
) {
this.id = id;
this.name = name;
Expand All @@ -28,5 +30,6 @@ export default class UnitType {
this.combatStrength = combatStrength;
this.combatStrengthOnAttackStructure = combatStrengthOnAttackStructure;
this.canRetreat = canRetreat;
this.baseVisibilityRange = baseVisibilityRange;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const footman = new UnitType("footman", "Footman", "Adds 1 Combat Strengt
export const knight = new UnitType("knight", "Knight", "Adds 2 Combat Strength in battle. Costs 2 points of mustering (or 1 point if upgraded from a Footman).", RegionKind.LAND, 2);
export const siegeEngine = new UnitType("siege-engine", "Siege Engine", "Adds 4 Combat Strength when attacking (or supporting an attack against) an area containing a Castle or Stronghold, otherwise it adds 0. Siege Engines may not retreat when losing combat; they are destroyed instead. Costs 2 points of mustering (or 1 point if upgraded from a Footman).", RegionKind.LAND, 0, 4, null, false);
export const ship = new UnitType("ship", "Ship", "Adds 1 Combat Strength in battle. Costs 1 point of mustering.", RegionKind.SEA, 1, null, RegionKind.LAND);
export const dragon = new UnitType("dragon", "Dragon", "Adds 0-5 Combat Strength in battle, depending on the current position of the dragon strength token. Dragons are extraordinarily rare creatures and cannot be mustered like regular units. Instead, they are in play from the beginning of the game.", RegionKind.LAND, 0);
export const dragon = new UnitType("dragon", "Dragon", "Adds 0-5 Combat Strength in battle, depending on the current position of the dragon strength token. Dragons are extraordinarily rare creatures and cannot be mustered like regular units. Instead, they are in play from the beginning of the game.", RegionKind.LAND, 0, null, null, true, 2);

const unitTypes = new BetterMap<string, UnitType>([
[footman.id, footman],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import WesterosGameState from "../../westeros-game-state/WesterosGameState";

export default class DenseFogWesterosCardType extends WesterosCardType {
execute(westeros: WesterosGameState): void {
westeros.ingame.unitVisibilityRange = 0;
westeros.ingame.unitVisibilityRangeModifier = -1;
westeros.ingame.updateVisibleRegions(true);
westeros.onWesterosCardEnd();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default class Vote {
checkVoteFinished(): void {
if (this.state == VoteState.ACCEPTED) {
this.type.executeAccepted(this);
this.ingame.updateVisibleRegions();
this.ingame.updateVisibleRegions(true);
}
}

Expand Down
14 changes: 14 additions & 0 deletions agot-bg-game-server/src/server/serializedGameMigrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,20 @@ const serializedGameMigrations: {version: string; migrate: (serializeGamed: any)
serializedGame.gameSettings.houseCardsEvolutionRound = 5;
return serializedGame;
}
},
{
version: "122",
migrate: (serializedGame: any) => {
if (serializedGame.childGameState.type == "ingame") {
const ingame = serializedGame.childGameState;
if (ingame.unitVisibilityRange === undefined) {
ingame.unitVisibilityRangeModifier = 0;
} else {
ingame.unitVisibilityRangeModifier = ingame.unitVisibilityRange - 1;
}
}
return serializedGame;
}
}
];

Expand Down

0 comments on commit 93db562

Please sign in to comment.