mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 01:15:21 +00:00
Merge branch 'main' into 2393-show-boat-troops-as-attacking-troops
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 655 KiB After Width: | Height: | Size: 655 KiB |
@@ -582,6 +582,7 @@
|
||||
"ignore": "Ignore",
|
||||
"unit_voluntarily_deleted": "Unit voluntarily deleted",
|
||||
"enemy_boat_destroyed": "{name} boat was destroyed"
|
||||
"betrayal_debuff_ends": "{time} seconds left until betrayal debuff ends"
|
||||
},
|
||||
"unit_info_modal": {
|
||||
"structure_info": "Structure Info",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"map": {
|
||||
"height": 1396,
|
||||
"num_land_tiles": 933860,
|
||||
"num_land_tiles": 933706,
|
||||
"width": 2000
|
||||
},
|
||||
"map16x": {
|
||||
"height": 349,
|
||||
"num_land_tiles": 55046,
|
||||
"num_land_tiles": 55009,
|
||||
"width": 500
|
||||
},
|
||||
"map4x": {
|
||||
"height": 698,
|
||||
"num_land_tiles": 228988,
|
||||
"num_land_tiles": 228914,
|
||||
"width": 1000
|
||||
},
|
||||
"name": "Britannia",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -1018,6 +1018,30 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBetrayalDebuffTimer() {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (!myPlayer || !myPlayer.isTraitor()) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const remainingTicks = myPlayer.getTraitorRemainingTicks();
|
||||
const remainingSeconds = Math.ceil(remainingTicks / 10);
|
||||
|
||||
if (remainingSeconds <= 0) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.renderButton({
|
||||
content: html`${translateText("events_display.betrayal_debuff_ends", {
|
||||
time: remainingSeconds,
|
||||
})}`,
|
||||
className: "text-left text-yellow-400",
|
||||
translate: false,
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.active || !this._isVisible) {
|
||||
return html``;
|
||||
@@ -1222,6 +1246,24 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
`
|
||||
: ""}
|
||||
|
||||
<!--- Betrayal debuff timer row -->
|
||||
${(() => {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
return (
|
||||
myPlayer &&
|
||||
myPlayer.isTraitor() &&
|
||||
myPlayer.getTraitorRemainingTicks() > 0
|
||||
);
|
||||
})()
|
||||
? html`
|
||||
<tr class="lg:px-2 lg:py-1 p-1">
|
||||
<td class="lg:px-2 lg:py-1 p-1 text-left">
|
||||
${this.renderBetrayalDebuffTimer()}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<!--- Outgoing attacks row -->
|
||||
${this.outgoingAttacks.length > 0
|
||||
? html`
|
||||
@@ -1260,7 +1302,15 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
this.incomingAttacks.length === 0 &&
|
||||
this.outgoingAttacks.length === 0 &&
|
||||
this.outgoingLandAttacks.length === 0 &&
|
||||
this.outgoingBoats.length === 0
|
||||
this.outgoingBoats.length === 0 &&
|
||||
!(() => {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
return (
|
||||
myPlayer &&
|
||||
myPlayer.isTraitor() &&
|
||||
myPlayer.getTraitorRemainingTicks() > 0
|
||||
);
|
||||
})()
|
||||
? html`
|
||||
<tr>
|
||||
<td
|
||||
|
||||
@@ -118,6 +118,21 @@ export class NameLayer implements Layer {
|
||||
this.container.style.zIndex = "2";
|
||||
document.body.appendChild(this.container);
|
||||
|
||||
// Add CSS keyframes for traitor icon flashing animation
|
||||
// Append to container instead of document.head to keep styles scoped to this component
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
@keyframes traitorFlash {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
`;
|
||||
this.container.appendChild(style);
|
||||
|
||||
this.eventBus.on(AlternateViewEvent, (e) => this.onAlternateViewChange(e));
|
||||
}
|
||||
|
||||
@@ -410,16 +425,44 @@ export class NameLayer implements Layer {
|
||||
}
|
||||
|
||||
// Traitor icon
|
||||
const existingTraitor = iconsDiv.querySelector('[data-icon="traitor"]');
|
||||
let existingTraitor = iconsDiv.querySelector('[data-icon="traitor"]');
|
||||
if (render.player.isTraitor()) {
|
||||
const remainingTicks = render.player.getTraitorRemainingTicks();
|
||||
// Use precise seconds (not rounded) for smoother transitions, rounded to 0.5s intervals
|
||||
const remainingSeconds = Math.round((remainingTicks / 10) * 2) / 2;
|
||||
|
||||
if (!existingTraitor) {
|
||||
iconsDiv.appendChild(
|
||||
this.createIconElement(
|
||||
this.traitorIconImage.src,
|
||||
iconSize,
|
||||
"traitor",
|
||||
),
|
||||
existingTraitor = this.createIconElement(
|
||||
this.traitorIconImage.src,
|
||||
iconSize,
|
||||
"traitor",
|
||||
);
|
||||
iconsDiv.appendChild(existingTraitor);
|
||||
}
|
||||
|
||||
// Apply flashing animation - smooth speed increase starting at 15s
|
||||
if (existingTraitor instanceof HTMLImageElement) {
|
||||
if (remainingSeconds <= 15) {
|
||||
// Smooth transition: starts at 1s at 15 seconds, decreases to 0.2s at 0 seconds
|
||||
// Using cubic ease-out for slower, more gradual acceleration
|
||||
const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds));
|
||||
const normalizedTime = clampedSeconds / 15; // 0 to 1 (1 = 15s remaining, 0 = 0s remaining)
|
||||
|
||||
// Cubic ease-out: slower acceleration, smoother transition
|
||||
const easedProgress = 1 - Math.pow(1 - normalizedTime, 3);
|
||||
|
||||
const maxDuration = 1.0; // Slow flash at 15 seconds
|
||||
const minDuration = 0.2; // Fast flash at 0 seconds
|
||||
const duration =
|
||||
minDuration + (maxDuration - minDuration) * easedProgress;
|
||||
const animationDuration = `${duration.toFixed(2)}s`;
|
||||
|
||||
existingTraitor.style.animation = `traitorFlash ${animationDuration} infinite`;
|
||||
existingTraitor.style.animationTimingFunction = "ease-in-out";
|
||||
} else {
|
||||
// Don't flash if more than 15 seconds remaining
|
||||
existingTraitor.style.animation = "none";
|
||||
}
|
||||
}
|
||||
} else if (existingTraitor) {
|
||||
existingTraitor.remove();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Cell,
|
||||
Difficulty,
|
||||
Execution,
|
||||
Game,
|
||||
Gold,
|
||||
@@ -46,6 +47,11 @@ export class FakeHumanExecution implements Execution {
|
||||
private readonly lastMIRVSent: [Tick, TileRef][] = [];
|
||||
private readonly embargoMalusApplied = new Set<PlayerID>();
|
||||
|
||||
// Track our transport ships we currently own
|
||||
private trackedTransportShips: Set<Unit> = new Set();
|
||||
// Track our trade ships we currently own
|
||||
private trackedTradeShips: Set<Unit> = new Set();
|
||||
|
||||
/** MIRV Strategy Constants */
|
||||
|
||||
/** Ticks until MIRV can be attempted again */
|
||||
@@ -133,6 +139,16 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
tick(ticks: number) {
|
||||
// Ship tracking
|
||||
if (
|
||||
this.player !== null &&
|
||||
this.player.isAlive() &&
|
||||
this.mg.config().gameConfig().difficulty !== Difficulty.Easy
|
||||
) {
|
||||
this.trackTransportShipsAndRetaliate();
|
||||
this.trackTradeShipsAndRetaliate();
|
||||
}
|
||||
|
||||
if (ticks % this.attackRate !== this.attackTick) {
|
||||
return;
|
||||
}
|
||||
@@ -901,6 +917,70 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
// Send out a warship if our transport ship got captured
|
||||
private trackTransportShipsAndRetaliate(): void {
|
||||
if (this.player === null) return;
|
||||
|
||||
// Add any currently owned transport ships to our tracking set
|
||||
this.player
|
||||
.units(UnitType.TransportShip)
|
||||
.forEach((u) => this.trackedTransportShips.add(u));
|
||||
|
||||
// Iterate tracked transport ships; if it got destroyed by an enemy: retaliate
|
||||
for (const ship of Array.from(this.trackedTransportShips)) {
|
||||
if (!ship.isActive()) {
|
||||
// Distinguish between arrival/retreat and enemy destruction
|
||||
if (ship.wasDestroyedByEnemy()) {
|
||||
this.maybeRetaliateWithWarship(ship.tile());
|
||||
}
|
||||
this.trackedTransportShips.delete(ship);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send out a warship if our trade ship got captured
|
||||
private trackTradeShipsAndRetaliate(): void {
|
||||
if (this.player === null) return;
|
||||
|
||||
// Add any currently owned trade ships to our tracking map
|
||||
this.player
|
||||
.units(UnitType.TradeShip)
|
||||
.forEach((u) => this.trackedTradeShips.add(u));
|
||||
|
||||
// Iterate tracked trade ships; if we no longer own it, it was captured: retaliate
|
||||
for (const ship of Array.from(this.trackedTradeShips)) {
|
||||
if (!ship.isActive()) {
|
||||
this.trackedTradeShips.delete(ship);
|
||||
continue;
|
||||
}
|
||||
if (ship.owner().id() !== this.player.id()) {
|
||||
// Ship was ours and is now owned by someone else -> captured
|
||||
this.maybeRetaliateWithWarship(ship.tile());
|
||||
this.trackedTradeShips.delete(ship);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private maybeRetaliateWithWarship(tile: TileRef): void {
|
||||
if (this.player === null) return;
|
||||
|
||||
const { difficulty } = this.mg.config().gameConfig();
|
||||
// In Easy never retaliate. In Medium retaliate with 15% chance. Hard with 50%, Impossible with 80%.
|
||||
if (
|
||||
(difficulty === Difficulty.Medium && this.random.nextInt(0, 100) < 15) ||
|
||||
(difficulty === Difficulty.Hard && this.random.nextInt(0, 100) < 50) ||
|
||||
(difficulty === Difficulty.Impossible && this.random.nextInt(0, 100) < 80)
|
||||
) {
|
||||
const canBuild = this.player.canBuild(UnitType.Warship, tile);
|
||||
if (canBuild === false) {
|
||||
return;
|
||||
}
|
||||
this.mg.addExecution(
|
||||
new ConstructionExecution(this.player, UnitType.Warship, tile),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -451,6 +451,7 @@ export interface Unit {
|
||||
toUpdate(): UnitUpdate;
|
||||
hasTrainStation(): boolean;
|
||||
setTrainStation(trainStation: boolean): void;
|
||||
wasDestroyedByEnemy(): boolean;
|
||||
|
||||
// Train
|
||||
trainType(): TrainType | undefined;
|
||||
|
||||
@@ -24,6 +24,7 @@ export class UnitImpl implements Unit {
|
||||
private _retreating: boolean = false;
|
||||
private _targetedBySAM = false;
|
||||
private _reachedTarget = false;
|
||||
private _wasDestroyedByEnemy: boolean = false;
|
||||
private _lastSetSafeFromPirates: number; // Only for trade ships
|
||||
private _constructionType: UnitType | undefined;
|
||||
private _lastOwner: PlayerImpl | null = null;
|
||||
@@ -254,6 +255,10 @@ export class UnitImpl implements Unit {
|
||||
if (!this.isActive()) {
|
||||
throw new Error(`cannot delete ${this} not active`);
|
||||
}
|
||||
|
||||
// Record whether this unit was destroyed by an enemy (vs. arrived / retreated)
|
||||
this._wasDestroyedByEnemy = destroyer !== undefined;
|
||||
|
||||
this._owner._units = this._owner._units.filter((b) => b !== this);
|
||||
this._active = false;
|
||||
this.mg.addUpdate(this.toUpdate());
|
||||
@@ -307,6 +312,10 @@ export class UnitImpl implements Unit {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
wasDestroyedByEnemy(): boolean {
|
||||
return this._wasDestroyedByEnemy;
|
||||
}
|
||||
|
||||
retreating(): boolean {
|
||||
return this._retreating;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user