Skip to content

Commit 2f2f280

Browse files
committed
Bump version to 1.0.564 and implement emergency hold mechanics for fighter units
1 parent e6d5701 commit 2f2f280

4 files changed

Lines changed: 195 additions & 35 deletions

File tree

assets/js/version.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "idlegames",
3-
"version": "1.0.562",
3+
"version": "1.0.564",
44
"description": "IdleGames PWA build tooling and deployment scripts.",
55
"author": "Timeless Prototype",
66
"license": "MIT",

sandfront.html

Lines changed: 191 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,8 @@ <h1 id="menuTitle">Sandfront Command</h1>
732732
const FIGHTER_GUARD_ACQUIRE_RADIUS = CELL * 5.5;
733733
const FIGHTER_TURN_RATE = Math.PI * 1.85;
734734
const FIGHTER_GLIDE_CAPTURE_RADIUS = CELL * 1.15;
735+
const FIGHTER_EMERGENCY_HOLD_RADIUS = CELL * 7;
736+
const FIGHTER_EMERGENCY_HOLD_DURATION = 15;
735737
const AI_STRUCTURE_MIN_GAP = {
736738
refinery: 2,
737739
silo: 2,
@@ -3113,7 +3115,7 @@ <h1 id="menuTitle">Sandfront Command</h1>
31133115
...snapshot.keyboardCursor,
31143116
anchor: snapshot.keyboardCursor?.anchor ? { ...snapshot.keyboardCursor.anchor } : null
31153117
};
3116-
state.settings = { ...(snapshot.settings || {}), ...state.settings };
3118+
state.settings = { ...state.settings, ...(snapshot.settings || {}) };
31173119
state.commandMode = snapshot.commandMode || 'context';
31183120
state.touchMode = ['select', 'pan', 'move'].includes(snapshot.touchMode) ? snapshot.touchMode : 'select';
31193121
state.pendingBuild = snapshot.pendingBuild || null;
@@ -3208,6 +3210,23 @@ <h1 id="menuTitle">Sandfront Command</h1>
32083210
if (transport && transport.kind === 'unit' && transport.hp > 0) syncEmbarkedUnitPosition(unit, transport);
32093211
else unit.garrisonedIn = null;
32103212
}
3213+
if (unit.type === 'fighter') {
3214+
if (unit.assignedAirfieldId && !getAssignedAirfield(unit)) unit.assignedAirfieldId = null;
3215+
if (unit.order?.type === 'landed' || unit.order?.type === 'takeoff' || unit.order?.type === 'return' || unit.order?.type === 'landing') {
3216+
const referencedAirfieldId = unit.order.airfieldId || unit.assignedAirfieldId || null;
3217+
const isValidRunway = !!getOperationalAirfieldById(referencedAirfieldId, unit.teamId);
3218+
if (!isValidRunway) {
3219+
recoverFighterAirfieldState(unit, unit.order?.nextOrder || null, false, referencedAirfieldId);
3220+
}
3221+
} else if (unit.order?.type === 'emergency-hold') {
3222+
const hub = unit.order.hubId ? getEntityById(unit.order.hubId) : null;
3223+
if (hub && hub.kind === 'building' && hub.hp > 0 && hub.type === 'hq') {
3224+
const center = getBuildingGeometry(hub).visualCenter;
3225+
unit.order.x = center.x;
3226+
unit.order.y = center.y;
3227+
}
3228+
}
3229+
}
32113230
}
32123231
recomputeDynamicBlocked();
32133232
refreshFog();
@@ -3415,14 +3434,20 @@ <h1 id="menuTitle">Sandfront Command</h1>
34153434
return airfield;
34163435
}
34173436

3437+
function getOperationalAirfieldById(airfieldId, teamId) {
3438+
const airfield = airfieldId ? getEntityById(airfieldId) : null;
3439+
if (!airfield || airfield.kind !== 'building' || airfield.hp <= 0 || airfield.type !== 'airfield' || airfield.teamId !== teamId) return null;
3440+
return airfield;
3441+
}
3442+
34183443
function clearFighterAirfieldAssignment(unit) {
34193444
if (!unit) return;
34203445
const airfield = getAssignedAirfield(unit);
34213446
if (airfield && airfield.assignedFighterId === unit.id) airfield.assignedFighterId = null;
34223447
unit.assignedAirfieldId = null;
34233448
}
34243449

