Release v0.1.4 2026-07-12
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { BULL_CHARGE } from "./bossMechanics";
|
||||
import { clampToArena } from "./arena";
|
||||
import { CINDER_BREATH } from "./bosses/cindermaw";
|
||||
import { SKY_SWEEPER_BREATH } from "./bosses/skySweeper";
|
||||
import { moveToward, pointOutsideCircle, pointOutsideLane, pointToSegmentDistance } from "./geometry";
|
||||
import type { BossMotionState, MemberId, PartyMember, WorldPosition } from "./types";
|
||||
|
||||
@@ -87,6 +87,21 @@ export const stackForPounceBehavior: PartyBehavior = {
|
||||
},
|
||||
};
|
||||
|
||||
export const stackForPooledSoakBehavior: PartyBehavior = {
|
||||
id: "stack-for-pooled-soak",
|
||||
decide: ({ memberId, bossMotion, time }) => {
|
||||
const soak = bossMotion.poolTelegraphs.find((telegraph) =>
|
||||
telegraph.kind === "soak" && !telegraph.resolved && telegraph.activatesAt > time && telegraph.activatesAt - time <= 2.2,
|
||||
);
|
||||
if (!soak) return null;
|
||||
const offset = STACK_OFFSETS[memberId];
|
||||
return {
|
||||
target: [soak.center[0] + offset[0], soak.center[1] + offset[1]],
|
||||
speed: 3.8,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const breakTetherBehavior: PartyBehavior = {
|
||||
id: "break-tether",
|
||||
decide: ({ memberId, current, bossMotion, partyPositions }) => {
|
||||
@@ -131,7 +146,7 @@ export const avoidBreathBehavior: PartyBehavior = {
|
||||
decide: ({ memberId, bossMotion }) => {
|
||||
if (bossMotion.mode !== "breath_telegraph" && bossMotion.mode !== "breath_sweeping") return null;
|
||||
const side = EVADE_SIDES[memberId];
|
||||
const safeAngle = bossMotion.breathAngle + side * (CINDER_BREATH.halfAngle + Math.PI * 0.42);
|
||||
const safeAngle = bossMotion.breathAngle + side * (SKY_SWEEPER_BREATH.halfAngle + Math.PI * 0.42);
|
||||
const radius = memberId === "brann" ? 4.1 : memberId === "vale" ? 3.5 : 5.4;
|
||||
return {
|
||||
target: clampToArena([
|
||||
@@ -164,9 +179,10 @@ export const evadeSlashLanesBehavior: PartyBehavior = {
|
||||
origin[0] + Math.sin(angle) * radius,
|
||||
origin[1] + Math.cos(angle) * radius,
|
||||
]);
|
||||
const laneDistance = Math.min(...bossMotion.slashLanes.map((lane) =>
|
||||
pointToSegmentDistance(candidate, lane.start, lane.end),
|
||||
));
|
||||
let laneDistance = Number.POSITIVE_INFINITY;
|
||||
for (const lane of bossMotion.slashLanes) {
|
||||
laneDistance = Math.min(laneDistance, pointToSegmentDistance(candidate, lane.start, lane.end));
|
||||
}
|
||||
const travelPenalty = Math.hypot(candidate[0] - current[0], candidate[1] - current[1]) * 0.08;
|
||||
const score = laneDistance - travelPenalty;
|
||||
if (score > bestScore) {
|
||||
@@ -197,6 +213,57 @@ export const avoidCircleHazardsBehavior: PartyBehavior = {
|
||||
},
|
||||
};
|
||||
|
||||
export const reactToPooledTelegraphsBehavior: PartyBehavior = {
|
||||
id: "react-to-pooled-telegraphs",
|
||||
decide: ({ memberId, current, formationTarget, bossMotion, time }) => {
|
||||
for (const telegraph of bossMotion.poolTelegraphs) {
|
||||
if (telegraph.resolved || telegraph.kind === "soak" || telegraph.kind === "memory" || telegraph.activatesAt - time > 2.2) continue;
|
||||
const fallbackAngle = (AI_MEMBER_IDS.indexOf(memberId) / AI_MEMBER_IDS.length) * Math.PI * 2;
|
||||
if (telegraph.kind === "beam" && telegraph.start && telegraph.end) {
|
||||
const clearance = (telegraph.width ?? 0) * 0.5 + 0.7;
|
||||
const currentUnsafe = pointToSegmentDistance(current, telegraph.start, telegraph.end) < clearance;
|
||||
const formationUnsafe = pointToSegmentDistance(formationTarget, telegraph.start, telegraph.end) < clearance;
|
||||
if (!currentUnsafe && !formationUnsafe) continue;
|
||||
const source = formationUnsafe ? formationTarget : current;
|
||||
return {
|
||||
target: clampToArena(pointOutsideLane(source, telegraph.start, telegraph.end, clearance, EVADE_SIDES[memberId])),
|
||||
speed: 4.8,
|
||||
};
|
||||
}
|
||||
|
||||
const distanceFromCenter = (position: WorldPosition) => Math.hypot(position[0] - telegraph.center[0], position[1] - telegraph.center[1]);
|
||||
if (telegraph.kind === "donut") {
|
||||
const innerSafeRadius = Math.max(0.5, (telegraph.innerRadius ?? 0) - 0.45);
|
||||
const isUnsafe = (position: WorldPosition) => {
|
||||
const distance = distanceFromCenter(position);
|
||||
return distance >= innerSafeRadius && distance <= telegraph.radius + 0.45;
|
||||
};
|
||||
if (!isUnsafe(current) && !isUnsafe(formationTarget)) continue;
|
||||
const source = isUnsafe(formationTarget) ? formationTarget : current;
|
||||
const sourceDistance = distanceFromCenter(source);
|
||||
const direction = sourceDistance > 0.01
|
||||
? [(source[0] - telegraph.center[0]) / sourceDistance, (source[1] - telegraph.center[1]) / sourceDistance] as WorldPosition
|
||||
: [Math.sin(fallbackAngle), Math.cos(fallbackAngle)] as WorldPosition;
|
||||
return {
|
||||
target: clampToArena([
|
||||
telegraph.center[0] + direction[0] * innerSafeRadius,
|
||||
telegraph.center[1] + direction[1] * innerSafeRadius,
|
||||
]),
|
||||
speed: 4.3,
|
||||
};
|
||||
}
|
||||
|
||||
const clearance = telegraph.radius + 0.55;
|
||||
const currentUnsafe = distanceFromCenter(current) < clearance;
|
||||
const formationUnsafe = distanceFromCenter(formationTarget) < clearance;
|
||||
if (!currentUnsafe && !formationUnsafe) continue;
|
||||
const source = formationUnsafe ? formationTarget : current;
|
||||
return { target: clampToArena(pointOutsideCircle(source, telegraph.center, clearance, fallbackAngle)), speed: 4.1 };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
export const maintainFormationBehavior: PartyBehavior = {
|
||||
id: "maintain-formation",
|
||||
decide: ({ formationTarget, bossMotion, memberId }) => {
|
||||
@@ -208,9 +275,11 @@ export const maintainFormationBehavior: PartyBehavior = {
|
||||
export const DEFAULT_PARTY_BEHAVIORS: readonly PartyBehavior[] = [
|
||||
breakTetherBehavior,
|
||||
stackForPounceBehavior,
|
||||
stackForPooledSoakBehavior,
|
||||
evadeChargeBehavior,
|
||||
evadeSlashLanesBehavior,
|
||||
avoidBreathBehavior,
|
||||
reactToPooledTelegraphsBehavior,
|
||||
avoidCircleHazardsBehavior,
|
||||
maintainFormationBehavior,
|
||||
];
|
||||
@@ -234,10 +303,14 @@ export function updatePartyPositions(
|
||||
vale: [current.vale[0], current.vale[1]],
|
||||
};
|
||||
if (!activeMotions.length) return next;
|
||||
const formationOrigin: WorldPosition = [
|
||||
activeMotions.reduce((sum, motion) => sum + (DASH_MODES.includes(motion.mode) ? motion.chargeStart[0] : motion.position[0]), 0) / activeMotions.length,
|
||||
activeMotions.reduce((sum, motion) => sum + (DASH_MODES.includes(motion.mode) ? motion.chargeStart[1] : motion.position[1]), 0) / activeMotions.length,
|
||||
];
|
||||
let formationX = 0;
|
||||
let formationZ = 0;
|
||||
for (const motion of activeMotions) {
|
||||
const origin = DASH_MODES.includes(motion.mode) ? motion.chargeStart : motion.position;
|
||||
formationX += origin[0];
|
||||
formationZ += origin[1];
|
||||
}
|
||||
const formationOrigin: WorldPosition = [formationX / activeMotions.length, formationZ / activeMotions.length];
|
||||
const formation = combatFormation(formationOrigin);
|
||||
|
||||
for (let index = 0; index < AI_MEMBER_IDS.length; index += 1) {
|
||||
|
||||
Reference in New Issue
Block a user