Skip to content

Commit bcb3841

Browse files
committed
Merge branch 'sam-nuke-colors' of https://github.com/bibizu/openfrontio into sam-nuke-colors
2 parents 650bab7 + 663892a commit bcb3841

File tree

15 files changed

+811
-100
lines changed

15 files changed

+811
-100
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/lang/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,10 +796,18 @@
796796
"send_troops": "Send Troops",
797797
"send_gold": "Send Gold",
798798
"emotes": "Emojis",
799+
"moderation": "Moderation",
800+
"kick": "Kick player",
801+
"kicked": "Already kicked",
802+
"kick_confirm": "Kick {name}?\n\nThey won't be able to rejoin this game.",
799803
"arc_up": "Upward arc",
800804
"arc_down": "Downward arc",
801805
"flip_rocket_trajectory": "Flip rocket trajectory"
802806
},
807+
"kick_reason": {
808+
"duplicate_session": "Kicked from game (you may have been playing on another tab)",
809+
"lobby_creator": "Kicked by lobby creator"
810+
},
803811
"send_troops_modal": {
804812
"title_with_name": "Send Troops to {name}",
805813
"available_tooltip": "Your current available troops",

src/client/ClientGameRunner.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,9 @@ function showErrorModal(
770770
return;
771771
}
772772

773+
const translatedError = translateText(error);
774+
const displayError = translatedError === error ? error : translatedError;
775+
773776
const modal = document.createElement("div");
774777
modal.id = "error-modal";
775778

@@ -778,7 +781,7 @@ function showErrorModal(
778781
translateText(heading),
779782
`game id: ${gameID}`,
780783
`client id: ${clientID}`,
781-
`Error: ${error}`,
784+
`Error: ${displayError}`,
782785
message ? `Message: ${message}` : null,
783786
]
784787
.filter(Boolean)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { html, LitElement } from "lit";
2+
import { customElement, property } from "lit/decorators.js";
3+
import { EventBus } from "../../../core/EventBus";
4+
import { PlayerType } from "../../../core/game/Game";
5+
import { PlayerView } from "../../../core/game/GameView";
6+
import { actionButton } from "../../components/ui/ActionButton";
7+
import { SendKickPlayerIntentEvent } from "../../Transport";
8+
import { translateText } from "../../Utils";
9+
import kickIcon from "/images/ExitIconWhite.svg?url";
10+
import shieldIcon from "/images/ShieldIconWhite.svg?url";
11+
12+
@customElement("player-moderation-modal")
13+
export class PlayerModerationModal extends LitElement {
14+
@property({ attribute: false }) eventBus: EventBus | null = null;
15+
@property({ attribute: false }) myPlayer: PlayerView | null = null;
16+
@property({ attribute: false }) target: PlayerView | null = null;
17+
18+
@property({ type: Boolean }) open: boolean = false;
19+
@property({ type: Boolean }) alreadyKicked: boolean = false;
20+
21+
createRenderRoot() {
22+
return this;
23+
}
24+
25+
updated(changed: Map<string, unknown>) {
26+
if (changed.has("open") && this.open) {
27+
queueMicrotask(() =>
28+
(this.querySelector('[role="dialog"]') as HTMLElement | null)?.focus(),
29+
);
30+
}
31+
}
32+
33+
private closeModal() {
34+
this.dispatchEvent(new CustomEvent("close"));
35+
}
36+
37+
private handleKeydown = (e: KeyboardEvent) => {
38+
if (e.key === "Escape") {
39+
e.preventDefault();
40+
this.closeModal();
41+
}
42+
};
43+
44+
private canKick(my: PlayerView, other: PlayerView): boolean {
45+
return (
46+
my.isLobbyCreator() &&
47+
other !== my &&
48+
other.type() === PlayerType.Human &&
49+
!!other.clientID()
50+
);
51+
}
52+
53+
private handleKickClick = (e: MouseEvent) => {
54+
e.stopPropagation();
55+
56+
const my = this.myPlayer;
57+
const other = this.target;
58+
const eventBus = this.eventBus;
59+
60+
if (!my || !other) return;
61+
if (!this.canKick(my, other) || this.alreadyKicked) return;
62+
if (!eventBus) return;
63+
64+
const targetClientID = other.clientID();
65+
if (!targetClientID || targetClientID.length === 0) return;
66+
67+
const confirmed = confirm(
68+
translateText("player_panel.kick_confirm", { name: other.name() }),
69+
);
70+
if (!confirmed) return;
71+
72+
eventBus.emit(new SendKickPlayerIntentEvent(targetClientID));
73+
this.dispatchEvent(
74+
new CustomEvent("kicked", { detail: { playerId: String(other.id()) } }),
75+
);
76+
this.closeModal();
77+
};
78+
79+
render() {
80+
if (!this.open) return html``;
81+
82+
const my = this.myPlayer;
83+
const other = this.target;
84+
if (!my || !other) return html``;
85+
86+
const canKick = this.canKick(my, other);
87+
const alreadyKicked = this.alreadyKicked;
88+
89+
const moderationTitle = translateText("player_panel.moderation");
90+
const kickTitle = alreadyKicked
91+
? translateText("player_panel.kicked")
92+
: translateText("player_panel.kick");
93+
94+
return html`
95+
<div class="absolute inset-0 z-1200 flex items-center justify-center p-4">
96+
<div
97+
class="absolute inset-0 bg-black/60 rounded-2xl"
98+
@click=${() => this.closeModal()}
99+
></div>
100+
101+
<div
102+
role="dialog"
103+
aria-modal="true"
104+
aria-labelledby="moderation-title"
105+
class="relative z-10 w-full max-w-120 focus:outline-hidden"
106+
tabindex="0"
107+
@keydown=${this.handleKeydown}
108+
>
109+
<div
110+
class="rounded-2xl bg-zinc-900 p-5 shadow-2xl ring-1 ring-zinc-800 max-h-[90vh] text-zinc-200"
111+
@click=${(e: MouseEvent) => e.stopPropagation()}
112+
>
113+
<div class="mb-3 flex items-center justify-between relative">
114+
<div class="flex items-center gap-2">
115+
<img
116+
src=${shieldIcon}
117+
alt=""
118+
aria-hidden="true"
119+
class="h-5 w-5"
120+
/>
121+
<h2
122+
id="moderation-title"
123+
class="text-lg font-semibold tracking-tight text-zinc-100"
124+
>
125+
${moderationTitle}
126+
</h2>
127+
</div>
128+
129+
<button
130+
type="button"
131+
@click=${() => this.closeModal()}
132+
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-white shadow-sm hover:bg-red-500 transition-colors focus-visible:ring-2 focus-visible:ring-white/30 focus:outline-hidden"
133+
aria-label=${translateText("common.close")}
134+
title=${translateText("common.close")}
135+
>
136+
137+
</button>
138+
</div>
139+
140+
<div
141+
class="mb-4 rounded-xl border border-white/10 bg-white/5 px-3 py-2"
142+
>
143+
<div
144+
class="text-sm font-semibold text-zinc-100 truncate"
145+
title=${other.name()}
146+
>
147+
${other.name()}
148+
</div>
149+
</div>
150+
151+
<div class="grid auto-cols-fr grid-flow-col gap-1">
152+
${actionButton({
153+
onClick: this.handleKickClick,
154+
icon: kickIcon,
155+
iconAlt: "Kick",
156+
title: kickTitle,
157+
label: kickTitle,
158+
type: "red",
159+
disabled: alreadyKicked || !canKick,
160+
})}
161+
</div>
162+
</div>
163+
</div>
164+
</div>
165+
`;
166+
}
167+
}

src/client/graphics/layers/PlayerPanel.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ import { UIState } from "../UIState";
3737
import { ChatModal } from "./ChatModal";
3838
import { EmojiTable } from "./EmojiTable";
3939
import { Layer } from "./Layer";
40+
import "./PlayerModerationModal";
4041
import "./SendResourceModal";
4142
import allianceIcon from "/images/AllianceIconWhite.svg?url";
4243
import chatIcon from "/images/ChatIconWhite.svg?url";
4344
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
4445
import donateTroopIcon from "/images/DonateTroopIconWhite.svg?url";
4546
import emojiIcon from "/images/EmojiIconWhite.svg?url";
47+
import shieldIcon from "/images/ShieldIconWhite.svg?url";
4648
import stopTradingIcon from "/images/StopIconWhite.png?url";
4749
import targetIcon from "/images/TargetIconWhite.svg?url";
4850
import startTradingIcon from "/images/TradingIconWhite.png?url";
@@ -59,6 +61,7 @@ export class PlayerPanel extends LitElement implements Layer {
5961
private actions: PlayerActions | null = null;
6062
private tile: TileRef | null = null;
6163
private _profileForPlayerId: number | null = null;
64+
private kickedPlayerIDs = new Set<string>();
6265

6366
@state() private sendTarget: PlayerView | null = null;
6467
@state() private sendMode: "troops" | "gold" | "none" = "none";
@@ -67,6 +70,7 @@ export class PlayerPanel extends LitElement implements Layer {
6770
@state() private allianceExpirySeconds: number | null = null;
6871
@state() private otherProfile: PlayerProfile | null = null;
6972
@state() private suppressNextHide: boolean = false;
73+
@state() private moderationTarget: PlayerView | null = null;
7074

7175
private ctModal: ChatModal;
7276

@@ -142,6 +146,7 @@ export class PlayerPanel extends LitElement implements Layer {
142146
public show(actions: PlayerActions, tile: TileRef) {
143147
this.actions = actions;
144148
this.tile = tile;
149+
this.moderationTarget = null;
145150
this.isVisible = true;
146151
this.requestUpdate();
147152
}
@@ -156,6 +161,7 @@ export class PlayerPanel extends LitElement implements Layer {
156161
this.tile = tile;
157162
this.sendTarget = target;
158163
this.sendMode = "gold";
164+
this.moderationTarget = null;
159165
this.isVisible = true;
160166
this.requestUpdate();
161167
}
@@ -164,6 +170,7 @@ export class PlayerPanel extends LitElement implements Layer {
164170
this.isVisible = false;
165171
this.sendMode = "none";
166172
this.sendTarget = null;
173+
this.moderationTarget = null;
167174
this.requestUpdate();
168175
}
169176

@@ -305,6 +312,23 @@ export class PlayerPanel extends LitElement implements Layer {
305312
this.hide();
306313
}
307314

315+
private openModeration(e: MouseEvent, other: PlayerView) {
316+
e.stopPropagation();
317+
this.suppressNextHide = true;
318+
this.moderationTarget = other;
319+
}
320+
321+
private closeModeration = () => {
322+
this.moderationTarget = null;
323+
};
324+
325+
private handleModerationKicked = (e: CustomEvent<{ playerId?: string }>) => {
326+
const playerId = e.detail?.playerId;
327+
if (playerId) this.kickedPlayerIDs.add(String(playerId));
328+
this.closeModeration();
329+
this.hide();
330+
};
331+
308332
private handleToggleRocketDirection(e: Event) {
309333
e.stopPropagation();
310334
const next = !this.uiState.rocketDirectionUp;
@@ -419,6 +443,25 @@ export class PlayerPanel extends LitElement implements Layer {
419443
`;
420444
}
421445

446+
private renderModeration(my: PlayerView, other: PlayerView) {
447+
if (!my.isLobbyCreator()) return html``;
448+
const moderationTitle = translateText("player_panel.moderation");
449+
450+
return html`
451+
<ui-divider></ui-divider>
452+
<div class="grid auto-cols-fr grid-flow-col gap-1">
453+
${actionButton({
454+
onClick: (e: MouseEvent) => this.openModeration(e, other),
455+
icon: shieldIcon,
456+
iconAlt: "Moderation",
457+
title: moderationTitle,
458+
label: moderationTitle,
459+
type: "red",
460+
})}
461+
</div>
462+
`;
463+
}
464+
422465
private renderRelationPillIfNation(other: PlayerView, my: PlayerView) {
423466
if (other.type() !== PlayerType.Nation) return html``;
424467
if (other.isTraitor()) return html``;
@@ -804,6 +847,7 @@ export class PlayerPanel extends LitElement implements Layer {
804847
})}
805848
</div>`
806849
: ""}
850+
${this.renderModeration(my, other)}
807851
</div>
808852
`;
809853
}
@@ -914,6 +958,21 @@ export class PlayerPanel extends LitElement implements Layer {
914958
></send-resource-modal>
915959
`
916960
: ""}
961+
${this.moderationTarget
962+
? html`
963+
<player-moderation-modal
964+
.open=${true}
965+
.myPlayer=${my}
966+
.target=${this.moderationTarget}
967+
.eventBus=${this.eventBus}
968+
.alreadyKicked=${this.kickedPlayerIDs.has(
969+
String(this.moderationTarget.id()),
970+
)}
971+
@close=${this.closeModeration}
972+
@kicked=${this.handleModerationKicked}
973+
></player-moderation-modal>
974+
`
975+
: ""}
917976
918977
<ui-divider></ui-divider>
919978

0 commit comments

Comments
 (0)