diff --git a/TODO.txt b/TODO.txt index 81832030e..85e694077 100644 --- a/TODO.txt +++ b/TODO.txt @@ -245,21 +245,26 @@ * send client logs back to server DONE 12/18/2024 * render more info in info overlay DONE 12/19/2024 * create alternate view for freinds enemies DONE 12/19/2024 -* give naval units health +* give naval units health DONE 12/20/2024 +* make shells larger DONE 12/20/2024 +* make shells more frequent & less attack DONE 12/20/2024 +* bug: NPCs don't have money +* have game start every 15 mins * create more prominant discord link -* right click brings up player info menu -* create new view for enemies & personal units -* send client logs back to server DONE 12/17/2024 -* make info panel merge with X, display more info +* make attack bonus based on current attack size +* make fallout harder to capture, h-bombs little smaller +* make atom bombs a bit cheaper & smaller +* make defense post stronger & larger radius * right click brings up player info menu * seperate server config from client config +* make event box wider +* highlight player spawn * bug: player names not updating sometimes * make player editeable configs * couldn't scroll left on build menu to deploy bombs (mobile) * UI/test too big on mobile * bug: build city 25k bump doesn't match troop/worker ratio * bug: mobile: if you don't have enough money can't get rid of build menu -* highlight player spawn * show players joined username in private lobby * have bots build cities & defense posts * allow longer names and allow them to be displayed in the Rank UI not be cut diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 5cdb26699..3a08afa80 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -1,7 +1,7 @@ import { LitElement, html, css } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { Layer } from './Layer'; -import { Game, Player, Unit } from '../../../core/game/Game'; +import { Game, Player, Unit, UnitType } from '../../../core/game/Game'; import { ClientID } from '../../../core/Schemas'; import { EventBus } from '../../../core/EventBus'; import { TransformHandler } from '../TransformHandler'; @@ -55,7 +55,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { this.player = owner; this.setVisible(true); } else if (!tile.isLand()) { - const units = this.game.units() + const units = this.game.units(UnitType.Destroyer, UnitType.Battleship) .filter(u => euclideanDist(worldCoord, u.tile().cell()) < 50) .sort(distSortUnit(tile)); @@ -71,9 +71,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { } tick() { - if (this.game.ticks() % 10 == 0) { - this.requestUpdate() - } + this.requestUpdate() // Implementation for Layer interface } @@ -111,13 +109,16 @@ 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` -
-
${unit.owner().name()}
-
-
${unit.type()}
-
+
+
${unit.owner().name()}
+
+
${unit.type()}
+ ${unit.hasHealth() ? html` +
Health: ${unit.health()}
+ ` : ''}
- ` +
+ ` } render() { diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 8cc18dc9d..30a127684 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -25,6 +25,8 @@ export class UnitLayer implements Layer { private myPlayer: Player | null = null + private oldShellTile = new Map() + constructor(private game: Game, private eventBus: EventBus, private clientID: ClientID) { this.theme = game.config().theme(); @@ -143,11 +145,18 @@ export class UnitLayer implements Layer { private handleShellEvent(event: UnitEvent) { const rel = this.relationship(event.unit) + this.clearCell(event.oldTile.cell()) + if (this.oldShellTile.has(event.unit)) { + this.clearCell(this.oldShellTile.get(event.unit).cell()) + } + + this.oldShellTile.set(event.unit, event.oldTile) if (!event.unit.isActive()) { return } this.paintCell(event.unit.tile().cell(), rel, this.theme.borderColor(event.unit.owner().info()), 255) + this.paintCell(event.oldTile.cell(), rel, this.theme.borderColor(event.unit.owner().info()), 255) } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index e109a8f23..383baface 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -53,22 +53,25 @@ export abstract class DefaultConfig implements Config { case UnitType.TransportShip: return { cost: () => 0, - territoryBound: false + territoryBound: false, } case UnitType.Destroyer: return { cost: (p: Player) => (p.units(UnitType.Destroyer).length + 1) * 250_000, - territoryBound: false + territoryBound: false, + maxHealth: 1000, } case UnitType.Battleship: return { cost: (p: Player) => (p.units(UnitType.Battleship).length + 1) * 500_000, - territoryBound: false + territoryBound: false, + maxHealth: 5000 } case UnitType.Shell: return { cost: () => 0, - territoryBound: false + territoryBound: false, + damage: 250 } case UnitType.Port: return { diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 1ad7819a8..00861975e 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -16,7 +16,7 @@ export const devConfig = new class extends DefaultConfig { return 95 } numSpawnPhaseTurns(gameType: GameType): number { - return gameType == GameType.Singleplayer ? 40 : 100 + return gameType == GameType.Singleplayer ? 40 : 200 // return 100 } gameCreationRate(): number { diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts index 50cd9a5a8..d2f330c9f 100644 --- a/src/core/execution/BattleshipExecution.ts +++ b/src/core/execution/BattleshipExecution.ts @@ -1,4 +1,4 @@ -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, Tile, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, TerrainType, Tile, Unit, UnitType } from "../game/Game"; import { PathFinder } from "../pathfinding/PathFinding"; import { PathFindResultType } from "../pathfinding/AStar"; import { SerialAStar } from "../pathfinding/SerialAStar"; @@ -22,9 +22,11 @@ export class BattleshipExecution implements Execution { // TODO: put in config private searchRange = 100 - private attackRate = 20 + private attackRate = 5 private lastAttack = 0 + private alreadyTargeted = new Set() + constructor( private playerID: PlayerID, private cell: Cell, @@ -41,6 +43,11 @@ export class BattleshipExecution implements Execution { } tick(ticks: number): void { + this.alreadyTargeted.forEach(u => { + if (!u.isActive()) { + this.alreadyTargeted.delete(u) + } + }) if (this.battleship == null) { const spawn = this._owner.canBuild(UnitType.Battleship, this.patrolTile) if (spawn == false) { @@ -82,11 +89,17 @@ export class BattleshipExecution implements Execution { .filter(u => u.owner() != this.battleship.owner()) .filter(u => u != this.battleship) .filter(u => !u.owner().isAlliedWith(this.battleship.owner())) + .filter(u => !this.alreadyTargeted.has(u)) .sort(distSortUnit(this.battleship)); if (ships.length > 0) { + const toAttack = ships[0] + if (!toAttack.hasHealth()) { + // Don't send multiple shells to target if it can be one-shotted. + this.alreadyTargeted.add(toAttack) + } this.lastAttack = this.mg.ticks() - this.mg.addExecution(new ShellExecution(this.battleship.tile(), this.battleship.owner(), ships[0])) + this.mg.addExecution(new ShellExecution(this.battleship.tile(), this.battleship.owner(), this.battleship, toAttack)) } } diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 1ac1da493..73bc96453 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -58,6 +58,7 @@ export class DestroyerExecution implements Execution { if (this.target == null) { const ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship) .filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell()) < 100) + .filter(u => u.type() != UnitType.Destroyer || u.health() < this.destroyer.health()) // only attack Destroyers weaker than it. .filter(u => u.owner() != this.destroyer.owner()) .filter(u => u != this.destroyer) .filter(u => !u.owner().isAlliedWith(this.destroyer.owner())) @@ -93,14 +94,16 @@ export class DestroyerExecution implements Execution { case PathFindResultType.Completed: switch (this.target.type()) { case UnitType.TransportShip: + case UnitType.Battleship: this.target.delete() break case UnitType.TradeShip: this.owner().captureUnit(this.target) break case UnitType.Destroyer: - this.target.delete() - this.destroyer.delete() + const health = this.target.health() + this.target.modifyHealth(-this.destroyer.health()) + this.destroyer.modifyHealth(-health) break } this.target = null diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 981e4a408..606d0cc9e 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -30,6 +30,11 @@ export class PlayerExecution implements Execution { tick(ticks: number) { this.player.units().forEach(u => { + if (u.health() <= 0) { + u.delete() + return + } + u.modifyHealth(1) const tileOwner = u.tile().owner() if (u.info().territoryBound) { if (tileOwner.isPlayer()) { diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 834ef4421..cad5b2f6b 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -9,7 +9,7 @@ export class ShellExecution implements Execution { private pathFinder: PathFinder private shell: MutableUnit - constructor(private spawn: Tile, private _owner: MutablePlayer, private target: MutableUnit) { + constructor(private spawn: Tile, private _owner: MutablePlayer, private ownerUnit: Unit, private target: MutableUnit) { } @@ -25,17 +25,17 @@ export class ShellExecution implements Execution { this.active = false return } - if (!this.target.isActive()) { + if (!this.target.isActive() || !this.ownerUnit.isActive()) { this.shell.delete(false) this.active = false return } for (let i = 0; i < 3; i++) { - const result = this.pathFinder.nextTile(this.shell.tile(), this.target.tile()) + const result = this.pathFinder.nextTile(this.shell.tile(), this.target.tile(), 3) switch (result.type) { case PathFindResultType.Completed: this.active = false - this.target.delete() + this.target.modifyHealth(-this.shell.info().damage) this.shell.delete(false) return case PathFindResultType.NextTile: diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 67bfac745..a4698603d 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -35,6 +35,8 @@ export interface UnitInfo { cost: (player: Player) => Gold // Determines if its owner changes when its tile is conquered. territoryBound: boolean + maxHealth?: number, + damage?: number } export enum UnitType { @@ -193,6 +195,8 @@ export interface Unit { owner(): Player isActive(): boolean info(): UnitInfo + hasHealth(): boolean + health(): number } export interface MutableUnit extends Unit { @@ -200,6 +204,7 @@ export interface MutableUnit extends Unit { owner(): MutablePlayer setTroops(troops: number): void delete(displayerMessage?: boolean): void + modifyHealth(delta: number): void } export interface TerraNullius { diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 5d5559dc5..e402235ef 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,5 +1,5 @@ import { MessageType } from "../../client/graphics/layers/EventsDisplay"; -import { simpleHash } from "../Util"; +import { simpleHash, within } from "../Util"; import { MutableUnit, Tile, TerraNullius, UnitType, Player, UnitInfo } from "./Game"; import { GameImpl } from "./GameImpl"; import { PlayerImpl } from "./PlayerImpl"; @@ -8,6 +8,7 @@ import { TerraNulliusImpl } from "./TerraNulliusImpl"; export class UnitImpl implements MutableUnit { private _active = true; + private _health: number constructor( private _type: UnitType, @@ -15,7 +16,10 @@ export class UnitImpl implements MutableUnit { private _tile: Tile, private _troops: number, public _owner: PlayerImpl, - ) { } + ) { + // default to half health (or 1 is no health specified) + this._health = (this.g.unitInfo(_type).maxHealth ?? 2) / 2 + } type(): UnitType { return this._type @@ -35,6 +39,12 @@ export class UnitImpl implements MutableUnit { troops(): number { return this._troops; } + health(): number { + return this._health + } + hasHealth(): boolean { + return this.info().maxHealth != undefined + } tile(): Tile { return this._tile; } @@ -57,6 +67,15 @@ export class UnitImpl implements MutableUnit { ) } + modifyHealth(delta: number): void { + this._health = within( + this._health + delta, + 0, + this.info().maxHealth ?? 1 + ) + } + + delete(displayMessage: boolean = true): void { if (!this.isActive()) { throw new Error(`cannot delete ${this} not active`)