mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:40:46 +00:00
Merge branch 'main' into svg-logo
This commit is contained in:
@@ -198,7 +198,35 @@ export class HostLobbyModal extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.option-card input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label.option-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.checkbox-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #aaa;
|
||||
border-radius: 6px;
|
||||
margin: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.option-card.selected .checkbox-icon {
|
||||
border-color: #4a9eff;
|
||||
background: #4a9eff;
|
||||
}
|
||||
|
||||
.option-card.selected .checkbox-icon::after {
|
||||
content: "✓";
|
||||
color: white;
|
||||
}
|
||||
/* HostLobbyModal css */
|
||||
.clipboard-icon {
|
||||
display: flex;
|
||||
@@ -358,6 +386,54 @@ export class HostLobbyModal extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Options -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">Options</div>
|
||||
<div class="option-cards">
|
||||
<label
|
||||
for="disable-bots"
|
||||
class="option-card ${this.disableBots ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-bots"
|
||||
@change=${this.handleDisableBotsChange}
|
||||
.checked=${this.disableBots}
|
||||
/>
|
||||
<div class="option-card-title">Disable Bots</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="disable-npcs"
|
||||
class="option-card ${this.disableNPCs ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
.checked=${this.disableNPCs}
|
||||
/>
|
||||
<div class="option-card-title">Disable NPCs</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="creative-mode"
|
||||
class="option-card ${this.creativeMode ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="creative-mode"
|
||||
@change=${this.handleCreativeModeChange}
|
||||
.checked=${this.creativeMode}
|
||||
/>
|
||||
<div class="option-card-title">Creative Mode</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lobby Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">
|
||||
@@ -371,30 +447,6 @@ export class HostLobbyModal extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-bots"
|
||||
@change=${this.handleDisableBotsChange}
|
||||
/>
|
||||
<label for="disable-bots">Disable Bots</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
/>
|
||||
<label for="disable-npcs">Disable NPCs</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="creative-mode"
|
||||
@change=${this.handleCreativeModeChange}
|
||||
/>
|
||||
<label for="creative-mode">Creative mode</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click=${this.startGame}
|
||||
|
||||
@@ -9,6 +9,9 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
@state() private message: string = "";
|
||||
@query("#lobbyIdInput") private lobbyIdInput!: HTMLInputElement;
|
||||
@state() private hasJoined = false;
|
||||
@state() private players: string[] = [];
|
||||
|
||||
private playersInterval = null;
|
||||
|
||||
static styles = css`
|
||||
.modal-overlay {
|
||||
@@ -180,6 +183,39 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.options-section {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 12px 24px 24px 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.players-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.player-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 4px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
@@ -216,6 +252,24 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
<div class="message-area ${this.message ? "show" : ""}">
|
||||
${this.message}
|
||||
</div>
|
||||
<div class="options-layout">
|
||||
<!-- Lobby Selection -->
|
||||
${this.hasJoined && this.players.length > 0
|
||||
? html`<div class="options-section">
|
||||
<div class="option-title">
|
||||
${this.players.length}
|
||||
${this.players.length === 1 ? "Player" : "Players"}
|
||||
</div>
|
||||
|
||||
<div class="players-list">
|
||||
${this.players.map(
|
||||
(player) =>
|
||||
html`<span class="player-tag">${player}</span>`
|
||||
)}
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
${!this.hasJoined
|
||||
? html`<button class="start-game-button" @click=${this.joinLobby}>
|
||||
Join Lobby
|
||||
@@ -233,6 +287,10 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
public close() {
|
||||
this.isModalOpen = false;
|
||||
this.lobbyIdInput.value = null;
|
||||
if (this.playersInterval) {
|
||||
clearInterval(this.playersInterval);
|
||||
this.playersInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
public closeAndLeave() {
|
||||
@@ -284,6 +342,7 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
|
||||
} else {
|
||||
this.message = "Lobby not found. Please check the ID and try again.";
|
||||
}
|
||||
@@ -293,4 +352,22 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
this.message = "An error occurred. Please try again.";
|
||||
});
|
||||
}
|
||||
|
||||
private async pollPlayers() {
|
||||
if (!this.lobbyIdInput?.value) return;
|
||||
|
||||
fetch(`/lobby/${this.lobbyIdInput.value}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.players = data.players.map((p) => p.username);
|
||||
})
|
||||
.catch((error) => {
|
||||
consolex.error("Error polling players:", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,36 @@ export class SinglePlayerModal extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.option-card input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label.option-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.checkbox-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #aaa;
|
||||
border-radius: 6px;
|
||||
margin: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.option-card.selected .checkbox-icon {
|
||||
border-color: #4a9eff;
|
||||
background: #4a9eff;
|
||||
}
|
||||
|
||||
.option-card.selected .checkbox-icon::after {
|
||||
content: "✓";
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
@@ -251,56 +281,54 @@ export class SinglePlayerModal extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-bots"
|
||||
@change=${this.handleDisableBotsChange}
|
||||
/>
|
||||
<label for="disable-bots">Disable Bots</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
/>
|
||||
<label for="disable-npcs">Disable NPCs</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-bots"
|
||||
@change=${this.handleDisableBotsChange}
|
||||
/>
|
||||
<label for="disable-bots">Disable Bots</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
/>
|
||||
<label for="disable-npcs">Disable NPCs</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="creative-mode"
|
||||
@change=${this.handleCreativeModeChange}
|
||||
/>
|
||||
<label for="creative-mode">Creative mode</label>
|
||||
</div>
|
||||
<!-- Game Options -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">Options</div>
|
||||
<div class="option-cards">
|
||||
<label
|
||||
for="disable-bots"
|
||||
class="option-card ${this.disableBots ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-bots"
|
||||
@change=${this.handleDisableBotsChange}
|
||||
.checked=${this.disableBots}
|
||||
/>
|
||||
<div class="option-card-title">Disable Bots</div>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="creative-mode"
|
||||
@change=${this.handleCreativeModeChange}
|
||||
/>
|
||||
<label for="creative-mode">Creative mode</label>
|
||||
<label
|
||||
for="disable-npcs"
|
||||
class="option-card ${this.disableNPCs ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable-npcs"
|
||||
@change=${this.handleDisableNPCsChange}
|
||||
.checked=${this.disableNPCs}
|
||||
/>
|
||||
<div class="option-card-title">Disable NPCs</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
for="creative-mode"
|
||||
class="option-card ${this.creativeMode ? "selected" : ""}"
|
||||
>
|
||||
<div class="checkbox-icon"></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="creative-mode"
|
||||
@change=${this.handleCreativeModeChange}
|
||||
.checked=${this.creativeMode}
|
||||
/>
|
||||
<div class="option-card-title">Creative Mode</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click=${this.startGame} class="start-game-button">
|
||||
|
||||
@@ -201,11 +201,10 @@
|
||||
|
||||
<div class="bottom-0 w-full flex-col-reverse sm:flex-row z-50" style="position: fixed; pointer-events: none">
|
||||
<div class="w-full sm:w-2/3 sm:fixed sm:right-0 sm:bottom-0 sm:flex justify-end" style="pointer-events: auto">
|
||||
<events-display></events-display>
|
||||
</div>
|
||||
<div class="w-full sm:w-1/3" style="pointer-events: auto">
|
||||
<control-panel></control-panel>
|
||||
</div>
|
||||
<events-display></events-display>
|
||||
</div>
|
||||
<div class="w-full sm:w-1/3 md:max-w-72" style="pointer-events: auto">
|
||||
<control-panel></control-panel>
|
||||
</div>
|
||||
|
||||
<!-- Footer section -->
|
||||
|
||||
+4
-1
@@ -100,7 +100,8 @@ const GameConfigSchema = z.object({
|
||||
const SafeString = z
|
||||
.string()
|
||||
// Remove common dangerous characters and patterns
|
||||
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/]+$/)
|
||||
// The weird \u stuff is to allow emojis
|
||||
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]]+$/)
|
||||
// Reasonable max length to prevent DOS
|
||||
.max(1000);
|
||||
|
||||
@@ -323,3 +324,5 @@ export const GameRecordSchema = z.object({
|
||||
turns: z.array(TurnSchema),
|
||||
winner: ID.nullable(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const matcher = new RegExpMatcher({
|
||||
export const MIN_USERNAME_LENGTH = 3;
|
||||
export const MAX_USERNAME_LENGTH = 27;
|
||||
|
||||
const validPattern = /^[a-zA-Z0-9_\[\] 🐈🍀]+$/;
|
||||
const validPattern = /^[a-zA-Z0-9_\[\] 🐈🍀]+$/u;
|
||||
|
||||
const shadowNames = [
|
||||
"NicePeopleOnly",
|
||||
@@ -71,7 +71,7 @@ export function validateUsername(username: string): {
|
||||
|
||||
export function sanitizeUsername(str: string): string {
|
||||
const sanitized = str
|
||||
.replace(/[^a-zA-Z0-9_\[\] 🐈🍀]/g, "")
|
||||
.replace(/[^a-zA-Z0-9_\[\] 🐈🍀]/gu, "")
|
||||
.slice(0, MAX_USERNAME_LENGTH);
|
||||
return sanitized.padEnd(MIN_USERNAME_LENGTH, "x");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user