mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 19:56:43 +00:00
@@ -216,23 +216,37 @@
|
||||
* bufix: mini map doesn't load in time DONE 12/7/2023
|
||||
* bugfix: private game host game doesn't start DONE 12/8/2023
|
||||
* add NA map DONE 12/8/2023
|
||||
* add Oceania map 12/8/2023
|
||||
* store in BigQuery
|
||||
* clicking on a player's name in the rank UI should teleport you to him (pretty useful to know who's who and to locate small nations)
|
||||
* record commit hash of game
|
||||
* add Oceania map DONE 12/8/2023
|
||||
* max price for units DONE 12/9/2024
|
||||
* better unit scaling DONE 12/9/2024
|
||||
* make hard & impossible harder DONE 12/9/2024
|
||||
* clicking on a player's name in the rank UI should teleport you to him DONE 12/9/2024
|
||||
* emojis should be displayed on top of your name not under it DONE 12/9/2024
|
||||
* the notification for a successful trade should be shorter, example: " 70k Gold from trade with "X" " DONE 12/9/2024
|
||||
* countries don't actually spawn with some randomness, it's always the same exact spawn DONE 12/9/2024
|
||||
* you should get a notification and a reward (some money) for eliminating an enemy DONE 12/9/2024
|
||||
* alert on attack DONE 12/20/2024
|
||||
* alert on unit captured or destroyed 12/20/2024
|
||||
* only check islands/clusters when being attacked DONE 12/10/2024
|
||||
* only calculate name if tile changes DONE 12/10/2024
|
||||
* store in BigQuery DONE 12/10/2024
|
||||
* allow longer names and allow them to be displayed in the Rank UI not be cut
|
||||
* make boats work on lakes (& oceania)
|
||||
* record game winner
|
||||
* add panama canal NA
|
||||
* repaint canvas after tab away to prevent blank screen
|
||||
* on mobile can't click away from build menu
|
||||
* replay stored games
|
||||
* max price for units
|
||||
* when player dies, don't remove atom bombs
|
||||
* record commit hash of game
|
||||
* remove alliance when player dies
|
||||
* alert on attack
|
||||
* alert on unit captured or destroyed
|
||||
* nuking an enemy and accidentally destroying a trade ship shouldn't break the alliance and make you a traitor
|
||||
* you should get a notification and a reward (some money) for eliminating an enemy (perhaps take wtvr gold the enemy had)
|
||||
* emojis should be displayed on top of your name not under it
|
||||
* the notification for a successful trade should be shorter, example: " 70k Gold from trade with "X" "
|
||||
* countries don't actually spawn with some randomness, it's always the same exact spawn
|
||||
* allow longer names and allow them to be displayed in the Rank UI not be cut (many are cut for now, even for countries)
|
||||
* put delay on adjust troop ratio to reduce number of messages
|
||||
* make clientID & playerID smaller
|
||||
* remove dash from game id
|
||||
* pause button in single player
|
||||
* when hovering over another player have the name not appear ontop of the exit cross
|
||||
* have a visual thing of who your attacking and how many boats you sent
|
||||
* make the sliders bigger. Like vertically.
|
||||
* add way to hide leaderboard n such
|
||||
* create behavior tests
|
||||
* create perf test
|
||||
|
||||
@@ -265,3 +279,12 @@ FEB 1st
|
||||
* REFACTOR: give terranullius an ID, game.player() returns terranullius
|
||||
* REFACTOR: ocean is considered TerraNullius ?
|
||||
|
||||
Error: cannot delete [object Object] not active
|
||||
Stack: Error: cannot delete [object Object] not active
|
||||
at UnitImpl.delete (webpack://openfront-client/./src/core/game/UnitImpl.ts?:54:19)
|
||||
at ShellExecution.tick (webpack://openfront-client/./src/core/execution/ShellExecution.ts?:36:38)
|
||||
at eval (webpack://openfront-client/./src/core/game/GameImpl.ts?:133:19)
|
||||
at Array.forEach (<anonymous>)
|
||||
at GameImpl.executeNextTick (webpack://openfront-client/./src/core/game/GameImpl.ts?:131:20)
|
||||
at GameRunner.tick (webpack://openfront-client/./src/client/GameRunner.ts?:153:21)
|
||||
at eval (webpack://openfront-client/./src/client/GameRunner.ts?:110:50)
|
||||
Generated
+105
@@ -7,6 +7,7 @@
|
||||
"name": "openfront-client",
|
||||
"dependencies": {
|
||||
"@datastructures-js/priority-queue": "^6.3.1",
|
||||
"@google-cloud/bigquery": "^7.9.1",
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/express": "^4.17.21",
|
||||
@@ -2351,6 +2352,92 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/bigquery": {
|
||||
"version": "7.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-7.9.1.tgz",
|
||||
"integrity": "sha512-ZkcRMpBoFLxIh6TiQBywA22yT3c2j0f07AHWEMjtYqMQzZQbFrpxuJU2COp3tyjZ91ZIGHe4gY7/dGZL88cltg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google-cloud/common": "^5.0.0",
|
||||
"@google-cloud/paginator": "^5.0.2",
|
||||
"@google-cloud/precise-date": "^4.0.0",
|
||||
"@google-cloud/promisify": "^4.0.0",
|
||||
"arrify": "^2.0.1",
|
||||
"big.js": "^6.0.0",
|
||||
"duplexify": "^4.0.0",
|
||||
"extend": "^3.0.2",
|
||||
"is": "^3.3.0",
|
||||
"stream-events": "^1.0.5",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/bigquery/node_modules/arrify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/bigquery/node_modules/big.js": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz",
|
||||
"integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bigjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/bigquery/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/common": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-5.0.2.tgz",
|
||||
"integrity": "sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google-cloud/projectify": "^4.0.0",
|
||||
"@google-cloud/promisify": "^4.0.0",
|
||||
"arrify": "^2.0.1",
|
||||
"duplexify": "^4.1.1",
|
||||
"extend": "^3.0.2",
|
||||
"google-auth-library": "^9.0.0",
|
||||
"html-entities": "^2.5.2",
|
||||
"retry-request": "^7.0.0",
|
||||
"teeny-request": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/common/node_modules/arrify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/paginator": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
|
||||
@@ -2373,6 +2460,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/precise-date": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-4.0.0.tgz",
|
||||
"integrity": "sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/projectify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
|
||||
@@ -8767,6 +8863,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz",
|
||||
"integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@datastructures-js/priority-queue": "^6.3.1",
|
||||
"@google-cloud/bigquery": "^7.9.1",
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/express": "^4.17.21",
|
||||
|
||||
@@ -167,13 +167,13 @@ export class HostLobbyModal extends LitElement {
|
||||
}
|
||||
|
||||
private async handleMapChange(e: Event) {
|
||||
this.selectedMap = Number((e.target as HTMLSelectElement).value) as GameMap;
|
||||
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMap;
|
||||
console.log(`updating map to ${this.selectedMap}`)
|
||||
this.putGameConfig()
|
||||
}
|
||||
|
||||
private async handleDifficultyChange(e: Event) {
|
||||
this.selectedDiffculty = Number((e.target as HTMLSelectElement).value) as Difficulty;
|
||||
this.selectedDiffculty = String((e.target as HTMLSelectElement).value) as Difficulty;
|
||||
console.log(`updating difficulty to ${this.selectedDiffculty}`)
|
||||
this.putGameConfig()
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ export class LocalServer {
|
||||
console.log('local server ending game')
|
||||
clearInterval(this.endTurnIntervalID)
|
||||
const record = CreateGameRecord(this.gameID, this.gameConfig, this.turns, this.startedAt, Date.now())
|
||||
// Clear turns because beacon only supports up to 64kb
|
||||
record.turns = []
|
||||
// For unload events, sendBeacon is the only reliable method
|
||||
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
||||
type: 'application/json'
|
||||
|
||||
@@ -84,7 +84,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(([key, value]) => html`
|
||||
<option value=${value} ?selected=${this.selectedMap === value}>
|
||||
${key}
|
||||
${value}
|
||||
</option>
|
||||
`)}
|
||||
</select>
|
||||
@@ -96,7 +96,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
.filter(([key]) => isNaN(Number(key)))
|
||||
.map(([key, value]) => html`
|
||||
<option value=${value} ?selected=${this.selectedDifficulty === value}>
|
||||
${key}
|
||||
${value}
|
||||
</option>
|
||||
`)}
|
||||
</select>
|
||||
@@ -117,10 +117,10 @@ export class SinglePlayerModal extends LitElement {
|
||||
}
|
||||
|
||||
private handleMapChange(e: Event) {
|
||||
this.selectedMap = Number((e.target as HTMLSelectElement).value) as GameMap;
|
||||
this.selectedMap = String((e.target as HTMLSelectElement).value) as GameMap;
|
||||
}
|
||||
private handleDifficultyChange(e: Event) {
|
||||
this.selectedDifficulty = Number((e.target as HTMLSelectElement).value) as Difficulty;
|
||||
this.selectedDifficulty = String((e.target as HTMLSelectElement).value) as Difficulty;
|
||||
}
|
||||
private startGame() {
|
||||
console.log(`Starting single player game with map: ${GameMap[this.selectedMap]}`);
|
||||
|
||||
@@ -41,6 +41,7 @@ export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus:
|
||||
console.error('EmojiTable element not found in the DOM');
|
||||
}
|
||||
leaderboard.clientID = clientID
|
||||
leaderboard.eventBus = eventBus
|
||||
|
||||
|
||||
const controlPanel = document.querySelector('control-panel') as ControlPanel;
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import {EventBus} from "../../core/EventBus"
|
||||
import {Cell, Game} from "../../core/game/Game";
|
||||
import {ZoomEvent, DragEvent} from "../InputHandler";
|
||||
import { colord } from "colord";
|
||||
import { EventBus } from "../../core/EventBus"
|
||||
import { Cell, Game, Player } from "../../core/game/Game";
|
||||
import { calculateBoundingBox, calculateBoundingBoxCenter, manhattanDist } from "../../core/Util";
|
||||
import { ZoomEvent, DragEvent } from "../InputHandler";
|
||||
import { GoToPlayerEvent } from "./layers/Leaderboard";
|
||||
import { placeName } from "./NameBoxCalculator";
|
||||
|
||||
export class TransformHandler {
|
||||
public scale: number = 1.8
|
||||
private offsetX: number = -350
|
||||
private offsetY: number = -200
|
||||
|
||||
private target: Cell
|
||||
private intervalID = null
|
||||
|
||||
constructor(private game: Game, private eventBus: EventBus, private canvas: HTMLCanvasElement) {
|
||||
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e))
|
||||
this.eventBus.on(DragEvent, (e) => this.onMove(e))
|
||||
this.eventBus.on(GoToPlayerEvent, (e) => this.onGoToPlayer(e))
|
||||
}
|
||||
|
||||
boundingRect(): DOMRect {
|
||||
@@ -53,7 +61,6 @@ export class TransformHandler {
|
||||
|
||||
screenBoundingRect(): [Cell, Cell] {
|
||||
|
||||
// Calculate the world point we want to zoom towards
|
||||
const LeftX = (- this.game.width() / 2) / this.scale + this.offsetX;
|
||||
const TopY = (- this.game.height() / 2) / this.scale + this.offsetY;
|
||||
|
||||
@@ -61,7 +68,6 @@ export class TransformHandler {
|
||||
const gameTopY = TopY + this.game.height() / 2
|
||||
|
||||
|
||||
// Calculate the world point we want to zoom towards
|
||||
const rightX = (screen.width - this.game.width() / 2) / this.scale + this.offsetX;
|
||||
const rightY = (screen.height - this.game.height() / 2) / this.scale + this.offsetY;
|
||||
|
||||
@@ -71,7 +77,53 @@ export class TransformHandler {
|
||||
return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))]
|
||||
}
|
||||
|
||||
screenCenter(): { screenX: number, screenY: number } {
|
||||
const [upperLeft, bottomRight] = this.screenBoundingRect()
|
||||
return {
|
||||
screenX: upperLeft.x + Math.floor((bottomRight.x - upperLeft.x) / 2),
|
||||
screenY: upperLeft.y + Math.floor((bottomRight.y - upperLeft.y) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
onGoToPlayer(event: GoToPlayerEvent) {
|
||||
let unused = null;
|
||||
this.clearTarget();
|
||||
[this.target, unused] = placeName(this.game, event.player);
|
||||
this.intervalID = setInterval(() => this.goTo(), 1)
|
||||
}
|
||||
|
||||
private goTo() {
|
||||
const { screenX, screenY } = this.screenCenter()
|
||||
const screenMapCenter = new Cell(screenX, screenY)
|
||||
|
||||
if (manhattanDist(screenMapCenter, this.target) < 2) {
|
||||
this.clearTarget()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const dX = Math.abs(screenMapCenter.x - this.target.x)
|
||||
if (dX > 2) {
|
||||
const offsetDx = Math.max(1, Math.floor(dX / 25))
|
||||
if (screenMapCenter.x > this.target.x) {
|
||||
this.offsetX -= offsetDx
|
||||
} else {
|
||||
this.offsetX += offsetDx
|
||||
}
|
||||
}
|
||||
const dY = Math.abs(screenMapCenter.y - this.target.y)
|
||||
if (dY > 2) {
|
||||
const offsetDy = Math.max(1, Math.floor(dY / 25))
|
||||
if (screenMapCenter.y > this.target.y) {
|
||||
this.offsetY -= offsetDy
|
||||
} else {
|
||||
this.offsetY += offsetDy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onZoom(event: ZoomEvent) {
|
||||
this.clearTarget()
|
||||
const oldScale = this.scale;
|
||||
const zoomFactor = 1 + event.delta / 600;
|
||||
this.scale /= zoomFactor;
|
||||
@@ -93,7 +145,16 @@ export class TransformHandler {
|
||||
}
|
||||
|
||||
onMove(event: DragEvent) {
|
||||
this.clearTarget()
|
||||
this.offsetX -= event.deltaX / this.scale;
|
||||
this.offsetY -= event.deltaY / this.scale;
|
||||
}
|
||||
|
||||
private clearTarget() {
|
||||
if (this.intervalID != null) {
|
||||
clearInterval(this.intervalID)
|
||||
this.intervalID = null
|
||||
}
|
||||
this.target = null
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
TargetPlayerEvent
|
||||
TargetPlayerEvent,
|
||||
UnitEvent
|
||||
} from "../../../core/game/Game";
|
||||
import { ClientID } from "../../../core/Schemas";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -172,15 +173,15 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
|
||||
tick() {
|
||||
let remainingEvents = this.events.filter(event => {
|
||||
const shouldKeep = this.game.ticks() - event.createdAt < 50;
|
||||
const shouldKeep = this.game.ticks() - event.createdAt < 80;
|
||||
if (!shouldKeep && event.onDelete) {
|
||||
event.onDelete();
|
||||
}
|
||||
return shouldKeep;
|
||||
});
|
||||
|
||||
if (remainingEvents.length > 5) {
|
||||
remainingEvents = remainingEvents.slice(-5);
|
||||
if (remainingEvents.length > 10) {
|
||||
remainingEvents = remainingEvents.slice(-10);
|
||||
}
|
||||
|
||||
if (this.events.length !== remainingEvents.length) {
|
||||
|
||||
@@ -4,12 +4,18 @@ import { Layer } from './Layer';
|
||||
import { Game, Player } from '../../../core/game/Game';
|
||||
import { ClientID } from '../../../core/Schemas';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { EventBus, GameEvent } from '../../../core/EventBus';
|
||||
|
||||
interface Entry {
|
||||
name: string
|
||||
position: number
|
||||
score: string
|
||||
isMyPlayer: boolean
|
||||
player: Player
|
||||
}
|
||||
|
||||
export class GoToPlayerEvent implements GameEvent {
|
||||
constructor(public player: Player) { }
|
||||
}
|
||||
|
||||
@customElement('leader-board')
|
||||
@@ -17,6 +23,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
|
||||
private game: Game
|
||||
public clientID: ClientID
|
||||
public eventBus: EventBus
|
||||
|
||||
init(game: Game) {
|
||||
this.game = game
|
||||
@@ -51,7 +58,8 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
name: player.displayName(),
|
||||
position: index + 1,
|
||||
score: formatPercentage(player.numTilesOwned() / this.game.numLandTiles()),
|
||||
isMyPlayer: player == myPlayer
|
||||
isMyPlayer: player == myPlayer,
|
||||
player: player
|
||||
}));
|
||||
|
||||
if (myPlayer != null && this.players.find(p => p.isMyPlayer) == null) {
|
||||
@@ -69,13 +77,17 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
position: place,
|
||||
score: formatPercentage(myPlayer.numTilesOwned() / this.game.numLandTiles()),
|
||||
isMyPlayer: true,
|
||||
player: myPlayer
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.requestUpdate()
|
||||
}
|
||||
|
||||
private handleRowClick(player: Player) {
|
||||
this.eventBus.emit(new GoToPlayerEvent(player))
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
}
|
||||
shouldTransform(): boolean {
|
||||
@@ -87,15 +99,15 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
display: block;
|
||||
}
|
||||
img.emoji {
|
||||
height: 1em; // Match text height
|
||||
width: auto; // Maintain aspect ratio
|
||||
height: 1em;
|
||||
width: auto;
|
||||
}
|
||||
.leaderboard {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 9999;
|
||||
background-color: rgba(30, 30, 30, 0.7); /* Added transparency */
|
||||
background-color: rgba(30, 30, 30, 0.7);
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 10px;
|
||||
@@ -103,7 +115,7 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
width: 300px;
|
||||
backdrop-filter: blur(5px); /* Optional: adds a blur effect to content behind the leaderboard */
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
@@ -112,11 +124,11 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(51, 51, 51, 0.2); /* Made border slightly transparent */
|
||||
border-bottom: 1px solid rgba(51, 51, 51, 0.2);
|
||||
color: white;
|
||||
}
|
||||
th {
|
||||
background-color: rgba(44, 44, 44, 0.5); /* Made header slightly transparent */
|
||||
background-color: rgba(44, 44, 44, 0.5);
|
||||
color: white;
|
||||
}
|
||||
.myPlayer {
|
||||
@@ -127,10 +139,14 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: rgba(44, 44, 44, 0.5); /* Made alternating rows slightly transparent */
|
||||
background-color: rgba(44, 44, 44, 0.5);
|
||||
}
|
||||
tr:hover {
|
||||
background-color: rgba(58, 58, 58, 0.6); /* Made hover effect slightly transparent */
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background-color: rgba(78, 78, 78, 0.8);
|
||||
}
|
||||
.hidden {
|
||||
display: none !important;
|
||||
@@ -160,8 +176,11 @@ export class Leaderboard extends LitElement implements Layer {
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.players
|
||||
.map((player, index) => html`
|
||||
<tr class="${player.isMyPlayer ? 'myPlayer' : 'otherPlayer'}">
|
||||
.map((player) => html`
|
||||
<tr
|
||||
class="${player.isMyPlayer ? 'myPlayer' : 'otherPlayer'}"
|
||||
@click=${() => this.handleRowClick(player.player)}
|
||||
>
|
||||
<td>${player.position}</td>
|
||||
<td>${unsafeHTML(player.name)}</td>
|
||||
<td>${player.score}</td>
|
||||
|
||||
@@ -87,15 +87,21 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
}
|
||||
}
|
||||
const tickRefreshRate = Math.floor(this.refreshRate / 100) // 10 ticks
|
||||
for (const render of this.renders) {
|
||||
const shouldRecalc = render.boundingBox == null || this.game.ticks() - render.player.lastTileChange() < tickRefreshRate
|
||||
const now = Date.now()
|
||||
if (now - render.lastBoundingCalculated > this.refreshRate) {
|
||||
render.boundingBox = calculateBoundingBox(render.player.borderTiles());
|
||||
render.lastBoundingCalculated = now
|
||||
if (shouldRecalc) {
|
||||
render.boundingBox = calculateBoundingBox(render.player.borderTiles());
|
||||
}
|
||||
}
|
||||
if (render.isVisible && now - render.lastRenderCalc > this.refreshRate) {
|
||||
this.calculateRenderInfo(render)
|
||||
render.lastRenderCalc = now + this.rand.nextInt(-50, 50)
|
||||
render.lastRenderCalc = Date.now() + this.rand.nextInt(0, 100)
|
||||
if (shouldRecalc) {
|
||||
this.calculateRenderInfo(render)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +138,6 @@ export class NameLayer implements Layer {
|
||||
render.fontSize = 0
|
||||
return
|
||||
}
|
||||
render.lastRenderCalc = Date.now() + this.rand.nextInt(0, 100)
|
||||
const [cell, size] = placeName(this.game, render.player)
|
||||
render.location = cell
|
||||
render.fontSize = Math.max(1, Math.floor(size))
|
||||
@@ -188,17 +193,6 @@ export class NameLayer implements Layer {
|
||||
);
|
||||
}
|
||||
|
||||
if (myPlayer != null) {
|
||||
const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer)
|
||||
if (emojis.length > 0) {
|
||||
context.font = `${render.fontSize * 4}px ${this.theme.font()}`;
|
||||
context.fillStyle = this.theme.playerInfoColor(render.player.id()).toHex();
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
|
||||
context.fillText(emojis[0].emoji, nameCenterX, nameCenterY + render.fontSize / 2);
|
||||
}
|
||||
}
|
||||
|
||||
context.textRendering = "optimizeSpeed";
|
||||
|
||||
@@ -211,6 +205,19 @@ export class NameLayer implements Layer {
|
||||
context.font = `bold ${render.fontSize}px ${this.theme.font()}`;
|
||||
|
||||
context.fillText(renderTroops(render.player.troops()), nameCenterX, nameCenterY + render.fontSize);
|
||||
|
||||
|
||||
if (myPlayer != null) {
|
||||
const emojis = render.player.outgoingEmojis().filter(e => e.recipient == AllPlayers || e.recipient == myPlayer)
|
||||
if (emojis.length > 0) {
|
||||
context.font = `${render.fontSize * 4}px ${this.theme.font()}`;
|
||||
context.fillStyle = this.theme.playerInfoColor(render.player.id()).toHex();
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
|
||||
context.fillText(emojis[0].emoji, nameCenterX, nameCenterY + render.fontSize / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPlayer(): Player | null {
|
||||
|
||||
@@ -242,5 +242,6 @@ export const GameRecordSchema = z.object({
|
||||
durationSeconds: z.number(),
|
||||
date: z.string(),
|
||||
usernames: z.array(z.string()),
|
||||
num_turns: z.number(),
|
||||
turns: z.array(TurnSchema)
|
||||
})
|
||||
@@ -146,6 +146,14 @@ export function calculateBoundingBox(borderTiles: ReadonlySet<Tile>): { min: Cel
|
||||
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) }
|
||||
}
|
||||
|
||||
export function calculateBoundingBoxCenter(borderTiles: ReadonlySet<Tile>): Cell {
|
||||
const { min, max } = calculateBoundingBox(borderTiles)
|
||||
return new Cell(
|
||||
min.x + Math.floor((max.x - min.x) / 2),
|
||||
min.y + Math.floor((max.y - min.y) / 2)
|
||||
)
|
||||
}
|
||||
|
||||
export function inscribed(outer: { min: Cell; max: Cell }, inner: { min: Cell; max: Cell }): boolean {
|
||||
return (
|
||||
outer.min.x <= inner.min.x &&
|
||||
@@ -246,6 +254,7 @@ export function CreateGameRecord(id: GameID, gameConfig: GameConfig, turns: Turn
|
||||
}
|
||||
record.usernames = Array.from(usernames)
|
||||
record.durationSeconds = Math.floor((record.endTimestampMS - record.startTimestampMS) / 1000)
|
||||
record.num_turns = turns.length
|
||||
return record;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GameType, Gold, Player, PlayerID, PlayerInfo, TerraNullius, Tick, Tile, Unit, UnitInfo, UnitType } from "../game/Game";
|
||||
import { Difficulty, GameType, Gold, Player, PlayerID, PlayerInfo, TerraNullius, Tick, Tile, Unit, UnitInfo, UnitType } from "../game/Game";
|
||||
import { Colord, colord } from "colord";
|
||||
import { devConfig } from "./DevConfig";
|
||||
import { defaultConfig } from "./DefaultConfig";
|
||||
@@ -64,7 +64,7 @@ export interface Config {
|
||||
defensePostRange(): number
|
||||
defensePostDefenseBonus(): number
|
||||
falloutDefenseModifier(): number
|
||||
maxUnitCost(): number
|
||||
difficultyModifier(difficulty: Difficulty): number
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GameType, Gold, Player, PlayerInfo, PlayerType, TerrainType, TerraNullius, Tick, Tile, Unit, UnitInfo, UnitType } from "../game/Game";
|
||||
import { Difficulty, GameType, Gold, Player, PlayerInfo, PlayerType, TerrainType, TerraNullius, Tick, Tile, Unit, UnitInfo, UnitType } from "../game/Game";
|
||||
import { GameID } from "../Schemas";
|
||||
import { assertNever, distSort, manhattanDist, simpleHash, within } from "../Util";
|
||||
import { Config, Theme } from "./Config";
|
||||
@@ -7,11 +7,20 @@ import { pastelTheme } from "./PastelTheme";
|
||||
|
||||
|
||||
export class DefaultConfig implements Config {
|
||||
|
||||
maxUnitCost(): number {
|
||||
return 99_999_999
|
||||
difficultyModifier(difficulty: Difficulty): number {
|
||||
switch (difficulty) {
|
||||
case Difficulty.Easy:
|
||||
return 1
|
||||
case Difficulty.Medium:
|
||||
return 3
|
||||
case Difficulty.Hard:
|
||||
return 9
|
||||
case Difficulty.Impossible:
|
||||
return 18
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
cityPopulationIncrease(): number {
|
||||
return 250_000
|
||||
}
|
||||
@@ -38,71 +47,76 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
const fn = () => {
|
||||
switch (type) {
|
||||
case UnitType.TransportShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Destroyer:
|
||||
return {
|
||||
cost: (p: Player) => (p.units(UnitType.Destroyer).length + 1) * 250_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Battleship:
|
||||
return {
|
||||
cost: (p: Player) => (p.units(UnitType.Battleship).length + 1) * 500_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Shell:
|
||||
return {
|
||||
cost: (p: Player) => 0,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Port:
|
||||
return {
|
||||
cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 250_000,
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: () => 1_000_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
cost: () => 5_000_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.TradeShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
cost: () => 1_000_000,
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
cost: (p: Player) => Math.pow(2, p.units(UnitType.DefensePost).length) * 100_000,
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.City:
|
||||
return {
|
||||
cost: (p: Player) => Math.pow(2, p.units(UnitType.City).length) * 250_000,
|
||||
territoryBound: true
|
||||
}
|
||||
default:
|
||||
assertNever(type)
|
||||
}
|
||||
switch (type) {
|
||||
case UnitType.TransportShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Destroyer:
|
||||
return {
|
||||
cost: (p: Player) => (p.units(UnitType.Destroyer).length + 1) * 250_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Battleship:
|
||||
return {
|
||||
cost: (p: Player) => (p.units(UnitType.Battleship).length + 1) * 500_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Shell:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.Port:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.Port).length) * 250_000
|
||||
),
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
cost: () => 1_000_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
cost: () => 5_000_000,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.TradeShip:
|
||||
return {
|
||||
cost: () => 0,
|
||||
territoryBound: false
|
||||
}
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
cost: () => 1_000_000,
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
cost: (p: Player) =>
|
||||
Math.min(
|
||||
500_000,
|
||||
(p.units(UnitType.DefensePost).length + 1) * 100_000
|
||||
),
|
||||
territoryBound: true
|
||||
}
|
||||
case UnitType.City:
|
||||
return {
|
||||
cost: (p: Player) => Math.min(
|
||||
1_000_000,
|
||||
Math.pow(2, p.units(UnitType.City).length) * 125_000,
|
||||
),
|
||||
territoryBound: true
|
||||
}
|
||||
default:
|
||||
assertNever(type)
|
||||
}
|
||||
const ui = fn()
|
||||
const oldCost = ui.cost
|
||||
ui.cost = (p: Player) => Math.min(this.maxUnitCost(), oldCost(p))
|
||||
return ui
|
||||
}
|
||||
defaultDonationAmount(sender: Player): number {
|
||||
return Math.floor(sender.troops() / 3)
|
||||
|
||||
@@ -5,7 +5,7 @@ export const devConfig = new class extends DefaultConfig {
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
const info = super.unitInfo(type)
|
||||
const oldCost = info.cost
|
||||
info.cost = (p: Player) => oldCost(p) / 1000
|
||||
info.cost = (p: Player) => oldCost(p) / 1000000
|
||||
return info
|
||||
}
|
||||
maxUnitCost(): number {
|
||||
@@ -31,9 +31,9 @@ export const devConfig = new class extends DefaultConfig {
|
||||
tradeShipSpawnRate(): number {
|
||||
return 10
|
||||
}
|
||||
// boatMaxDistance(): number {
|
||||
// return 5000
|
||||
// }
|
||||
boatMaxDistance(): number {
|
||||
return 5000
|
||||
}
|
||||
|
||||
// numBots(): number {
|
||||
// return 0
|
||||
@@ -42,7 +42,4 @@ export const devConfig = new class extends DefaultConfig {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// boatMaxDistance(): number {
|
||||
// return 2000
|
||||
// }
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { PriorityQueue } from "@datastructures-js/priority-queue";
|
||||
import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, TerrainType, TerraNullius, Tile } from "../game/Game";
|
||||
import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, PlayerType, TerrainType, TerraNullius, Tile } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { manhattanDist } from "../Util";
|
||||
import { MessageType } from "../../client/graphics/layers/EventsDisplay";
|
||||
import { renderNumber } from "../../client/graphics/Utils";
|
||||
|
||||
export class AttackExecution implements Execution {
|
||||
private breakAlliance = false
|
||||
@@ -85,6 +87,9 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this._owner.type() != PlayerType.Bot && this.target.isPlayer() && this.target.type() == PlayerType.Human) {
|
||||
mg.displayMessage(`You are being attacked by ${this._owner.displayName()}`, MessageType.ERROR, this._targetID)
|
||||
}
|
||||
if (this.sourceCell != null) {
|
||||
this.addNeighbors(mg.tile(this.sourceCell))
|
||||
} else {
|
||||
@@ -157,7 +162,7 @@ export class AttackExecution implements Execution {
|
||||
this.target.removeTroops(defenderTroopLoss)
|
||||
}
|
||||
this._owner.conquer(tileToConquer)
|
||||
this.checkDefenderDead()
|
||||
this.handleDeadDefender()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +203,13 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private checkDefenderDead() {
|
||||
private handleDeadDefender() {
|
||||
if (this.target.isPlayer() && this.target.numTilesOwned() < 100) {
|
||||
const gold = this.target.gold()
|
||||
this.mg.displayMessage(`Conquered ${this.target.displayName()} received ${renderNumber(gold)} gold`, MessageType.SUCCESS, this._owner.id())
|
||||
this.target.removeGold(gold)
|
||||
this._owner.addGold(gold)
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (const tile of this.target.tiles()) {
|
||||
if (tile.borders(this._owner)) {
|
||||
|
||||
@@ -117,6 +117,7 @@ export class Executor {
|
||||
const execs = []
|
||||
for (const nation of this.gs.nations()) {
|
||||
execs.push(new FakeHumanExecution(
|
||||
this.gameID,
|
||||
this.workerClient,
|
||||
new PlayerInfo(
|
||||
nation.name,
|
||||
@@ -125,7 +126,7 @@ export class Executor {
|
||||
this.random.nextID()
|
||||
),
|
||||
nation.cell,
|
||||
nation.strength * this.gs.gameConfig().difficulty
|
||||
nation.strength * this.gs.config().difficultyModifier(this.gs.gameConfig().difficulty)
|
||||
))
|
||||
}
|
||||
return execs
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ParallelAStar, WorkerClient } from "../worker/WorkerClient";
|
||||
import { PathFinder } from "../pathfinding/PathFinding";
|
||||
import { DestroyerExecution } from "./DestroyerExecution";
|
||||
import { BattleshipExecution } from "./BattleshipExecution";
|
||||
import { GameID } from "../Schemas";
|
||||
|
||||
export class FakeHumanExecution implements Execution {
|
||||
|
||||
@@ -25,8 +26,8 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
private relations = new Map<Player, number>()
|
||||
|
||||
constructor(private worker: WorkerClient, private playerInfo: PlayerInfo, private cell: Cell, private strength: number) {
|
||||
this.random = new PseudoRandom(simpleHash(playerInfo.id))
|
||||
constructor(gameID: GameID, private worker: WorkerClient, private playerInfo: PlayerInfo, private cell: Cell, private strength: number) {
|
||||
this.random = new PseudoRandom(simpleHash(playerInfo.id) + simpleHash(gameID))
|
||||
}
|
||||
|
||||
init(mg: MutableGame, ticks: number) {
|
||||
|
||||
@@ -28,12 +28,6 @@ export class MissileSiloExecution implements Execution {
|
||||
}
|
||||
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, tile)
|
||||
}
|
||||
|
||||
if (!this.silo.tile().hasOwner()) {
|
||||
this.silo.delete()
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
owner(): MutablePlayer {
|
||||
|
||||
@@ -74,23 +74,29 @@ export class NukeExecution implements Execution {
|
||||
const ratio = Object.fromEntries(
|
||||
this.mg.players().map(p => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()])
|
||||
)
|
||||
const others = new Set<MutablePlayer>()
|
||||
const attacked = new Map<MutablePlayer, number>()
|
||||
for (const tile of toDestroy) {
|
||||
const owner = tile.owner()
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id())
|
||||
mp.relinquish(tile)
|
||||
mp.removeTroops(2 * ratio[mp.id()])
|
||||
others.add(mp)
|
||||
if (!attacked.has(mp)) {
|
||||
attacked.set(mp, 0)
|
||||
}
|
||||
const prev = attacked.get(mp)
|
||||
attacked.set(mp, prev + 1)
|
||||
}
|
||||
if (tile.isLand()) {
|
||||
this.mg.addFallout(tile)
|
||||
}
|
||||
}
|
||||
for (const other of others) {
|
||||
const alliance = this.player.allianceWith(other)
|
||||
if (alliance != null) {
|
||||
this.player.breakAlliance(alliance)
|
||||
for (const [other, tilesDestroyed] of attacked) {
|
||||
if (tilesDestroyed > 100) {
|
||||
const alliance = this.player.allianceWith(other)
|
||||
if (alliance != null) {
|
||||
this.player.breakAlliance(alliance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,11 +66,13 @@ export class PlayerExecution implements Execution {
|
||||
|
||||
if (ticks - this.lastCalc > this.ticksPerClusterCalc) {
|
||||
this.lastCalc = ticks
|
||||
const start = performance.now()
|
||||
this.removeClusters()
|
||||
const end = performance.now()
|
||||
if (end - start > 1000) {
|
||||
console.log(`player ${this.player.name()}, took ${end - start}ms`)
|
||||
if (ticks - this.player.lastTileChange() < this.ticksPerClusterCalc) {
|
||||
const start = performance.now()
|
||||
this.removeClusters()
|
||||
const end = performance.now()
|
||||
if (end - start > 1000) {
|
||||
console.log(`player ${this.player.name()}, took ${end - start}ms`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ export class ShellExecution implements Execution {
|
||||
if (this.shell == null) {
|
||||
this.shell = this._owner.buildUnit(UnitType.Shell, 0, this.spawn)
|
||||
}
|
||||
if (!this.shell.isActive()) {
|
||||
this.active = false
|
||||
return
|
||||
}
|
||||
if (!this.target.isActive()) {
|
||||
this.shell.delete()
|
||||
this.active = false
|
||||
|
||||
@@ -72,7 +72,7 @@ export class TradeShipExecution implements Execution {
|
||||
const gold = this.mg.config().tradeShipGold(this.srcPort, dstPort)
|
||||
this.tradeShip.owner().addGold(gold)
|
||||
this.mg.displayMessage(
|
||||
`Your trade ship captured from ${this.origOwner.displayName()}, giving you ${renderNumber(gold)} gold`,
|
||||
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
|
||||
MessageType.SUCCESS,
|
||||
this.tradeShip.owner().id()
|
||||
)
|
||||
@@ -99,8 +99,8 @@ export class TradeShipExecution implements Execution {
|
||||
const gold = this.mg.config().tradeShipGold(this.srcPort, this.dstPort)
|
||||
this.srcPort.owner().addGold(gold)
|
||||
this.dstPort.owner().addGold(gold)
|
||||
this.mg.displayMessage(`Trade ship from ${this.tradeShip.owner().displayName()} has reached your port, giving you ${renderNumber(gold)} gold`, MessageType.SUCCESS, this.dstPort.owner().id())
|
||||
this.mg.displayMessage(`Your trade ship reached ${this.dstPort.owner().displayName()}, giving you ${renderNumber(gold)} gold`, MessageType.SUCCESS, this._owner)
|
||||
this.mg.displayMessage(`Received ${renderNumber(gold)} gold from trade with ${this.tradeShip.owner().displayName()}`, MessageType.SUCCESS, this.dstPort.owner().id())
|
||||
this.mg.displayMessage(`Received ${renderNumber(gold)} gold from trade with ${this.tradeShip.owner().displayName()}`, MessageType.SUCCESS, this._owner)
|
||||
this.tradeShip.delete()
|
||||
return
|
||||
}
|
||||
|
||||
+13
-12
@@ -11,24 +11,24 @@ export type Gold = number
|
||||
export const AllPlayers = "AllPlayers" as const;
|
||||
|
||||
export enum Difficulty {
|
||||
Easy = 1,
|
||||
Medium = 3,
|
||||
Hard = 6,
|
||||
Impossible = 12,
|
||||
Easy = "Easy",
|
||||
Medium = "Medium",
|
||||
Hard = "Hard",
|
||||
Impossible = "Impossible",
|
||||
}
|
||||
|
||||
export enum GameMap {
|
||||
World,
|
||||
Europe,
|
||||
Mena,
|
||||
NorthAmerica,
|
||||
Oceania
|
||||
World = "World",
|
||||
Europe = "Europe",
|
||||
Mena = "Mena",
|
||||
NorthAmerica = "North America",
|
||||
Oceania = "Oceania"
|
||||
}
|
||||
|
||||
export enum GameType {
|
||||
Singleplayer,
|
||||
Public,
|
||||
Private,
|
||||
Singleplayer = "Singleplayer",
|
||||
Public = "Public",
|
||||
Private = "Private",
|
||||
}
|
||||
|
||||
export interface UnitInfo {
|
||||
@@ -253,6 +253,7 @@ export interface Player {
|
||||
|
||||
// If can build returns the spawn tile, false otherwise
|
||||
canBuild(type: UnitType, targetTile: Tile): Tile | false
|
||||
lastTileChange(): Tick
|
||||
}
|
||||
|
||||
export interface MutablePlayer extends Player {
|
||||
|
||||
@@ -329,12 +329,14 @@ export class GameImpl implements MutableGame {
|
||||
const tileImpl = tile as TileImpl
|
||||
let previousOwner = tileImpl._owner
|
||||
if (previousOwner.isPlayer()) {
|
||||
previousOwner._lastTileChange = this._ticks
|
||||
previousOwner._tiles.delete(tile.cell().toString())
|
||||
previousOwner._borderTiles.delete(tile)
|
||||
tileImpl._isBorder = false
|
||||
}
|
||||
tileImpl._owner = owner
|
||||
owner._tiles.set(tile.cell().toString(), tile)
|
||||
owner._lastTileChange = this._ticks
|
||||
this.updateBorders(tile)
|
||||
tileImpl._hasFallout = false
|
||||
this.eventBus.emit(new TileEvent(tile))
|
||||
@@ -350,6 +352,7 @@ export class GameImpl implements MutableGame {
|
||||
|
||||
const tileImpl = tile as TileImpl
|
||||
let previousOwner = tileImpl._owner as PlayerImpl
|
||||
previousOwner._lastTileChange = this._ticks
|
||||
previousOwner._tiles.delete(tile.cell().toString())
|
||||
previousOwner._borderTiles.delete(tile)
|
||||
tileImpl._isBorder = false
|
||||
|
||||
@@ -4,7 +4,6 @@ import { assertNever, bfs, closestOceanShoreFromPlayer, dist, distSortUnit, manh
|
||||
import { CellString, GameImpl } from "./GameImpl";
|
||||
import { UnitImpl } from "./UnitImpl";
|
||||
import { TileImpl } from "./TileImpl";
|
||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||
import { MessageType } from "../../client/graphics/layers/EventsDisplay";
|
||||
import { renderTroops } from "../../client/graphics/Utils";
|
||||
|
||||
@@ -19,6 +18,7 @@ class Donation {
|
||||
|
||||
export class PlayerImpl implements MutablePlayer {
|
||||
|
||||
public _lastTileChange: number = 0
|
||||
|
||||
private _gold: Gold
|
||||
private _troops: number
|
||||
@@ -435,6 +435,9 @@ export class PlayerImpl implements MutablePlayer {
|
||||
}
|
||||
return spawns[0].tile()
|
||||
}
|
||||
lastTileChange(): Tick {
|
||||
return this._lastTileChange
|
||||
}
|
||||
|
||||
hash(): number {
|
||||
return simpleHash(this.id()) * (this.population() + this.numTilesOwned()) + this._units.reduce((acc, unit) => acc + unit.hash(), 0)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MessageType } from "../../client/graphics/layers/EventsDisplay";
|
||||
import { simpleHash } from "../Util";
|
||||
import { MutableUnit, Tile, TerraNullius, UnitType, Player, UnitInfo } from "./Game";
|
||||
import { GameImpl } from "./GameImpl";
|
||||
@@ -46,14 +47,26 @@ export class UnitImpl implements MutableUnit {
|
||||
}
|
||||
|
||||
setOwner(newOwner: Player): void {
|
||||
const oldOwner = this._owner
|
||||
this._owner = newOwner as PlayerImpl
|
||||
this.g.fireUnitUpdateEvent(this, this.tile())
|
||||
this.g.displayMessage(
|
||||
`Your ${this.type()} was captured by ${newOwner.displayName()}`,
|
||||
MessageType.ERROR,
|
||||
oldOwner.id()
|
||||
)
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
if (!this.isActive()) {
|
||||
throw new Error(`cannot delete ${this} not active`)
|
||||
}
|
||||
this._owner._units = this._owner._units.filter(b => b != this);
|
||||
this._active = false;
|
||||
this.g.fireUnitUpdateEvent(this, this._tile);
|
||||
if (this.type() != UnitType.AtomBomb && this.type() != UnitType.HydrogenBomb) {
|
||||
this.g.displayMessage(`Your ${this.type()} was destroyed`, MessageType.ERROR, this.owner().id())
|
||||
}
|
||||
}
|
||||
isActive(): boolean {
|
||||
return this._active;
|
||||
|
||||
@@ -55,7 +55,6 @@ function initializeMap(data: { gameMap: GameMap }) {
|
||||
}
|
||||
|
||||
function findPath(terrainMap: TerrainMap, miniTerrainMap: TerrainMap, req: SearchRequest) {
|
||||
console.log(`terrain map height: ${terrainMap.height()}`)
|
||||
const aStar = new MiniAStar(
|
||||
terrainMap,
|
||||
miniTerrainMap,
|
||||
|
||||
+32
-7
@@ -1,17 +1,42 @@
|
||||
import { GameConfig, GameID, GameRecord, GameRecordSchema, Turn } from "../core/Schemas";
|
||||
import { Storage } from '@google-cloud/storage';
|
||||
import { BigQuery } from '@google-cloud/bigquery';
|
||||
|
||||
const storage = new Storage();
|
||||
const bigquery = new BigQuery();
|
||||
|
||||
|
||||
export async function archive(gameRecord: GameRecord) {
|
||||
try {
|
||||
console.log(`writing game ${gameRecord.id} to gcs`)
|
||||
const bucket = storage.bucket("openfront-games");
|
||||
const file = bucket.file(gameRecord.id);
|
||||
await file.save(JSON.stringify(GameRecordSchema.parse(gameRecord)), {
|
||||
contentType: 'application/json'
|
||||
});
|
||||
// Save metadata to BigQuery
|
||||
const row = {
|
||||
id: gameRecord.id,
|
||||
start: new Date(gameRecord.startTimestampMS),
|
||||
end: new Date(gameRecord.endTimestampMS),
|
||||
duration_seconds: gameRecord.durationSeconds,
|
||||
number_turns: gameRecord.num_turns,
|
||||
usernames: gameRecord.usernames,
|
||||
game_mode: gameRecord.gameConfig.gameType,
|
||||
winner: null,
|
||||
difficulty: gameRecord.gameConfig.difficulty,
|
||||
map: gameRecord.gameConfig.gameMap,
|
||||
};
|
||||
|
||||
await bigquery
|
||||
.dataset('game_archive')
|
||||
.table('game_results')
|
||||
.insert([row]);
|
||||
|
||||
console.log(`wrote game metadata to BigQuery: ${gameRecord.id}`);
|
||||
if (gameRecord.turns.length > 0) {
|
||||
console.log(`writing game ${gameRecord.id} to gcs`)
|
||||
const bucket = storage.bucket("openfront-games");
|
||||
const file = bucket.file(gameRecord.id);
|
||||
await file.save(JSON.stringify(GameRecordSchema.parse(gameRecord)), {
|
||||
contentType: 'application/json'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`error writing to gcs: ${error}`)
|
||||
console.log(`error archiving game record: ${error}`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user