mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
implement single player
This commit is contained in:
@@ -163,8 +163,10 @@
|
||||
* donate troops button DONE 10/7/2024
|
||||
* Make fake humans spawn by their country DONE 10/9/2024
|
||||
* UI: leader board DONE 10/12/2024
|
||||
* single player menu
|
||||
* private game menu
|
||||
* single player mode DONE 10/12/2024
|
||||
* single player select map
|
||||
* implement private game
|
||||
* private game can select map
|
||||
* optimize sendBoat function
|
||||
* Test on android
|
||||
* NPC more likely to accept alliance fewer alliance player has
|
||||
|
||||
@@ -67,7 +67,7 @@ class Client {
|
||||
]);
|
||||
console.log(`got ip ${clientIP}`)
|
||||
this.game = createClientGame(
|
||||
false,
|
||||
event.detail.singlePlayer,
|
||||
(): string => {return this.usernameInput.getCurrentUsername()},
|
||||
uuidv4(),
|
||||
uuidv4(),
|
||||
|
||||
@@ -13,9 +13,6 @@ import {WinCheckExecution} from "../core/execution/WinCheckExecution";
|
||||
import {SendAttackIntentEvent, SendSpawnIntentEvent, Transport} from "./Transport";
|
||||
import {createCanvas} from "./graphics/Utils";
|
||||
import {DisplayMessageEvent, MessageType} from "./graphics/layers/EventsDisplay";
|
||||
import {LocalSocket, LocalSocketFactory, SocketFactory, WebsocketFactory as WebSocketFactory} from "../core/GameSocket";
|
||||
import {LocalServer} from "../core/LocalServer";
|
||||
|
||||
|
||||
|
||||
export function createClientGame(isLocal: boolean, playerName: () => string, clientID: ClientID, playerID: PlayerID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame {
|
||||
@@ -25,17 +22,7 @@ export function createClientGame(isLocal: boolean, playerName: () => string, cli
|
||||
const canvas = createCanvas()
|
||||
let gameRenderer = createRenderer(canvas, game, eventBus, clientID)
|
||||
|
||||
let wsFactory: SocketFactory = null
|
||||
|
||||
if (isLocal) {
|
||||
wsFactory = new LocalSocketFactory(new LocalServer(config))
|
||||
} else {
|
||||
const wsHost = process.env.WEBSOCKET_URL || window.location.host;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
wsFactory = new WebSocketFactory(`${wsProtocol}//${wsHost}`)
|
||||
}
|
||||
|
||||
const transport = new Transport(wsFactory, eventBus, gameID, clientID, playerID, playerName)
|
||||
const transport = new Transport(isLocal, eventBus, gameID, clientID, playerID, config, playerName)
|
||||
|
||||
|
||||
return new ClientGame(
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import {Config} from "./configuration/Config";
|
||||
import {GameMessageEvent, LocalSocket} from "./GameSocket";
|
||||
import {ClientMessage, ClientMessageSchema, Intent, ServerTurnMessageSchema, Turn} from "./Schemas";
|
||||
import {Config} from "../core/configuration/Config";
|
||||
import {ClientMessage, ClientMessageSchema, Intent, ServerMessage, ServerTurnMessageSchema, Turn} from "../core/Schemas";
|
||||
|
||||
export class LocalServer {
|
||||
|
||||
private gameID = "LOCAL"
|
||||
|
||||
public localSocket: LocalSocket
|
||||
|
||||
private turns: Turn[] = []
|
||||
private intents: Intent[] = []
|
||||
|
||||
private endTurnIntervalID
|
||||
|
||||
|
||||
|
||||
constructor(private config: Config) {
|
||||
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
|
||||
constructor(private config: Config, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) {
|
||||
}
|
||||
|
||||
onConnect() {
|
||||
|
||||
start() {
|
||||
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
|
||||
this.clientConnect()
|
||||
this.clientMessage({
|
||||
type: "start",
|
||||
turns: [],
|
||||
})
|
||||
}
|
||||
|
||||
onMessage(message: string) {
|
||||
@@ -38,13 +38,9 @@ export class LocalServer {
|
||||
}
|
||||
this.turns.push(pastTurn)
|
||||
this.intents = []
|
||||
|
||||
const msg = JSON.stringify(ServerTurnMessageSchema.parse(
|
||||
{
|
||||
type: "turn",
|
||||
turn: pastTurn
|
||||
}
|
||||
))
|
||||
this.localSocket.onmessage(new GameMessageEvent(msg))
|
||||
this.clientMessage({
|
||||
type: "turn",
|
||||
turn: pastTurn
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export class PublicLobby extends LitElement {
|
||||
if (this.currLobby == null) {
|
||||
this.currLobby = lobby
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {lobby: lobby},
|
||||
detail: {lobby: lobby, singlePlayer: false},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
|
||||
@@ -83,8 +83,13 @@ button:hover {
|
||||
|
||||
private startGame() {
|
||||
console.log('Starting single player game...');
|
||||
this.dispatchEvent(new CustomEvent('single-player', {
|
||||
detail: {todo: "TODO"},
|
||||
this.dispatchEvent(new CustomEvent('join-lobby', {
|
||||
detail: {
|
||||
singlePlayer: true,
|
||||
lobby: {
|
||||
id: "LOCAL",
|
||||
}
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
|
||||
+35
-9
@@ -1,7 +1,8 @@
|
||||
import {Config} from "../core/configuration/Config"
|
||||
import {EventBus, GameEvent} from "../core/EventBus"
|
||||
import {AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType} from "../core/game/Game"
|
||||
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema} from "../core/Schemas"
|
||||
import {SocketFactory} from "../core/GameSocket"
|
||||
import {LocalServer} from "./LocalServer"
|
||||
|
||||
|
||||
export class SendAllianceRequestIntentEvent implements GameEvent {
|
||||
@@ -68,15 +69,17 @@ export class SendDonateIntentEvent implements GameEvent {
|
||||
|
||||
export class Transport {
|
||||
|
||||
public onconnect: () => {}
|
||||
private socket: WebSocket
|
||||
|
||||
private localServer: LocalServer
|
||||
|
||||
constructor(
|
||||
private socketFactory: SocketFactory,
|
||||
private isLocal: boolean,
|
||||
private eventBus: EventBus,
|
||||
private gameID: GameID,
|
||||
private clientID: ClientID,
|
||||
private playerID: PlayerID,
|
||||
private config: Config,
|
||||
private playerName: () => string,
|
||||
) {
|
||||
this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e))
|
||||
@@ -91,7 +94,22 @@ export class Transport {
|
||||
}
|
||||
|
||||
connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
|
||||
this.socket = this.socketFactory.createSocket()
|
||||
if (this.isLocal) {
|
||||
this.connectLocal(onconnect, onmessage, isActive)
|
||||
} else {
|
||||
this.connectRemote(onconnect, onmessage, isActive)
|
||||
}
|
||||
}
|
||||
|
||||
private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
|
||||
this.localServer = new LocalServer(this.config, onconnect, onmessage)
|
||||
this.localServer.start()
|
||||
}
|
||||
|
||||
private connectRemote(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
|
||||
const wsHost = process.env.WEBSOCKET_URL || window.location.host;
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
this.socket = new WebSocket(`${wsProtocol}//${wsHost}`)
|
||||
this.socket.onopen = () => {
|
||||
console.log('Connected to game server!');
|
||||
onconnect()
|
||||
@@ -115,7 +133,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
joinGame(clientIP: string | null, numTurns: number) {
|
||||
this.socket.send(
|
||||
this.sendMsg(
|
||||
JSON.stringify(
|
||||
ClientJoinMessageSchema.parse({
|
||||
type: "join",
|
||||
@@ -136,7 +154,7 @@ export class Transport {
|
||||
clientID: this.clientID,
|
||||
gameID: this.gameID,
|
||||
})
|
||||
this.socket.send(JSON.stringify(msg))
|
||||
this.sendMsg(JSON.stringify(msg))
|
||||
} else {
|
||||
console.log('WebSocket is not open. Current state:', this.socket.readyState);
|
||||
console.log('attempting reconnect')
|
||||
@@ -239,17 +257,25 @@ export class Transport {
|
||||
}
|
||||
|
||||
private sendIntent(intent: Intent) {
|
||||
if (this.socket.readyState === WebSocket.OPEN) {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientIntentMessageSchema.parse({
|
||||
type: "intent",
|
||||
clientID: this.clientID,
|
||||
gameID: this.gameID,
|
||||
intent: intent
|
||||
})
|
||||
this.socket.send(JSON.stringify(msg))
|
||||
this.sendMsg(JSON.stringify(msg))
|
||||
} else {
|
||||
console.log('WebSocket is not open. Current state:', this.socket.readyState);
|
||||
console.log('attempting reconnect')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sendMsg(msg: string) {
|
||||
if (this.isLocal) {
|
||||
this.localServer.onMessage(msg)
|
||||
} else {
|
||||
this.socket.send(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import {LocalServer} from "./LocalServer";
|
||||
|
||||
export interface SocketFactory {
|
||||
createSocket(): WebSocket;
|
||||
}
|
||||
|
||||
export interface Socket {
|
||||
onopen: ((event: Event) => void) | null;
|
||||
onmessage: ((event: MessageEvent) => void) | null;
|
||||
onerror: ((event: Event) => void) | null;
|
||||
onclose: ((event: CloseEvent) => void) | null;
|
||||
readyState: number;
|
||||
|
||||
connect(url: string): void;
|
||||
send(data: string): void;
|
||||
close(code?: number, reason?: string): void;
|
||||
}
|
||||
|
||||
export const WebSocketReadyState = {
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3
|
||||
};
|
||||
|
||||
export class WebsocketFactory implements SocketFactory {
|
||||
|
||||
constructor(private url: string) { }
|
||||
|
||||
createSocket(): WebSocket {
|
||||
return new WebSocket(this.url)
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSocketFactory implements SocketFactory {
|
||||
constructor(private localServer: LocalServer) { }
|
||||
|
||||
createSocket(): WebSocket {
|
||||
return new LocalSocket(this.localServer)
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalSocket implements WebSocket {
|
||||
|
||||
|
||||
constructor(private server: LocalServer) {
|
||||
server.localSocket = this
|
||||
}
|
||||
|
||||
binaryType: BinaryType;
|
||||
bufferedAmount: number;
|
||||
extensions: string;
|
||||
onclose: (this: WebSocket, ev: CloseEvent) => any;
|
||||
onerror: (this: WebSocket, ev: Event) => any;
|
||||
onmessage: (this: WebSocket, ev: MessageEvent) => any;
|
||||
onopen: (this: WebSocket, ev: Event) => any;
|
||||
protocol: string;
|
||||
readyState: number;
|
||||
url: string;
|
||||
close(code?: number, reason?: string): void {
|
||||
// this.server.onclose(new GameCloseEvent())
|
||||
}
|
||||
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
|
||||
this.server.onMessage(data as string)
|
||||
}
|
||||
CONNECTING: 0;
|
||||
OPEN: 1;
|
||||
CLOSING: 2;
|
||||
CLOSED: 3;
|
||||
addEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
removeEventListener(type: unknown, listener: unknown, options?: unknown): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
dispatchEvent(event: Event): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class GameMessageEvent implements MessageEvent {
|
||||
|
||||
readonly data: any;
|
||||
readonly origin: string;
|
||||
readonly lastEventId: string;
|
||||
readonly source: WindowProxy | null;
|
||||
readonly ports: ReadonlyArray<MessagePort>;
|
||||
|
||||
constructor(data: any) {
|
||||
this.data = data;
|
||||
}
|
||||
returnValue: boolean;
|
||||
srcElement: EventTarget;
|
||||
initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
NONE: 0;
|
||||
CAPTURING_PHASE: 1;
|
||||
AT_TARGET: 2;
|
||||
BUBBLING_PHASE: 3;
|
||||
|
||||
// MessageEvent interface methods
|
||||
initMessageEvent(type: string, bubbles?: boolean, cancelable?: boolean, data?: any, origin?: string, lastEventId?: string, source?: WindowProxy | null, ports?: MessagePort[]): void {
|
||||
// This method is deprecated, so we'll leave it as a no-op
|
||||
console.warn('initMessageEvent is deprecated');
|
||||
}
|
||||
|
||||
// Event interface properties and methods
|
||||
readonly bubbles: boolean = false;
|
||||
readonly cancelBubble: boolean = false;
|
||||
readonly cancelable: boolean = false;
|
||||
readonly composed: boolean = false;
|
||||
readonly currentTarget: EventTarget | null = null;
|
||||
readonly defaultPrevented: boolean = false;
|
||||
readonly eventPhase: number = Event.NONE;
|
||||
readonly isTrusted: boolean = false;
|
||||
readonly target: EventTarget | null = null;
|
||||
readonly timeStamp: number = Date.now();
|
||||
readonly type: string = 'message';
|
||||
|
||||
// Event interface methods
|
||||
composedPath(): EventTarget[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
preventDefault(): void {
|
||||
// No-op for this example
|
||||
}
|
||||
|
||||
stopImmediatePropagation(): void {
|
||||
// No-op for this example
|
||||
}
|
||||
|
||||
stopPropagation(): void {
|
||||
// No-op for this example
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user