From f713254081cd3c7fa1d47b49f2875cd9585b3a10 Mon Sep 17 00:00:00 2001 From: APuddle210 Date: Fri, 18 Apr 2025 21:05:27 -0400 Subject: [PATCH 1/2] Address-River-Criticisms (#536) ## Description: Please see description on: #535 fixes #535 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: aPuddle --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> --- src/core/configuration/Config.ts | 8 ++- src/core/configuration/DefaultConfig.ts | 30 +++++++- src/core/execution/DefensePostExecution.ts | 73 ++++++++++++++++++++ src/core/execution/ShellExecution.ts | 10 ++- src/core/execution/TradeShipExecution.ts | 9 ++- src/core/execution/TransportShipExecution.ts | 5 ++ src/core/execution/WarshipExecution.ts | 40 +++++++---- src/core/game/Game.ts | 3 + src/core/game/UnitImpl.ts | 19 ++++- 9 files changed, 173 insertions(+), 24 deletions(-) diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index aaed33112..64ef95be0 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -98,7 +98,7 @@ export interface Config { maxPopulation(player: Player | PlayerView): number; cityPopulationIncrease(): number; boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number; - warshipShellLifetime(): number; + shellLifetime(): number; boatMaxNumber(): number; allianceDuration(): Tick; allianceRequestCooldown(): Tick; @@ -111,12 +111,18 @@ export interface Config { unitInfo(type: UnitType): UnitInfo; tradeShipGold(dist: number): Gold; tradeShipSpawnRate(numberOfPorts: number): number; + safeFromPiratesCooldownMax(): number; defensePostRange(): number; SAMCooldown(): number; SiloCooldown(): number; defensePostDefenseBonus(): number; falloutDefenseModifier(percentOfFallout: number): number; difficultyModifier(difficulty: Difficulty): number; + warshipPatrolRange(): number; + warshipShellAttackRate(): number; + warshipTargettingRange(): number; + defensePostShellAttackRate(): number; + defensePostTargettingRange(): number; // 0-1 traitorDefenseDebuff(): number; traitorDuration(): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 03726a83b..95487a5cd 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -396,7 +396,7 @@ export class DefaultConfig implements Config { return 80; } boatMaxNumber(): number { - return 3; + return 9; } numSpawnPhaseTurns(): number { return this._gameConfig.gameType == GameType.Singleplayer ? 100 : 300; @@ -680,4 +680,32 @@ export class DefaultConfig implements Config { structureMinDist(): number { return 18; } + + shellLifetime(): number { + return 50; + } + + warshipPatrolRange(): number { + return 100; + } + + warshipTargettingRange(): number { + return 130; + } + + warshipShellAttackRate(): number { + return 20; + } + + defensePostShellAttackRate(): number { + return 100; + } + + safeFromPiratesCooldownMax(): number { + return 20; + } + + defensePostTargettingRange(): number { + return 75; + } } diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index 0da8ef983..b3524e339 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -8,6 +8,7 @@ import { UnitType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; +import { ShellExecution } from "./ShellExecution"; export class DefensePostExecution implements Execution { private player: Player; @@ -15,6 +16,11 @@ export class DefensePostExecution implements Execution { private post: Unit; private active: boolean = true; + private target: Unit = null; + private lastShellAttack = 0; + + private alreadySentShell = new Set(); + constructor( private ownerId: PlayerID, private tile: TileRef, @@ -30,6 +36,27 @@ export class DefensePostExecution implements Execution { this.player = mg.player(this.ownerId); } + private shoot() { + const shellAttackRate = this.mg.config().defensePostShellAttackRate(); + if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) { + this.lastShellAttack = this.mg.ticks(); + this.mg.addExecution( + new ShellExecution( + this.post.tile(), + this.post.owner(), + this.post, + this.target, + ), + ); + if (!this.target.hasHealth()) { + // Don't send multiple shells to target that can be oneshotted + this.alreadySentShell.add(this.target); + this.target = null; + return; + } + } + } + tick(ticks: number): void { if (this.post == null) { const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile); @@ -48,6 +75,52 @@ export class DefensePostExecution implements Execution { if (this.player != this.post.owner()) { this.player = this.post.owner(); } + + if (this.target != null && !this.target.isActive()) { + this.target = null; + } + + const ships = this.mg + .nearbyUnits( + this.post.tile(), + this.mg.config().defensePostTargettingRange(), + [UnitType.TransportShip, UnitType.Warship], + ) + .filter( + ({ unit }) => + unit.owner() !== this.post.owner() && + !unit.owner().isFriendly(this.post.owner()) && + !this.alreadySentShell.has(unit), + ); + + this.target = + ships.sort((a, b) => { + const { unit: unitA, distSquared: distA } = a; + const { unit: unitB, distSquared: distB } = b; + + // Prioritize TransportShip + if ( + unitA.type() === UnitType.TransportShip && + unitB.type() !== UnitType.TransportShip + ) + return -1; + if ( + unitA.type() !== UnitType.TransportShip && + unitB.type() === UnitType.TransportShip + ) + return 1; + + // If both are the same type, sort by distance (lower `distSquared` means closer) + return distA - distB; + })[0]?.unit ?? null; + + if (this.target == null || !this.target.isActive()) { + this.target = null; + return; + } else { + this.shoot(); + return; + } } isActive(): boolean { diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 286d2bdc9..d44159ed8 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -42,8 +42,7 @@ export class ShellExecution implements Execution { } if (this.destroyAtTick == -1 && !this.ownerUnit.isActive()) { - this.destroyAtTick = - this.mg.ticks() + this.mg.config().warshipShellLifetime(); + this.destroyAtTick = this.mg.ticks() + this.mg.config().shellLifetime(); } for (let i = 0; i < 3; i++) { @@ -55,7 +54,7 @@ export class ShellExecution implements Execution { switch (result.type) { case PathFindResultType.Completed: this.active = false; - this.target.modifyHealth(-this.shell.info().damage); + this.target.modifyHealth(-this.effectOnTarget()); this.shell.delete(false); return; case PathFindResultType.NextTile: @@ -72,6 +71,11 @@ export class ShellExecution implements Execution { } } + private effectOnTarget(): number { + const baseDamage: number = this.mg.config().unitInfo(UnitType.Shell).damage; + return baseDamage; + } + isActive(): boolean { return this.active; } diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 99663faad..297a0c4cf 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -47,6 +47,7 @@ export class TradeShipExecution implements Execution { } this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn, { dstPort: this._dstPort, + lastSetSafeFromPirates: ticks, }); } @@ -56,11 +57,11 @@ export class TradeShipExecution implements Execution { } if (this.origOwner != this.tradeShip.owner()) { - // Store as vairable in case ship is recaptured by previous owner + // Store as variable in case ship is recaptured by previous owner this.wasCaptured = true; } - // If a player captures an other player's port while trading we should delete + // If a player captures another player's port while trading we should delete // the ship. if (this._dstPort.owner().id() == this.srcPort.owner().id()) { this.tradeShip.delete(false); @@ -107,6 +108,10 @@ export class TradeShipExecution implements Execution { this.tradeShip.move(this.tradeShip.tile()); break; case PathFindResultType.NextTile: + // Update safeFromPirates status + if (this.mg.isWater(result.tile) && this.mg.isShoreline(result.tile)) { + this.tradeShip.setSafeFromPirates(); + } this.tradeShip.move(result.tile); break; case PathFindResultType.PathNotFound: diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index d89515956..056a39a46 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -26,6 +26,7 @@ export class TransportShipExecution implements Execution { private mg: Game; private attacker: Player; private target: Player | TerraNullius; + private embarkDelay = 10; // TODO make private public path: TileRef[]; @@ -136,6 +137,10 @@ export class TransportShipExecution implements Execution { this.active = false; return; } + if (this.embarkDelay > 0) { + this.embarkDelay--; + return; + } if (ticks - this.lastMove < this.ticksPerMove) { return; } diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 632bdc2bd..c676403e5 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -26,12 +26,7 @@ export class WarshipExecution implements Execution { private patrolTile: TileRef; - // TODO: put in config - private searchRange = 100; - - private shellAttackRate = 5; private lastShellAttack = 0; - private alreadySentShell = new Set(); constructor( @@ -72,7 +67,8 @@ export class WarshipExecution implements Execution { } private shoot() { - if (this.mg.ticks() - this.lastShellAttack > this.shellAttackRate) { + const shellAttackRate = this.mg.config().warshipShellAttackRate(); + if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) { this.lastShellAttack = this.mg.ticks(); this.mg.addExecution( new ShellExecution( @@ -137,7 +133,7 @@ export class WarshipExecution implements Execution { const ships = this.mg .nearbyUnits( this.warship.tile(), - 130, // Search range + this.mg.config().warshipTargettingRange(), [UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip], ) .filter( @@ -146,9 +142,11 @@ export class WarshipExecution implements Execution { unit !== this.warship && !unit.owner().isFriendly(this.warship.owner()) && !this.alreadySentShell.has(unit) && - (unit.type() !== UnitType.TradeShip || hasPort) && (unit.type() !== UnitType.TradeShip || - unit.dstPort()?.owner() !== this._owner), + (hasPort && + unit.dstPort()?.owner() !== this.warship.owner() && + !unit.dstPort()?.owner().isFriendly(this.warship.owner()) && + unit.isSafeFromPirates() !== true)), ); this.target = @@ -198,9 +196,10 @@ export class WarshipExecution implements Execution { if ( this.target == null || !this.target.isActive() || - this.target.owner() == this._owner + this.target.owner() == this._owner || + this.target.isSafeFromPirates() == true ) { - // In case another destroyer captured or destroyed target + // In case another warship captured or destroyed target, or the target escaped into safe waters this.target = null; return; } @@ -250,18 +249,29 @@ export class WarshipExecution implements Execution { } randomTile(): TileRef { - while (true) { + let warshipPatrolRange = this.mg.config().warshipPatrolRange(); + const maxAttemptBeforeExpand: number = warshipPatrolRange * 2; + let attemptCount: number = 0; + let expandCount: number = 0; + while (expandCount < 3) { const x = this.mg.x(this.patrolCenterTile) + - this.random.nextInt(-this.searchRange / 2, this.searchRange / 2); + this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2); const y = this.mg.y(this.patrolCenterTile) + - this.random.nextInt(-this.searchRange / 2, this.searchRange / 2); + this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2); if (!this.mg.isValidCoord(x, y)) { continue; } const tile = this.mg.ref(x, y); - if (!this.mg.isOcean(tile)) { + if (!this.mg.isOcean(tile) || this.mg.isShoreline(tile)) { + attemptCount++; + if (attemptCount === maxAttemptBeforeExpand) { + expandCount++; + attemptCount = 0; + warshipPatrolRange = + warshipPatrolRange + Math.floor(warshipPatrolRange / 2); + } continue; } return tile; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 10a10af3d..1f51e30b1 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -240,6 +240,7 @@ export class PlayerInfo { // Some units have info specific to them export interface UnitSpecificInfos { dstPort?: Unit; // Only for trade ships + lastSetSafeFromPirates?: number; // Only for trade ships detonationDst?: TileRef; // Only for nukes warshipTarget?: Unit; cooldownDuration?: number; @@ -273,6 +274,8 @@ export interface Unit { isCooldown(): boolean; setDstPort(dstPort: Unit): void; dstPort(): Unit; // Only for trade ships + setSafeFromPirates(): void; // Only for trade ships + isSafeFromPirates(): boolean; // Only for trade ships detonationDst(): TileRef; // Only for nukes setMoveTarget(cell: TileRef): void; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index f0dd2796d..4ce55c37c 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -17,11 +17,11 @@ export class UnitImpl implements Unit { private _active = true; private _health: bigint; private _lastTile: TileRef = null; - // Currently only warship use it private _target: Unit = null; private _moveTarget: TileRef = null; private _targetedBySAM = false; - + private _safeFromPiratesCooldown: number; // Only for trade ships + private _lastSetSafeFromPirates: number; // Only for trade ships private _constructionType: UnitType = undefined; private _cooldownTick: Tick | null = null; @@ -45,6 +45,10 @@ export class UnitImpl implements Unit { this._detonationDst = unitsSpecificInfos.detonationDst; this._warshipTarget = unitsSpecificInfos.warshipTarget; this._cooldownDuration = unitsSpecificInfos.cooldownDuration; + this._lastSetSafeFromPirates = unitsSpecificInfos.lastSetSafeFromPirates; + this._safeFromPiratesCooldown = this.mg + .config() + .safeFromPiratesCooldownMax(); } id() { @@ -233,4 +237,15 @@ export class UnitImpl implements Unit { targetedBySAM(): boolean { return this._targetedBySAM; } + + setSafeFromPirates(): void { + this._lastSetSafeFromPirates = this.mg.ticks(); + } + + isSafeFromPirates(): boolean { + return ( + this.mg.ticks() - this._lastSetSafeFromPirates < + this._safeFromPiratesCooldown + ); + } } From a4329d42ee9c454b3cd472c8f72323318a22e913 Mon Sep 17 00:00:00 2001 From: michaelabilliot Date: Sat, 19 Apr 2025 22:09:41 +0300 Subject: [PATCH 2/2] Add category to singleplayer (#449) ## Description: Just simply adds categories to the single player menu ![Screenshot 2025-04-11 at 05 40 49](https://github.com/user-attachments/assets/e6811b69-5abf-427f-9989-55f4538b034f) Fixes #453 ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [X] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: lunhuiyan1718 --- resources/lang/en.json | 5 +++ src/client/HostLobbyModal.ts | 60 +++++++++++++++++++++++---------- src/client/SinglePlayerModal.ts | 59 +++++++++++++++++++++----------- src/core/game/Game.ts | 23 +++++++++++++ 4 files changed, 109 insertions(+), 38 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index c2b7d0425..aa090c7b0 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -123,6 +123,11 @@ "knownworld": "Known World", "faroeislands": "Faroe Islands" }, + "map_categories": { + "continental": "Continental", + "regional": "Regional", + "fantasy": "Other" + }, "private_lobby": { "title": "Join Private Lobby", "enter_id": "Enter Lobby ID", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 5dbe32865..a9d605eb3 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -4,7 +4,12 @@ import randomMap from "../../resources/images/RandomMap.webp"; import { translateText } from "../client/Utils"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { consolex } from "../core/Consolex"; -import { Difficulty, GameMapType, GameMode } from "../core/game/Game"; +import { + Difficulty, + GameMapType, + GameMode, + mapCategories, +} from "../core/game/Game"; import { GameConfig, GameInfo } from "../core/Schemas"; import { generateID } from "../core/Util"; import "./components/baseComponents/Modal"; @@ -74,23 +79,40 @@ export class HostLobbyModal extends LitElement {
${translateText("map.map")}
-
- ${Object.entries(GameMapType) - .filter(([key]) => isNaN(Number(key))) - .map( - ([key, value]) => html` -
this.handleMapSelection(value)}> - +
+ + ${Object.entries(mapCategories).map( + ([categoryKey, maps]) => html` +
+

+ ${translateText(`map_categories.${categoryKey}`)} +

+
+ ${maps.map((mapValue) => { + const mapKey = Object.keys(GameMapType).find( + (key) => GameMapType[key] === mapValue, + ); + return html` +
this.handleMapSelection(mapValue)} + > + +
+ `; + })}
- `, - )} +
+ `, + )}
-
${translateText("map.random")}
+
+ ${translateText("map.random")} +
diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index d7dd675ff..6a8438f6f 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -3,7 +3,13 @@ import { customElement, query, state } from "lit/decorators.js"; import randomMap from "../../resources/images/RandomMap.webp"; import { translateText } from "../client/Utils"; import { consolex } from "../core/Consolex"; -import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game"; +import { + Difficulty, + GameMapType, + GameMode, + GameType, + mapCategories, +} from "../core/game/Game"; import { generateID } from "../core/Util"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; @@ -39,27 +45,40 @@ export class SinglePlayerModal extends LitElement {
${translateText("map.map")}
-
- ${Object.entries(GameMapType) - .filter(([key]) => isNaN(Number(key))) - .map( - ([key, value]) => html` -
+ + ${Object.entries(mapCategories).map( + ([categoryKey, maps]) => html` +
+

- + ${translateText(`map_categories.${categoryKey}`)} +

+
+ ${maps.map((mapValue) => { + const mapKey = Object.keys(GameMapType).find( + (key) => GameMapType[key] === mapValue, + ); + return html` +
this.handleMapSelection(mapValue)} + > + +
+ `; + })}
- `, - )} +
+ `, + )}
= { + continental: [ + GameMapType.World, + GameMapType.NorthAmerica, + GameMapType.SouthAmerica, + GameMapType.Europe, + GameMapType.Asia, + GameMapType.Africa, + GameMapType.Oceania, + ], + regional: [ + GameMapType.BlackSea, + GameMapType.Britannia, + GameMapType.GatewayToTheAtlantic, + GameMapType.BetweenTwoSeas, + GameMapType.Iceland, + GameMapType.Japan, + GameMapType.Mena, + GameMapType.Australia, + ], + fantasy: [GameMapType.Pangaea, GameMapType.Mars, GameMapType.KnownWorld], +}; + export enum GameType { Singleplayer = "Singleplayer", Public = "Public",