3425-
function assignFighterToAirfield(unit, airfield) {
3450+
function reserveFighterAirfield(unit, airfield) {
34263451
if (!unit) return false;
34273452
const previous = getAssignedAirfield(unit);
34283453
if (previous && previous.id !== airfield?.id && previous.assignedFighterId === unit.id) previous.assignedFighterId = null;
@@ -3436,9 +3461,13 @@ <h1 id="menuTitle">Sandfront Command</h1>
34363461
return true;
34373462
}
34383463

3439-
function findAvailableAirfield(teamId, preferredX = WORLD_WIDTH * 0.5, preferredY = WORLD_HEIGHT * 0.5, preferredId = null) {
3464+
function assignFighterToAirfield(unit, airfield) {
3465+
return reserveFighterAirfield(unit, airfield);
3466+
}
3467+
3468+
function findAvailableAirfield(teamId, preferredX = WORLD_WIDTH * 0.5, preferredY = WORLD_HEIGHT * 0.5, preferredId = null, excludedId = null) {
34403469
const candidates = state.buildings
3441-
.filter((building) => building.hp > 0 && building.teamId === teamId && building.type === 'airfield' && (!building.assignedFighterId || building.id === preferredId))
3470+
.filter((building) => building.hp > 0 && building.teamId === teamId && building.type === 'airfield' && building.id !== excludedId && (!building.assignedFighterId || building.id === preferredId))
34423471
.sort((a, b) => dist(preferredX, preferredY, getBuildingGeometry(a).visualCenter.x, getBuildingGeometry(a).visualCenter.y) - dist(preferredX, preferredY, getBuildingGeometry(b).visualCenter.x, getBuildingGeometry(b).visualCenter.y));
34433472
return candidates[0] || null;
34443473
}
@@ -3485,18 +3514,71 @@ <h1 id="menuTitle">Sandfront Command</h1>
34853514
return distance <= Math.max(3, step);
34863515
}
34873516

3488-
function beginFighterReturn(unit, nextOrder = null) {
3489-
const airfield = ensureFighterAirfield(unit);
3490-
if (!airfield) {
3491-
unit.order = nextOrder || { type: 'idle' };
3492-
unit.targetId = nextOrder?.targetId || null;
3493-
unit.path.length = 0;
3517+
function getNearestEmergencyCommandHub(teamId, worldX, worldY) {
3518+
const friendlyHub = nearestBuilding(teamId, 'hq', worldX, worldY);
3519+
if (friendlyHub) return friendlyHub;
3520+
let best = null;
3521+
let bestDist = Infinity;
3522+
for (const building of state.buildings) {
3523+
if (building.hp <= 0 || building.type !== 'hq') continue;
3524+
const center = getBuildingGeometry(building).visualCenter;
3525+
const distance = dist(worldX, worldY, center.x, center.y);
3526+
if (distance < bestDist) {
3527+
best = building;
3528+
bestDist = distance;
3529+
}
3530+
}
3531+
return best;
3532+
}
3533+
3534+
function makeFighterEmergencyHoldOrder(unit, hub = null, crashAt = state.time + FIGHTER_EMERGENCY_HOLD_DURATION) {
3535+
const targetHub = hub && hub.kind === 'building' && hub.hp > 0 && hub.type === 'hq'
3536+
? hub
3537+
: getNearestEmergencyCommandHub(unit.teamId, unit.x, unit.y);
3538+
const center = targetHub ? getBuildingGeometry(targetHub).visualCenter : { x: unit.x, y: unit.y };
3539+
const initialAngle = Math.atan2(unit.y - center.y, unit.x - center.x);
3540+
return {
3541+
type: 'emergency-hold',
3542+
hubId: targetHub?.id || null,
3543+
x: center.x,
3544+
y: center.y,
3545+
radius: FIGHTER_EMERGENCY_HOLD_RADIUS,
3546+
angle: Number.isFinite(initialAngle) ? wrapAngle(initialAngle) : 0,
3547+
crashAt,
3548+
reported: true
3549+
};
3550+
}
3551+
3552+
function enterFighterEmergencyHold(unit, refreshHud = true) {
3553+
if (!isFighterOperational(unit)) return false;
3554+
if (unit.order?.type === 'emergency-hold' && (unit.order.crashAt || 0) > state.time) {
3555+
if (refreshHud) refreshSelectionHudForUnits([unit]);
34943556
return false;
34953557
}
3496-
unit.order = { type: 'return', airfieldId: airfield.id, nextOrder };
3558+
clearFighterAirfieldAssignment(unit);
3559+
unit.order = makeFighterEmergencyHoldOrder(unit);
34973560
unit.targetId = null;
34983561
unit.path.length = 0;
3499-
return true;
3562+
if (unit.teamId === TEAM_PLAYER) toast('Making a landing request.');
3563+
if (refreshHud) refreshSelectionHudForUnits([unit]);
3564+
return false;
3565+
}
3566+
3567+
function routeFighterToLandingOption(unit, nextOrder = null, refreshHud = true, excludedAirfieldId = null) {
3568+
const fallbackOrder = getSafeFighterResumeOrder(unit, nextOrder);
3569+
const airfield = findAvailableAirfield(unit.teamId, unit.x, unit.y, unit.assignedAirfieldId, excludedAirfieldId);
3570+
if (airfield && assignFighterToAirfield(unit, airfield)) {
3571+
unit.order = { type: 'return', airfieldId: airfield.id, nextOrder: fallbackOrder };
3572+
unit.targetId = null;
3573+
unit.path.length = 0;
3574+
if (refreshHud) refreshSelectionHudForUnits([unit]);
3575+
return true;
3576+
}
3577+
return enterFighterEmergencyHold(unit, refreshHud);
3578+
}
3579+
3580+
function beginFighterReturn(unit, nextOrder = null) {
3581+
return routeFighterToLandingOption(unit, nextOrder, false);
35003582
}
35013583

35023584
function issueFighterLandingOrder(unit, airfield, nextOrder = null) {
@@ -3535,9 +3617,24 @@ <h1 id="menuTitle">Sandfront Command</h1>
35353617
if (order.type === 'return') return { type: 'return', airfieldId: order.airfieldId, nextOrder: cloneFighterResumeOrder(order.nextOrder || null) };
35363618
if (order.type === 'strike') return { type: 'strike', targetId: order.targetId, lockTarget: !!order.lockTarget };
35373619
if (order.type === 'waypoint') return { type: 'waypoint', x: order.x, y: order.y, nextOrder: cloneFighterResumeOrder(order.nextOrder || null) };
3620+
if (order.type === 'emergency-hold') return { type: 'emergency-hold', hubId: order.hubId || null, x: order.x, y: order.y, radius: order.radius, angle: order.angle, crashAt: order.crashAt, reported: !!order.reported };
35383621
return { ...order };
35393622
}
35403623

3624+
function getSafeFighterResumeOrder(unit, order) {
3625+
if (!order) return null;
3626+
if (order.type === 'return') {
3627+
const airfield = getOperationalAirfieldById(order.airfieldId, unit.teamId);
3628+
if (!airfield) return getSafeFighterResumeOrder(unit, order.nextOrder || null);
3629+
}
3630+
return cloneFighterResumeOrder(order);
3631+
}
3632+
3633+
function recoverFighterAirfieldState(unit, preferredOrder = null, refreshHud = true, excludedAirfieldId = null) {
3634+
if (!isFighterOperational(unit)) return false;
3635+
return routeFighterToLandingOption(unit, preferredOrder, refreshHud, excludedAirfieldId);
3636+
}
3637+
35413638
function makeFighterStrikeOrder(targetId, lockTarget = false) {
35423639
return { type: 'strike', targetId, lockTarget };
35433640
}
@@ -3574,14 +3671,17 @@ <h1 id="menuTitle">Sandfront Command</h1>
35743671
}
35753672

35763673
function queueFighterMission(unit, nextOrder) {
3577-
const airfield = ensureFighterAirfield(unit);
3674+
const airfield = unit.order?.type === 'landed'
3675+
? (unit.order.airfieldId ? getOperationalAirfieldById(unit.order.airfieldId, unit.teamId) : getAssignedAirfield(unit))
3676+
: ensureFighterAirfield(unit);
35783677
const ready = unit.hp >= unit.maxHp && unit.missiles >= (UNIT_TYPES.fighter.maxMissiles || FIGHTER_MISSILES);
35793678
if (unit.order?.type === 'takeoff' || unit.order?.type === 'return' || unit.order?.type === 'landing') {
35803679
unit.order.nextOrder = nextOrder;
35813680
return;
35823681
}
35833682
if (unit.order?.type === 'landed') {
3584-
unit.order = ready && airfield
3683+
const canDepart = !!airfield && ready;
3684+
unit.order = canDepart
35853685
? { type: 'takeoff', airfieldId: airfield.id, nextOrder }
35863686
: makeFighterLandedOrder(airfield?.id || null, nextOrder);
35873687
return;
@@ -3845,9 +3945,15 @@ <h1 id="menuTitle">Sandfront Command</h1>
38453945
}
38463946

38473947
function updateFighterLanded(unit, dt) {
3848-
const airfield = unit.order.airfieldId ? getEntityById(unit.order.airfieldId) : ensureFighterAirfield(unit);
3948+
const airfield = unit.order.airfieldId
3949+
? getOperationalAirfieldById(unit.order.airfieldId, unit.teamId)
3950+
: ensureFighterAirfield(unit);
3951+
if (!airfield || airfield.kind !== 'building' || airfield.hp <= 0 || airfield.type !== 'airfield') {
3952+
recoverFighterAirfieldState(unit, unit.order.nextOrder || null);
3953+
return;
3954+
}
38493955
if (airfield && airfield.kind === 'building' && airfield.hp > 0 && airfield.type === 'airfield') {
3850-
assignFighterToAirfield(unit, airfield);
3956+
reserveFighterAirfield(unit, airfield);
38513957
const runway = getAirfieldRunwayPoints(airfield);
38523958
unit.x = runway.parkX;
38533959
unit.y = runway.parkY;
@@ -3871,12 +3977,12 @@ <h1 id="menuTitle">Sandfront Command</h1>
38713977
}
38723978

38733979
function updateFighterTakeoff(unit, dt) {
3874-
const airfield = getEntityById(unit.order.airfieldId) || ensureFighterAirfield(unit);
3980+
const airfield = getOperationalAirfieldById(unit.order.airfieldId, unit.teamId) || ensureFighterAirfield(unit);
38753981
if (!airfield || airfield.kind !== 'building' || airfield.hp <= 0 || airfield.type !== 'airfield') {
3876-
unit.order = unit.order.nextOrder || { type: 'idle' };
3982+
recoverFighterAirfieldState(unit, unit.order.nextOrder || null, true, unit.order.airfieldId || unit.assignedAirfieldId || null);
38773983
return;
38783984
}
3879-
assignFighterToAirfield(unit, airfield);
3985+
reserveFighterAirfield(unit, airfield);
38803986
const runway = getAirfieldRunwayPoints(airfield);
38813987
if (!unit.order.phase) unit.order.phase = 'roll';
38823988
const targetX = unit.order.phase === 'roll' ? runway.launchX : runway.departX;
@@ -3888,9 +3994,9 @@ <h1 id="menuTitle">Sandfront Command</h1>
38883994
const nextOrder = unit.order.nextOrder || { type: 'idle' };
38893995
if (nextOrder.type === 'return' && nextOrder.airfieldId && nextOrder.airfieldId !== airfield.id) {
38903996
airfield.assignedFighterId = airfield.assignedFighterId === unit.id ? null : airfield.assignedFighterId;
3891-
const destinationAirfield = getEntityById(nextOrder.airfieldId);
3997+
const destinationAirfield = getOperationalAirfieldById(nextOrder.airfieldId, unit.teamId);
38923998
if (destinationAirfield && destinationAirfield.kind === 'building' && destinationAirfield.type === 'airfield' && destinationAirfield.hp > 0) {
3893-
assignFighterToAirfield(unit, destinationAirfield);
3999+
reserveFighterAirfield(unit, destinationAirfield);
38944000
}
38954001
}
38964002
unit.order = nextOrder;
@@ -3899,12 +4005,12 @@ <h1 id="menuTitle">Sandfront Command</h1>
38994005
}
39004006

39014007
function updateFighterLanding(unit, dt) {
3902-
const airfield = getEntityById(unit.order.airfieldId) || ensureFighterAirfield(unit);
4008+
const airfield = getOperationalAirfieldById(unit.order.airfieldId, unit.teamId) || ensureFighterAirfield(unit);
39034009
if (!airfield || airfield.kind !== 'building' || airfield.hp <= 0 || airfield.type !== 'airfield') {
3904-
unit.order = makeFighterLandedOrder(null, null);
4010+
recoverFighterAirfieldState(unit, unit.order.nextOrder || null, true, unit.order.airfieldId || unit.assignedAirfieldId || null);
39054011
return;
39064012
}
3907-
assignFighterToAirfield(unit, airfield);
4013+
reserveFighterAirfield(unit, airfield);
39084014
const runway = getAirfieldRunwayPoints(airfield);
39094015
if (!unit.order.phase) unit.order.phase = 'touchdown';
39104016
const targetX = unit.order.phase === 'touchdown'
@@ -3935,12 +4041,12 @@ <h1 id="menuTitle">Sandfront Command</h1>
39354041
}
39364042

39374043
function updateFighterReturn(unit, dt) {
3938-
const airfield = getEntityById(unit.order.airfieldId) || ensureFighterAirfield(unit);
4044+
const airfield = getOperationalAirfieldById(unit.order.airfieldId, unit.teamId) || ensureFighterAirfield(unit);
39394045
if (!airfield || airfield.kind !== 'building' || airfield.hp <= 0 || airfield.type !== 'airfield') {
3940-
unit.order = makeFighterLandedOrder(null, unit.order.nextOrder || null);
4046+
recoverFighterAirfieldState(unit, unit.order.nextOrder || null, true, unit.order.airfieldId || unit.assignedAirfieldId || null);
39414047
return;
39424048
}
3943-
assignFighterToAirfield(unit, airfield);
4049+
reserveFighterAirfield(unit, airfield);
39444050
const runway = getAirfieldRunwayPoints(airfield);
39454051
const approachDistance = dist(unit.x, unit.y, runway.approachX, runway.approachY);
39464052
if (approachDistance <= FIGHTER_GLIDE_CAPTURE_RADIUS || moveFlyingUnitDirect(unit, dt, runway.approachX, runway.approachY, 1.02)) {
@@ -3951,6 +4057,54 @@ <h1 id="menuTitle">Sandfront Command</h1>
39514057
}
39524058
}
39534059

4060+
function crashFighterFromEmergencyHold(unit) {
4061+
if (!unit || unit.hp <= 0) return;
4062+
if (unit.teamId === TEAM_PLAYER) {
4063+
playUiCue('error');
4064+
toast('Fighter crashed after its landing request went unanswered.');
4065+
}
4066+
applyDamage(unit, unit.hp, unit.teamId, unit.id, { kind: 'unit', type: 'fighter' });
4067+
}
4068+
4069+
function tryResolveEmergencyLandingRequest(unit, refreshHud = true) {
4070+
if (!isFighterOperational(unit)) return false;
4071+
const reservedAirfield = findAvailableAirfield(unit.teamId, unit.x, unit.y, unit.assignedAirfieldId);
4072+
if (!reservedAirfield || !assignFighterToAirfield(unit, reservedAirfield)) return false;
4073+
unit.order = { type: 'return', airfieldId: reservedAirfield.id, nextOrder: null };
4074+
unit.targetId = null;
4075+
unit.path.length = 0;
4076+
if (refreshHud) refreshSelectionHudForUnits([unit]);
4077+
return true;
4078+
}
4079+
4080+
function updateFighterEmergencyHold(unit, dt) {
4081+
const order = unit.order;
4082+
const hub = order.hubId ? getEntityById(order.hubId) : null;
4083+
if (hub && hub.kind === 'building' && hub.hp > 0 && hub.type === 'hq') {
4084+
const center = getBuildingGeometry(hub).visualCenter;
4085+
order.x = center.x;
4086+
order.y = center.y;
4087+
}
4088+
if (tryResolveEmergencyLandingRequest(unit)) {
4089+
return;
4090+
}
4091+
if ((order.crashAt || 0) <= state.time) {
4092+
crashFighterFromEmergencyHold(unit);
4093+
return;
4094+
}
4095+
if (order.angle === null || order.angle === undefined) {
4096+
order.angle = Math.atan2(unit.y - order.y, unit.x - order.x);
4097+
}
4098+
const radius = order.radius || FIGHTER_EMERGENCY_HOLD_RADIUS;
4099+
const currentRadius = Math.hypot(unit.x - order.x, unit.y - order.y);
4100+
order.angle = wrapAngle((order.angle ?? 0) + dt * Math.max(0.88, Math.min(1.24, unit.speed / Math.max(radius, 1))));
4101+
const targetX = order.x + Math.cos(order.angle) * radius;
4102+
const targetY = order.y + Math.sin(order.angle) * radius;
4103+
const speedMultiplier = currentRadius > radius * 1.45 ? 1.08 : 0.96;
4104+
moveFlyingUnitDirect(unit, dt, targetX, targetY, speedMultiplier);
4105+
unit.targetId = null;
4106+
}
4107+
39544108
function updateFighterGuard(unit, dt) {
39554109
const order = unit.order;
39564110
const guardOrder = { ...order };
@@ -4023,6 +4177,8 @@ <h1 id="menuTitle">Sandfront Command</h1>
40234177
} else if (unit.order.type === 'strike' || (unit.order.type === 'attack' && unit.order.attackMode === 'fighter-strike')) {
40244178
if (unit.order.type === 'attack' && unit.order.attackMode === 'fighter-strike') unit.order = makeFighterStrikeOrder(unit.order.targetId, !!unit.order.lockTarget);
40254179
updateFighterStrike(unit, dt);
4180+
} else if (unit.order.type === 'emergency-hold') {
4181+
updateFighterEmergencyHold(unit, dt);
40264182
} else if (unit.order.type === 'idle') {
40274183
if (unit.missiles < (UNIT_TYPES.fighter.maxMissiles || FIGHTER_MISSILES) || unit.hp < unit.maxHp) beginFighterReturn(unit, null);
40284184
} else {
@@ -4038,7 +4194,7 @@ <h1 id="menuTitle">Sandfront Command</h1>
40384194
unit.assignedAirfieldId = null;
40394195
if (unit.order?.airfieldId === airfieldId) unit.order.airfieldId = null;
40404196
if (unit.order?.type === 'takeoff' || unit.order?.type === 'return' || unit.order?.type === 'landing' || unit.order?.type === 'landed') {
4041-
unit.order = makeFighterLandedOrder(null, unit.order?.nextOrder || null);
4197+
recoverFighterAirfieldState(unit, unit.order?.nextOrder || null, false, airfieldId);
40424198
}
40434199
}
40444200
}
@@ -7395,9 +7551,13 @@ <h1 id="menuTitle">Sandfront Command</h1>
73957551
if (unit.type === 'fighter') {
73967552
if (unit.order?.type === 'landed') {
73977553
const airfield = unit.order.airfieldId ? getEntityById(unit.order.airfieldId) : getAssignedAirfield(unit);
7398-
unit.order = makeFighterLandedOrder(airfield?.id || null, null);
7399-
unit.targetId = null;
7400-
unit.path.length = 0;
7554+
if (airfield && airfield.kind === 'building' && airfield.hp > 0 && airfield.type === 'airfield') {
7555+
unit.order = makeFighterLandedOrder(airfield.id, null);
7556+
unit.targetId = null;
7557+
unit.path.length = 0;
7558+
} else {
7559+
recoverFighterAirfieldState(unit, null);
7560+
}
74017561
} else {
74027562
beginFighterReturn(unit, null);
74037563
}

0 commit comments

Comments
 (0)