Files
OpenFrontIO/src/core/game/GameUpdates.ts
T
VariableVince 69fc14f1fd Fix user having to click 3-4x times before building is deleted (#2195)
## Description:

There is a 5 second cooldown between building deletions (as was proposed
here:
https://github.com/openfrontio/OpenFrontIO/pull/1609#issuecomment-3146188728).

But this cooldown is only checked when in DeleteUnitExecution from
canDeleteUnit in PlayerImpl.
The delete button in RadialMenuElements always gets True back from
canDeleteUnit in GameView.

This results in a user seeing an enabled Delete button after just
deleting another building. And being able to click it, but no deletion
would happen. So they have to click 3-4 times before it 'works'.

Fix: also apply the 5s cooldown when deciding to disable the button. 

In the fix in canDeleteUnit in GameView, added +1 to the current game
tick. So the Delete button is enabled 1 tick before DeleteUnitExecution
would get True back from canDeleteUnit in PlayerImpl. Between seeing and
clicking the button is probably 1 tick anyway, and sometimes also after
clicking it and the execution it takes another tick so this is safe.
Also tested the other way around: after deletion the button should
immediately not be visible, while it can take 2 ticks before GameView
gets lastDeleteUnitTick back from PlayerImpl. But since the Radial menu
is closed after clicking the button, i couldn't open it fast enough
again to still see the button enabled (ie. it was always already neatly
disabled). And if someone with an auto-clicker were to be able to click
it still, their attempt would still be blocked by the cooldown check in
DeleteUnitExecution.

BEFORE:

https://github.com/user-attachments/assets/17242c39-982e-47c6-89f2-6fe22a296c7d

AFTER:

https://github.com/user-attachments/assets/dfe20111-3313-4ad2-95a9-0152c0270c08

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

tryout33

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
2025-10-14 11:18:50 -07:00

269 lines
5.2 KiB
TypeScript

import { AllPlayersStats, ClientID, Winner } from "../Schemas";
import {
EmojiMessage,
GameUpdates,
Gold,
MessageType,
NameViewData,
PlayerID,
PlayerType,
Team,
Tick,
TrainType,
UnitType,
} from "./Game";
import { TileRef, TileUpdate } from "./GameMap";
export interface GameUpdateViewData {
tick: number;
updates: GameUpdates;
packedTileUpdates: BigUint64Array;
playerNameViewData: Record<string, NameViewData>;
}
export interface ErrorUpdate {
errMsg: string;
stack?: string;
}
export enum GameUpdateType {
Tile,
Unit,
Player,
DisplayEvent,
DisplayChatEvent,
AllianceRequest,
AllianceRequestReply,
BrokeAlliance,
AllianceExpired,
AllianceExtension,
TargetPlayer,
Emoji,
Win,
Hash,
UnitIncoming,
BonusEvent,
RailroadEvent,
ConquestEvent,
EmbargoEvent,
}
export type GameUpdate =
| TileUpdateWrapper
| UnitUpdate
| PlayerUpdate
| AllianceRequestUpdate
| AllianceRequestReplyUpdate
| BrokeAllianceUpdate
| AllianceExpiredUpdate
| DisplayMessageUpdate
| DisplayChatMessageUpdate
| TargetPlayerUpdate
| EmojiUpdate
| WinUpdate
| HashUpdate
| UnitIncomingUpdate
| AllianceExtensionUpdate
| BonusEventUpdate
| RailroadUpdate
| ConquestUpdate
| EmbargoUpdate;
export interface BonusEventUpdate {
type: GameUpdateType.BonusEvent;
player: PlayerID;
tile: TileRef;
gold: number;
troops: number;
}
export enum RailType {
VERTICAL,
HORIZONTAL,
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT,
}
export interface RailTile {
tile: TileRef;
railType: RailType;
}
export interface RailroadUpdate {
type: GameUpdateType.RailroadEvent;
isActive: boolean;
railTiles: RailTile[];
}
export interface ConquestUpdate {
type: GameUpdateType.ConquestEvent;
conquerorId: PlayerID;
conqueredId: PlayerID;
gold: Gold;
}
export interface TileUpdateWrapper {
type: GameUpdateType.Tile;
update: TileUpdate;
}
export interface UnitUpdate {
type: GameUpdateType.Unit;
unitType: UnitType;
troops: number;
id: number;
ownerID: number;
lastOwnerID?: number;
// TODO: make these tilerefs
pos: TileRef;
lastPos: TileRef;
isActive: boolean;
reachedTarget: boolean;
retreating: boolean;
targetable: boolean;
targetUnitId?: number; // Only for trade ships
targetTile?: TileRef; // Only for nukes
health?: number;
constructionType?: UnitType;
missileTimerQueue: number[];
level: number;
hasTrainStation: boolean;
trainType?: TrainType; // Only for trains
loaded?: boolean; // Only for trains
}
export interface AttackUpdate {
attackerID: number;
targetID: number;
troops: number;
id: string;
retreating: boolean;
}
export interface PlayerUpdate {
type: GameUpdateType.Player;
nameViewData?: NameViewData;
clientID: ClientID | null;
name: string;
displayName: string;
id: PlayerID;
team?: Team;
smallID: number;
playerType: PlayerType;
isAlive: boolean;
isDisconnected: boolean;
tilesOwned: number;
gold: Gold;
troops: number;
allies: number[];
embargoes: Set<PlayerID>;
isTraitor: boolean;
traitorRemainingTicks?: number;
targets: number[];
outgoingEmojis: EmojiMessage[];
outgoingAttacks: AttackUpdate[];
incomingAttacks: AttackUpdate[];
outgoingAllianceRequests: PlayerID[];
alliances: AllianceView[];
hasSpawned: boolean;
betrayals?: bigint;
lastDeleteUnitTick: Tick;
}
export interface AllianceView {
id: number;
other: PlayerID;
createdAt: Tick;
expiresAt: Tick;
}
export interface AllianceRequestUpdate {
type: GameUpdateType.AllianceRequest;
requestorID: number;
recipientID: number;
createdAt: Tick;
}
export interface AllianceRequestReplyUpdate {
type: GameUpdateType.AllianceRequestReply;
request: AllianceRequestUpdate;
accepted: boolean;
}
export interface BrokeAllianceUpdate {
type: GameUpdateType.BrokeAlliance;
traitorID: number;
betrayedID: number;
}
export interface AllianceExpiredUpdate {
type: GameUpdateType.AllianceExpired;
player1ID: number;
player2ID: number;
}
export interface AllianceExtensionUpdate {
type: GameUpdateType.AllianceExtension;
playerID: number;
allianceID: number;
}
export interface TargetPlayerUpdate {
type: GameUpdateType.TargetPlayer;
playerID: number;
targetID: number;
}
export interface EmojiUpdate {
type: GameUpdateType.Emoji;
emoji: EmojiMessage;
}
export interface DisplayMessageUpdate {
type: GameUpdateType.DisplayEvent;
message: string;
messageType: MessageType;
goldAmount?: bigint;
playerID: number | null;
params?: Record<string, string | number>;
}
export type DisplayChatMessageUpdate = {
type: GameUpdateType.DisplayChatEvent;
key: string;
category: string;
target: string | undefined;
playerID: number | null;
isFrom: boolean;
recipient: string;
};
export interface WinUpdate {
type: GameUpdateType.Win;
allPlayersStats: AllPlayersStats;
winner: Winner;
}
export interface HashUpdate {
type: GameUpdateType.Hash;
tick: Tick;
hash: number;
}
export interface UnitIncomingUpdate {
type: GameUpdateType.UnitIncoming;
unitID: number;
message: string;
messageType: MessageType;
playerID: number;
}
export interface EmbargoUpdate {
type: GameUpdateType.EmbargoEvent;
event: "start" | "stop";
playerID: number;
embargoedID: number;
}