mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-15 19:35:50 +00:00
Merge branch 'main' of https://github.com/openfrontio/OpenFrontIO into SPA
This commit is contained in:
@@ -64,13 +64,13 @@ export interface LobbyConfig {
|
||||
|
||||
export function joinLobby(
|
||||
lobbyConfig: LobbyConfig,
|
||||
onjoin: () => void
|
||||
onjoin: () => void,
|
||||
): () => void {
|
||||
const eventBus = new EventBus();
|
||||
initRemoteSender(eventBus);
|
||||
|
||||
consolex.log(
|
||||
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`
|
||||
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`,
|
||||
);
|
||||
|
||||
const serverConfig = getServerConfig();
|
||||
@@ -91,7 +91,7 @@ export function joinLobby(
|
||||
lobbyConfig,
|
||||
gameConfig,
|
||||
eventBus,
|
||||
serverConfig
|
||||
serverConfig,
|
||||
);
|
||||
|
||||
const onconnect = () => {
|
||||
@@ -103,7 +103,7 @@ export function joinLobby(
|
||||
consolex.log("lobby: game started");
|
||||
onjoin();
|
||||
createClientGame(lobbyConfig, message.config, eventBus, transport).then(
|
||||
(r) => r.start()
|
||||
(r) => r.start(),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -118,7 +118,7 @@ export async function createClientGame(
|
||||
lobbyConfig: LobbyConfig,
|
||||
gameConfig: GameConfig,
|
||||
eventBus: EventBus,
|
||||
transport: Transport
|
||||
transport: Transport,
|
||||
): Promise<ClientGameRunner> {
|
||||
const config = getConfig(gameConfig);
|
||||
|
||||
@@ -126,14 +126,14 @@ export async function createClientGame(
|
||||
const worker = new WorkerClient(
|
||||
lobbyConfig.gameID,
|
||||
gameConfig,
|
||||
lobbyConfig.clientID
|
||||
lobbyConfig.clientID,
|
||||
);
|
||||
await worker.initialize();
|
||||
const gameView = new GameView(
|
||||
worker,
|
||||
config,
|
||||
gameMap.gameMap,
|
||||
lobbyConfig.clientID
|
||||
lobbyConfig.clientID,
|
||||
);
|
||||
|
||||
consolex.log("going to init path finder");
|
||||
@@ -143,11 +143,11 @@ export async function createClientGame(
|
||||
canvas,
|
||||
gameView,
|
||||
eventBus,
|
||||
lobbyConfig.clientID
|
||||
lobbyConfig.clientID,
|
||||
);
|
||||
|
||||
consolex.log(
|
||||
`creating private game got difficulty: ${gameConfig.difficulty}`
|
||||
`creating private game got difficulty: ${gameConfig.difficulty}`,
|
||||
);
|
||||
|
||||
return new ClientGameRunner(
|
||||
@@ -157,7 +157,7 @@ export async function createClientGame(
|
||||
new InputHandler(canvas, eventBus),
|
||||
transport,
|
||||
worker,
|
||||
gameView
|
||||
gameView,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export class ClientGameRunner {
|
||||
private input: InputHandler,
|
||||
private transport: Transport,
|
||||
private worker: WorkerClient,
|
||||
private gameView: GameView
|
||||
private gameView: GameView,
|
||||
) {}
|
||||
|
||||
public start() {
|
||||
@@ -223,7 +223,7 @@ export class ClientGameRunner {
|
||||
}
|
||||
if (this.turnsSeen != message.turn.turnNumber) {
|
||||
consolex.error(
|
||||
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`
|
||||
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,
|
||||
);
|
||||
} else {
|
||||
this.worker.sendTurn(message.turn);
|
||||
@@ -246,7 +246,7 @@ export class ClientGameRunner {
|
||||
}
|
||||
const cell = this.renderer.transformHandler.screenToWorldCoordinates(
|
||||
event.x,
|
||||
event.y
|
||||
event.y,
|
||||
);
|
||||
if (!this.gameView.isValidCoord(cell.x, cell.y)) {
|
||||
return;
|
||||
@@ -276,8 +276,8 @@ export class ClientGameRunner {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
this.gameView.owner(tile).id(),
|
||||
this.myPlayer.troops() * this.renderer.uiState.attackRatio
|
||||
)
|
||||
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+372
-119
@@ -3,12 +3,15 @@ import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
|
||||
import { Lobby } from "../core/Schemas";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import "./components/Difficulties";
|
||||
import { DifficultyDescription } from "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
|
||||
@customElement("host-lobby-modal")
|
||||
export class HostLobbyModal extends LitElement {
|
||||
@state() private isModalOpen = false;
|
||||
@state() private selectedMap: GameMapType = GameMapType.World;
|
||||
@state() private selectedDiffculty: Difficulty = Difficulty.Medium;
|
||||
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
|
||||
@state() private disableNPCs = false;
|
||||
@state() private disableBots = false;
|
||||
@state() private creativeMode = false;
|
||||
@@ -28,16 +31,57 @@ export class HostLobbyModal extends LitElement {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 15% auto;
|
||||
background-color: rgb(35 35 35 / 0.8);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
max-width: 1280px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add custom scrollbar styles */
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.close {
|
||||
@@ -50,55 +94,211 @@ export class HostLobbyModal extends LitElement {
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
.start-game-button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 15px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
.start-game-button:not(:disabled):hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
width: 200px;
|
||||
.start-game-button:disabled {
|
||||
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.lobby-id-container {
|
||||
.options-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.option-cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
width: 100%;
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
padding: 4px 4px 0 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: #4a9eff;
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.option-card-title {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.option-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/2;
|
||||
color: #aaa;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.copy-success {
|
||||
position: relative;
|
||||
transform: translateY(-10px);
|
||||
color: green;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.copy-success-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.lobby-id-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 8px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.clipboard-icon {
|
||||
.lobby-id-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.clipboard-icon:hover {
|
||||
opacity: 0.7;
|
||||
.lobby-id-button:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.copy-success {
|
||||
color: green;
|
||||
.lobby-id {
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
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);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -106,97 +306,157 @@ export class HostLobbyModal extends LitElement {
|
||||
return html`
|
||||
<div
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "block" : "none"}"
|
||||
style="display: ${this.isModalOpen ? "flex" : "none"}"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
<h2>Private Lobby</h2>
|
||||
<div class="lobby-id-container">
|
||||
<h3>Lobby ID: ${this.lobbyId}</h3>
|
||||
<svg
|
||||
|
||||
<div class="title">Private Lobby</div>
|
||||
<div class="lobby-id-box">
|
||||
<button
|
||||
class="lobby-id-button"
|
||||
@click=${this.copyToClipboard}
|
||||
class="clipboard-icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
?disabled=${this.copySuccess}
|
||||
>
|
||||
<path
|
||||
d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"
|
||||
></path>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
${this.copySuccess
|
||||
? html`<p class="copy-success">Copied to clipboard!</p>`
|
||||
: ""}
|
||||
<div>
|
||||
<label for="map-select">Map: </label>
|
||||
<select id="map-select" @change=${this.handleMapChange}>
|
||||
${Object.entries(GameMapType)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<option
|
||||
value=${value}
|
||||
?selected=${this.selectedMap === value}
|
||||
<span class="lobby-id">${this.lobbyId}</span>
|
||||
${this.copySuccess
|
||||
? html`<span class="copy-success-icon">✓</span>`
|
||||
: html`
|
||||
<svg
|
||||
class="clipboard-icon"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="18px"
|
||||
width="18px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
${key}
|
||||
</option>
|
||||
`,
|
||||
<path
|
||||
d="M296 48H176.5C154.4 48 136 65.4 136 87.5V96h-7.5C106.4 96 88 113.4 88 135.5v288c0 22.1 18.4 40.5 40.5 40.5h208c22.1 0 39.5-18.4 39.5-40.5V416h8.5c22.1 0 39.5-18.4 39.5-40.5V176L296 48zm0 44.6l83.4 83.4H296V92.6zm48 330.9c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5h7.5v255.5c0 22.1 10.4 32.5 32.5 32.5H344v7.5zm48-48c0 4.7-3.4 8.5-7.5 8.5h-208c-4.4 0-8.5-4.1-8.5-8.5v-288c0-4.1 3.8-7.5 8.5-7.5H264v128h128v167.5z"
|
||||
></path>
|
||||
</svg>
|
||||
`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="options-layout">
|
||||
<!-- Map Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">Map</div>
|
||||
<div class="option-cards">
|
||||
${Object.entries(GameMapType)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div @click=${() => this.handleMapSelection(value)}>
|
||||
<map-display
|
||||
.mapKey=${key}
|
||||
.selected=${this.selectedMap === value}
|
||||
></map-display>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Difficulty Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">Difficulty</div>
|
||||
<div class="option-cards">
|
||||
${Object.entries(Difficulty)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div
|
||||
class="option-card ${this.selectedDifficulty === value
|
||||
? "selected"
|
||||
: ""}"
|
||||
@click=${() => this.handleDifficultySelection(value)}
|
||||
>
|
||||
<difficulty-display
|
||||
.difficultyKey=${key}
|
||||
></difficulty-display>
|
||||
<p class="option-card-title">
|
||||
${DifficultyDescription[key]}
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</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">
|
||||
${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>`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="map-select">Difficulty: </label>
|
||||
<select id="map-select" @change=${this.handleDifficultyChange}>
|
||||
${Object.entries(Difficulty)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<option
|
||||
value=${value}
|
||||
?selected=${this.selectedDiffculty === value}
|
||||
>
|
||||
${key}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
</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>
|
||||
<button @click=${this.startGame}>Start Game</button>
|
||||
<div>
|
||||
<p>Players: ${this.players.join(", ")}</p>
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click=${this.startGame}
|
||||
?disabled=${this.players.length < 2}
|
||||
class="start-game-button"
|
||||
>
|
||||
${this.players.length === 1
|
||||
? "Waiting for players..."
|
||||
: "Start Game"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -217,7 +477,7 @@ export class HostLobbyModal extends LitElement {
|
||||
id: this.lobbyId,
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDiffculty,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableBots: this.disableBots,
|
||||
disableNPCs: this.disableNPCs,
|
||||
creativeMode: this.creativeMode,
|
||||
@@ -240,19 +500,12 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMapChange(e: Event) {
|
||||
this.selectedMap = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as GameMapType;
|
||||
consolex.log(`updating map to ${this.selectedMap}`);
|
||||
private async handleMapSelection(value: GameMapType) {
|
||||
this.selectedMap = value;
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
private async handleDifficultyChange(e: Event) {
|
||||
this.selectedDiffculty = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as Difficulty;
|
||||
consolex.log(`updating difficulty to ${this.selectedDiffculty}`);
|
||||
private async handleDifficultySelection(value: Difficulty) {
|
||||
this.selectedDifficulty = value;
|
||||
this.putGameConfig();
|
||||
}
|
||||
|
||||
@@ -282,7 +535,7 @@ export class HostLobbyModal extends LitElement {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
gameMap: this.selectedMap,
|
||||
difficulty: this.selectedDiffculty,
|
||||
difficulty: this.selectedDifficulty,
|
||||
disableBots: this.disableBots,
|
||||
disableNPCs: this.disableNPCs,
|
||||
creativeMode: this.creativeMode,
|
||||
|
||||
@@ -72,6 +72,12 @@ export class InputHandler {
|
||||
|
||||
private alternateView = false;
|
||||
|
||||
private moveInterval: any = null;
|
||||
private activeKeys = new Set<string>();
|
||||
|
||||
private readonly PAN_SPEED = 5;
|
||||
private readonly ZOOM_SPEED = 10;
|
||||
|
||||
constructor(
|
||||
private canvas: HTMLCanvasElement,
|
||||
private eventBus: EventBus,
|
||||
@@ -95,14 +101,67 @@ export class InputHandler {
|
||||
});
|
||||
this.pointers.clear();
|
||||
|
||||
// Initialize the combined movement interval
|
||||
this.moveInterval = setInterval(() => {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
// Handle both WASD and arrow keys
|
||||
if (this.activeKeys.has("KeyW") || this.activeKeys.has("ArrowUp"))
|
||||
deltaY += this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyS") || this.activeKeys.has("ArrowDown"))
|
||||
deltaY -= this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyA") || this.activeKeys.has("ArrowLeft"))
|
||||
deltaX += this.PAN_SPEED;
|
||||
if (this.activeKeys.has("KeyD") || this.activeKeys.has("ArrowRight"))
|
||||
deltaX -= this.PAN_SPEED;
|
||||
|
||||
if (deltaX !== 0 || deltaY !== 0) {
|
||||
this.eventBus.emit(new DragEvent(deltaX, deltaY));
|
||||
}
|
||||
|
||||
// Handle zooming
|
||||
const screenCenterX = window.innerWidth / 2;
|
||||
const screenCenterY = window.innerHeight / 2;
|
||||
|
||||
if (this.activeKeys.has("Minus")) {
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(screenCenterX, screenCenterY, this.ZOOM_SPEED),
|
||||
);
|
||||
}
|
||||
if (this.activeKeys.has("Equal")) {
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(screenCenterX, screenCenterY, -this.ZOOM_SPEED),
|
||||
);
|
||||
}
|
||||
}, 1);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.code === "Space") {
|
||||
e.preventDefault(); // Prevent page scrolling
|
||||
e.preventDefault();
|
||||
if (!this.alternateView) {
|
||||
this.alternateView = true;
|
||||
this.eventBus.emit(new AlternateViewEvent(true));
|
||||
}
|
||||
}
|
||||
|
||||
// Add all movement keys to activeKeys
|
||||
if (
|
||||
[
|
||||
"KeyW",
|
||||
"KeyA",
|
||||
"KeyS",
|
||||
"KeyD",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"ArrowRight",
|
||||
"Minus",
|
||||
"Equal",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.add(e.code);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("keyup", (e) => {
|
||||
@@ -115,6 +174,24 @@ export class InputHandler {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||||
}
|
||||
|
||||
// Remove all movement keys from activeKeys
|
||||
if (
|
||||
[
|
||||
"KeyW",
|
||||
"KeyA",
|
||||
"KeyS",
|
||||
"KeyD",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"ArrowRight",
|
||||
"Minus",
|
||||
"Equal",
|
||||
].includes(e.code)
|
||||
) {
|
||||
this.activeKeys.delete(e.code);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,7 +270,6 @@ export class InputHandler {
|
||||
const pinchDelta = currentPinchDistance - this.lastPinchDistance;
|
||||
|
||||
if (Math.abs(pinchDelta) > 1) {
|
||||
// Threshold to avoid tiny zoom adjustments
|
||||
const zoomCenter = this.getPinchCenter();
|
||||
this.eventBus.emit(
|
||||
new ZoomEvent(zoomCenter.x, zoomCenter.y, -pinchDelta * 2),
|
||||
@@ -222,4 +298,9 @@ export class InputHandler {
|
||||
y: (pointerEvents[0].clientY + pointerEvents[1].clientY) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
clearInterval(this.moveInterval);
|
||||
this.activeKeys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -20,16 +23,58 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 15% auto;
|
||||
background-color: rgb(35 35 35 / 0.8);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add custom scrollbar styles */
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
@@ -37,47 +82,41 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
|
||||
.start-game-button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 15px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
display: inline-block;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
button:hover {
|
||||
|
||||
.start-game-button:not(:disabled):hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.lobby-id-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.lobby-id-container input {
|
||||
flex-grow: 1;
|
||||
max-width: 200px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.lobby-id-container button {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
.join-button {
|
||||
margin-top: 10px;
|
||||
|
||||
.start-game-button:disabled {
|
||||
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* JoinPrivateLobbyModal css */
|
||||
|
||||
.message-area {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
@@ -104,26 +143,135 @@ export class JoinPrivateLobbyModal extends LitElement {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.lobby-id-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 40px 0px 0px 0px;
|
||||
}
|
||||
.lobby-id-box input {
|
||||
flex-grow: 1;
|
||||
max-width: 200px;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.lobby-id-paste-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.lobby-id-paste-button:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.lobby-id-paste-button-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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() {
|
||||
return html`
|
||||
<div
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "block" : "none"}"
|
||||
style="display: ${this.isModalOpen ? "flex" : "none"}"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.closeAndLeave}>×</span>
|
||||
<h2>Join Private Lobby</h2>
|
||||
<div class="lobby-id-container">
|
||||
<div class="title">Join Private Lobby</div>
|
||||
<div class="lobby-id-box">
|
||||
<input type="text" id="lobbyIdInput" placeholder="Enter Lobby ID" />
|
||||
<button @click=${this.pasteFromClipboard}>Paste</button>
|
||||
<button
|
||||
@click=${this.pasteFromClipboard}
|
||||
class="lobby-id-paste-button"
|
||||
>
|
||||
<svg
|
||||
class="lobby-id-paste-button-icon"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 32 32"
|
||||
height="18px"
|
||||
width="18px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<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="join-button" @click=${this.joinLobby}>
|
||||
? html`<button class="start-game-button" @click=${this.joinLobby}>
|
||||
Join Lobby
|
||||
</button>`
|
||||
: ""}
|
||||
@@ -139,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() {
|
||||
@@ -192,6 +344,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.";
|
||||
}
|
||||
@@ -201,4 +354,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+138
-149
@@ -1,98 +1,90 @@
|
||||
import { ClientGameRunner, joinLobby } from './ClientGameRunner';
|
||||
import backgroundImage from '../../resources/images/EuropeBackground.svg';
|
||||
import favicon from '../../resources/images/Favicon.svg';
|
||||
import { ClientGameRunner, joinLobby } from "./ClientGameRunner";
|
||||
import backgroundImage from "../../resources/images/EuropeBackground.svg";
|
||||
import favicon from "../../resources/images/Favicon.svg";
|
||||
|
||||
import './PublicLobby';
|
||||
import './UsernameInput';
|
||||
import './styles.css';
|
||||
import { UsernameInput } from './UsernameInput';
|
||||
import { SinglePlayerModal } from './SinglePlayerModal';
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from './HostLobbyModal';
|
||||
import { JoinPrivateLobbyModal } from './JoinPrivateLobbyModal';
|
||||
import { generateID } from '../core/Util';
|
||||
import { generateCryptoRandomUUID } from './Utils';
|
||||
import { consolex } from '../core/Consolex';
|
||||
import './components/FlagInput';
|
||||
import { FlagInput } from './components/FlagInput';
|
||||
import "./PublicLobby";
|
||||
import "./UsernameInput";
|
||||
import "./styles.css";
|
||||
import { UsernameInput } from "./UsernameInput";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
|
||||
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
|
||||
import { generateID } from "../core/Util";
|
||||
import { generateCryptoRandomUUID } from "./Utils";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import "./components/FlagInput";
|
||||
import { FlagInput } from "./components/FlagInput";
|
||||
import page from 'page';
|
||||
|
||||
class Client {
|
||||
private gameStop: () => void;
|
||||
private gameStop: () => void;
|
||||
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | null = null;
|
||||
private usernameInput: UsernameInput | null = null;
|
||||
private flagInput: FlagInput | null = null;
|
||||
|
||||
private joinModal: JoinPrivateLobbyModal;
|
||||
constructor() {}
|
||||
private joinModal: JoinPrivateLobbyModal;
|
||||
constructor() {}
|
||||
|
||||
initialize(): void {
|
||||
this.flagInput = document.querySelector('flag-input') as FlagInput;
|
||||
if (!this.flagInput) {
|
||||
consolex.warn('Flag input element not found');
|
||||
}
|
||||
initialize(): void {
|
||||
this.flagInput = document.querySelector("flag-input") as FlagInput;
|
||||
if (!this.flagInput) {
|
||||
consolex.warn("Flag input element not found");
|
||||
}
|
||||
|
||||
this.usernameInput = document.querySelector(
|
||||
'username-input'
|
||||
) as UsernameInput;
|
||||
if (!this.usernameInput) {
|
||||
consolex.warn('Username input element not found');
|
||||
}
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
consolex.log('Browser is closing');
|
||||
if (this.gameStop != null) {
|
||||
this.gameStop();
|
||||
}
|
||||
});
|
||||
this.usernameInput = document.querySelector(
|
||||
"username-input",
|
||||
) as UsernameInput;
|
||||
if (!this.usernameInput) {
|
||||
consolex.warn("Username input element not found");
|
||||
}
|
||||
window.addEventListener("beforeunload", (event) => {
|
||||
consolex.log("Browser is closing");
|
||||
if (this.gameStop != null) {
|
||||
this.gameStop();
|
||||
}
|
||||
});
|
||||
|
||||
setFavicon();
|
||||
document.addEventListener(
|
||||
'join-lobby',
|
||||
this.handleJoinLobby.bind(this)
|
||||
);
|
||||
document.addEventListener(
|
||||
'leave-lobby',
|
||||
this.handleLeaveLobby.bind(this)
|
||||
);
|
||||
document.addEventListener(
|
||||
'single-player',
|
||||
this.handleSinglePlayer.bind(this)
|
||||
);
|
||||
setFavicon();
|
||||
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
|
||||
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
|
||||
document.addEventListener(
|
||||
"single-player",
|
||||
this.handleSinglePlayer.bind(this),
|
||||
);
|
||||
|
||||
const spModal = document.querySelector(
|
||||
'single-player-modal'
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
document
|
||||
.getElementById('single-player')
|
||||
.addEventListener('click', () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
spModal.open();
|
||||
}
|
||||
});
|
||||
const spModal = document.querySelector(
|
||||
"single-player-modal",
|
||||
) as SinglePlayerModal;
|
||||
spModal instanceof SinglePlayerModal;
|
||||
document.getElementById("single-player").addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
spModal.open();
|
||||
}
|
||||
});
|
||||
|
||||
const hostModal = document.querySelector(
|
||||
'host-lobby-modal'
|
||||
) as HostPrivateLobbyModal;
|
||||
hostModal instanceof HostPrivateLobbyModal;
|
||||
document
|
||||
.getElementById('host-lobby-button')
|
||||
.addEventListener('click', () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
hostModal.open();
|
||||
}
|
||||
});
|
||||
const hostModal = document.querySelector(
|
||||
"host-lobby-modal",
|
||||
) as HostPrivateLobbyModal;
|
||||
hostModal instanceof HostPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("host-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
hostModal.open();
|
||||
}
|
||||
});
|
||||
|
||||
this.joinModal = document.querySelector(
|
||||
'join-private-lobby-modal'
|
||||
) as JoinPrivateLobbyModal;
|
||||
this.joinModal instanceof JoinPrivateLobbyModal;
|
||||
document
|
||||
.getElementById('join-private-lobby-button')
|
||||
.addEventListener('click', () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
this.joinModal.open();
|
||||
}
|
||||
});
|
||||
this.joinModal = document.querySelector(
|
||||
"join-private-lobby-modal",
|
||||
) as JoinPrivateLobbyModal;
|
||||
this.joinModal instanceof JoinPrivateLobbyModal;
|
||||
document
|
||||
.getElementById("join-private-lobby-button")
|
||||
.addEventListener("click", () => {
|
||||
if (this.usernameInput.isValid()) {
|
||||
this.joinModal.open();
|
||||
}
|
||||
});
|
||||
|
||||
page('/join/:id', (ctx) => {
|
||||
// TODO: Implement logic for joining a lobby
|
||||
@@ -101,88 +93,85 @@ class Client {
|
||||
});
|
||||
|
||||
page();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleJoinLobby(event: CustomEvent) {
|
||||
const lobby = event.detail.lobby;
|
||||
consolex.log(`joining lobby ${lobby.id}`);
|
||||
if (this.gameStop != null) {
|
||||
consolex.log('joining lobby, stopping existing game');
|
||||
this.gameStop();
|
||||
}
|
||||
this.gameStop = joinLobby(
|
||||
{
|
||||
gameType: event.detail.gameType,
|
||||
flag: (): string => this.flagInput.getCurrentFlag(),
|
||||
playerName: (): string =>
|
||||
this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
map: event.detail.map,
|
||||
difficulty: event.detail.difficulty,
|
||||
disableBots: event.detail.disableBots,
|
||||
disableNPCs: event.detail.disableNPCs,
|
||||
creativeMode: event.detail.creativeMode,
|
||||
},
|
||||
() => this.joinModal.close()
|
||||
);
|
||||
}
|
||||
private async handleJoinLobby(event: CustomEvent) {
|
||||
const lobby = event.detail.lobby;
|
||||
consolex.log(`joining lobby ${lobby.id}`);
|
||||
if (this.gameStop != null) {
|
||||
consolex.log("joining lobby, stopping existing game");
|
||||
this.gameStop();
|
||||
}
|
||||
this.gameStop = joinLobby(
|
||||
{
|
||||
gameType: event.detail.gameType,
|
||||
flag: (): string => this.flagInput.getCurrentFlag(),
|
||||
playerName: (): string => this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
map: event.detail.map,
|
||||
difficulty: event.detail.difficulty,
|
||||
disableBots: event.detail.disableBots,
|
||||
disableNPCs: event.detail.disableNPCs,
|
||||
creativeMode: event.detail.creativeMode,
|
||||
},
|
||||
() => this.joinModal.close(),
|
||||
);
|
||||
}
|
||||
|
||||
private async handleLeaveLobby(event: CustomEvent) {
|
||||
if (this.gameStop == null) {
|
||||
return;
|
||||
}
|
||||
consolex.log('leaving lobby, cancelling game');
|
||||
this.gameStop();
|
||||
this.gameStop = null;
|
||||
}
|
||||
private async handleLeaveLobby(event: CustomEvent) {
|
||||
if (this.gameStop == null) {
|
||||
return;
|
||||
}
|
||||
consolex.log("leaving lobby, cancelling game");
|
||||
this.gameStop();
|
||||
this.gameStop = null;
|
||||
}
|
||||
|
||||
private async handleSinglePlayer(event: CustomEvent) {
|
||||
alert('coming soon');
|
||||
}
|
||||
private async handleSinglePlayer(event: CustomEvent) {
|
||||
alert("coming soon");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the client when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new Client().initialize();
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new Client().initialize();
|
||||
});
|
||||
|
||||
document.body.style.backgroundImage = `url(${backgroundImage})`;
|
||||
|
||||
function setFavicon(): void {
|
||||
const link = document.createElement('link');
|
||||
link.type = 'image/x-icon';
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = favicon;
|
||||
document.head.appendChild(link);
|
||||
const link = document.createElement("link");
|
||||
link.type = "image/x-icon";
|
||||
link.rel = "shortcut icon";
|
||||
link.href = favicon;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
// WARNING: DO NOT EXPOSE THIS ID
|
||||
export function getPersistentIDFromCookie(): string {
|
||||
const COOKIE_NAME = 'player_persistent_id';
|
||||
const COOKIE_NAME = "player_persistent_id";
|
||||
|
||||
// Try to get existing cookie
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [cookieName, cookieValue] = cookie
|
||||
.split('=')
|
||||
.map((c) => c.trim());
|
||||
if (cookieName === COOKIE_NAME) {
|
||||
return cookieValue;
|
||||
}
|
||||
}
|
||||
// Try to get existing cookie
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
const [cookieName, cookieValue] = cookie.split("=").map((c) => c.trim());
|
||||
if (cookieName === COOKIE_NAME) {
|
||||
return cookieValue;
|
||||
}
|
||||
}
|
||||
|
||||
// If no cookie exists, create new ID and set cookie
|
||||
const newID = generateCryptoRandomUUID();
|
||||
document.cookie = [
|
||||
`${COOKIE_NAME}=${newID}`,
|
||||
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
|
||||
'path=/',
|
||||
'SameSite=Strict',
|
||||
'Secure',
|
||||
].join(';');
|
||||
// If no cookie exists, create new ID and set cookie
|
||||
const newID = generateCryptoRandomUUID();
|
||||
document.cookie = [
|
||||
`${COOKIE_NAME}=${newID}`,
|
||||
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
|
||||
"path=/",
|
||||
"SameSite=Strict",
|
||||
"Secure",
|
||||
].join(";");
|
||||
|
||||
return newID;
|
||||
return newID;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class PublicLobby extends LitElement {
|
||||
this.fetchAndUpdateLobbies();
|
||||
this.lobbiesInterval = window.setInterval(
|
||||
() => this.fetchAndUpdateLobbies(),
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export class PublicLobby extends LitElement {
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.dispatchEvent(
|
||||
@@ -119,7 +119,7 @@ export class PublicLobby extends LitElement {
|
||||
detail: { lobby: this.currLobby },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
this.currLobby = null;
|
||||
}
|
||||
|
||||
+268
-84
@@ -3,6 +3,9 @@ import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
|
||||
import { generateID as generateID } from "../core/Util";
|
||||
import { consolex } from "../core/Consolex";
|
||||
import "./components/Difficulties";
|
||||
import { DifficultyDescription } from "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
|
||||
@customElement("single-player-modal")
|
||||
export class SinglePlayerModal extends LitElement {
|
||||
@@ -23,16 +26,57 @@ export class SinglePlayerModal extends LitElement {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
margin: 15% auto;
|
||||
background-color: rgb(35 35 35 / 0.8);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
max-width: 1280px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add custom scrollbar styles */
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.close {
|
||||
@@ -45,33 +89,139 @@ export class SinglePlayerModal extends LitElement {
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
.start-game-button {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 15px 20px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s;
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
.start-game-button:not(:disabled):hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 8px;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
width: 200px;
|
||||
.start-game-button:disabled {
|
||||
background: linear-gradient(to right, #4a4a4a, #3d3d3d);
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.options-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.option-cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
width: 100%;
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
padding: 4px 4px 0 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: #4a9eff;
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.option-card-title {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.option-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/2;
|
||||
color: #aaa;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -79,72 +229,111 @@ export class SinglePlayerModal extends LitElement {
|
||||
return html`
|
||||
<div
|
||||
class="modal-overlay"
|
||||
style="display: ${this.isModalOpen ? "block" : "none"}"
|
||||
style="display: ${this.isModalOpen ? "flex" : "none"}"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<span class="close" @click=${this.close}>×</span>
|
||||
<h2>Start Single Player Game</h2>
|
||||
<div>
|
||||
<label for="map-select">Map: </label>
|
||||
<select id="map-select" @change=${this.handleMapChange}>
|
||||
${Object.entries(GameMapType)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<option
|
||||
value=${value}
|
||||
?selected=${this.selectedMap === value}
|
||||
>
|
||||
${value}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="map-select">Difficulty: </label>
|
||||
<select id="map-select" @change=${this.handleDifficultyChange}>
|
||||
${Object.entries(Difficulty)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<option
|
||||
value=${value}
|
||||
?selected=${this.selectedDifficulty === value}
|
||||
>
|
||||
${value}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
</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 class="title">Single Player</div>
|
||||
|
||||
<div class="options-layout">
|
||||
<!-- Map Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">Map</div>
|
||||
<div class="option-cards">
|
||||
${Object.entries(GameMapType)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div @click=${() => this.handleMapSelection(value)}>
|
||||
<map-display
|
||||
.mapKey=${key}
|
||||
.selected=${this.selectedMap === value}
|
||||
></map-display>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Difficulty Selection -->
|
||||
<div class="options-section">
|
||||
<div class="option-title">Difficulty</div>
|
||||
<div class="option-cards">
|
||||
${Object.entries(Difficulty)
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div
|
||||
class="option-card ${this.selectedDifficulty === value
|
||||
? "selected"
|
||||
: ""}"
|
||||
@click=${() => this.handleDifficultySelection(value)}
|
||||
>
|
||||
<difficulty-display
|
||||
.difficultyKey=${key}
|
||||
></difficulty-display>
|
||||
<p class="option-card-title">
|
||||
${DifficultyDescription[key]}
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="creative-mode"
|
||||
@change=${this.handleCreativeModeChange}
|
||||
/>
|
||||
<label for="creative-mode">Creative mode</label>
|
||||
</div>
|
||||
|
||||
<button @click=${this.startGame}>Start Game</button>
|
||||
<button @click=${this.startGame} class="start-game-button">
|
||||
Start Game
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -157,16 +346,11 @@ export class SinglePlayerModal extends LitElement {
|
||||
public close() {
|
||||
this.isModalOpen = false;
|
||||
}
|
||||
|
||||
private handleMapChange(e: Event) {
|
||||
this.selectedMap = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as GameMapType;
|
||||
private handleMapSelection(value: GameMapType) {
|
||||
this.selectedMap = value;
|
||||
}
|
||||
private handleDifficultyChange(e: Event) {
|
||||
this.selectedDifficulty = String(
|
||||
(e.target as HTMLSelectElement).value,
|
||||
) as Difficulty;
|
||||
private handleDifficultySelection(value: Difficulty) {
|
||||
this.selectedDifficulty = value;
|
||||
}
|
||||
private handleDisableBotsChange(e: Event) {
|
||||
this.disableBots = Boolean((e.target as HTMLInputElement).checked);
|
||||
|
||||
@@ -3,6 +3,7 @@ export function renderTroops(troops: number): string {
|
||||
}
|
||||
|
||||
export function renderNumber(num: number) {
|
||||
num = Math.max(num, 0);
|
||||
let numStr = "";
|
||||
if (num >= 10_000_000) {
|
||||
numStr = (num / 1000000).toFixed(1) + "M";
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
export enum DifficultyDescription {
|
||||
Easy = "Relaxed",
|
||||
Medium = "Balanced",
|
||||
Hard = "Intense",
|
||||
Impossible = "Challenging",
|
||||
}
|
||||
|
||||
@customElement("difficulty-display")
|
||||
export class DifficultyDisplay extends LitElement {
|
||||
@property({ type: String }) difficultyKey = "";
|
||||
|
||||
static styles = css`
|
||||
.difficulty-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
gap: 6px;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.difficulty-skull {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.3;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.difficulty-skull.big {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.difficulty-skull.active {
|
||||
opacity: 1;
|
||||
color: #ff3838;
|
||||
filter: drop-shadow(0 0 4px rgba(255, 56, 56, 0.4));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
:host(:hover) .difficulty-skull.active {
|
||||
filter: drop-shadow(0 0 6px rgba(255, 56, 56, 0.6));
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
`;
|
||||
|
||||
private getDifficultyIcon(difficultyKey: string) {
|
||||
const skull = html`<svg
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
height="100%"
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m12.5 17-.5-1-.5 1h1z"></path>
|
||||
<path
|
||||
d="M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z"
|
||||
></path>
|
||||
<circle cx="15" cy="12" r="1"></circle>
|
||||
<circle cx="9" cy="12" r="1"></circle>
|
||||
</svg>`;
|
||||
|
||||
const burningSkull = html`<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="100%"
|
||||
width="100%"
|
||||
>
|
||||
<path
|
||||
d="M268.725 389.28l3.74 28.7h-30.89l3.74-28.7a11.705 11.705 0 1 1 23.41 0zm33.84-71.83a29.5 29.5 0 1 0 29.5 29.5 29.5 29.5 0 0 0-29.51-29.5zm-94.4 0a29.5 29.5 0 1 0 29.5 29.5 29.5 29.5 0 0 0-29.51-29.5zm245.71-62c0 98.2-48.22 182.68-117.39 220.24-46 28.26-112.77 28.26-156.19 2.5-71.72-36.21-122.17-122.29-122.17-222.73 0-78.16 30.54-147.63 77.89-191.67 0 0-42.08 82.86 9.1 135-11.67-173.77 169.28-63 118-184 151.79 83.33 9.14 105 84.1 148.21 0 0 66.21 47 36.4-91.73 42.95 43.99 70.25 110.3 70.25 184.19zm-68.54 29.87c-2.45-65.49-54.88-119.59-120.26-124.07-3.06-.21-6.15-.31-9.16-.31a129.4 129.4 0 0 0-129.43 129.35 132.15 132.15 0 0 0 24.51 76v25a35 35 0 0 0 34.74 34.69h6.26v16.61a34.66 34.66 0 0 0 34.71 34.39h61.78a34.48 34.48 0 0 0 34.51-34.39v-16.61h5.38a34.89 34.89 0 0 0 34.62-34.75v-28a129.32 129.32 0 0 0 22.33-77.9z"
|
||||
></path>
|
||||
</svg>`;
|
||||
|
||||
const kingSkull = html`<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 512 512"
|
||||
height="100%"
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M92.406 13.02l-.164 156.353c3.064.507 6.208 1.38 9.39 2.627 36.496 14.306 74.214 22.435 111.864 25.473l43.402-60.416 42.317 58.906c36.808-4.127 72.566-12.502 105.967-24.09 3.754-1.302 7.368-2.18 10.818-2.6l1.523-156.252-75.82 95.552-34.084-95.55-53.724 103.74-53.722-103.74-35.442 95.55-72.32-95.55h-.006zm164.492 156.07l-28.636 39.86 28.634 39.86 28.637-39.86-28.635-39.86zM86.762 187.55c-2.173-.08-3.84.274-5.012.762-2.345.977-3.173 2.19-3.496 4.196-.645 4.01 2.825 14.35 23.03 21.36 41.7 14.468 84.262 23.748 126.778 26.833l-17.75-24.704c-38.773-3.285-77.69-11.775-115.5-26.596-3.197-1.253-5.877-1.77-8.05-1.85zm333.275.19c-2.156.052-5.048.512-8.728 1.79-33.582 11.65-69.487 20.215-106.523 24.646l-19.264 26.818c40.427-2.602 80.433-11.287 119.22-26.96 15.913-6.43 21.46-17.81 21.36-22.362-.052-2.276-.278-2.566-1.753-3.274-.738-.353-2.157-.71-4.313-.658zm-18.117 47.438c-42.5 15.87-86.26 23.856-130.262 25.117l-14.76 20.547-14.878-20.71c-44.985-1.745-89.98-10.23-133.905-24.306-12.78 28.51-18.94 61.14-19.603 93.44 37.52 17.497 62.135 39.817 75.556 64.63C177 417.8 179.282 443.62 174.184 467.98c7.72 5.007 16.126 9.144 24.98 12.432l5.557-47.89 18.563 2.154-5.935 51.156c9.57 2.21 19.443 3.53 29.377 3.982v-54.67h18.69v54.49c9.903-.638 19.705-2.128 29.155-4.484l-5.857-50.474 18.564-2.155 5.436 46.852c8.747-3.422 17.004-7.643 24.506-12.69-5.758-24.413-3.77-49.666 9.01-72.988 13.28-24.234 37.718-46 74.803-64.29-.62-33.526-6.687-66.122-19.113-94.23zm-266.733 47.006c34.602.23 68.407 12.236 101.358 36.867-46.604 33.147-129.794 34.372-108.29-36.755 2.315-.09 4.626-.127 6.933-.11zm242.825 0c2.307-.016 4.617.022 6.93.11 21.506 71.128-61.684 69.903-108.288 36.757 32.95-24.63 66.756-36.637 101.358-36.866zM255.164 332.14c11.77 21.725 19.193 43.452 25.367 65.178h-50.737c4.57-21.726 13.77-43.45 25.37-65.18z"
|
||||
></path>
|
||||
</svg>`;
|
||||
|
||||
const questionMark = html`<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
stroke-width="0"
|
||||
viewBox="0 0 24 24"
|
||||
height="100%"
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
d="M11.07 12.85c.77-1.39 2.25-2.21 3.11-3.44.91-1.29.4-3.7-2.18-3.7-1.69 0-2.52 1.28-2.87 2.34L6.54 6.96C7.25 4.83 9.18 3 11.99 3c2.35 0 3.96 1.07 4.78 2.41.7 1.15 1.11 3.3.03 4.9-1.2 1.77-2.35 2.31-2.97 3.45-.25.46-.35.76-.35 2.24h-2.89c-.01-.78-.13-2.05.48-3.15zM14 20c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2z"
|
||||
></path>
|
||||
</svg>`;
|
||||
|
||||
switch (difficultyKey) {
|
||||
case "Easy":
|
||||
return html`
|
||||
<div class="difficulty-skull active">${skull}</div>
|
||||
<div class="difficulty-skull">${skull}</div>
|
||||
<div class="difficulty-skull">${skull}</div>
|
||||
`;
|
||||
case "Medium":
|
||||
return html`
|
||||
<div class="difficulty-skull active">${skull}</div>
|
||||
<div class="difficulty-skull active">${skull}</div>
|
||||
<div class="difficulty-skull">${skull}</div>
|
||||
`;
|
||||
case "Hard":
|
||||
return html`
|
||||
<div class="difficulty-skull active">${skull}</div>
|
||||
<div class="difficulty-skull active">${skull}</div>
|
||||
<div class="difficulty-skull active">${skull}</div>
|
||||
`;
|
||||
case "Impossible":
|
||||
return html`
|
||||
<div class="difficulty-skull big active">${burningSkull}</div>
|
||||
`;
|
||||
default:
|
||||
return html`<div class="difficulty-skull big active">
|
||||
${questionMark}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="difficulty-indicator">
|
||||
${this.getDifficultyIcon(this.difficultyKey)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ export class FlagInput extends LitElement {
|
||||
left: 0;
|
||||
width: 560px;
|
||||
height: 500px;
|
||||
max-height: 50vh;
|
||||
background-color: rgb(35 35 35 / 0.8);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
@@ -140,7 +141,7 @@ export class FlagInput extends LitElement {
|
||||
detail: { flag: this.flag },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { GameMapType } from "../../core/game/Game";
|
||||
|
||||
// Add map descriptions
|
||||
export const MapDescription: Record<keyof typeof GameMapType, string> = {
|
||||
World: "World",
|
||||
Europe: "Europe",
|
||||
Mena: "MENA",
|
||||
NorthAmerica: "North America",
|
||||
Oceania: "Oceania",
|
||||
BlackSea: "Black Sea",
|
||||
};
|
||||
|
||||
import world from "../../../resources/maps/WorldMap.png";
|
||||
import oceania from "../../../resources/maps/Oceania.png";
|
||||
import europe from "../../../resources/maps/Europe.png";
|
||||
import mena from "../../../resources/maps/Mena.png";
|
||||
import northAmerica from "../../../resources/maps/NorthAmerica.png";
|
||||
import blackSea from "../../../resources/maps/BlackSea.png";
|
||||
|
||||
@customElement("map-display")
|
||||
export class MapDisplay extends LitElement {
|
||||
@property({ type: String }) mapKey = "";
|
||||
@property({ type: Boolean }) selected = false;
|
||||
|
||||
static styles = css`
|
||||
.option-card {
|
||||
width: 100%;
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
padding: 4px 4px 0 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(40, 40, 40, 0.95);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: #4a9eff;
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.option-card-title {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.option-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/2;
|
||||
color: #aaa;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
private getMapsImage(map: GameMapType): string {
|
||||
switch (map) {
|
||||
case GameMapType.World:
|
||||
return world;
|
||||
case GameMapType.Oceania:
|
||||
return oceania;
|
||||
case GameMapType.Europe:
|
||||
return europe;
|
||||
case GameMapType.Mena:
|
||||
return mena;
|
||||
case GameMapType.NorthAmerica:
|
||||
return northAmerica;
|
||||
case GameMapType.BlackSea:
|
||||
return blackSea;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const mapValue = GameMapType[this.mapKey as keyof typeof GameMapType];
|
||||
|
||||
return html`
|
||||
<div class="option-card ${this.selected ? "selected" : ""}">
|
||||
${this.getMapsImage(mapValue)
|
||||
? html`<img
|
||||
src="${this.getMapsImage(mapValue)}"
|
||||
alt="${this.mapKey}"
|
||||
class="option-image"
|
||||
/>`
|
||||
: html`<div class="option-image">
|
||||
<p>${this.mapKey}</p>
|
||||
</div>`}
|
||||
<div class="option-card-title">
|
||||
${MapDescription[this.mapKey as keyof typeof GameMapType]}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
+849
-487
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ export function createRenderer(
|
||||
canvas: HTMLCanvasElement,
|
||||
game: GameView,
|
||||
eventBus: EventBus,
|
||||
clientID: ClientID
|
||||
clientID: ClientID,
|
||||
): GameRenderer {
|
||||
const transformHandler = new TransformHandler(game, eventBus, canvas);
|
||||
|
||||
@@ -65,7 +65,7 @@ export function createRenderer(
|
||||
controlPanel.game = game;
|
||||
|
||||
const eventsDisplay = document.querySelector(
|
||||
"events-display"
|
||||
"events-display",
|
||||
) as EventsDisplay;
|
||||
if (!(eventsDisplay instanceof EventsDisplay)) {
|
||||
consolex.error("events display not found");
|
||||
@@ -75,7 +75,7 @@ export function createRenderer(
|
||||
eventsDisplay.clientID = clientID;
|
||||
|
||||
const playerInfo = document.querySelector(
|
||||
"player-info-overlay"
|
||||
"player-info-overlay",
|
||||
) as PlayerInfoOverlay;
|
||||
if (!(playerInfo instanceof PlayerInfoOverlay)) {
|
||||
consolex.error("player info overlay not found");
|
||||
@@ -129,7 +129,7 @@ export function createRenderer(
|
||||
buildMenu,
|
||||
uiState,
|
||||
playerInfo,
|
||||
playerPanel
|
||||
playerPanel,
|
||||
),
|
||||
new SpawnTimer(game, transformHandler),
|
||||
leaderboard,
|
||||
@@ -147,7 +147,7 @@ export function createRenderer(
|
||||
canvas,
|
||||
transformHandler,
|
||||
uiState,
|
||||
layers
|
||||
layers,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export class GameRenderer {
|
||||
private canvas: HTMLCanvasElement,
|
||||
public transformHandler: TransformHandler,
|
||||
public uiState: UIState,
|
||||
private layers: Layer[]
|
||||
private layers: Layer[],
|
||||
) {
|
||||
this.context = canvas.getContext("2d");
|
||||
}
|
||||
@@ -183,7 +183,7 @@ export class GameRenderer {
|
||||
this.transformHandler = new TransformHandler(
|
||||
this.game,
|
||||
this.eventBus,
|
||||
this.canvas
|
||||
this.canvas,
|
||||
);
|
||||
|
||||
requestAnimationFrame(() => this.renderGame());
|
||||
@@ -229,7 +229,7 @@ export class GameRenderer {
|
||||
const duration = performance.now() - start;
|
||||
if (duration > 50) {
|
||||
console.warn(
|
||||
`tick ${this.game.ticks()} took ${duration}ms to render frame`
|
||||
`tick ${this.game.ticks()} took ${duration}ms to render frame`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export class BuildMenu extends LitElement {
|
||||
|
||||
public onBuildSelected = (item: BuildItemDisplay) => {
|
||||
this.eventBus.emit(
|
||||
new BuildUnitIntentEvent(item.unitType, this.clickedCell)
|
||||
new BuildUnitIntentEvent(item.unitType, this.clickedCell),
|
||||
);
|
||||
this.hideMenu();
|
||||
};
|
||||
@@ -235,7 +235,7 @@ export class BuildMenu extends LitElement {
|
||||
? this.game
|
||||
.unitInfo(item.unitType)
|
||||
.cost(this.myPlayer)
|
||||
: 0
|
||||
: 0,
|
||||
)}
|
||||
<img
|
||||
src=${goldCoinIcon}
|
||||
@@ -246,10 +246,10 @@ export class BuildMenu extends LitElement {
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -10,7 +10,7 @@ const emojiTable: string[][] = [
|
||||
["😎", "❤️", "💰", "🤝", "🖕"],
|
||||
["💥", "🆘", "🕊️", "➡️", "⬅️"],
|
||||
["↙️", "↖️", "↗️", "⬆️", "↘️"],
|
||||
["⬇️", "❓", "⏳", "☢️", "⚠️"]
|
||||
["⬇️", "❓", "⏳", "☢️", "⚠️"],
|
||||
];
|
||||
|
||||
@customElement("emoji-table")
|
||||
@@ -110,10 +110,10 @@ export class EmojiTable extends LitElement {
|
||||
>
|
||||
${emoji}
|
||||
</button>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { AllPlayers, MessageType } from "../../../core/game/Game";
|
||||
import { DisplayMessageUpdate } from "../../../core/game/GameUpdates";
|
||||
import { AllPlayers, MessageType, PlayerType } from "../../../core/game/Game";
|
||||
import {
|
||||
AttackUpdate,
|
||||
DisplayMessageUpdate,
|
||||
} from "../../../core/game/GameUpdates";
|
||||
import { EmojiUpdate } from "../../../core/game/GameUpdates";
|
||||
import { TargetPlayerUpdate } from "../../../core/game/GameUpdates";
|
||||
import { AllianceExpiredUpdate } from "../../../core/game/GameUpdates";
|
||||
@@ -16,6 +19,7 @@ import { SendAllianceReplyIntentEvent } from "../../Transport";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { onlyImages, sanitize } from "../../../core/Util";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { renderTroops } from "../../Utils";
|
||||
|
||||
interface Event {
|
||||
description: string;
|
||||
@@ -38,6 +42,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
public clientID: ClientID;
|
||||
|
||||
private events: Event[] = [];
|
||||
@state() private incomingAttacks: AttackUpdate[] = [];
|
||||
@state() private outgoingAttacks: AttackUpdate[] = [];
|
||||
|
||||
private updateMap = new Map([
|
||||
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
|
||||
@@ -54,6 +60,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
constructor() {
|
||||
super();
|
||||
this.events = [];
|
||||
this.incomingAttacks = [];
|
||||
this.outgoingAttacks = [];
|
||||
}
|
||||
|
||||
init() {}
|
||||
@@ -85,12 +93,18 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
if (!myPlayer) {
|
||||
return;
|
||||
}
|
||||
myPlayer.incomingAttacks().forEach((a) => {
|
||||
// console.log(`got incoming attack: ${JSON.stringify(a)}`);
|
||||
});
|
||||
myPlayer.outgoingAttacks().forEach((a) => {
|
||||
// console.log(`got outgoing attack: ${JSON.stringify(a)}`);
|
||||
|
||||
// Update attacks
|
||||
this.incomingAttacks = myPlayer.incomingAttacks().filter((a) => {
|
||||
const t = (this.game.playerBySmallID(a.attackerID) as PlayerView).type();
|
||||
return t != PlayerType.Bot;
|
||||
});
|
||||
|
||||
this.outgoingAttacks = myPlayer
|
||||
.outgoingAttacks()
|
||||
.filter((a) => a.targetID != 0);
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private addEvent(event: Event) {
|
||||
@@ -136,10 +150,10 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
const requestor = this.game.playerBySmallID(
|
||||
update.requestorID
|
||||
update.requestorID,
|
||||
) as PlayerView;
|
||||
const recipient = this.game.playerBySmallID(
|
||||
update.recipientID
|
||||
update.recipientID,
|
||||
) as PlayerView;
|
||||
|
||||
this.addEvent({
|
||||
@@ -150,7 +164,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
className: "btn",
|
||||
action: () =>
|
||||
this.eventBus.emit(
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, true)
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, true),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -158,7 +172,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
className: "btn-info",
|
||||
action: () =>
|
||||
this.eventBus.emit(
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, false)
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, false),
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -167,7 +181,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
createdAt: this.game.ticks(),
|
||||
onDelete: () =>
|
||||
this.eventBus.emit(
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, false)
|
||||
new SendAllianceReplyIntentEvent(requestor, recipient, false),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -179,7 +193,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
const recipient = this.game.playerBySmallID(
|
||||
update.request.recipientID
|
||||
update.request.recipientID,
|
||||
) as PlayerView;
|
||||
|
||||
this.addEvent({
|
||||
@@ -224,8 +238,8 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
update.player1ID === myPlayer.smallID()
|
||||
? update.player2ID
|
||||
: update.player2ID === myPlayer.smallID()
|
||||
? update.player1ID
|
||||
: null;
|
||||
? update.player1ID
|
||||
: null;
|
||||
const other = this.game.playerBySmallID(otherID) as PlayerView;
|
||||
if (!other || !myPlayer.isAlive() || !other.isAlive()) return;
|
||||
|
||||
@@ -261,7 +275,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
? AllPlayers
|
||||
: this.game.playerBySmallID(update.emoji.recipientID);
|
||||
const sender = this.game.playerBySmallID(
|
||||
update.emoji.senderID
|
||||
update.emoji.senderID,
|
||||
) as PlayerView;
|
||||
|
||||
if (recipient == myPlayer) {
|
||||
@@ -300,8 +314,62 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private renderAttacks() {
|
||||
if (
|
||||
this.incomingAttacks.length === 0 &&
|
||||
this.outgoingAttacks.length === 0
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.incomingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-red-400">
|
||||
${this.incomingAttacks.map(
|
||||
(attack) => html`
|
||||
<div class="ml-2">
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(
|
||||
attack.attackerID,
|
||||
) as PlayerView
|
||||
)?.name()}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
${this.outgoingAttacks.length > 0
|
||||
? html`
|
||||
<tr class="border-t border-gray-700">
|
||||
<td class="lg:p-3 p-1 text-left text-blue-400">
|
||||
${this.outgoingAttacks.map(
|
||||
(attack) => html`
|
||||
<div class="ml-2">
|
||||
${renderTroops(attack.troops)}
|
||||
${(
|
||||
this.game.playerBySmallID(attack.targetID) as PlayerView
|
||||
)?.name()}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.events.length === 0) {
|
||||
if (
|
||||
this.events.length === 0 &&
|
||||
this.incomingAttacks.length === 0 &&
|
||||
this.outgoingAttacks.length === 0
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -317,7 +385,7 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
(event, index) => html`
|
||||
<tr
|
||||
class="border-b border-opacity-0 ${this.getMessageTypeClasses(
|
||||
event.type
|
||||
event.type,
|
||||
)}"
|
||||
>
|
||||
<td class="lg:p-3 p-1 text-left">
|
||||
@@ -342,15 +410,16 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
${btn.text}
|
||||
</button>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
${this.renderAttacks()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -358,6 +427,6 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this; // Required for Tailwind classes to work with Lit
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
name: myPlayer.displayName(),
|
||||
position: place,
|
||||
score: formatPercentage(
|
||||
myPlayer.numTilesOwned() / this.game.numLandTiles()
|
||||
myPlayer.numTilesOwned() / this.game.numLandTiles(),
|
||||
),
|
||||
gold: renderNumber(myPlayer.gold()),
|
||||
isMyPlayer: true,
|
||||
@@ -205,7 +205,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
<td>${player.score}</td>
|
||||
<td>${player.gold}</td>
|
||||
</tr>
|
||||
`
|
||||
`,
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -26,7 +26,7 @@ class RenderInfo {
|
||||
public lastRenderCalc: number,
|
||||
public location: Cell,
|
||||
public fontSize: number,
|
||||
public element: HTMLElement
|
||||
public element: HTMLElement,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class NameLayer implements Layer {
|
||||
private game: GameView,
|
||||
private theme: Theme,
|
||||
private transformHandler: TransformHandler,
|
||||
private clientID: ClientID
|
||||
private clientID: ClientID,
|
||||
) {
|
||||
this.traitorIconImage = new Image();
|
||||
this.traitorIconImage.src = traitorIcon;
|
||||
@@ -101,7 +101,13 @@ export class NameLayer implements Layer {
|
||||
if (!this.seenPlayers.has(player)) {
|
||||
this.seenPlayers.add(player);
|
||||
this.renders.push(
|
||||
new RenderInfo(player, 0, null, 0, this.createPlayerElement(player))
|
||||
new RenderInfo(
|
||||
player,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
this.createPlayerElement(player),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -110,11 +116,11 @@ export class NameLayer implements Layer {
|
||||
|
||||
public renderLayer(mainContex: CanvasRenderingContext2D) {
|
||||
const screenPosOld = this.transformHandler.worldToScreenCoordinates(
|
||||
new Cell(0, 0)
|
||||
new Cell(0, 0),
|
||||
);
|
||||
const screenPos = new Cell(
|
||||
screenPosOld.x - window.innerWidth / 2,
|
||||
screenPosOld.y - window.innerHeight / 2
|
||||
screenPosOld.y - window.innerHeight / 2,
|
||||
);
|
||||
this.container.style.transform = `translate(${screenPos.x}px, ${screenPos.y}px) scale(${this.transformHandler.scale})`;
|
||||
|
||||
@@ -131,7 +137,7 @@ export class NameLayer implements Layer {
|
||||
0,
|
||||
0,
|
||||
mainContex.canvas.width,
|
||||
mainContex.canvas.height
|
||||
mainContex.canvas.height,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,10 +149,9 @@ export class NameLayer implements Layer {
|
||||
element.style.alignItems = "center";
|
||||
element.style.gap = "0px";
|
||||
|
||||
|
||||
if (player.flag()) {
|
||||
const flagImg = document.createElement("img");
|
||||
flagImg.classList.add('player-flag');
|
||||
flagImg.classList.add("player-flag");
|
||||
flagImg.style.marginBottom = "-5%";
|
||||
flagImg.style.opacity = '0.8';
|
||||
flagImg.src = '/flags/' + sanitize(player.flag()) + '.svg';
|
||||
@@ -157,8 +162,9 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
|
||||
const nameDiv = document.createElement("div");
|
||||
nameDiv.classList.add('player-name');
|
||||
nameDiv.innerHTML = (player.type() !== PlayerType.Human ? "🤖 " : '') + player.name();
|
||||
nameDiv.classList.add("player-name");
|
||||
nameDiv.innerHTML =
|
||||
(player.type() !== PlayerType.Human ? "🤖 " : "") + player.name();
|
||||
nameDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
|
||||
nameDiv.style.fontFamily = this.theme.font();
|
||||
nameDiv.style.whiteSpace = "nowrap";
|
||||
@@ -167,9 +173,8 @@ export class NameLayer implements Layer {
|
||||
nameDiv.style.zIndex = "3";
|
||||
element.appendChild(nameDiv);
|
||||
|
||||
|
||||
const troopsDiv = document.createElement("div");
|
||||
troopsDiv.classList.add('player-troops');
|
||||
troopsDiv.classList.add("player-troops");
|
||||
troopsDiv.textContent = renderTroops(player.troops());
|
||||
troopsDiv.style.color = this.theme.playerInfoColor(player.id()).toHex();
|
||||
troopsDiv.style.fontFamily = this.theme.font();
|
||||
@@ -178,7 +183,7 @@ export class NameLayer implements Layer {
|
||||
element.appendChild(troopsDiv);
|
||||
|
||||
const iconsDiv = document.createElement("div");
|
||||
iconsDiv.classList.add('player-icons');
|
||||
iconsDiv.classList.add("player-icons");
|
||||
iconsDiv.style.display = "flex";
|
||||
iconsDiv.style.gap = "4px";
|
||||
iconsDiv.style.justifyContent = "center";
|
||||
@@ -189,6 +194,9 @@ export class NameLayer implements Layer {
|
||||
iconsDiv.style.height = "100%";
|
||||
element.appendChild(iconsDiv);
|
||||
|
||||
// Start off invisible so it doesn't flash at 0,0
|
||||
element.style.display = "none";
|
||||
|
||||
this.container.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
@@ -203,7 +211,7 @@ export class NameLayer implements Layer {
|
||||
const oldLocation = render.location;
|
||||
render.location = new Cell(
|
||||
render.player.nameLocation().x,
|
||||
render.player.nameLocation().y
|
||||
render.player.nameLocation().y,
|
||||
);
|
||||
|
||||
// Calculate base size and scale
|
||||
@@ -226,14 +234,20 @@ export class NameLayer implements Layer {
|
||||
render.lastRenderCalc = now + this.rand.nextInt(0, 100);
|
||||
|
||||
// Update text sizes
|
||||
const nameDiv = render.element.querySelector(".player-name") as HTMLDivElement;
|
||||
const troopsDiv = render.element.querySelector(".player-troops") as HTMLDivElement;
|
||||
const nameDiv = render.element.querySelector(
|
||||
".player-name",
|
||||
) as HTMLDivElement;
|
||||
const troopsDiv = render.element.querySelector(
|
||||
".player-troops",
|
||||
) as HTMLDivElement;
|
||||
nameDiv.style.fontSize = `${render.fontSize}px`;
|
||||
troopsDiv.style.fontSize = `${render.fontSize}px`;
|
||||
troopsDiv.textContent = renderTroops(render.player.troops());
|
||||
|
||||
// Handle icons
|
||||
const iconsDiv = render.element.querySelector(".player-icons") as HTMLDivElement;
|
||||
const iconsDiv = render.element.querySelector(
|
||||
".player-icons",
|
||||
) as HTMLDivElement;
|
||||
const iconSize = Math.min(render.fontSize * 1.5, 48);
|
||||
const myPlayer = this.getPlayer();
|
||||
|
||||
@@ -242,7 +256,7 @@ export class NameLayer implements Layer {
|
||||
if (render.player === this.firstPlace) {
|
||||
if (!existingCrown) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(this.crownIconImage.src, iconSize, "crown")
|
||||
this.createIconElement(this.crownIconImage.src, iconSize, "crown"),
|
||||
);
|
||||
}
|
||||
} else if (existingCrown) {
|
||||
@@ -254,7 +268,11 @@ export class NameLayer implements Layer {
|
||||
if (render.player.isTraitor()) {
|
||||
if (!existingTraitor) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(this.traitorIconImage.src, iconSize, "traitor")
|
||||
this.createIconElement(
|
||||
this.traitorIconImage.src,
|
||||
iconSize,
|
||||
"traitor",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (existingTraitor) {
|
||||
@@ -269,8 +287,8 @@ export class NameLayer implements Layer {
|
||||
this.createIconElement(
|
||||
this.allianceIconImage.src,
|
||||
iconSize,
|
||||
"alliance"
|
||||
)
|
||||
"alliance",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (existingAlliance) {
|
||||
@@ -285,7 +303,7 @@ export class NameLayer implements Layer {
|
||||
) {
|
||||
if (!existingTarget) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(this.targetIconImage.src, iconSize, "target")
|
||||
this.createIconElement(this.targetIconImage.src, iconSize, "target"),
|
||||
);
|
||||
}
|
||||
} else if (existingTarget) {
|
||||
@@ -299,7 +317,7 @@ export class NameLayer implements Layer {
|
||||
.filter(
|
||||
(emoji) =>
|
||||
emoji.recipientID == AllPlayers ||
|
||||
emoji.recipientID == myPlayer?.smallID()
|
||||
emoji.recipientID == myPlayer?.smallID(),
|
||||
);
|
||||
|
||||
if (emojis.length > 0) {
|
||||
@@ -332,7 +350,7 @@ export class NameLayer implements Layer {
|
||||
private createIconElement(
|
||||
src: string,
|
||||
size: number,
|
||||
id: string
|
||||
id: string,
|
||||
): HTMLImageElement {
|
||||
const icon = document.createElement("img");
|
||||
icon.src = src;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { renderNumber, renderTroops } from "../../Utils";
|
||||
function euclideanDistWorld(
|
||||
coord: { x: number; y: number },
|
||||
tileRef: TileRef,
|
||||
game: GameView
|
||||
game: GameView,
|
||||
): number {
|
||||
const x = game.x(tileRef);
|
||||
const y = game.y(tileRef);
|
||||
@@ -71,7 +71,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
|
||||
init() {
|
||||
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) =>
|
||||
this.onMouseEvent(e)
|
||||
this.onMouseEvent(e),
|
||||
);
|
||||
this._isActive = true;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private handleAllianceClick(
|
||||
e: Event,
|
||||
myPlayer: PlayerView,
|
||||
other: PlayerView
|
||||
other: PlayerView,
|
||||
) {
|
||||
e.stopPropagation();
|
||||
this.eventBus.emit(new SendAllianceRequestIntentEvent(myPlayer, other));
|
||||
@@ -63,7 +63,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private handleBreakAllianceClick(
|
||||
e: Event,
|
||||
myPlayer: PlayerView,
|
||||
other: PlayerView
|
||||
other: PlayerView,
|
||||
) {
|
||||
e.stopPropagation();
|
||||
this.eventBus.emit(new SendBreakAllianceIntentEvent(myPlayer, other));
|
||||
|
||||
@@ -99,7 +99,7 @@ export class RadialMenu implements Layer {
|
||||
private buildMenu: BuildMenu,
|
||||
private uiState: UIState,
|
||||
private playerInfoOverlay: PlayerInfoOverlay,
|
||||
private playerPanel: PlayerPanel
|
||||
private playerPanel: PlayerPanel,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@@ -108,7 +108,7 @@ export class RadialMenu implements Layer {
|
||||
this.eventBus.on(ShowBuildMenuEvent, (e) => {
|
||||
const clickedCell = this.transformHandler.screenToWorldCoordinates(
|
||||
e.x,
|
||||
e.y
|
||||
e.y,
|
||||
);
|
||||
if (clickedCell == null) {
|
||||
return;
|
||||
@@ -144,7 +144,7 @@ export class RadialMenu implements Layer {
|
||||
.append("g")
|
||||
.attr(
|
||||
"transform",
|
||||
`translate(${this.menuSize / 2},${this.menuSize / 2})`
|
||||
`translate(${this.menuSize / 2},${this.menuSize / 2})`,
|
||||
);
|
||||
|
||||
const pie = d3
|
||||
@@ -169,7 +169,7 @@ export class RadialMenu implements Layer {
|
||||
.append("path")
|
||||
.attr("d", arc)
|
||||
.attr("fill", (d) =>
|
||||
d.data.disabled ? this.disabledColor : d.data.color
|
||||
d.data.disabled ? this.disabledColor : d.data.color,
|
||||
)
|
||||
.attr("stroke", "#ffffff")
|
||||
.attr("stroke-width", "2")
|
||||
@@ -294,7 +294,7 @@ export class RadialMenu implements Layer {
|
||||
|
||||
this.clickedCell = this.transformHandler.screenToWorldCoordinates(
|
||||
event.x,
|
||||
event.y
|
||||
event.y,
|
||||
);
|
||||
if (!this.g.isValidCoord(this.clickedCell.x, this.clickedCell.y)) {
|
||||
return;
|
||||
@@ -323,7 +323,7 @@ export class RadialMenu implements Layer {
|
||||
private handlePlayerActions(
|
||||
myPlayer: PlayerView,
|
||||
actions: PlayerActions,
|
||||
tile: TileRef
|
||||
tile: TileRef,
|
||||
) {
|
||||
this.activateMenuElement(Slot.Build, "#ebe250", buildIcon, () => {
|
||||
this.buildMenu.showMenu(myPlayer, this.clickedCell);
|
||||
@@ -341,8 +341,8 @@ export class RadialMenu implements Layer {
|
||||
new SendBoatAttackIntentEvent(
|
||||
this.g.owner(tile).id(),
|
||||
this.clickedCell,
|
||||
this.uiState.attackRatio * myPlayer.troops()
|
||||
)
|
||||
this.uiState.attackRatio * myPlayer.troops(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -394,8 +394,8 @@ export class RadialMenu implements Layer {
|
||||
this.eventBus.emit(
|
||||
new SendAttackIntentEvent(
|
||||
this.g.owner(clicked).id(),
|
||||
this.uiState.attackRatio * myPlayer.troops()
|
||||
)
|
||||
this.uiState.attackRatio * myPlayer.troops(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -406,7 +406,7 @@ export class RadialMenu implements Layer {
|
||||
slot: Slot,
|
||||
color: string,
|
||||
icon: string,
|
||||
action: () => void
|
||||
action: () => void,
|
||||
) {
|
||||
const menuItem = this.menuItems.get(slot);
|
||||
menuItem.action = action;
|
||||
|
||||
@@ -50,7 +50,10 @@ export class StructureLayer implements Layer {
|
||||
},
|
||||
};
|
||||
|
||||
constructor(private game: GameView, private eventBus: EventBus) {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
this.loadIconData();
|
||||
}
|
||||
@@ -72,11 +75,11 @@ export class StructureLayer implements Layer {
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height
|
||||
tempCanvas.height,
|
||||
);
|
||||
this.unitIcons.set(unitType, iconData);
|
||||
console.log(
|
||||
`icond data width height: ${iconData.width}, ${iconData.height}`
|
||||
`icond data width height: ${iconData.width}, ${iconData.height}`,
|
||||
);
|
||||
};
|
||||
});
|
||||
@@ -89,9 +92,9 @@ export class StructureLayer implements Layer {
|
||||
tick() {
|
||||
this.game
|
||||
.updatesSinceLastTick()
|
||||
[GameUpdateType.Unit].forEach((u) =>
|
||||
this.handleUnitRendering(this.game.unit(u.id))
|
||||
);
|
||||
[
|
||||
GameUpdateType.Unit
|
||||
].forEach((u) => this.handleUnitRendering(this.game.unit(u.id)));
|
||||
}
|
||||
|
||||
init() {
|
||||
@@ -113,7 +116,7 @@ export class StructureLayer implements Layer {
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +136,7 @@ export class StructureLayer implements Layer {
|
||||
// Clear previous rendering
|
||||
for (const tile of this.game.bfs(
|
||||
unit.tile(),
|
||||
euclDistFN(unit.tile(), config.borderRadius)
|
||||
euclDistFN(unit.tile(), config.borderRadius),
|
||||
)) {
|
||||
this.clearCell(new Cell(this.game.x(tile), this.game.y(tile)));
|
||||
}
|
||||
@@ -145,27 +148,27 @@ export class StructureLayer implements Layer {
|
||||
// Draw border and territory
|
||||
for (const tile of this.game.bfs(
|
||||
unit.tile(),
|
||||
euclDistFN(unit.tile(), config.borderRadius)
|
||||
euclDistFN(unit.tile(), config.borderRadius),
|
||||
)) {
|
||||
this.paintCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
unit.type() == UnitType.Construction
|
||||
? underConstructionColor
|
||||
: this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
for (const tile of this.game.bfs(
|
||||
unit.tile(),
|
||||
euclDistFN(unit.tile(), config.territoryRadius)
|
||||
euclDistFN(unit.tile(), config.territoryRadius),
|
||||
)) {
|
||||
this.paintCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
unit.type() == UnitType.Construction
|
||||
? underConstructionColor
|
||||
: this.theme.territoryColor(unit.owner().info()),
|
||||
130
|
||||
130,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,7 +184,7 @@ export class StructureLayer implements Layer {
|
||||
startY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
unit: UnitView
|
||||
unit: UnitView,
|
||||
) {
|
||||
let color = this.theme.borderColor(unit.owner().info());
|
||||
if (unit.type() == UnitType.Construction) {
|
||||
|
||||
@@ -50,7 +50,10 @@ export class TerritoryLayer implements Layer {
|
||||
private refreshRate = 50;
|
||||
private lastRefresh = 0;
|
||||
|
||||
constructor(private game: GameView, private eventBus: EventBus) {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
}
|
||||
|
||||
@@ -67,7 +70,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.game
|
||||
.bfs(
|
||||
tile,
|
||||
manhattanDistFN(tile, this.game.config().defensePostRange())
|
||||
manhattanDistFN(tile, this.game.config().defensePostRange()),
|
||||
)
|
||||
.forEach((t) => {
|
||||
if (
|
||||
@@ -91,7 +94,7 @@ export class TerritoryLayer implements Layer {
|
||||
0,
|
||||
0,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
this.game.height(),
|
||||
);
|
||||
const humans = this.game
|
||||
.playerViews()
|
||||
@@ -111,7 +114,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.paintHighlightCell(
|
||||
new Cell(this.game.x(tile), this.game.y(tile)),
|
||||
this.theme.spawnHighlightColor(),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -138,7 +141,7 @@ export class TerritoryLayer implements Layer {
|
||||
0,
|
||||
0,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
this.game.height(),
|
||||
);
|
||||
this.initImageData();
|
||||
this.canvas.width = this.game.width();
|
||||
@@ -185,7 +188,7 @@ export class TerritoryLayer implements Layer {
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
this.game.height(),
|
||||
);
|
||||
if (this.game.inSpawnPhase()) {
|
||||
context.drawImage(
|
||||
@@ -193,7 +196,7 @@ export class TerritoryLayer implements Layer {
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -224,7 +227,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.falloutColor(),
|
||||
150
|
||||
150,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -241,14 +244,14 @@ export class TerritoryLayer implements Layer {
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.defendedBorderColor(owner.info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
} else {
|
||||
this.paintCell(
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.borderColor(owner.info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -256,7 +259,7 @@ export class TerritoryLayer implements Layer {
|
||||
this.game.x(tile),
|
||||
this.game.y(tile),
|
||||
this.theme.territoryColor(owner.info()),
|
||||
150
|
||||
150,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,16 +26,19 @@ export class TopBar extends LitElement implements Layer {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const myPlayer = this.game?.myPlayer();
|
||||
if (!myPlayer?.isAlive() || this.game?.inSpawnPhase()) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const popRate = this.game.config().populationIncreaseRate(myPlayer) * 10;
|
||||
const maxPop = this.game.config().maxPopulation(myPlayer);
|
||||
const goldPerSecond = this.game.config().goldAdditionRate(myPlayer) * 10;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed top-0 z-50 bg-black/90 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden"
|
||||
class="fixed top-0 z-50 bg-gray-800/70 text-white text-sm p-1 rounded grid grid-cols-1 sm:grid-cols-2 w-1/2 sm:w-2/3 md:w-1/2 lg:hidden backdrop-blur"
|
||||
>
|
||||
<!-- Pop section (takes 2 columns on desktop) -->
|
||||
<div
|
||||
@@ -48,7 +51,6 @@ export class TopBar extends LitElement implements Layer {
|
||||
>
|
||||
<span>(+${renderTroops(popRate)})</span>
|
||||
</div>
|
||||
|
||||
<!-- Gold section (takes 1 column on desktop) -->
|
||||
<div
|
||||
class="flex items-center space-x-2 overflow-x-auto whitespace-nowrap"
|
||||
|
||||
@@ -36,7 +36,7 @@ export class UnitLayer implements Layer {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private eventBus: EventBus,
|
||||
private clientID: ClientID
|
||||
private clientID: ClientID,
|
||||
) {
|
||||
this.theme = game.config().theme();
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export class UnitLayer implements Layer {
|
||||
-this.game.width() / 2,
|
||||
-this.game.height() / 2,
|
||||
this.game.width(),
|
||||
this.game.height()
|
||||
this.game.height(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ export class UnitLayer implements Layer {
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), 6)
|
||||
euclDistFN(unit.lastTile(), 6),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
@@ -147,21 +147,21 @@ export class UnitLayer implements Layer {
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 4)
|
||||
manhattanDistFN(unit.tile(), 4),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export class UnitLayer implements Layer {
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -198,14 +198,14 @@ export class UnitLayer implements Layer {
|
||||
this.game.y(unit.tile()),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
this.paintCell(
|
||||
this.game.x(unit.lastTile()),
|
||||
this.game.y(unit.lastTile()),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ export class UnitLayer implements Layer {
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), 2)
|
||||
euclDistFN(unit.lastTile(), 2),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
@@ -228,7 +228,7 @@ export class UnitLayer implements Layer {
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,7 @@ export class UnitLayer implements Layer {
|
||||
this.game.y(unit.tile()),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ export class UnitLayer implements Layer {
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
euclDistFN(unit.lastTile(), 3)
|
||||
euclDistFN(unit.lastTile(), 3),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
@@ -266,28 +266,28 @@ export class UnitLayer implements Layer {
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 2)
|
||||
manhattanDistFN(unit.tile(), 2),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 1)
|
||||
manhattanDistFN(unit.tile(), 1),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -305,7 +305,7 @@ export class UnitLayer implements Layer {
|
||||
// Clear previous area
|
||||
for (const t of this.game.bfs(
|
||||
unit.lastTile(),
|
||||
manhattanDistFN(unit.lastTile(), 3)
|
||||
manhattanDistFN(unit.lastTile(), 3),
|
||||
)) {
|
||||
this.clearCell(this.game.x(t), this.game.y(t));
|
||||
}
|
||||
@@ -318,35 +318,35 @@ export class UnitLayer implements Layer {
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
150
|
||||
150,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint border
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 2)
|
||||
manhattanDistFN(unit.tile(), 2),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.borderColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint territory
|
||||
for (const t of this.game.bfs(
|
||||
unit.tile(),
|
||||
manhattanDistFN(unit.tile(), 1)
|
||||
manhattanDistFN(unit.tile(), 1),
|
||||
)) {
|
||||
this.paintCell(
|
||||
this.game.x(t),
|
||||
this.game.y(t),
|
||||
rel,
|
||||
this.theme.territoryColor(unit.owner().info()),
|
||||
255
|
||||
255,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -362,7 +362,7 @@ export class UnitLayer implements Layer {
|
||||
y: number,
|
||||
relationship: Relationship,
|
||||
color: Colord,
|
||||
alpha: number
|
||||
alpha: number,
|
||||
) {
|
||||
this.clearCell(x, y);
|
||||
if (this.alternateView) {
|
||||
|
||||
+270
-162
@@ -1,178 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenFront (ALPHA)</title>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenFront (ALPHA)</title>
|
||||
<!-- Critical CSS to prevent FOUC -->
|
||||
<style>
|
||||
.preload * {
|
||||
-webkit-transition: none !important;
|
||||
-moz-transition: none !important;
|
||||
-ms-transition: none !important;
|
||||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
<!-- Preload critical assets -->
|
||||
<link rel="preload" href="../../resources/images/OpenFrontLogo.svg" as="image" />
|
||||
<link rel="preload" href="../../resources/images/DiscordIcon.svg" as="image" />
|
||||
html {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
<!-- Critical CSS to prevent FOUC -->
|
||||
<style>
|
||||
.preload * {
|
||||
-webkit-transition: none !important;
|
||||
-moz-transition: none !important;
|
||||
-ms-transition: none !important;
|
||||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
html.preload {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html.preload {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
/* Critical styles to prevent layout shift */
|
||||
.container {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: -1;
|
||||
}
|
||||
.logo-glow {
|
||||
fill: url(#logo-gradient);
|
||||
filter: drop-shadow(1px 1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(-1px -1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(1px -1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(-1px 1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(3px 3px 0px rgb(255, 255, 255));
|
||||
}
|
||||
.logo-version {
|
||||
color: #2563eb;
|
||||
filter: drop-shadow(1px 1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(-1px -1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(1px -1px 0px rgb(255, 255, 255))
|
||||
drop-shadow(-1px 1px 0px rgb(255, 255, 255));
|
||||
}
|
||||
</style>
|
||||
|
||||
/* Critical styles to prevent layout shift */
|
||||
.container {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
<!-- Immediate execution to prevent FOUC -->
|
||||
<script>
|
||||
document.documentElement.className = "preload";
|
||||
</script>
|
||||
|
||||
<!-- Immediate execution to prevent FOUC -->
|
||||
<script>
|
||||
document.documentElement.className = "preload";
|
||||
</script>
|
||||
<!-- Analytics -->
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=AW-16702609763"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "AW-16702609763");
|
||||
</script>
|
||||
<script
|
||||
async
|
||||
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7035513310742290"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</head>
|
||||
|
||||
<!-- Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-16702609763"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "AW-16702609763");
|
||||
</script>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7035513310742290"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="h-full select-none font-sans min-h-screen bg-opacity-0 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-col">
|
||||
<!-- Main container with responsive padding -->
|
||||
<!-- Logo section remains the same -->
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8 flex-grow">
|
||||
<div class="flex justify-center">
|
||||
<img src="../../resources/images/OpenFrontLogo.png" alt="OpenFront.io" />
|
||||
</div>
|
||||
<div class="flex justify-center text-sm font-bold mt-[-10px] pb-6 md:pb-12">
|
||||
v0.15.0
|
||||
</div>
|
||||
|
||||
<div class="relative items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4">
|
||||
<flag-input></flag-input>
|
||||
<username-input class="w-full"></username-input>
|
||||
</div>
|
||||
|
||||
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2">
|
||||
<public-lobby class="w-full"></public-lobby>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex gap-4 sm:gap-6 lg:gap-8 max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto">
|
||||
<button id="host-lobby-button"
|
||||
class="bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md w-full border-none cursor-pointer transition-colors duration-300">
|
||||
Create Lobby
|
||||
</button>
|
||||
<button id="join-private-lobby-button"
|
||||
class="bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md w-full border-none cursor-pointer transition-colors duration-300">
|
||||
Join Lobby
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto mt-4 sm:mt-6 lg:mt-8">
|
||||
<button id="single-player"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white p-3 sm:p-4 lg:p-5 font-bold text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300">
|
||||
Single Player
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game components -->
|
||||
<div id="customMenu" class="mt-4 sm:mt-6 lg:mt-8">
|
||||
<ul></ul>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<div id="radialMenu" class="radial-menu"></div>
|
||||
|
||||
<!-- Game modals and overlays -->
|
||||
<single-player-modal></single-player-modal>
|
||||
<host-lobby-modal></host-lobby-modal>
|
||||
<join-private-lobby-modal></join-private-lobby-modal>
|
||||
<emoji-table></emoji-table>
|
||||
<leader-board></leader-board>
|
||||
<build-menu></build-menu>
|
||||
<win-modal></win-modal>
|
||||
<top-bar></top-bar>
|
||||
<player-panel></player-panel>
|
||||
|
||||
<div class="fixed right-0 top-0 z-50 flex flex-col w-32 sm:w-32 lg:w-48">
|
||||
<options-menu></options-menu>
|
||||
<player-info-overlay></player-info-overlay>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Footer section -->
|
||||
<div class="w-full bg-gray-900/80 backdrop-blur-md py-4">
|
||||
<body
|
||||
class="h-full select-none font-sans min-h-screen bg-opacity-0 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-col"
|
||||
>
|
||||
<!-- Logo section remains the same -->
|
||||
<div
|
||||
class="max-w-7xl mx-auto px-4 flex justify-between items-center text-white sm:flex-row flex-col sm:gap-0 gap-4">
|
||||
<div class="flex sm:flex-row flex-col sm:gap-8 gap-2">
|
||||
<a href="https://youtu.be/jvHEvbko3uw?si=znspkP84P76B1w5I"
|
||||
class="text-white/70 hover:text-white transition-colors duration-300" target="_blank">How to Play</a>
|
||||
<a href="https://discord.gg/k22YrnAzGp" class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank">Discord</a>
|
||||
<a href="https://openfront.fandom.com/wiki/Openfront_Wiki" class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank">Wiki</a>
|
||||
</div>
|
||||
<div class="text-white/70">
|
||||
© 2025
|
||||
<a href="https://github.com/openfrontio/OpenFrontIO" class="hover:text-white transition-colors duration-300"
|
||||
target="_blank">OpenFront.io</a>.
|
||||
class="flex justify-center items-center flex-col w-full bg-gray-900/80 backdrop-blur-md pt-8 pb-2 px-8"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1364 259"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="currentColor"
|
||||
class="max-w-[450px] h-full logo-glow"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="logo-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color: #2563eb" />
|
||||
<stop offset="100%" style="stop-color: #3b82f6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<path
|
||||
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
||||
/>
|
||||
<path
|
||||
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
|
||||
/>
|
||||
<path
|
||||
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
|
||||
/>
|
||||
<path
|
||||
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
|
||||
/>
|
||||
<path
|
||||
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
|
||||
/>
|
||||
<path
|
||||
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
|
||||
/>
|
||||
<path
|
||||
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
|
||||
/>
|
||||
<path
|
||||
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
|
||||
/>
|
||||
<path
|
||||
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
|
||||
/>
|
||||
<path
|
||||
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
|
||||
/>
|
||||
<path
|
||||
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="flex justify-center text-sm font-bold mt-[-5px] logo-version">
|
||||
v0.15.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
// Remove preload class after everything is loaded
|
||||
window.addEventListener("load", function () {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.classList.remove("preload");
|
||||
<!-- Main container with responsive padding -->
|
||||
<div class="flex justify-center items-center flex-grow">
|
||||
<div class="container px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8">
|
||||
<div
|
||||
class="relative items-center max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4"
|
||||
>
|
||||
<flag-input></flag-input>
|
||||
<username-input class="w-full"></username-input>
|
||||
</div>
|
||||
|
||||
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2">
|
||||
<public-lobby class="w-full"></public-lobby>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pt-4 flex gap-4 sm:gap-6 lg:gap-8 max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto"
|
||||
>
|
||||
<button
|
||||
id="host-lobby-button"
|
||||
class="bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md w-full border-none cursor-pointer transition-colors duration-300"
|
||||
>
|
||||
Create Lobby
|
||||
</button>
|
||||
<button
|
||||
id="join-private-lobby-button"
|
||||
class="bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md w-full border-none cursor-pointer transition-colors duration-300"
|
||||
>
|
||||
Join Lobby
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto mt-4 sm:mt-6 lg:mt-8"
|
||||
>
|
||||
<button
|
||||
id="single-player"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white p-3 sm:p-4 lg:p-5 font-bold text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300"
|
||||
>
|
||||
Single Player
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game components -->
|
||||
<div id="customMenu" class="mt-4 sm:mt-6 lg:mt-8">
|
||||
<ul></ul>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<div id="radialMenu" class="radial-menu"></div>
|
||||
|
||||
<!-- Game modals and overlays -->
|
||||
<single-player-modal></single-player-modal>
|
||||
<host-lobby-modal></host-lobby-modal>
|
||||
<join-private-lobby-modal></join-private-lobby-modal>
|
||||
<emoji-table></emoji-table>
|
||||
<leader-board></leader-board>
|
||||
<build-menu></build-menu>
|
||||
<win-modal></win-modal>
|
||||
<top-bar></top-bar>
|
||||
<player-panel></player-panel>
|
||||
|
||||
<div class="fixed right-0 top-0 z-50 flex flex-col w-32 sm:w-32 lg:w-48">
|
||||
<options-menu></options-menu>
|
||||
<player-info-overlay></player-info-overlay>
|
||||
</div>
|
||||
|
||||
<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 md:max-w-72" style="pointer-events: auto">
|
||||
<control-panel></control-panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer section -->
|
||||
<div class="w-full bg-gray-900/80 backdrop-blur-md py-4">
|
||||
<div
|
||||
class="max-w-7xl mx-auto px-4 flex justify-between items-center text-white sm:flex-row flex-col sm:gap-0 gap-4"
|
||||
>
|
||||
<div class="flex sm:flex-row flex-col sm:gap-8 gap-2">
|
||||
<a
|
||||
href="https://youtu.be/jvHEvbko3uw?si=znspkP84P76B1w5I"
|
||||
class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank"
|
||||
>How to Play</a
|
||||
>
|
||||
<a
|
||||
href="https://discord.gg/k22YrnAzGp"
|
||||
class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank"
|
||||
>Discord</a
|
||||
>
|
||||
<a
|
||||
href="https://openfront.fandom.com/wiki/Openfront_Wiki"
|
||||
class="text-white/70 hover:text-white transition-colors duration-300"
|
||||
target="_blank"
|
||||
>Wiki</a
|
||||
>
|
||||
</div>
|
||||
<div class="text-white/70">
|
||||
© 2025
|
||||
<a
|
||||
href="https://github.com/openfrontio/OpenFrontIO"
|
||||
class="hover:text-white transition-colors duration-300"
|
||||
target="_blank"
|
||||
>OpenFront.io</a
|
||||
>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
// Remove preload class after everything is loaded
|
||||
window.addEventListener("load", function () {
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.classList.remove("preload");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<!-- Analytics -->
|
||||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||
data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}'></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<!-- Analytics -->
|
||||
<script
|
||||
defer
|
||||
src="https://static.cloudflareinsights.com/beacon.min.js"
|
||||
data-cf-beacon='{"token": "03d93e6fefb349c28ee69b408fa25a13"}'
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-10
@@ -29,7 +29,7 @@ export async function createGameRunner(
|
||||
gameID: string,
|
||||
gameConfig: GameConfig,
|
||||
clientID: ClientID,
|
||||
callBack: (gu: GameUpdateViewData) => void
|
||||
callBack: (gu: GameUpdateViewData) => void,
|
||||
): Promise<GameRunner> {
|
||||
const config = getConfig(gameConfig);
|
||||
const gameMap = await loadGameMap(gameConfig.gameMap);
|
||||
@@ -37,9 +37,13 @@ export async function createGameRunner(
|
||||
gameMap.gameMap,
|
||||
gameMap.miniGameMap,
|
||||
gameMap.nationMap,
|
||||
config
|
||||
config,
|
||||
);
|
||||
const gr = new GameRunner(
|
||||
game as Game,
|
||||
new Executor(game, gameID, clientID),
|
||||
callBack,
|
||||
);
|
||||
const gr = new GameRunner(game as Game, new Executor(game, gameID, clientID), callBack);
|
||||
gr.init();
|
||||
return gr;
|
||||
}
|
||||
@@ -54,13 +58,13 @@ export class GameRunner {
|
||||
constructor(
|
||||
public game: Game,
|
||||
private execManager: Executor,
|
||||
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void
|
||||
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
if (this.game.config().spawnBots()) {
|
||||
this.game.addExecution(
|
||||
...this.execManager.spawnBots(this.game.config().numBots())
|
||||
...this.execManager.spawnBots(this.game.config().numBots()),
|
||||
);
|
||||
}
|
||||
if (this.game.config().spawnNPCs()) {
|
||||
@@ -83,7 +87,7 @@ export class GameRunner {
|
||||
this.isExecuting = true;
|
||||
|
||||
this.game.addExecution(
|
||||
...this.execManager.createExecs(this.turns[this.currTurn])
|
||||
...this.execManager.createExecs(this.turns[this.currTurn]),
|
||||
);
|
||||
this.currTurn++;
|
||||
|
||||
@@ -107,10 +111,10 @@ export class GameRunner {
|
||||
.players()
|
||||
.filter(
|
||||
(p) =>
|
||||
p.type() == PlayerType.Human || p.type() == PlayerType.FakeHuman
|
||||
p.type() == PlayerType.Human || p.type() == PlayerType.FakeHuman,
|
||||
)
|
||||
.forEach(
|
||||
(p) => (this.playerViewData[p.id()] = placeName(this.game, p))
|
||||
(p) => (this.playerViewData[p.id()] = placeName(this.game, p)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,7 +140,7 @@ export class GameRunner {
|
||||
public playerActions(
|
||||
playerID: PlayerID,
|
||||
x: number,
|
||||
y: number
|
||||
y: number,
|
||||
): PlayerActions {
|
||||
const player = this.game.player(playerID);
|
||||
const tile = this.game.ref(x, y);
|
||||
@@ -144,7 +148,7 @@ export class GameRunner {
|
||||
canBoat: player.canBoat(tile),
|
||||
canAttack: player.canAttack(tile),
|
||||
buildableUnits: Object.values(UnitType).filter(
|
||||
(ut) => player.canBuild(ut, tile) != false
|
||||
(ut) => player.canBuild(ut, tile) != false,
|
||||
),
|
||||
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
|
||||
} as PlayerActions;
|
||||
|
||||
+5
-2
@@ -100,7 +100,10 @@ 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);
|
||||
|
||||
@@ -110,7 +113,7 @@ const EmojiSchema = z.string().refine(
|
||||
},
|
||||
{
|
||||
message: "Must contain at least one emoji character",
|
||||
}
|
||||
},
|
||||
);
|
||||
const ID = z
|
||||
.string()
|
||||
|
||||
+19
-19
@@ -16,7 +16,7 @@ import { andFN, GameMap, manhattanDistFN, TileRef } from "./game/GameMap";
|
||||
export function manhattanDistWrapped(
|
||||
c1: Cell,
|
||||
c2: Cell,
|
||||
width: number
|
||||
width: number,
|
||||
): number {
|
||||
// Calculate x distance
|
||||
let dx = Math.abs(c1.x - c2.x);
|
||||
@@ -36,7 +36,7 @@ export function within(value: number, min: number, max: number): number {
|
||||
|
||||
export function distSort(
|
||||
gm: GameMap,
|
||||
target: TileRef
|
||||
target: TileRef,
|
||||
): (a: TileRef, b: TileRef) => number {
|
||||
return (a: TileRef, b: TileRef) => {
|
||||
return gm.manhattanDist(a, target) - gm.manhattanDist(b, target);
|
||||
@@ -45,7 +45,7 @@ export function distSort(
|
||||
|
||||
export function distSortUnit(
|
||||
gm: GameMap,
|
||||
target: Unit | TileRef
|
||||
target: Unit | TileRef,
|
||||
): (a: Unit, b: Unit) => number {
|
||||
const targetRef = typeof target === "number" ? target : target.tile();
|
||||
|
||||
@@ -61,7 +61,7 @@ export function distSortUnit(
|
||||
export function sourceDstOceanShore(
|
||||
gm: Game,
|
||||
src: Player,
|
||||
tile: TileRef
|
||||
tile: TileRef,
|
||||
): [TileRef | null, TileRef | null] {
|
||||
const dst = gm.owner(tile);
|
||||
let srcTile = closestOceanShoreFromPlayer(gm, src, tile);
|
||||
@@ -88,10 +88,10 @@ export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
|
||||
export function closestOceanShoreFromPlayer(
|
||||
gm: GameMap,
|
||||
player: Player,
|
||||
target: TileRef
|
||||
target: TileRef,
|
||||
): TileRef | null {
|
||||
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
|
||||
gm.isOceanShore(t)
|
||||
gm.isOceanShore(t),
|
||||
);
|
||||
if (shoreTiles.length == 0) {
|
||||
return null;
|
||||
@@ -101,12 +101,12 @@ export function closestOceanShoreFromPlayer(
|
||||
const closestDistance = manhattanDistWrapped(
|
||||
gm.cell(target),
|
||||
gm.cell(closest),
|
||||
gm.width()
|
||||
gm.width(),
|
||||
);
|
||||
const currentDistance = manhattanDistWrapped(
|
||||
gm.cell(target),
|
||||
gm.cell(current),
|
||||
gm.width()
|
||||
gm.width(),
|
||||
);
|
||||
return currentDistance < closestDistance ? current : closest;
|
||||
});
|
||||
@@ -115,13 +115,13 @@ export function closestOceanShoreFromPlayer(
|
||||
function closestOceanShoreTN(
|
||||
gm: GameMap,
|
||||
tile: TileRef,
|
||||
searchDist: number
|
||||
searchDist: number,
|
||||
): TileRef {
|
||||
const tn = Array.from(
|
||||
gm.bfs(
|
||||
tile,
|
||||
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist))
|
||||
)
|
||||
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)),
|
||||
),
|
||||
)
|
||||
.filter((t) => gm.isOceanShore(t))
|
||||
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
|
||||
@@ -143,7 +143,7 @@ export function simpleHash(str: string): number {
|
||||
|
||||
export function calculateBoundingBox(
|
||||
gm: GameMap,
|
||||
borderTiles: ReadonlySet<TileRef>
|
||||
borderTiles: ReadonlySet<TileRef>,
|
||||
): { min: Cell; max: Cell } {
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
@@ -163,18 +163,18 @@ export function calculateBoundingBox(
|
||||
|
||||
export function calculateBoundingBoxCenter(
|
||||
gm: GameMap,
|
||||
borderTiles: ReadonlySet<TileRef>
|
||||
borderTiles: ReadonlySet<TileRef>,
|
||||
): Cell {
|
||||
const { min, max } = calculateBoundingBox(gm, borderTiles);
|
||||
return new Cell(
|
||||
min.x + Math.floor((max.x - min.x) / 2),
|
||||
min.y + Math.floor((max.y - min.y) / 2)
|
||||
min.y + Math.floor((max.y - min.y) / 2),
|
||||
);
|
||||
}
|
||||
|
||||
export function inscribed(
|
||||
outer: { min: Cell; max: Cell },
|
||||
inner: { min: Cell; max: Cell }
|
||||
inner: { min: Cell; max: Cell },
|
||||
): boolean {
|
||||
return (
|
||||
outer.min.x <= inner.min.x &&
|
||||
@@ -238,7 +238,7 @@ export function processName(name: string): string {
|
||||
// Add CSS for the emoji images
|
||||
const withEmojiStyles = styledHTML.replace(
|
||||
/<img/g,
|
||||
'<img style="height: 1.2em; width: 1.2em; vertical-align: -0.2em; margin: 0 0.05em 0 0.1em;"'
|
||||
'<img style="height: 1.2em; width: 1.2em; vertical-align: -0.2em; margin: 0 0.05em 0 0.1em;"',
|
||||
);
|
||||
|
||||
// Sanitize the final HTML, allowing styles and specific attributes
|
||||
@@ -262,7 +262,7 @@ export function CreateGameRecord(
|
||||
turns: Turn[],
|
||||
start: number,
|
||||
end: number,
|
||||
winner: ClientID | null
|
||||
winner: ClientID | null,
|
||||
): GameRecord {
|
||||
const record: GameRecord = {
|
||||
id: id,
|
||||
@@ -289,7 +289,7 @@ export function CreateGameRecord(
|
||||
}
|
||||
record.players = players;
|
||||
record.durationSeconds = Math.floor(
|
||||
(record.endTimestampMS - record.startTimestampMS) / 1000
|
||||
(record.endTimestampMS - record.startTimestampMS) / 1000,
|
||||
);
|
||||
record.num_turns = turns.length;
|
||||
record.winner = winner;
|
||||
@@ -303,7 +303,7 @@ export function assertNever(x: never): never {
|
||||
export function generateID(): GameID {
|
||||
const nanoid = customAlphabet(
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
8
|
||||
8,
|
||||
);
|
||||
return nanoid();
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ export interface Config {
|
||||
numBots(): number;
|
||||
spawnNPCs(): boolean;
|
||||
spawnBots(): boolean;
|
||||
creativeMode(): boolean;
|
||||
numSpawnPhaseTurns(): number;
|
||||
|
||||
startManpower(playerInfo: PlayerInfo): number;
|
||||
@@ -81,14 +82,14 @@ export interface Config {
|
||||
attckTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
numAdjacentTilesWithEnemy: number
|
||||
numAdjacentTilesWithEnemy: number,
|
||||
): number;
|
||||
attackLogic(
|
||||
gm: Game,
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
tileToConquer: TileRef
|
||||
tileToConquer: TileRef,
|
||||
): {
|
||||
attackerTroopLoss: number;
|
||||
defenderTroopLoss: number;
|
||||
|
||||
@@ -34,7 +34,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
export class DefaultConfig implements Config {
|
||||
constructor(
|
||||
private _serverConfig: ServerConfig,
|
||||
private _gameConfig: GameConfig
|
||||
private _gameConfig: GameConfig,
|
||||
) {}
|
||||
spawnImmunityDuration(): Tick {
|
||||
return 5 * 10;
|
||||
@@ -100,7 +100,10 @@ export class DefaultConfig implements Config {
|
||||
};
|
||||
case UnitType.Warship:
|
||||
return {
|
||||
cost: (p: Player) => this.creativeMode() ? 0 : (p.units(UnitType.Warship).length + 1) * 250_000,
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.creativeMode()
|
||||
? 0
|
||||
: (p.units(UnitType.Warship).length + 1) * 250_000,
|
||||
territoryBound: false,
|
||||
maxHealth: 1000,
|
||||
};
|
||||
@@ -113,27 +116,33 @@ export class DefaultConfig implements Config {
|
||||
case UnitType.Port:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
this.creativeMode() ? 0 :
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.Port).length) * 250_000
|
||||
),
|
||||
p.type() == PlayerType.Human && this.creativeMode()
|
||||
? 0
|
||||
: Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.Port).length) * 250_000,
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.creativeMode() ? 0 : 2 * 10,
|
||||
};
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: () => this.creativeMode() ? 0 : 750_000,
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.creativeMode() ? 0 : 750_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
cost: () => this.creativeMode() ? 0 : 5_000_000,
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.creativeMode() ? 0 : 5_000_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.MIRV:
|
||||
return {
|
||||
cost: () => this.creativeMode() ? 0 : 10_000_000,
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.creativeMode()
|
||||
? 0
|
||||
: 10_000_000,
|
||||
territoryBound: false,
|
||||
};
|
||||
case UnitType.MIRVWarhead:
|
||||
@@ -148,29 +157,32 @@ export class DefaultConfig implements Config {
|
||||
};
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
cost: () => this.creativeMode() ? 0 : 1_000_000,
|
||||
cost: (p: Player) =>
|
||||
p.type() == PlayerType.Human && this.creativeMode() ? 0 : 1_000_000,
|
||||
territoryBound: true,
|
||||
constructionDuration: this.creativeMode() ? 0 : 10 * 10,
|
||||
};
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
this.creativeMode() ? 0 :
|
||||
Math.min(
|
||||
250_000,
|
||||
(p.units(UnitType.DefensePost).length + 1) * 50_000
|
||||
),
|
||||
p.type() == PlayerType.Human && this.creativeMode()
|
||||
? 0
|
||||
: Math.min(
|
||||
250_000,
|
||||
(p.units(UnitType.DefensePost).length + 1) * 50_000,
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.creativeMode() ? 0 : 5 * 10,
|
||||
};
|
||||
case UnitType.City:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
this.creativeMode() ? 0 :
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.City).length) * 125_000
|
||||
),
|
||||
p.type() == PlayerType.Human && this.creativeMode()
|
||||
? 0
|
||||
: Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.City).length) * 125_000,
|
||||
),
|
||||
territoryBound: true,
|
||||
constructionDuration: this.creativeMode() ? 0 : 2 * 10,
|
||||
};
|
||||
@@ -231,7 +243,7 @@ export class DefaultConfig implements Config {
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
tileToConquer: TileRef
|
||||
tileToConquer: TileRef,
|
||||
): {
|
||||
attackerTroopLoss: number;
|
||||
defenderTroopLoss: number;
|
||||
@@ -302,7 +314,7 @@ export class DefaultConfig implements Config {
|
||||
tilesPerTickUsed: within(
|
||||
(2000 * Math.max(10, speed)) / attackTroops,
|
||||
5,
|
||||
100
|
||||
100,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -312,7 +324,7 @@ export class DefaultConfig implements Config {
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
numAdjacentTilesWithEnemy: number
|
||||
numAdjacentTilesWithEnemy: number,
|
||||
): number {
|
||||
if (defender.isPlayer()) {
|
||||
return (
|
||||
@@ -357,9 +369,11 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
maxPopulation(player: Player | PlayerView): number {
|
||||
let maxPop = this.creativeMode() ? 999_999_999_999 :
|
||||
2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
|
||||
player.units(UnitType.City).length * this.cityPopulationIncrease();
|
||||
let maxPop =
|
||||
player.type() == PlayerType.Human && this.creativeMode()
|
||||
? 999_999_999_999
|
||||
: 2 * (Math.pow(player.numTilesOwned(), 0.6) * 1000 + 50000) +
|
||||
player.units(UnitType.City).length * this.cityPopulationIncrease();
|
||||
|
||||
if (player.type() == PlayerType.Bot) {
|
||||
return maxPop / 2;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Colord, colord, random } from "colord";
|
||||
import { Game, PlayerID, PlayerInfo, TerrainType } from "../game/Game";
|
||||
import {
|
||||
Game,
|
||||
PlayerID,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
TerrainType,
|
||||
} from "../game/Game";
|
||||
import { Theme } from "./Config";
|
||||
import { time } from "console";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
@@ -122,6 +128,112 @@ export const pastelTheme = new (class implements Theme {
|
||||
colord({ r: 170, g: 150, b: 170 }), // Dusty Rose
|
||||
];
|
||||
|
||||
private humanColors: Colord[] = [
|
||||
// Original set
|
||||
colord({ r: 235, g: 75, b: 75 }), // Bright Red
|
||||
colord({ r: 67, g: 190, b: 84 }), // Fresh Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Royal Blue
|
||||
colord({ r: 245, g: 158, b: 11 }), // Amber
|
||||
colord({ r: 236, g: 72, b: 153 }), // Deep Pink
|
||||
colord({ r: 48, g: 178, b: 180 }), // Teal
|
||||
colord({ r: 168, g: 85, b: 247 }), // Vibrant Purple
|
||||
colord({ r: 251, g: 191, b: 36 }), // Marigold
|
||||
colord({ r: 74, g: 222, b: 128 }), // Mint
|
||||
colord({ r: 239, g: 68, b: 68 }), // Crimson
|
||||
colord({ r: 34, g: 197, b: 94 }), // Emerald
|
||||
colord({ r: 96, g: 165, b: 250 }), // Sky Blue
|
||||
colord({ r: 249, g: 115, b: 22 }), // Tangerine
|
||||
colord({ r: 192, g: 132, b: 252 }), // Lavender
|
||||
colord({ r: 45, g: 212, b: 191 }), // Turquoise
|
||||
colord({ r: 244, g: 114, b: 182 }), // Rose
|
||||
colord({ r: 132, g: 204, b: 22 }), // Lime
|
||||
colord({ r: 56, g: 189, b: 248 }), // Light Blue
|
||||
colord({ r: 234, g: 179, b: 8 }), // Sunflower
|
||||
colord({ r: 217, g: 70, b: 239 }), // Fuchsia
|
||||
colord({ r: 16, g: 185, b: 129 }), // Sea Green
|
||||
colord({ r: 251, g: 146, b: 60 }), // Light Orange
|
||||
colord({ r: 147, g: 51, b: 234 }), // Bright Purple
|
||||
colord({ r: 79, g: 70, b: 229 }), // Indigo
|
||||
colord({ r: 245, g: 101, b: 101 }), // Coral
|
||||
colord({ r: 134, g: 239, b: 172 }), // Light Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Cerulean
|
||||
colord({ r: 253, g: 164, b: 175 }), // Salmon Pink
|
||||
colord({ r: 147, g: 197, b: 253 }), // Powder Blue
|
||||
colord({ r: 252, g: 211, b: 77 }), // Golden
|
||||
colord({ r: 190, g: 92, b: 251 }), // Amethyst
|
||||
colord({ r: 82, g: 183, b: 136 }), // Jade
|
||||
colord({ r: 248, g: 113, b: 113 }), // Warm Red
|
||||
colord({ r: 99, g: 202, b: 253 }), // Azure
|
||||
colord({ r: 240, g: 171, b: 252 }), // Orchid
|
||||
colord({ r: 163, g: 230, b: 53 }), // Yellow Green
|
||||
colord({ r: 234, g: 88, b: 12 }), // Burnt Orange
|
||||
colord({ r: 125, g: 211, b: 252 }), // Crystal Blue
|
||||
colord({ r: 251, g: 113, b: 133 }), // Watermelon
|
||||
colord({ r: 52, g: 211, b: 153 }), // Spearmint
|
||||
colord({ r: 167, g: 139, b: 250 }), // Periwinkle
|
||||
colord({ r: 245, g: 158, b: 11 }), // Honey
|
||||
colord({ r: 110, g: 231, b: 183 }), // Seafoam
|
||||
colord({ r: 233, g: 213, b: 255 }), // Light Lilac
|
||||
colord({ r: 202, g: 138, b: 4 }), // Rich Gold
|
||||
colord({ r: 151, g: 255, b: 187 }), // Fresh Mint
|
||||
colord({ r: 220, g: 38, b: 38 }), // Ruby
|
||||
colord({ r: 124, g: 58, b: 237 }), // Royal Purple
|
||||
colord({ r: 45, g: 212, b: 191 }), // Ocean
|
||||
colord({ r: 252, g: 165, b: 165 }), // Peach
|
||||
|
||||
// Additional 50 colors
|
||||
colord({ r: 179, g: 136, b: 255 }), // Light Purple
|
||||
colord({ r: 133, g: 77, b: 14 }), // Chocolate
|
||||
colord({ r: 52, g: 211, b: 153 }), // Aquamarine
|
||||
colord({ r: 234, g: 179, b: 8 }), // Mustard
|
||||
colord({ r: 236, g: 72, b: 153 }), // Hot Pink
|
||||
colord({ r: 147, g: 197, b: 253 }), // Sky
|
||||
colord({ r: 249, g: 115, b: 22 }), // Pumpkin
|
||||
colord({ r: 167, g: 139, b: 250 }), // Iris
|
||||
colord({ r: 16, g: 185, b: 129 }), // Pine
|
||||
colord({ r: 251, g: 146, b: 60 }), // Mango
|
||||
colord({ r: 192, g: 132, b: 252 }), // Wisteria
|
||||
colord({ r: 79, g: 70, b: 229 }), // Sapphire
|
||||
colord({ r: 245, g: 101, b: 101 }), // Salmon
|
||||
colord({ r: 134, g: 239, b: 172 }), // Spring Green
|
||||
colord({ r: 59, g: 130, b: 246 }), // Ocean Blue
|
||||
colord({ r: 253, g: 164, b: 175 }), // Rose Gold
|
||||
colord({ r: 16, g: 185, b: 129 }), // Forest
|
||||
colord({ r: 252, g: 211, b: 77 }), // Sunshine
|
||||
colord({ r: 190, g: 92, b: 251 }), // Grape
|
||||
colord({ r: 82, g: 183, b: 136 }), // Eucalyptus
|
||||
colord({ r: 248, g: 113, b: 113 }), // Cherry
|
||||
colord({ r: 99, g: 202, b: 253 }), // Arctic
|
||||
colord({ r: 240, g: 171, b: 252 }), // Lilac
|
||||
colord({ r: 163, g: 230, b: 53 }), // Chartreuse
|
||||
colord({ r: 234, g: 88, b: 12 }), // Rust
|
||||
colord({ r: 125, g: 211, b: 252 }), // Ice Blue
|
||||
colord({ r: 251, g: 113, b: 133 }), // Strawberry
|
||||
colord({ r: 52, g: 211, b: 153 }), // Sage
|
||||
colord({ r: 167, g: 139, b: 250 }), // Violet
|
||||
colord({ r: 245, g: 158, b: 11 }), // Apricot
|
||||
colord({ r: 110, g: 231, b: 183 }), // Mint Green
|
||||
colord({ r: 233, g: 213, b: 255 }), // Thistle
|
||||
colord({ r: 202, g: 138, b: 4 }), // Bronze
|
||||
colord({ r: 151, g: 255, b: 187 }), // Pistachio
|
||||
colord({ r: 220, g: 38, b: 38 }), // Fire Engine
|
||||
colord({ r: 124, g: 58, b: 237 }), // Electric Purple
|
||||
colord({ r: 45, g: 212, b: 191 }), // Caribbean
|
||||
colord({ r: 252, g: 165, b: 165 }), // Melon
|
||||
colord({ r: 168, g: 85, b: 247 }), // Byzantium
|
||||
colord({ r: 74, g: 222, b: 128 }), // Kelly Green
|
||||
colord({ r: 239, g: 68, b: 68 }), // Cardinal
|
||||
colord({ r: 34, g: 197, b: 94 }), // Shamrock
|
||||
colord({ r: 96, g: 165, b: 250 }), // Marina
|
||||
colord({ r: 249, g: 115, b: 22 }), // Carrot
|
||||
colord({ r: 192, g: 132, b: 252 }), // Heliotrope
|
||||
colord({ r: 45, g: 212, b: 191 }), // Lagoon
|
||||
colord({ r: 244, g: 114, b: 182 }), // Bubble Gum
|
||||
colord({ r: 132, g: 204, b: 22 }), // Apple
|
||||
colord({ r: 56, g: 189, b: 248 }), // Electric Blue
|
||||
colord({ r: 234, g: 179, b: 8 }), // Daffodil
|
||||
];
|
||||
|
||||
private _selfColor = colord({ r: 0, g: 255, b: 0 });
|
||||
private _allyColor = colord({ r: 255, g: 255, b: 0 });
|
||||
private _enemyColor = colord({ r: 255, g: 0, b: 0 });
|
||||
@@ -133,6 +245,11 @@ export const pastelTheme = new (class implements Theme {
|
||||
}
|
||||
|
||||
territoryColor(playerInfo: PlayerInfo): Colord {
|
||||
if (playerInfo.playerType == PlayerType.Human) {
|
||||
return this.humanColors[
|
||||
simpleHash(playerInfo.name) % this.humanColors.length
|
||||
];
|
||||
}
|
||||
return this.territoryColors[
|
||||
simpleHash(playerInfo.name) % this.territoryColors.length
|
||||
];
|
||||
|
||||
@@ -45,7 +45,7 @@ export class AttackExecution implements Execution {
|
||||
private _ownerID: PlayerID,
|
||||
private _targetID: PlayerID | null,
|
||||
private sourceTile: TileRef | null,
|
||||
private removeTroops: boolean = true
|
||||
private removeTroops: boolean = true,
|
||||
) {}
|
||||
|
||||
public targetID(): PlayerID {
|
||||
@@ -95,7 +95,7 @@ export class AttackExecution implements Execution {
|
||||
this.attack = this._owner.createAttack(
|
||||
this.target,
|
||||
this.startTroops,
|
||||
this.sourceTile
|
||||
this.sourceTile,
|
||||
);
|
||||
|
||||
for (const incoming of this._owner.incomingAttacks()) {
|
||||
@@ -174,7 +174,7 @@ export class AttackExecution implements Execution {
|
||||
this.attack.troops(),
|
||||
this._owner,
|
||||
this.target,
|
||||
this.border.size + this.random.nextInt(0, 5)
|
||||
this.border.size + this.random.nextInt(0, 5),
|
||||
);
|
||||
// consolex.log(`num tiles per tick: ${numTilesPerTick}`)
|
||||
// consolex.log(`num execs: ${this.mg.executions().length}`)
|
||||
@@ -212,7 +212,7 @@ export class AttackExecution implements Execution {
|
||||
this.attack.troops(),
|
||||
this._owner,
|
||||
this.target,
|
||||
tileToConquer
|
||||
tileToConquer,
|
||||
);
|
||||
numTilesPerTick -= tilesPerTickUsed;
|
||||
this.attack.setTroops(this.attack.troops() - attackerTroopLoss);
|
||||
@@ -253,8 +253,8 @@ export class AttackExecution implements Execution {
|
||||
new TileContainer(
|
||||
neighbor,
|
||||
dist / 100 + this.random.nextInt(0, 2) - numOwnedByMe + mag,
|
||||
this.mg.ticks()
|
||||
)
|
||||
this.mg.ticks(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -264,10 +264,10 @@ export class AttackExecution implements Execution {
|
||||
const gold = this.target.gold();
|
||||
this.mg.displayMessage(
|
||||
`Conquered ${this.target.displayName()} received ${renderNumber(
|
||||
gold
|
||||
gold,
|
||||
)} gold`,
|
||||
MessageType.SUCCESS,
|
||||
this._owner.id()
|
||||
this._owner.id(),
|
||||
);
|
||||
this.target.removeGold(gold);
|
||||
this._owner.addGold(gold);
|
||||
@@ -306,6 +306,6 @@ class TileContainer {
|
||||
constructor(
|
||||
public readonly tile: TileRef,
|
||||
public readonly priority: number,
|
||||
public readonly tick: number
|
||||
public readonly tick: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ export class BotExecution implements Execution {
|
||||
this.bot.id(),
|
||||
toAttack.isPlayer() ? toAttack.id() : null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ConstructionExecution implements Execution {
|
||||
constructor(
|
||||
private ownerId: PlayerID,
|
||||
private tile: TileRef,
|
||||
private constructionType: UnitType
|
||||
private constructionType: UnitType,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
@@ -56,7 +56,7 @@ export class ConstructionExecution implements Execution {
|
||||
this.construction = this.player.buildUnit(
|
||||
UnitType.Construction,
|
||||
0,
|
||||
spawnTile
|
||||
spawnTile,
|
||||
);
|
||||
this.cost = this.mg.unitInfo(this.constructionType).cost(this.player);
|
||||
this.player.removeGold(this.cost);
|
||||
@@ -88,7 +88,7 @@ export class ConstructionExecution implements Execution {
|
||||
case UnitType.AtomBomb:
|
||||
case UnitType.HydrogenBomb:
|
||||
this.mg.addExecution(
|
||||
new NukeExecution(this.constructionType, player.id(), this.tile)
|
||||
new NukeExecution(this.constructionType, player.id(), this.tile),
|
||||
);
|
||||
break;
|
||||
case UnitType.MIRV:
|
||||
|
||||
@@ -16,7 +16,10 @@ export class DefensePostExecution implements Execution {
|
||||
private post: Unit;
|
||||
private active: boolean = true;
|
||||
|
||||
constructor(private ownerId: PlayerID, private tile: TileRef) {}
|
||||
constructor(
|
||||
private ownerId: PlayerID,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
|
||||
@@ -41,7 +41,7 @@ export class Executor {
|
||||
constructor(
|
||||
private mg: Game,
|
||||
private gameID: GameID,
|
||||
private clientID: ClientID
|
||||
private clientID: ClientID,
|
||||
) {
|
||||
// Add one to avoid id collisions with bots.
|
||||
this.random = new PseudoRandom(simpleHash(gameID) + 1);
|
||||
@@ -58,7 +58,7 @@ export class Executor {
|
||||
intent.troops,
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
null
|
||||
null,
|
||||
);
|
||||
}
|
||||
case "spawn":
|
||||
@@ -71,16 +71,16 @@ export class Executor {
|
||||
: fixProfaneUsername(sanitize(intent.name)),
|
||||
intent.playerType,
|
||||
intent.clientID,
|
||||
intent.playerID
|
||||
intent.playerID,
|
||||
),
|
||||
this.mg.ref(intent.x, intent.y)
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
);
|
||||
case "boat":
|
||||
return new TransportShipExecution(
|
||||
intent.attackerID,
|
||||
intent.targetID,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.troops
|
||||
intent.troops,
|
||||
);
|
||||
case "allianceRequest":
|
||||
return new AllianceRequestExecution(intent.requestor, intent.recipient);
|
||||
@@ -88,7 +88,7 @@ export class Executor {
|
||||
return new AllianceRequestReplyExecution(
|
||||
intent.requestor,
|
||||
intent.recipient,
|
||||
intent.accept
|
||||
intent.accept,
|
||||
);
|
||||
case "breakAlliance":
|
||||
return new BreakAllianceExecution(intent.requestor, intent.recipient);
|
||||
@@ -98,13 +98,13 @@ export class Executor {
|
||||
return new EmojiExecution(
|
||||
intent.sender,
|
||||
intent.recipient,
|
||||
intent.emoji
|
||||
intent.emoji,
|
||||
);
|
||||
case "donate":
|
||||
return new DonateExecution(
|
||||
intent.sender,
|
||||
intent.recipient,
|
||||
intent.troops
|
||||
intent.troops,
|
||||
);
|
||||
case "troop_ratio":
|
||||
return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
|
||||
@@ -112,7 +112,7 @@ export class Executor {
|
||||
return new ConstructionExecution(
|
||||
intent.player,
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.unit
|
||||
intent.unit,
|
||||
);
|
||||
default:
|
||||
throw new Error(`intent type ${intent} not found`);
|
||||
@@ -137,9 +137,9 @@ export class Executor {
|
||||
PlayerType.FakeHuman,
|
||||
null,
|
||||
this.random.nextID(),
|
||||
nation
|
||||
)
|
||||
)
|
||||
nation,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return execs;
|
||||
|
||||
@@ -40,9 +40,12 @@ export class FakeHumanExecution implements Execution {
|
||||
private lastEnemyUpdateTick: number = 0;
|
||||
private lastEmojiSent = new Map<Player, Tick>();
|
||||
|
||||
constructor(gameID: GameID, private playerInfo: PlayerInfo) {
|
||||
constructor(
|
||||
gameID: GameID,
|
||||
private playerInfo: PlayerInfo,
|
||||
) {
|
||||
this.random = new PseudoRandom(
|
||||
simpleHash(playerInfo.id) + simpleHash(gameID)
|
||||
simpleHash(playerInfo.id) + simpleHash(gameID),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,7 +102,7 @@ export class FakeHumanExecution implements Execution {
|
||||
const enemyborder = Array.from(this.player.borderTiles())
|
||||
.flatMap((t) => this.mg.neighbors(t))
|
||||
.filter(
|
||||
(t) => this.mg.isLand(t) && this.mg.ownerID(t) != this.player.smallID()
|
||||
(t) => this.mg.isLand(t) && this.mg.ownerID(t) != this.player.smallID(),
|
||||
);
|
||||
|
||||
if (enemyborder.length == 0) {
|
||||
@@ -114,7 +117,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
const enemiesWithTN = enemyborder.map((t) =>
|
||||
this.mg.playerBySmallID(this.mg.ownerID(t))
|
||||
this.mg.playerBySmallID(this.mg.ownerID(t)),
|
||||
);
|
||||
if (enemiesWithTN.filter((o) => !o.isPlayer()).length > 0) {
|
||||
this.sendAttack(this.mg.terraNullius());
|
||||
@@ -194,7 +197,7 @@ export class FakeHumanExecution implements Execution {
|
||||
this.lastEnemyUpdateTick = this.mg.ticks();
|
||||
if (target.ally.type() == PlayerType.Human) {
|
||||
this.mg.addExecution(
|
||||
new EmojiExecution(this.player.id(), target.ally.id(), "👍")
|
||||
new EmojiExecution(this.player.id(), target.ally.id(), "👍"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -215,8 +218,8 @@ export class FakeHumanExecution implements Execution {
|
||||
new EmojiExecution(
|
||||
this.player.id(),
|
||||
this.enemy.id(),
|
||||
this.random.randElement(["🤡", "😡"])
|
||||
)
|
||||
this.random.randElement(["🤡", "😡"]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -260,7 +263,7 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
if (this.player.canBuild(UnitType.AtomBomb, tile)) {
|
||||
this.mg.addExecution(
|
||||
new NukeExecution(UnitType.AtomBomb, this.player.id(), tile)
|
||||
new NukeExecution(UnitType.AtomBomb, this.player.id(), tile),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -271,9 +274,9 @@ export class FakeHumanExecution implements Execution {
|
||||
const closest = closestTwoTiles(
|
||||
this.mg,
|
||||
Array.from(this.player.borderTiles()).filter((t) =>
|
||||
this.mg.isOceanShore(t)
|
||||
this.mg.isOceanShore(t),
|
||||
),
|
||||
Array.from(other.borderTiles()).filter((t) => this.mg.isOceanShore(t))
|
||||
Array.from(other.borderTiles()).filter((t) => this.mg.isOceanShore(t)),
|
||||
);
|
||||
if (closest == null) {
|
||||
return;
|
||||
@@ -287,8 +290,8 @@ export class FakeHumanExecution implements Execution {
|
||||
this.player.id(),
|
||||
other.id(),
|
||||
closest.y,
|
||||
this.player.troops() / 5
|
||||
)
|
||||
this.player.troops() / 5,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -297,12 +300,12 @@ export class FakeHumanExecution implements Execution {
|
||||
const ports = this.player.units(UnitType.Port);
|
||||
if (ports.length == 0 && this.player.gold() > this.cost(UnitType.Port)) {
|
||||
const oceanTiles = Array.from(this.player.borderTiles()).filter((t) =>
|
||||
this.mg.isOceanShore(t)
|
||||
this.mg.isOceanShore(t),
|
||||
);
|
||||
if (oceanTiles.length > 0) {
|
||||
const buildTile = this.random.randElement(oceanTiles);
|
||||
this.mg.addExecution(
|
||||
new ConstructionExecution(this.player.id(), buildTile, UnitType.Port)
|
||||
new ConstructionExecution(this.player.id(), buildTile, UnitType.Port),
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -310,7 +313,7 @@ export class FakeHumanExecution implements Execution {
|
||||
this.maybeSpawnStructure(
|
||||
UnitType.City,
|
||||
2,
|
||||
(t) => new ConstructionExecution(this.player.id(), t, UnitType.City)
|
||||
(t) => new ConstructionExecution(this.player.id(), t, UnitType.City),
|
||||
);
|
||||
if (this.maybeSpawnWarship()) {
|
||||
return;
|
||||
@@ -319,14 +322,14 @@ export class FakeHumanExecution implements Execution {
|
||||
UnitType.MissileSilo,
|
||||
1,
|
||||
(t) =>
|
||||
new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo)
|
||||
new ConstructionExecution(this.player.id(), t, UnitType.MissileSilo),
|
||||
);
|
||||
}
|
||||
|
||||
private maybeSpawnStructure(
|
||||
type: UnitType,
|
||||
maxNum: number,
|
||||
build: (tile: TileRef) => Execution
|
||||
build: (tile: TileRef) => Execution,
|
||||
) {
|
||||
const units = this.player.units(type);
|
||||
if (units.length >= maxNum) {
|
||||
@@ -373,8 +376,8 @@ export class FakeHumanExecution implements Execution {
|
||||
new ConstructionExecution(
|
||||
this.player.id(),
|
||||
targetTile,
|
||||
UnitType.Warship
|
||||
)
|
||||
UnitType.Warship,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -403,11 +406,11 @@ export class FakeHumanExecution implements Execution {
|
||||
for (let attempts = 0; attempts < 50; attempts++) {
|
||||
const randX = this.random.nextInt(
|
||||
this.mg.x(portTile) - radius,
|
||||
this.mg.x(portTile) + radius
|
||||
this.mg.x(portTile) + radius,
|
||||
);
|
||||
const randY = this.random.nextInt(
|
||||
this.mg.y(portTile) - radius,
|
||||
this.mg.y(portTile) + radius
|
||||
this.mg.y(portTile) + radius,
|
||||
);
|
||||
if (!this.mg.isValidCoord(randX, randY)) {
|
||||
continue;
|
||||
@@ -457,8 +460,8 @@ export class FakeHumanExecution implements Execution {
|
||||
new AllianceRequestReplyExecution(
|
||||
req.requestor().id(),
|
||||
this.player.id(),
|
||||
accept
|
||||
)
|
||||
accept,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -469,7 +472,7 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
if (oceanShore == null) {
|
||||
oceanShore = Array.from(this.player.borderTiles()).filter((t) =>
|
||||
this.mg.isOceanShore(t)
|
||||
this.mg.isOceanShore(t),
|
||||
);
|
||||
}
|
||||
if (oceanShore.length == 0) {
|
||||
@@ -482,9 +485,9 @@ export class FakeHumanExecution implements Execution {
|
||||
src,
|
||||
andFN(
|
||||
(gm, t) => gm.isOcean(t) || gm.isOceanShore(t),
|
||||
manhattanDistFN(src, 200)
|
||||
)
|
||||
)
|
||||
manhattanDistFN(src, 200),
|
||||
),
|
||||
),
|
||||
).filter((t) => this.mg.isOceanShore(t) && this.mg.owner(t) != this.player);
|
||||
|
||||
if (otherShore.length == 0) {
|
||||
@@ -508,8 +511,8 @@ export class FakeHumanExecution implements Execution {
|
||||
this.player.id(),
|
||||
this.mg.hasOwner(dst) ? this.mg.owner(dst).id() : null,
|
||||
dst,
|
||||
this.player.troops() / 5
|
||||
)
|
||||
this.player.troops() / 5,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -548,8 +551,8 @@ export class FakeHumanExecution implements Execution {
|
||||
this.player.id(),
|
||||
toAttack.isPlayer() ? toAttack.id() : null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -557,7 +560,7 @@ export class FakeHumanExecution implements Execution {
|
||||
return (
|
||||
this.mg.bfs(
|
||||
tile,
|
||||
andFN((gm, t) => gm.isLand(t), manhattanDistFN(tile, 10))
|
||||
andFN((gm, t) => gm.isLand(t), manhattanDistFN(tile, 10)),
|
||||
).size < 50
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private separateDst: TileRef;
|
||||
|
||||
constructor(private senderID: PlayerID, private dst: TileRef) {}
|
||||
constructor(
|
||||
private senderID: PlayerID,
|
||||
private dst: TileRef,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.random = new PseudoRandom(mg.ticks() + simpleHash(this.senderID));
|
||||
@@ -57,7 +60,7 @@ export class MirvExecution implements Execution {
|
||||
}
|
||||
this.nuke = this.player.buildUnit(UnitType.MIRV, 0, spawn);
|
||||
const x = Math.floor(
|
||||
(this.mg.x(this.dst) + this.mg.x(this.mg.x(this.nuke.tile()))) / 2
|
||||
(this.mg.x(this.dst) + this.mg.x(this.mg.x(this.nuke.tile()))) / 2,
|
||||
);
|
||||
const y = Math.max(0, this.mg.y(this.dst) - 500) + 50;
|
||||
this.separateDst = this.mg.ref(x, y);
|
||||
@@ -66,7 +69,7 @@ export class MirvExecution implements Execution {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const result = this.pathFinder.nextTile(
|
||||
this.nuke.tile(),
|
||||
this.separateDst
|
||||
this.separateDst,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
@@ -81,7 +84,7 @@ export class MirvExecution implements Execution {
|
||||
break;
|
||||
case PathFindResultType.PathNotFound:
|
||||
consolex.warn(
|
||||
`nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`
|
||||
`nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
@@ -103,7 +106,7 @@ export class MirvExecution implements Execution {
|
||||
console.log(`dsts: ${dsts.length}`);
|
||||
dsts.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst)
|
||||
this.mg.manhattanDist(b, this.dst) - this.mg.manhattanDist(a, this.dst),
|
||||
);
|
||||
console.log(`got ${dsts.length} dsts!!`);
|
||||
|
||||
@@ -116,8 +119,8 @@ export class MirvExecution implements Execution {
|
||||
this.nuke.tile(),
|
||||
15 + Math.floor((i / this.warheadCount) * 5),
|
||||
// this.random.nextInt(5, 9),
|
||||
this.random.nextInt(0, 15)
|
||||
)
|
||||
this.random.nextInt(0, 15),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (this.targetPlayer.isPlayer()) {
|
||||
@@ -138,11 +141,11 @@ export class MirvExecution implements Execution {
|
||||
tries++;
|
||||
const x = this.random.nextInt(
|
||||
this.mg.x(ref) - this.mirvRange,
|
||||
this.mg.x(ref) + this.mirvRange
|
||||
this.mg.x(ref) + this.mirvRange,
|
||||
);
|
||||
const y = this.random.nextInt(
|
||||
this.mg.y(ref) - this.mirvRange,
|
||||
this.mg.y(ref) + this.mirvRange
|
||||
this.mg.y(ref) + this.mirvRange,
|
||||
);
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue;
|
||||
|
||||
@@ -29,7 +29,7 @@ export class NukeExecution implements Execution {
|
||||
private dst: TileRef,
|
||||
private src?: TileRef,
|
||||
private speed: number = 4,
|
||||
private waitTicks = 0
|
||||
private waitTicks = 0,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
@@ -77,7 +77,7 @@ export class NukeExecution implements Execution {
|
||||
let nextY = y;
|
||||
|
||||
const ratio = Math.floor(
|
||||
1 + Math.abs(dstY - y) / (Math.abs(dstX - x) + 1)
|
||||
1 + Math.abs(dstY - y) / (Math.abs(dstX - x) + 1),
|
||||
);
|
||||
|
||||
if (this.random.chance(ratio) && x != dstX) {
|
||||
@@ -123,7 +123,7 @@ export class NukeExecution implements Execution {
|
||||
const ratio = Object.fromEntries(
|
||||
this.mg
|
||||
.players()
|
||||
.map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()])
|
||||
.map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]),
|
||||
);
|
||||
const attacked = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
|
||||
@@ -25,7 +25,10 @@ export class PortExecution implements Execution {
|
||||
private portPaths = new Map<Unit, TileRef[]>();
|
||||
private computingPaths = new Map<Unit, MiniAStar>();
|
||||
|
||||
constructor(private _owner: PlayerID, private tile: TileRef) {}
|
||||
constructor(
|
||||
private _owner: PlayerID,
|
||||
private tile: TileRef,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
@@ -46,7 +49,7 @@ export class PortExecution implements Execution {
|
||||
.filter((t) => this.mg.isOceanShore(t) && this.mg.owner(t) == player)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile)
|
||||
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
|
||||
);
|
||||
|
||||
if (spawns.length == 0) {
|
||||
@@ -68,7 +71,7 @@ export class PortExecution implements Execution {
|
||||
const alliedPortsSet = new Set(alliedPorts);
|
||||
|
||||
const allyConnections = new Set(
|
||||
Array.from(this.portPaths.keys()).map((p) => p.owner())
|
||||
Array.from(this.portPaths.keys()).map((p) => p.owner()),
|
||||
);
|
||||
allyConnections;
|
||||
|
||||
@@ -100,7 +103,7 @@ export class PortExecution implements Execution {
|
||||
port.tile(),
|
||||
(tr: TileRef) => this.mg.miniMap().isOcean(tr),
|
||||
10_000,
|
||||
25
|
||||
25,
|
||||
);
|
||||
this.computingPaths.set(port, pf);
|
||||
}
|
||||
@@ -123,7 +126,7 @@ export class PortExecution implements Execution {
|
||||
if (path != null) {
|
||||
const pf = PathFinder.Mini(this.mg, 10000, false);
|
||||
this.mg.addExecution(
|
||||
new TradeShipExecution(this.player().id(), this.port, port, pf, path)
|
||||
new TradeShipExecution(this.player().id(), this.port, port, pf, path),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
|
||||
constructor(private playerID: PlayerID, private patrolCenterTile: TileRef) {}
|
||||
constructor(
|
||||
private playerID: PlayerID,
|
||||
private patrolCenterTile: TileRef,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathfinder = PathFinder.Mini(mg, 5000, false);
|
||||
@@ -68,7 +71,7 @@ export class WarshipExecution implements Execution {
|
||||
const ships = this.mg
|
||||
.units(UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip)
|
||||
.filter(
|
||||
(u) => this.mg.manhattanDist(u.tile(), this.warship.tile()) < 130
|
||||
(u) => this.mg.manhattanDist(u.tile(), this.warship.tile()) < 130,
|
||||
)
|
||||
.filter((u) => u.owner() != this.warship.owner())
|
||||
.filter((u) => u != this.warship)
|
||||
@@ -80,7 +83,7 @@ export class WarshipExecution implements Execution {
|
||||
// Patrol unless we are hunting down a tradeship
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.patrolTile
|
||||
this.patrolTile,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
@@ -115,8 +118,8 @@ export class WarshipExecution implements Execution {
|
||||
this.warship.tile(),
|
||||
this.warship.owner(),
|
||||
this.warship,
|
||||
this.target
|
||||
)
|
||||
this.target,
|
||||
),
|
||||
);
|
||||
if (!this.target.hasHealth()) {
|
||||
// Don't send multiple shells to target that can be oneshotted
|
||||
@@ -134,7 +137,7 @@ export class WarshipExecution implements Execution {
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.target.tile(),
|
||||
5
|
||||
5,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
|
||||
@@ -13,7 +13,10 @@ export class AllianceRequestExecution implements Execution {
|
||||
private requestor: Player;
|
||||
private recipient: Player;
|
||||
|
||||
constructor(private requestorID: PlayerID, private recipientID: PlayerID) {}
|
||||
constructor(
|
||||
private requestorID: PlayerID,
|
||||
private recipientID: PlayerID,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
|
||||
@@ -9,7 +9,7 @@ export class AttackImpl implements Attack {
|
||||
private _target: Player | TerraNullius,
|
||||
private _attacker: Player,
|
||||
private _troops: number,
|
||||
private _sourceTile: TileRef | null
|
||||
private _sourceTile: TileRef | null,
|
||||
) {}
|
||||
|
||||
sourceTile(): TileRef | null {
|
||||
|
||||
@@ -6,13 +6,16 @@ export class DefenseGrid {
|
||||
private grid: Set<Unit | UnitView>[][];
|
||||
private readonly cellSize = 100;
|
||||
|
||||
constructor(private gm: GameMap, private searchRange: number) {
|
||||
constructor(
|
||||
private gm: GameMap,
|
||||
private searchRange: number,
|
||||
) {
|
||||
this.grid = Array(Math.ceil(gm.height() / this.cellSize))
|
||||
.fill(null)
|
||||
.map(() =>
|
||||
Array(Math.ceil(gm.width() / this.cellSize))
|
||||
.fill(null)
|
||||
.map(() => new Set<Unit | UnitView>())
|
||||
.map(() => new Set<Unit | UnitView>()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export class Nation {
|
||||
public readonly flag: string,
|
||||
public readonly name: string,
|
||||
public readonly cell: Cell,
|
||||
public readonly strength: number
|
||||
public readonly strength: number,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,10 @@ export class Cell {
|
||||
|
||||
private strRepr: string;
|
||||
|
||||
constructor(public readonly x, public readonly y) {
|
||||
constructor(
|
||||
public readonly x,
|
||||
public readonly y,
|
||||
) {
|
||||
this.strRepr = `Cell[${this.x},${this.y}]`;
|
||||
}
|
||||
|
||||
@@ -176,7 +179,7 @@ export class PlayerInfo {
|
||||
public readonly clientID: ClientID | null,
|
||||
// TODO: make player id the small id
|
||||
public readonly id: PlayerID,
|
||||
public readonly nation?: Nation | null
|
||||
public readonly nation?: Nation | null,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -302,7 +305,7 @@ export interface Player {
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
sourceTile: TileRef
|
||||
sourceTile: TileRef,
|
||||
): Attack;
|
||||
outgoingAttacks(): Attack[];
|
||||
incomingAttacks(): Attack[];
|
||||
@@ -350,7 +353,7 @@ export interface Game extends GameMap {
|
||||
displayMessage(
|
||||
message: string,
|
||||
type: MessageType,
|
||||
playerID: PlayerID | null
|
||||
playerID: PlayerID | null,
|
||||
): void;
|
||||
|
||||
// Nations
|
||||
|
||||
+14
-14
@@ -35,7 +35,7 @@ export function createGame(
|
||||
gameMap: GameMap,
|
||||
miniGameMap: GameMap,
|
||||
nationMap: NationMap,
|
||||
config: Config
|
||||
config: Config,
|
||||
): Game {
|
||||
return new GameImpl(gameMap, miniGameMap, nationMap, config);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export class GameImpl implements Game {
|
||||
private _map: GameMap,
|
||||
private miniGameMap: GameMap,
|
||||
nationMap: NationMap,
|
||||
private _config: Config
|
||||
private _config: Config,
|
||||
) {
|
||||
this._terraNullius = new TerraNulliusImpl();
|
||||
this._width = _map.width();
|
||||
@@ -81,12 +81,12 @@ export class GameImpl implements Game {
|
||||
n.flag || "",
|
||||
n.name,
|
||||
new Cell(n.coordinates[0], n.coordinates[1]),
|
||||
n.strength
|
||||
)
|
||||
n.strength,
|
||||
),
|
||||
);
|
||||
this.defenseGrid = new DefenseGrid(
|
||||
this._map,
|
||||
this._config.defensePostRange()
|
||||
this._config.defensePostRange(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,11 +173,11 @@ export class GameImpl implements Game {
|
||||
this,
|
||||
request.requestor() as PlayerImpl,
|
||||
request.recipient() as PlayerImpl,
|
||||
this._ticks
|
||||
this._ticks,
|
||||
);
|
||||
this.alliances_.push(alliance);
|
||||
(request.requestor() as PlayerImpl).pastOutgoingAllianceRequests.push(
|
||||
request
|
||||
request,
|
||||
);
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.AllianceRequestReply,
|
||||
@@ -189,7 +189,7 @@ export class GameImpl implements Game {
|
||||
rejectAllianceRequest(request: AllianceRequestImpl) {
|
||||
this.allianceRequests = this.allianceRequests.filter((ar) => ar != request);
|
||||
(request.requestor() as PlayerImpl).pastOutgoingAllianceRequests.push(
|
||||
request
|
||||
request,
|
||||
);
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.AllianceRequestReply,
|
||||
@@ -296,7 +296,7 @@ export class GameImpl implements Game {
|
||||
removeExecution(exec: Execution) {
|
||||
this.execs = this.execs.filter((execution) => execution !== exec);
|
||||
this.unInitExecs = this.unInitExecs.filter(
|
||||
(execution) => execution !== exec
|
||||
(execution) => execution !== exec,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ export class GameImpl implements Game {
|
||||
}
|
||||
if (!breaker.isAlliedWith(other)) {
|
||||
throw new Error(
|
||||
`${breaker} not allied with ${other}, cannot break alliance`
|
||||
`${breaker} not allied with ${other}, cannot break alliance`,
|
||||
);
|
||||
}
|
||||
if (!other.isTraitor()) {
|
||||
@@ -459,7 +459,7 @@ export class GameImpl implements Game {
|
||||
const alliances = other.alliances().filter((a) => breakerSet.has(a));
|
||||
if (alliances.length != 1) {
|
||||
throw new Error(
|
||||
`must have exactly one alliance, have ${alliances.length}`
|
||||
`must have exactly one alliance, have ${alliances.length}`,
|
||||
);
|
||||
}
|
||||
this.alliances_ = this.alliances_.filter((a) => a != alliances[0]);
|
||||
@@ -478,7 +478,7 @@ export class GameImpl implements Game {
|
||||
.filter((a) => p1Set.has(a));
|
||||
if (alliances.length != 1) {
|
||||
throw new Error(
|
||||
`cannot expire alliance: must have exactly one alliance, have ${alliances.length}`
|
||||
`cannot expire alliance: must have exactly one alliance, have ${alliances.length}`,
|
||||
);
|
||||
}
|
||||
this.alliances_ = this.alliances_.filter((a) => a != alliances[0]);
|
||||
@@ -506,7 +506,7 @@ export class GameImpl implements Game {
|
||||
displayMessage(
|
||||
message: string,
|
||||
type: MessageType,
|
||||
playerID: PlayerID | null
|
||||
playerID: PlayerID | null,
|
||||
): void {
|
||||
let id = null;
|
||||
if (playerID != null) {
|
||||
@@ -614,7 +614,7 @@ export class GameImpl implements Game {
|
||||
}
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean,
|
||||
): Set<TileRef> {
|
||||
return this._map.bfs(tile, filter);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface PlayerUpdate {
|
||||
type: GameUpdateType.Player;
|
||||
nameViewData?: NameViewData;
|
||||
clientID: ClientID;
|
||||
flag: string,
|
||||
flag: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
id: PlayerID;
|
||||
|
||||
+19
-10
@@ -36,7 +36,10 @@ export class UnitView {
|
||||
public _wasUpdated = true;
|
||||
public lastPos: MapPos[] = [];
|
||||
|
||||
constructor(private gameView: GameView, private data: UnitUpdate) {
|
||||
constructor(
|
||||
private gameView: GameView,
|
||||
private data: UnitUpdate,
|
||||
) {
|
||||
this.lastPos.push(data.pos);
|
||||
}
|
||||
|
||||
@@ -95,14 +98,14 @@ export class PlayerView {
|
||||
constructor(
|
||||
private game: GameView,
|
||||
public data: PlayerUpdate,
|
||||
public nameData: NameViewData
|
||||
public nameData: NameViewData,
|
||||
) {}
|
||||
|
||||
async actions(tile: TileRef): Promise<PlayerActions> {
|
||||
return this.game.worker.playerInteraction(
|
||||
this.id(),
|
||||
this.game.x(tile),
|
||||
this.game.y(tile)
|
||||
this.game.y(tile),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,12 +159,12 @@ export class PlayerView {
|
||||
}
|
||||
allies(): PlayerView[] {
|
||||
return this.data.allies.map(
|
||||
(a) => this.game.playerBySmallID(a) as PlayerView
|
||||
(a) => this.game.playerBySmallID(a) as PlayerView,
|
||||
);
|
||||
}
|
||||
targets(): PlayerView[] {
|
||||
return this.data.targets.map(
|
||||
(id) => this.game.playerBySmallID(id) as PlayerView
|
||||
(id) => this.game.playerBySmallID(id) as PlayerView,
|
||||
);
|
||||
}
|
||||
gold(): Gold {
|
||||
@@ -199,7 +202,13 @@ export class PlayerView {
|
||||
return this.data.outgoingEmojis;
|
||||
}
|
||||
info(): PlayerInfo {
|
||||
return new PlayerInfo(this.flag(), this.name(), this.type(), this.clientID(), this.id());
|
||||
return new PlayerInfo(
|
||||
this.flag(),
|
||||
this.name(),
|
||||
this.type(),
|
||||
this.clientID(),
|
||||
this.id(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +227,7 @@ export class GameView implements GameMap {
|
||||
public worker: WorkerClient,
|
||||
private _config: Config,
|
||||
private _map: GameMap,
|
||||
private _myClientID: ClientID
|
||||
private _myClientID: ClientID,
|
||||
) {
|
||||
this.lastUpdate = {
|
||||
tick: 0,
|
||||
@@ -250,7 +259,7 @@ export class GameView implements GameMap {
|
||||
} else {
|
||||
this._players.set(
|
||||
pu.id,
|
||||
new PlayerView(this, pu, gu.playerNameViewData[pu.id])
|
||||
new PlayerView(this, pu, gu.playerNameViewData[pu.id]),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -347,7 +356,7 @@ export class GameView implements GameMap {
|
||||
return Array.from(this._units.values()).filter((u) => u.isActive());
|
||||
}
|
||||
return Array.from(this._units.values()).filter(
|
||||
(u) => u.isActive() && types.includes(u.type())
|
||||
(u) => u.isActive() && types.includes(u.type()),
|
||||
);
|
||||
}
|
||||
unit(id: number): UnitView {
|
||||
@@ -443,7 +452,7 @@ export class GameView implements GameMap {
|
||||
}
|
||||
bfs(
|
||||
tile: TileRef,
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean,
|
||||
): Set<TileRef> {
|
||||
return this._map.bfs(tile, filter);
|
||||
}
|
||||
|
||||
+31
-28
@@ -46,7 +46,10 @@ interface Target {
|
||||
}
|
||||
|
||||
class Donation {
|
||||
constructor(public readonly recipient: Player, public readonly tick: Tick) {}
|
||||
constructor(
|
||||
public readonly recipient: Player,
|
||||
public readonly tick: Tick,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class PlayerImpl implements Player {
|
||||
@@ -85,7 +88,7 @@ export class PlayerImpl implements Player {
|
||||
private mg: GameImpl,
|
||||
private _smallID: number,
|
||||
private readonly playerInfo: PlayerInfo,
|
||||
startPopulation: number
|
||||
startPopulation: number,
|
||||
) {
|
||||
this._flag = playerInfo.flag;
|
||||
this._name = playerInfo.name;
|
||||
@@ -125,7 +128,7 @@ export class PlayerImpl implements Player {
|
||||
attackerID: a.attacker().smallID(),
|
||||
targetID: a.target().smallID(),
|
||||
troops: a.troops(),
|
||||
} as AttackUpdate)
|
||||
}) as AttackUpdate,
|
||||
),
|
||||
incomingAttacks: this._incomingAttacks.map(
|
||||
(a) =>
|
||||
@@ -133,7 +136,7 @@ export class PlayerImpl implements Player {
|
||||
attackerID: a.attacker().smallID(),
|
||||
targetID: a.target().smallID(),
|
||||
troops: a.troops(),
|
||||
} as AttackUpdate)
|
||||
}) as AttackUpdate,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -203,7 +206,7 @@ export class PlayerImpl implements Player {
|
||||
const owner = this.mg.map().ownerID(neighbor);
|
||||
if (owner != this.smallID()) {
|
||||
ns.add(
|
||||
this.mg.playerBySmallID(owner) as PlayerImpl | TerraNulliusImpl
|
||||
this.mg.playerBySmallID(owner) as PlayerImpl | TerraNulliusImpl,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -249,7 +252,7 @@ export class PlayerImpl implements Player {
|
||||
|
||||
alliances(): MutableAlliance[] {
|
||||
return this.mg.alliances_.filter(
|
||||
(a) => a.requestor() == this || a.recipient() == this
|
||||
(a) => a.requestor() == this || a.recipient() == this,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -269,7 +272,7 @@ export class PlayerImpl implements Player {
|
||||
return null;
|
||||
}
|
||||
return this.alliances().find(
|
||||
(a) => a.recipient() == other || a.requestor() == other
|
||||
(a) => a.recipient() == other || a.requestor() == other,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -398,7 +401,7 @@ export class PlayerImpl implements Player {
|
||||
targets(): PlayerImpl[] {
|
||||
return this.targets_
|
||||
.filter(
|
||||
(t) => this.mg.ticks() - t.tick < this.mg.config().targetDuration()
|
||||
(t) => this.mg.ticks() - t.tick < this.mg.config().targetDuration(),
|
||||
)
|
||||
.map((t) => t.target as PlayerImpl);
|
||||
}
|
||||
@@ -430,7 +433,7 @@ export class PlayerImpl implements Player {
|
||||
.filter(
|
||||
(e) =>
|
||||
this.mg.ticks() - e.createdAt <
|
||||
this.mg.config().emojiMessageDuration()
|
||||
this.mg.config().emojiMessageDuration(),
|
||||
)
|
||||
.sort((a, b) => b.createdAt - a.createdAt);
|
||||
}
|
||||
@@ -439,7 +442,7 @@ export class PlayerImpl implements Player {
|
||||
const recipientID =
|
||||
recipient == AllPlayers ? AllPlayers : recipient.smallID();
|
||||
const prevMsgs = this.outgoingEmojis_.filter(
|
||||
(msg) => msg.recipientID == recipientID
|
||||
(msg) => msg.recipientID == recipientID,
|
||||
);
|
||||
for (const msg of prevMsgs) {
|
||||
if (
|
||||
@@ -475,12 +478,12 @@ export class PlayerImpl implements Player {
|
||||
this.mg.displayMessage(
|
||||
`Sent ${renderTroops(troops)} troops to ${recipient.name()}`,
|
||||
MessageType.INFO,
|
||||
this.id()
|
||||
this.id(),
|
||||
);
|
||||
this.mg.displayMessage(
|
||||
`Recieved ${renderTroops(troops)} troops from ${this.name()}`,
|
||||
MessageType.SUCCESS,
|
||||
recipient.id()
|
||||
recipient.id(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -495,7 +498,7 @@ export class PlayerImpl implements Player {
|
||||
removeGold(toRemove: Gold): void {
|
||||
if (toRemove > this._gold) {
|
||||
throw Error(
|
||||
`Player ${this} does not enough gold (${toRemove} vs ${this._gold}))`
|
||||
`Player ${this} does not enough gold (${toRemove} vs ${this._gold}))`,
|
||||
);
|
||||
}
|
||||
this._gold -= toRemove;
|
||||
@@ -521,7 +524,7 @@ export class PlayerImpl implements Player {
|
||||
setTargetTroopRatio(target: number): void {
|
||||
if (target < 0 || target > 1) {
|
||||
throw new Error(
|
||||
`invalid targetTroopRatio ${target} set on player ${PlayerImpl}`
|
||||
`invalid targetTroopRatio ${target} set on player ${PlayerImpl}`,
|
||||
);
|
||||
}
|
||||
this._targetTroopRatio = target;
|
||||
@@ -553,7 +556,7 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
const prev = unit.owner();
|
||||
(prev as PlayerImpl)._units = (prev as PlayerImpl)._units.filter(
|
||||
(u) => u != unit
|
||||
(u) => u != unit,
|
||||
);
|
||||
(unit as UnitImpl)._owner = this;
|
||||
this._units.push(unit as UnitImpl);
|
||||
@@ -561,12 +564,12 @@ export class PlayerImpl implements Player {
|
||||
this.mg.displayMessage(
|
||||
`${unit.type()} captured by ${this.displayName()}`,
|
||||
MessageType.ERROR,
|
||||
prev.id()
|
||||
prev.id(),
|
||||
);
|
||||
this.mg.displayMessage(
|
||||
`Captured ${unit.type()} from ${prev.displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.id()
|
||||
this.id(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -578,7 +581,7 @@ export class PlayerImpl implements Player {
|
||||
spawnTile,
|
||||
troops,
|
||||
this.mg.nextUnitID(),
|
||||
this
|
||||
this,
|
||||
);
|
||||
this._units.push(b);
|
||||
this.removeGold(cost);
|
||||
@@ -641,7 +644,7 @@ export class PlayerImpl implements Player {
|
||||
.filter((t) => this.mg.owner(t) == this && this.mg.isOceanShore(t))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile)
|
||||
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
|
||||
);
|
||||
if (spawns.length == 0) {
|
||||
return false;
|
||||
@@ -657,12 +660,12 @@ export class PlayerImpl implements Player {
|
||||
.filter(
|
||||
(u) =>
|
||||
this.mg.manhattanDist(u.tile(), tile) <
|
||||
this.mg.config().boatMaxDistance()
|
||||
this.mg.config().boatMaxDistance(),
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
this.mg.manhattanDist(a.tile(), tile) -
|
||||
this.mg.manhattanDist(b.tile(), tile)
|
||||
this.mg.manhattanDist(b.tile(), tile),
|
||||
);
|
||||
if (spawns.length == 0) {
|
||||
return false;
|
||||
@@ -690,7 +693,7 @@ export class PlayerImpl implements Player {
|
||||
|
||||
tradeShipSpawn(targetTile: TileRef): TileRef | false {
|
||||
const spawns = this.units(UnitType.Port).filter(
|
||||
(u) => u.tile() == targetTile
|
||||
(u) => u.tile() == targetTile,
|
||||
);
|
||||
if (spawns.length == 0) {
|
||||
return false;
|
||||
@@ -721,7 +724,7 @@ export class PlayerImpl implements Player {
|
||||
this.allRelationsSorted().map(({ player, relation }) => [
|
||||
player.smallID(),
|
||||
relation,
|
||||
])
|
||||
]),
|
||||
),
|
||||
alliances: this.alliances().map((a) => a.other(this).smallID()),
|
||||
};
|
||||
@@ -765,8 +768,8 @@ export class PlayerImpl implements Player {
|
||||
tile,
|
||||
andFN(
|
||||
(gm, t) => gm.ownerID(t) == gm.ownerID(tile) && gm.isLand(t),
|
||||
manhattanDistFN(tile, 25)
|
||||
)
|
||||
manhattanDistFN(tile, 25),
|
||||
),
|
||||
)) {
|
||||
if (this.mg.isOceanShore(t)) {
|
||||
nearOcean = true;
|
||||
@@ -790,7 +793,7 @@ export class PlayerImpl implements Player {
|
||||
createAttack(
|
||||
target: Player | TerraNullius,
|
||||
troops: number,
|
||||
sourceTile: TileRef
|
||||
sourceTile: TileRef,
|
||||
): Attack {
|
||||
const attack = new AttackImpl(target, this, troops, sourceTile);
|
||||
this._outgoingAttacks.push(attack);
|
||||
@@ -835,8 +838,8 @@ export class PlayerImpl implements Player {
|
||||
tile,
|
||||
andFN(
|
||||
(gm, t) => !gm.hasOwner(t) && gm.isLand(t),
|
||||
manhattanDistFN(tile, 200)
|
||||
)
|
||||
manhattanDistFN(tile, 200),
|
||||
),
|
||||
)) {
|
||||
for (const n of this.mg.neighbors(t)) {
|
||||
if (this.mg.owner(n) == this) {
|
||||
|
||||
@@ -20,7 +20,7 @@ export class UnitImpl implements Unit {
|
||||
private _tile: TileRef,
|
||||
private _troops: number,
|
||||
private _id: number,
|
||||
public _owner: PlayerImpl
|
||||
public _owner: PlayerImpl,
|
||||
) {
|
||||
// default to half health (or 1 is no health specified)
|
||||
this._health = (this.mg.unitInfo(_type).maxHealth ?? 2) / 2;
|
||||
@@ -89,7 +89,7 @@ export class UnitImpl implements Unit {
|
||||
this.mg.displayMessage(
|
||||
`Your ${this.type()} was captured by ${newOwner.displayName()}`,
|
||||
MessageType.ERROR,
|
||||
oldOwner.id()
|
||||
oldOwner.id(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export class UnitImpl implements Unit {
|
||||
this.mg.displayMessage(
|
||||
`Your ${this.type()} was destroyed`,
|
||||
MessageType.ERROR,
|
||||
this.owner().id()
|
||||
this.owner().id(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -61,7 +61,8 @@ export function validateUsername(username: string): {
|
||||
if (!validPattern.test(username)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: "Username can only contain letters, numbers, spaces, underscores, and [square brackets].",
|
||||
error:
|
||||
"Username can only contain letters, numbers, spaces, underscores, and [square brackets].",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,7 +72,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");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
message.gameID,
|
||||
message.gameConfig,
|
||||
message.clientID,
|
||||
gameUpdate
|
||||
gameUpdate,
|
||||
).then((gr) => {
|
||||
sendMessage({
|
||||
type: "initialized",
|
||||
@@ -72,7 +72,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
const actions = (await gameRunner).playerActions(
|
||||
message.playerID,
|
||||
message.x,
|
||||
message.y
|
||||
message.y,
|
||||
);
|
||||
sendMessage({
|
||||
type: "player_actions_result",
|
||||
|
||||
@@ -14,13 +14,13 @@ export class WorkerClient {
|
||||
private isInitialized = false;
|
||||
private messageHandlers: Map<string, (message: WorkerMessage) => void>;
|
||||
private gameUpdateCallback?: (
|
||||
update: GameUpdateViewData | ErrorUpdate
|
||||
update: GameUpdateViewData | ErrorUpdate,
|
||||
) => void;
|
||||
|
||||
constructor(
|
||||
private gameID: GameID,
|
||||
private gameConfig: GameConfig,
|
||||
private clientID: ClientID
|
||||
private clientID: ClientID,
|
||||
) {
|
||||
this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url));
|
||||
this.messageHandlers = new Map();
|
||||
@@ -28,7 +28,7 @@ export class WorkerClient {
|
||||
// Set up global message handler
|
||||
this.worker.addEventListener(
|
||||
"message",
|
||||
this.handleWorkerMessage.bind(this)
|
||||
this.handleWorkerMessage.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export class WorkerClient {
|
||||
playerInteraction(
|
||||
playerID: PlayerID,
|
||||
x: number,
|
||||
y: number
|
||||
y: number,
|
||||
): Promise<PlayerActions> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isInitialized) {
|
||||
|
||||
@@ -52,7 +52,7 @@ export class GameManager {
|
||||
disableBots: false,
|
||||
disableNPCs: false,
|
||||
creativeMode: false,
|
||||
})
|
||||
}),
|
||||
);
|
||||
return id;
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class GameManager {
|
||||
hasActiveGame(gameID: GameID): boolean {
|
||||
const game = this.games
|
||||
.filter(
|
||||
(g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active
|
||||
(g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active,
|
||||
)
|
||||
.find((g) => g.id == gameID);
|
||||
return game != null;
|
||||
@@ -93,7 +93,7 @@ export class GameManager {
|
||||
disableBots: false,
|
||||
disableNPCs: false,
|
||||
creativeMode: false,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ export class GameServer {
|
||||
if (gameConfig.disableNPCs != null) {
|
||||
this.gameConfig.disableNPCs = gameConfig.disableNPCs;
|
||||
}
|
||||
if (gameConfig.creativeMode != null) {
|
||||
this.gameConfig.creativeMode = gameConfig.creativeMode;
|
||||
}
|
||||
}
|
||||
|
||||
public addClient(client: Client, lastTurn: number) {
|
||||
|
||||
@@ -134,7 +134,7 @@ wss.on("connection", (ws, req) => {
|
||||
ws.on("message", (message: string) => {
|
||||
try {
|
||||
const clientMsg: ClientMessage = ClientMessageSchema.parse(
|
||||
JSON.parse(message)
|
||||
JSON.parse(message),
|
||||
);
|
||||
if (clientMsg.type == "join") {
|
||||
const forwarded = req.headers["x-forwarded-for"];
|
||||
@@ -147,7 +147,7 @@ wss.on("connection", (ws, req) => {
|
||||
const { isValid, error } = validateUsername(clientMsg.username);
|
||||
if (!isValid) {
|
||||
console.log(
|
||||
`game ${clientMsg.gameID}, client ${clientMsg.clientID} received invalid username, ${error}`
|
||||
`game ${clientMsg.gameID}, client ${clientMsg.clientID} received invalid username, ${error}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -158,10 +158,10 @@ wss.on("connection", (ws, req) => {
|
||||
clientMsg.persistentID,
|
||||
ip,
|
||||
clientMsg.username,
|
||||
ws
|
||||
ws,
|
||||
),
|
||||
clientMsg.gameID,
|
||||
clientMsg.lastTurn
|
||||
clientMsg.lastTurn,
|
||||
);
|
||||
}
|
||||
if (clientMsg.type == "log") {
|
||||
|
||||
Reference in New Issue
Block a user