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.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`)