Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,10 @@
"betrayed_you": "{name} broke their alliance with you",
"about_to_expire": "Your alliance with {name} is about to expire!",
"alliance_expired": "Your alliance with {name} expired",
"alliances_disabled_warning_5min": "⚠ All alliances will be disabled in 5 minutes (at the 45 minute mark) ⚠",
"alliances_disabled_warning": "⚠ All alliances will be disabled at the 45 minute mark (1 minute left) ⚠",
"alliances_disabled": "All alliances have been dissolved. No new alliances can be formed.",
"alliances_ending_countdown": "Alliances ending in {time}",
"attack_request": "{name} requests you attack {target}",
"sent_emoji": "Sent {name}: {emoji}",
"renew_alliance": "Request to renew",
Expand Down
1 change: 1 addition & 0 deletions src/client/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ export function getMessageTypeClasses(type: MessageType): string {
case MessageType.HYDROGEN_BOMB_INBOUND:
case MessageType.SAM_MISS:
case MessageType.ALLIANCE_EXPIRED:
case MessageType.ALLIANCES_DISABLED:
case MessageType.NAVAL_INVASION_INBOUND:
case MessageType.RENEW_ALLIANCE:
return severityColors["warn"];
Expand Down
5 changes: 4 additions & 1 deletion src/client/graphics/PlayerIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ export function getPlayerIcons(
const userSettings = game.config().userSettings();
const isDarkMode = darkMode ?? userSettings?.darkMode() ?? false;
const emojisEnabled = userSettings?.emojis() ?? false;
const alliancesOff = alliancesDisabled ?? game.config().disableAlliances();
const cutoff = game.config().alliancesCutoffTick();
const pastCutoff = cutoff !== null && game.ticks() >= cutoff;
const alliancesOff =
alliancesDisabled ?? (game.config().disableAlliances() || pastCutoff);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const icons: PlayerIconDescriptor[] = [];

Expand Down
82 changes: 82 additions & 0 deletions src/client/graphics/layers/EventsDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export class EventsDisplay extends LitElement implements Layer {
[MessageCategory.ALLIANCE, false],
[MessageCategory.CHAT, false],
]);
@state() private allianceCutoffCountdown: string | null = null;
@state() private allianceCutoffCenterWarning: boolean = false;

@query(".events-container")
private _eventsContainer?: HTMLDivElement;
Expand Down Expand Up @@ -237,6 +239,7 @@ export class EventsDisplay extends LitElement implements Layer {
}

this.checkForAllianceExpirations();
this.updateAllianceCutoffCountdown();

const updates = this.game.updatesSinceLastTick();
if (updates) {
Expand Down Expand Up @@ -278,6 +281,9 @@ export class EventsDisplay extends LitElement implements Layer {
const myPlayer = this.game.myPlayer();
if (!myPlayer?.isAlive()) return;

const cutoff = this.game.config().alliancesCutoffTick();
if (cutoff !== null && this.game.ticks() >= cutoff) return;

Comment thread
Zixer1 marked this conversation as resolved.
Outdated
const currentAllianceIds = new Set<number>();

for (const alliance of myPlayer.alliances()) {
Expand Down Expand Up @@ -352,6 +358,32 @@ export class EventsDisplay extends LitElement implements Layer {
this.requestUpdate();
}

private updateAllianceCutoffCountdown() {
const cutoff = this.game.config().alliancesCutoffTick();
if (cutoff === null) {
this.allianceCutoffCountdown = null;
return;
}
const fiveMinStart = cutoff - 5 * 60 * 10;
const oneMinStart = cutoff - 60 * 10;
const ticks = this.game.ticks();
if (ticks < fiveMinStart || ticks >= cutoff) {
this.allianceCutoffCountdown = null;
return;
}
const remainingTicks = cutoff - ticks;
const seconds = Math.ceil(remainingTicks / 10);
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
this.allianceCutoffCountdown = `${mins}:${secs.toString().padStart(2, "0")}`;
if (ticks === oneMinStart && !this.allianceCutoffCenterWarning) {
this.allianceCutoffCenterWarning = true;
setTimeout(() => {
this.allianceCutoffCenterWarning = false;
}, 5000);
}
}

private removeEvent(index: number) {
this.events = [
...this.events.slice(0, index),
Expand Down Expand Up @@ -799,6 +831,24 @@ export class EventsDisplay extends LitElement implements Layer {
transform: scale(1);
}
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.9);
}
15% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
85% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.9);
}
}
Comment thread
Zixer1 marked this conversation as resolved.
</style>
`;

Expand All @@ -818,6 +868,24 @@ export class EventsDisplay extends LitElement implements Layer {

return html`
${styles}
<!-- Alliance Cutoff Center Warning -->
${this.allianceCutoffCenterWarning
? html`
<div
class="fixed top-1/2 left-1/2 z-[10000] pointer-events-none"
style="animation: fadeInOut 5s ease-in-out forwards;"
>
<p
class="text-red-300/90 text-lg lg:text-2xl font-bold text-center m-0 whitespace-nowrap"
style="text-shadow: 0 0 4px #000, 0 0 4px #000, 0 0 4px #000, 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000;"
>
${translateText(
"events_display.alliances_disabled_warning",
)}
</p>
</div>
`
: ""}
<!-- Events Toggle (when hidden) -->
${this._hidden
? html`
Expand Down Expand Up @@ -889,6 +957,20 @@ export class EventsDisplay extends LitElement implements Layer {
</div>
</div>

<!-- Alliance Cutoff Countdown -->
${this.allianceCutoffCountdown
? html`
<div
class="w-full px-3 py-1.5 bg-amber-600/90 text-white text-xs lg:text-sm font-semibold text-center"
>
⚠ ${translateText(
"events_display.alliances_ending_countdown",
{ time: this.allianceCutoffCountdown },
)}
</div>
`
: ""}

<!-- Content Area -->
<div
class="bg-gray-800/92 backdrop-blur-sm max-h-[15vh] lg:max-h-[30vh] overflow-y-auto w-full h-full min-[1200px]:rounded-b-xl events-container"
Expand Down
6 changes: 5 additions & 1 deletion src/client/graphics/layers/NameLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,13 @@ export class NameLayer implements Layer {
}

public tick() {
// Precompute the first-place player for performance
this.firstPlace = getFirstPlacePlayer(this.game);

const cutoff = this.config.alliancesCutoffTick();
if (cutoff !== null && !this.alliancesDisabled && this.game.ticks() >= cutoff) {
this.alliancesDisabled = true;
}
Comment thread
Zixer1 marked this conversation as resolved.

for (const player of this.game.playerViews()) {
if (player.isAlive()) {
if (!this.seenPlayers.has(player)) {
Expand Down
1 change: 1 addition & 0 deletions src/core/configuration/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export interface Config {
structureMinDist(): number;
isReplay(): boolean;
allianceExtensionPromptOffset(): number;
alliancesCutoffTick(): Tick | null;
}

export interface Theme {
Expand Down
5 changes: 5 additions & 0 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1011,4 +1011,9 @@ export class DefaultConfig implements Config {
allianceExtensionPromptOffset(): number {
return 300; // 30 seconds before expiration
}

alliancesCutoffTick(): Tick | null {
if (this._gameConfig.disableAlliances) return 0;
return this.numSpawnPhaseTurns() + 45 * 60 * 10;
}
}
4 changes: 3 additions & 1 deletion src/core/execution/TribeExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export class TribeExecution implements Execution {
}

private acceptAllAllianceRequests() {
// Accept all alliance requests
const cutoff = this.mg.config().alliancesCutoffTick();
if (cutoff !== null && this.mg.ticks() >= cutoff) return;

for (const req of this.tribe.incomingAllianceRequests()) {
req.accept();
}
Expand Down
3 changes: 3 additions & 0 deletions src/core/execution/alliance/AllianceExtensionExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export class AllianceExtensionExecution implements Execution {
return;
}

const cutoff = mg.config().alliancesCutoffTick();
if (cutoff !== null && mg.ticks() >= cutoff) return;

const alliance = this.from.allianceWith(to);
if (!alliance) {
console.warn(
Expand Down
12 changes: 9 additions & 3 deletions src/core/execution/nation/NationAllianceBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export class NationAllianceBehavior {
private emojiBehavior: NationEmojiBehavior,
) {}

private isAlliancesBlocked(): boolean {
if (this.game.config().disableAlliances()) return true;
const cutoff = this.game.config().alliancesCutoffTick();
return cutoff !== null && this.game.ticks() >= cutoff;
}

handleAllianceRequests() {
if (this.game.config().disableAlliances()) return;
if (this.isAlliancesBlocked()) return;

for (const req of this.player.incomingAllianceRequests()) {
// Alliance Request intents created during the spawn phase are executed on
Expand All @@ -46,7 +52,7 @@ export class NationAllianceBehavior {
}

handleAllianceExtensionRequests() {
if (this.game.config().disableAlliances()) return;
if (this.isAlliancesBlocked()) return;

for (const alliance of this.player.alliances()) {
// Alliance expiration tracked by Events Panel, only human ally can click Request to Renew
Expand All @@ -63,7 +69,7 @@ export class NationAllianceBehavior {
}

maybeSendAllianceRequests(borderingEnemies: Player[]) {
if (this.game.config().disableAlliances()) return;
if (this.isAlliancesBlocked()) return;

// Only easy nations are allowed to send alliance requests to bots
const isAcceptablePlayerType = (p: Player) =>
Expand Down
2 changes: 2 additions & 0 deletions src/core/game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,7 @@ export enum MessageType {
RECEIVED_TROOPS_FROM_PLAYER,
CHAT,
RENEW_ALLIANCE,
ALLIANCES_DISABLED,
}

// Message categories used for filtering events in the EventsDisplay
Expand Down Expand Up @@ -1071,6 +1072,7 @@ export const MESSAGE_TYPE_CATEGORIES: Record<MessageType, MessageCategory> = {
[MessageType.ALLIANCE_BROKEN]: MessageCategory.ALLIANCE,
[MessageType.ALLIANCE_EXPIRED]: MessageCategory.ALLIANCE,
[MessageType.RENEW_ALLIANCE]: MessageCategory.ALLIANCE,
[MessageType.ALLIANCES_DISABLED]: MessageCategory.ALLIANCE,
[MessageType.SENT_GOLD_TO_PLAYER]: MessageCategory.TRADE,
[MessageType.RECEIVED_GOLD_FROM_PLAYER]: MessageCategory.TRADE,
[MessageType.RECEIVED_GOLD_FROM_TRADE]: MessageCategory.TRADE,
Expand Down
50 changes: 50 additions & 0 deletions src/core/game/GameImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ export class GameImpl implements Game {
for (const tile of waterChangedTiles) {
this.recordTileUpdate(tile);
}
this.checkAlliancesCutoff();
this._ticks++;
return this.updates;
}
Expand Down Expand Up @@ -817,6 +818,55 @@ export class GameImpl implements Game {
);
}

private checkAlliancesCutoff(): void {
const cutoff = this._config.alliancesCutoffTick();
if (cutoff === null) return;

const fiveMinWarningTick = cutoff - 5 * 60 * 10;
if (this._ticks === fiveMinWarningTick && fiveMinWarningTick > 0) {
for (const player of this._players.values()) {
if (player.isAlive()) {
this.displayMessage(
"events_display.alliances_disabled_warning_5min",
MessageType.ALLIANCES_DISABLED,
player.id(),
);
}
}
}

const oneMinWarningTick = cutoff - 60 * 10;
if (this._ticks === oneMinWarningTick && oneMinWarningTick > 0) {
for (const player of this._players.values()) {
if (player.isAlive()) {
this.displayMessage(
"events_display.alliances_disabled_warning",
MessageType.ALLIANCES_DISABLED,
player.id(),
);
}
}
}

if (this._ticks !== cutoff) return;

for (const alliance of [...this.alliances_]) {
this.expireAlliance(alliance);
}
for (const req of [...this.allianceRequests]) {
req.reject();
}
for (const player of this._players.values()) {
if (player.isAlive()) {
this.displayMessage(
"events_display.alliances_disabled",
MessageType.ALLIANCES_DISABLED,
player.id(),
);
}
}
}

public isSpawnImmunityActive(): boolean {
return (
this.config().numSpawnPhaseTurns() +
Expand Down
4 changes: 4 additions & 0 deletions src/core/game/PlayerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ export class PlayerImpl implements Player {
if (this.mg.config().disableAlliances()) {
return false;
}
const cutoff = this.mg.config().alliancesCutoffTick();
if (cutoff !== null && this.mg.ticks() >= cutoff) {
return false;
}
if (other === this) {
return false;
}
Expand Down
Loading
Loading