Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/js/constants/Events.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const EVENTS = {
PLANET_RAID_STATUS_CHANGED: 'PLANET_RAID_STATUS_CHANGED',
REFRESH_ACTION_BAR: 'REFRESH_ACTION_BAR',
REFRESH_ACTION_BAR_IF_SELECTED: 'REFRESH_ACTION_BAR_IF_SELECTED',
REFRESH_ATTACK_TARGETS: 'REFRESH_ATTACK_TARGETS',
RENDER_ALL_STRUCTS: 'RENDER_ALL_STRUCTS',
RENDER_DEPLOYMENT_INDICATOR: 'RENDER_DEPLOYMENT_INDICATOR',
RENDER_PLAYER_PFP: 'RENDER_PLAYER_PFP',
Expand Down
11 changes: 11 additions & 0 deletions src/js/events/RefreshAttackTargetsEvent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {EVENTS} from "../constants/Events";

export class RefreshAttackTargetsEvent extends CustomEvent {
/**
* @param {string} mapId
*/
constructor(mapId) {
super(EVENTS.REFRESH_ATTACK_TARGETS);
this.mapId = mapId;
}
}
5 changes: 4 additions & 1 deletion src/js/factories/AnimationEventFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ export class AnimationEventFactory {
(
attackStructType === STRUCT_TYPES.CRUISER
&& attackStructOperatingAmbit === AMBITS.WATER
&& targetStructOperatingAmbit === AMBITS.LAND
&& (
targetStructOperatingAmbit === AMBITS.LAND
|| targetStructOperatingAmbit === AMBITS.WATER
)
&& weaponSystem === STRUCT_WEAPON_SYSTEM.PRIMARY_WEAPON
)
|| (
Expand Down
65 changes: 58 additions & 7 deletions src/js/grass_listeners/StructListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {UpdateTileStructIdEvent} from "../events/UpdateTileStructIdEvent";
import {AnimationEventFactory} from "../factories/AnimationEventFactory";
import {AnimationEvent} from "../events/AnimationEvent";
import {ANIMATION} from "../constants/AnimationConstants";
import {RefreshAttackTargetsEvent} from "../events/RefreshAttackTargetsEvent";
import {ClearAttackTargetsEvent} from "../events/ClearAttackTargetsEvent";
import {ClearMoveTargetsEvent} from "../events/ClearMoveTargetsEvent";
import {ClearDefendTargetsEvent} from "../events/ClearDefendTargetsEvent";

export class StructListener extends AbstractGrassListener {

Expand Down Expand Up @@ -118,6 +122,9 @@ export class StructListener extends AbstractGrassListener {

let removePendingBuild = false;
let renderStruct = true;
let structDestroyed = false;
/** @type {AnimationEvent|null} */
let stealthAnimationEvent = null;
const mapType = this.gameState.keyPlayers[this.targetPlayerType].planetMapType;
const mapId = this.gameState[mapType]?.mapId ?? null;

Expand Down Expand Up @@ -145,22 +152,22 @@ export class StructListener extends AbstractGrassListener {
&& (messageData.detail.status & STRUCT_STATUS_FLAGS.HIDDEN) > 0
) {
renderStruct = false;
const animationEvent = this.animationEventFactory.makeStealthActivateAnimationEvent(
stealthAnimationEvent = this.animationEventFactory.makeStealthActivateAnimationEvent(
messageData.detail.struct_id,
mapId
);
this.gameState.animationEventQueue.enqueue(animationEvent);
this.gameState.animationEventQueue.enqueue(stealthAnimationEvent);

} else if (
(messageData.detail.status_old & STRUCT_STATUS_FLAGS.HIDDEN) > 0
&& (messageData.detail.status & STRUCT_STATUS_FLAGS.HIDDEN) === 0
) {
renderStruct = false;
const animationEvent = this.animationEventFactory.makeStealthDeactivateAnimationEvent(
stealthAnimationEvent = this.animationEventFactory.makeStealthDeactivateAnimationEvent(
messageData.detail.struct_id,
mapId
);
this.gameState.animationEventQueue.enqueue(animationEvent);
this.gameState.animationEventQueue.enqueue(stealthAnimationEvent);

} else if (
(messageData.detail.status_old & STRUCT_STATUS_FLAGS.DESTROYED) === 0
Expand All @@ -173,6 +180,7 @@ export class StructListener extends AbstractGrassListener {
// instance), stalling the animation queue so the destroy animation never
// dequeues.
renderStruct = false;
structDestroyed = true;

} else {
// Any other status transition (e.g. ONLINE/OFFLINE, LOCKED, STORED, or
Expand All @@ -186,14 +194,46 @@ export class StructListener extends AbstractGrassListener {
renderStruct = false;
}

this.structManager.refreshStruct(
const refreshStructPromise = this.structManager.refreshStruct(
messageData.detail.struct_id,
mapType,
removePendingBuild,
renderStruct
).then((struct) => {
);

this.gameState.actionBarLock.clear();
if (stealthAnimationEvent && mapId) {
// Entering or leaving stealth changes which weapons can reach the struct,
// so any targeting mode still open needs its markers re-evaluated. Hold
// that until the cloak animation has played and the struct data
// confirming it has landed, otherwise the markers contradict what the
// player still sees on the map and are re-derived from a stale status.
stealthAnimationEvent.onAnimationEnd = () => {
refreshStructPromise.then(() => {
window.dispatchEvent(new RefreshAttackTargetsEvent(mapId));
});
};
}

refreshStructPromise.then((struct) => {

// Only settle the lock when one is actually held. Every player on the
// map feeds this handler, so clearing unconditionally lets an enemy's
// status change cancel a targeting mode the player is still in the
// middle of, which holds no lock until a target is picked.
if (this.gameState.actionBarLock.isLocked()) {
this.gameState.actionBarLock.clear();
} else if (structDestroyed && this.isActionSourceStruct(messageData.detail.struct_id)) {
// Nothing holds the lock during target selection, so a destroyed source
// struct would otherwise leave the player picking a target for a struct
// that no longer exists and sending an action the chain will reject.
this.gameState.actionBarLock.clear();

if (mapId) {
window.dispatchEvent(new ClearAttackTargetsEvent(mapId));
window.dispatchEvent(new ClearMoveTargetsEvent(mapId));
window.dispatchEvent(new ClearDefendTargetsEvent(mapId));
}
}

// Only kill build tasks for the player's own structs
if (
Expand All @@ -206,6 +246,17 @@ export class StructListener extends AbstractGrassListener {
});
}

/**
* @param {string} structId
* @return {boolean} whether the struct is the one driving the action the
* player currently has open on the action bar
*/
isActionSourceStruct(structId) {
const actionSourceStruct = this.gameState.actionBarLock.getActionSourceStruct();

return !!actionSourceStruct && actionSourceStruct.id === structId;
}

/**
* @param {string} subject
* @param {object} messageData
Expand Down
9 changes: 9 additions & 0 deletions src/js/util/AmbitUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@ export class AmbitUtil {
});
}

/**
* @param {string|null} ambitA
* @param {string|null} ambitB
* @return {boolean}
*/
isSame(ambitA, ambitB) {
return !!ambitA && !!ambitB && ambitA.toLowerCase() === ambitB.toLowerCase();
}

}
109 changes: 109 additions & 0 deletions src/js/util/AttackTargetUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {AmbitUtil} from "./AmbitUtil";
import {PLAYER_TYPES} from "../constants/PlayerTypes";
import {STRUCT_ACTIONS} from "../constants/StructConstants";
import {Struct} from "../models/Struct";

/**
* Single source of truth for whether a struct can be attacked by the struct the
* player is currently acting with.
*
* The map draws invalid targets and the tile selection layer accepts clicks on
* valid ones, and the two run at different times off different inputs. Sharing
* the rule keeps a tile from being clickable while it looks invalid, or the
* reverse.
*/
export class AttackTargetUtil {

/**
* @param {GameState} gameState
* @param {StructManager} structManager
*/
constructor(gameState, structManager) {
this.gameState = gameState;
this.structManager = structManager;
this.ambitUtil = new AmbitUtil();
}

/**
* A struct in stealth mode is only reachable from its own ambit, regardless of
* what the attacking weapon would otherwise be able to reach.
*
* @param {Struct} attackingStruct
* @param {Struct} targetStruct
* @return {boolean}
*/
isConcealedFrom(attackingStruct, targetStruct) {
return targetStruct.isHidden()
&& !this.ambitUtil.isSame(attackingStruct.operating_ambit, targetStruct.operating_ambit);
}

/**
* @param {Struct|null} attackingStruct
* @param {Struct|null} targetStruct
* @param {string[]} weaponAmbitsArray - Valid target ambits for the weapon (e.g. ["space", "air"])
* @return {boolean}
*/
isValidTarget(attackingStruct, targetStruct, weaponAmbitsArray) {
if (!attackingStruct || !targetStruct || !weaponAmbitsArray) {
return false;
}

if (targetStruct.owner === this.gameState.keyPlayers[PLAYER_TYPES.PLAYER].id) {
return false;
}

if (!this.ambitUtil.contains(
weaponAmbitsArray,
targetStruct.operating_ambit,
attackingStruct.operating_ambit
)) {
return false;
}

return !this.isConcealedFrom(attackingStruct, targetStruct);
}

/**
* @param {string|null} targetStructId
* @param {Struct|null} attackingStruct
* @param {string[]} weaponAmbitsArray
* @return {boolean}
*/
isValidTargetById(targetStructId, attackingStruct, weaponAmbitsArray) {
return this.isValidTarget(
attackingStruct,
this.structManager.getStructById(targetStructId),
weaponAmbitsArray
);
}

/**
* The ambits the action bar's active weapon can currently target, derived from
* the action bar rather than from the event that opened targeting mode, so a
* re-evaluation mid-selection reads the same state the click handler will.
*
* @return {string[]|null} null when the action bar is not in attack mode
*/
getActiveWeaponAmbitsArray() {
const attackingStruct = this.gameState.actionBarLock.getActionSourceStruct();

if (!attackingStruct) {
return null;
}

const structType = this.gameState.structTypes.getStructTypeById(attackingStruct.type);

if (!structType) {
return null;
}

switch (this.gameState.actionBarLock.getCurrentAction()) {
case STRUCT_ACTIONS.ATTACK_PRIMARY_WEAPON:
return structType.primary_weapon_ambits_array;
case STRUCT_ACTIONS.ATTACK_SECONDARY_WEAPON:
return structType.secondary_weapon_ambits_array;
default:
return null;
}
}
}
10 changes: 9 additions & 1 deletion src/js/view_models/components/MapStructViewerComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,19 @@ export class MapStructViewerComponent {
}

animation.onCompleteCallback = () => {
let isHidden = false;
// Rebuilding the still discards a submersible's hidden art, so the
// struct's own status decides the state to restore. Only the stealth
// transitions know better than gameState: their animation is enqueued
// alongside the refresh that confirms them, so the new flag may not
// have landed yet by the time this fires.
const struct = this.structManager.getStructById(this.structId);
let isHidden = !!struct && struct.isHidden();

if (animationName === ANIMATION.NAMES.STEALTH.ACTIVATE) {
isHidden = true;
this.setStealthActive(true);
} else if (animationName === ANIMATION.NAMES.STEALTH.DEACTIVATE) {
isHidden = false;
this.setStealthActive(false);
}

Expand Down
Loading