implement pause feature,

bugfix: server stops in the beginning if no ticks found
This commit is contained in:
evanpelle
2024-12-25 15:19:35 -08:00
parent 57cbf5c55e
commit 8e61632b8b
5 changed files with 115 additions and 51 deletions
+1 -1
View File
@@ -188,7 +188,7 @@ export class GameRunner {
} catch (error) {
const errorText = `Error: ${error.message}\nStack: ${error.stack}`;
consolex.error(errorText)
alert(`Game crashed! client id: ${this.clientID}\n Please paste the following your bug report in Discord:\n` + errorText);
alert(`Game crashed! client id: ${this.clientID}\n Please paste the following in your bug report in Discord:\n` + errorText);
}
this.renderer.tick()
this.currTurn++
+24 -3
View File
@@ -1,19 +1,21 @@
import { Config, ServerConfig } from "../core/configuration/Config";
import { Config, GameEnv, ServerConfig } from "../core/configuration/Config";
import { consolex } from "../core/Consolex";
import { GameEvent } from "../core/EventBus";
import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, PlayerRecord, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
import { CreateGameRecord, generateID } from "../core/Util";
import { LobbyConfig } from "./GameRunner";
import { getPersistentIDFromCookie } from "./Main";
export class LocalServer {
private turns: Turn[] = []
private intents: Intent[] = []
private startedAt: number
private endTurnIntervalID
private paused = false
constructor(
private serverConfig: ServerConfig,
@@ -35,14 +37,33 @@ export class LocalServer {
}))
}
pause() {
this.paused = true
}
resume() {
this.paused = false
}
onMessage(message: string) {
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "intent") {
if (this.paused) {
if (clientMsg.intent.type == "troop_ratio") {
// Store troop change events because otherwise they are
// not registered when game is paused.
this.intents.push(clientMsg.intent)
}
return
}
this.intents.push(clientMsg.intent)
}
}
private endTurn() {
if (this.paused) {
return
}
const pastTurn: Turn = {
turnNumber: this.turns.length,
gameID: this.lobbyConfig.gameID,
+16
View File
@@ -6,6 +6,9 @@ import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, I
import { LobbyConfig } from "./GameRunner"
import { LocalServer } from "./LocalServer"
export class PauseGameEvent implements GameEvent {
constructor(public readonly paused: boolean) { }
}
export class SendAllianceRequestIntentEvent implements GameEvent {
constructor(
@@ -121,6 +124,7 @@ export class Transport {
this.eventBus.on(BuildUnitIntentEvent, (e) => this.onBuildUnitIntent(e))
this.eventBus.on(SendLogEvent, (e) => this.onSendLogEvent(e))
this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e))
}
private startPing() {
@@ -352,6 +356,18 @@ export class Transport {
})
}
private onPauseGameEvent(event: PauseGameEvent) {
if (!this.isLocal) {
console.log(`cannot pause multiplayer games`)
return
}
if (event.paused) {
this.localServer.pause()
} else {
this.localServer.resume()
}
}
private sendIntent(intent: Intent) {
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
const msg = ClientIntentMessageSchema.parse({
+71 -47
View File
@@ -1,13 +1,14 @@
import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Layer } from './Layer';
import { Game, Player, Unit, UnitType } from '../../../core/game/Game';
import { Game, GameType, Player, Unit, UnitType } from '../../../core/game/Game';
import { ClientID } from '../../../core/Schemas';
import { EventBus } from '../../../core/EventBus';
import { TransformHandler } from '../TransformHandler';
import { MouseMoveEvent } from '../../InputHandler';
import { euclideanDist, distSortUnit } from '../../../core/Util';
import { renderNumber, renderTroops } from '../../Utils';
import { PauseGameEvent } from '../../Transport';
@customElement('player-info-overlay')
export class PlayerInfoOverlay extends LitElement implements Layer {
@@ -30,11 +31,21 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private unit: Unit | null = null;
@state()
private _isVisible: boolean = false;
private showPauseButton: boolean = true
@state()
private _isInfoVisible: boolean = false;
@state()
private _isPaused: boolean = false;
private _isActive = false
init(game: Game) {
this.game = game;
this.eventBus.on(MouseMoveEvent, (e: MouseMoveEvent) => this.onMouseEvent(e));
this._isActive = true
this.showPauseButton = this.game.config().gameConfig().gameType == GameType.Singleplayer
}
private onMouseEvent(event: MouseMoveEvent) {
@@ -45,7 +56,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
const worldCoord = this.transform.screenToWorldCoordinates(event.x, event.y);
if (!this.game.isOnMap(worldCoord)) {
return;
return;
}
const tile = this.game.tile(worldCoord);
@@ -70,9 +80,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
window.location.reload();
}
private onPauseButtonClick() {
this._isPaused = !this._isPaused;
this.eventBus.emit(new PauseGameEvent(this._isPaused));
}
tick() {
this.requestUpdate()
// Implementation for Layer interface
this.requestUpdate();
}
renderLayer(context: CanvasRenderingContext2D) {
@@ -84,7 +98,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
}
setVisible(visible: boolean) {
this._isVisible = visible;
this._isInfoVisible = visible;
this.requestUpdate();
}
@@ -109,25 +123,31 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
private renderUnitInfo(unit: Unit) {
const isAlly = (unit.owner() == this.myPlayer() || this.myPlayer()?.isAlliedWith(unit.owner())) ?? false;
return html`
<div class="info-content">
<div class="player-name ${isAlly ? 'ally' : ''}">${unit.owner().name()}</div>
<div class="unit-details">
<div class="type-label">${unit.type()}</div>
${unit.hasHealth() ? html`
<div class="type-label">Health: ${unit.health()}</div>
` : ''}
<div class="info-content">
<div class="player-name ${isAlly ? 'ally' : ''}">${unit.owner().name()}</div>
<div class="unit-details">
<div class="type-label">${unit.type()}</div>
${unit.hasHealth() ? html`
<div class="type-label">Health: ${unit.health()}</div>
` : ''}
</div>
</div>
</div>
`
`;
}
render() {
if (!this._isActive) {
return html``
}
return html`
<div class="container">
<div class="controls">
<button class="exit-button" @click=${this.onExitButtonClick}>×</button>
<button class="control-button pause-button ${!this.showPauseButton ? 'hidden' : ''}" @click=${this.onPauseButtonClick}>
${this._isPaused ? '▶' : '⏸'}
</button>
<button class="control-button exit-button" @click=${this.onExitButtonClick}>×</button>
</div>
<div class="player-info ${!this._isVisible ? 'hidden' : ''}">
<div class="player-info ${!this._isInfoVisible ? 'hidden' : ''}">
${this.player != null ? this.renderPlayerInfo(this.player) : ''}
${this.unit != null ? this.renderUnitInfo(this.unit) : ''}
</div>
@@ -153,6 +173,31 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
align-self: flex-end;
margin-bottom: 4px;
z-index: 2;
display: flex;
gap: 8px;
}
.control-button {
background: rgba(30, 30, 30, 0.7);
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
opacity: 0.7;
transition: opacity 0.2s, background-color 0.2s;
backdrop-filter: blur(5px);
}
.control-button:hover {
opacity: 1;
background: rgba(40, 40, 40, 0.8);
}
.pause-button {
font-size: 20px;
padding: 4px 10px;
}
.player-info {
@@ -167,6 +212,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
min-width: 120px;
text-align: left;
}
.hidden {
opacity: 0;
visibility: hidden;
@@ -176,6 +222,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
.info-content {
margin-top: 8px;
}
.player-name {
font-weight: bold;
margin-bottom: 4px;
@@ -194,35 +241,6 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
margin-top: 4px;
}
.health-bar {
height: 4px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 2px;
margin-top: 4px;
}
.health-fill {
height: 100%;
background-color: #4CAF50;
border-radius: 2px;
transition: width 0.2s ease-out;
}
.exit-button {
background: none;
border: none;
color: white;
font-size: 40px;
cursor: pointer;
padding: 4px;
opacity: 0.7;
transition: opacity 0.2s;
}
.exit-button:hover {
opacity: 1;
}
@media (max-width: 768px) {
.container {
top: 5px;
@@ -235,8 +253,14 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
min-width: 100px;
}
.exit-button {
.control-button {
font-size: 16px;
padding: 3px 6px;
}
.pause-button {
font-size: 14px;
padding: 3px 8px;
}
.type-label {
+3
View File
@@ -120,6 +120,9 @@ export class GameServer {
public start() {
this._hasStarted = true
this._startTime = Date.now()
// Set last ping to start so we don't immediately stop the game
// if no client connects/pings.
this.lastPingUpdate = Date.now()
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
this.activeClients.forEach(c => {