Android build v1.1.18
This commit is contained in:
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.warren.iwanttoheal"
|
applicationId "com.warren.iwanttoheal"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 96
|
versionCode 97
|
||||||
versionName "1.1.17"
|
versionName "1.1.18"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Set<String> currentTokens = new HashSet<>();
|
Set<String> currentTokens = new HashSet<>();
|
||||||
|
float leftStickX = event.getAxisValue(MotionEvent.AXIS_X);
|
||||||
|
float leftStickY = event.getAxisValue(MotionEvent.AXIS_Y);
|
||||||
|
dispatchNativeControllerMotion(leftStickX, leftStickY);
|
||||||
addAxisTokens(
|
addAxisTokens(
|
||||||
currentTokens,
|
currentTokens,
|
||||||
event.getAxisValue(MotionEvent.AXIS_HAT_X),
|
event.getAxisValue(MotionEvent.AXIS_HAT_X),
|
||||||
@@ -178,13 +181,13 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
);
|
);
|
||||||
addAxisTokens(
|
addAxisTokens(
|
||||||
currentTokens,
|
currentTokens,
|
||||||
event.getAxisValue(MotionEvent.AXIS_X),
|
leftStickX,
|
||||||
"Axis0-",
|
"Axis0-",
|
||||||
"Axis0+"
|
"Axis0+"
|
||||||
);
|
);
|
||||||
addAxisTokens(
|
addAxisTokens(
|
||||||
currentTokens,
|
currentTokens,
|
||||||
event.getAxisValue(MotionEvent.AXIS_Y),
|
leftStickY,
|
||||||
"Axis1-",
|
"Axis1-",
|
||||||
"Axis1+"
|
"Axis1+"
|
||||||
);
|
);
|
||||||
@@ -240,6 +243,19 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void dispatchNativeControllerMotion(float x, float y) {
|
||||||
|
if (bridge == null || bridge.getWebView() == null) return;
|
||||||
|
String script =
|
||||||
|
"window.dispatchEvent(new CustomEvent('ashen-halls-native-controller-motion',"
|
||||||
|
+ "{detail:{x:" + x + ",y:" + y + "}}));";
|
||||||
|
bridge.getWebView().post(
|
||||||
|
() -> {
|
||||||
|
bridge.getWebView().requestFocus();
|
||||||
|
bridge.getWebView().evaluateJavascript(script, null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldThrottleDpad(String token) {
|
private boolean shouldThrottleDpad(String token) {
|
||||||
int buttonIndex = Integer.parseInt(token.substring("Button".length()));
|
int buttonIndex = Integer.parseInt(token.substring("Button".length()));
|
||||||
long now = SystemClock.uptimeMillis();
|
long now = SystemClock.uptimeMillis();
|
||||||
|
|||||||
+23
-3
@@ -134,6 +134,7 @@ const STORAGE_KEY = 'ashen-halls-input-bindings-v1'
|
|||||||
const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
|
const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
|
||||||
const GAME_ACTION_EVENT = 'ashen-halls-game-action'
|
const GAME_ACTION_EVENT = 'ashen-halls-game-action'
|
||||||
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
|
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
|
||||||
|
const NATIVE_CONTROLLER_MOTION_EVENT = 'ashen-halls-native-controller-motion'
|
||||||
const FOCUSABLE_SELECTOR = 'button:not(:disabled):not([data-controller-nav="skip"]), input:not(:disabled):not([data-controller-nav="skip"]), select:not(:disabled):not([data-controller-nav="skip"]), textarea:not(:disabled):not([data-controller-nav="skip"]), [tabindex]:not([tabindex="-1"]):not([data-controller-nav="skip"])'
|
const FOCUSABLE_SELECTOR = 'button:not(:disabled):not([data-controller-nav="skip"]), input:not(:disabled):not([data-controller-nav="skip"]), select:not(:disabled):not([data-controller-nav="skip"]), textarea:not(:disabled):not([data-controller-nav="skip"]), [tabindex]:not([tabindex="-1"]):not([data-controller-nav="skip"])'
|
||||||
const MAIN_CONTENT_SELECTOR = '.auth-shell, .menu-screen, .content-screen, .dungeon-run-screen, .dual-bottom-display, .iwt2-bottom-display'
|
const MAIN_CONTENT_SELECTOR = '.auth-shell, .menu-screen, .content-screen, .dungeon-run-screen, .dual-bottom-display, .iwt2-bottom-display'
|
||||||
const HEADER_CONTENT_SELECTOR = '.app-header'
|
const HEADER_CONTENT_SELECTOR = '.app-header'
|
||||||
@@ -142,6 +143,7 @@ const GAMEPAD_MENU_POLL_MS = 1000 / 30
|
|||||||
const GAMEPAD_BROWSER_DISCONNECTED_POLL_MS = 250
|
const GAMEPAD_BROWSER_DISCONNECTED_POLL_MS = 250
|
||||||
const CONTROLLER_REPEAT_INITIAL_MS = 260
|
const CONTROLLER_REPEAT_INITIAL_MS = 260
|
||||||
const CONTROLLER_REPEAT_MS = 85
|
const CONTROLLER_REPEAT_MS = 85
|
||||||
|
const NATIVE_STICK_TIMEOUT_MS = 140
|
||||||
const DPAD_NAV_ACTIONS: Partial<Record<string, InputAction>> = {
|
const DPAD_NAV_ACTIONS: Partial<Record<string, InputAction>> = {
|
||||||
Button12: 'navigateUp',
|
Button12: 'navigateUp',
|
||||||
Button13: 'navigateDown',
|
Button13: 'navigateDown',
|
||||||
@@ -1087,6 +1089,8 @@ export function useMovementVectorRef(enabled = true) {
|
|||||||
|
|
||||||
let frame = 0
|
let frame = 0
|
||||||
const pressedKeys = new Set<string>()
|
const pressedKeys = new Set<string>()
|
||||||
|
let nativeStick: MovementVector = { x: 0, y: 0 }
|
||||||
|
let nativeStickUpdatedAt = 0
|
||||||
const keyboardVector = () => normalizedVector(
|
const keyboardVector = () => normalizedVector(
|
||||||
(pressedKeys.has('KeyD') ? 1 : 0) - (pressedKeys.has('KeyA') ? 1 : 0),
|
(pressedKeys.has('KeyD') ? 1 : 0) - (pressedKeys.has('KeyA') ? 1 : 0),
|
||||||
(pressedKeys.has('KeyS') ? 1 : 0) - (pressedKeys.has('KeyW') ? 1 : 0),
|
(pressedKeys.has('KeyS') ? 1 : 0) - (pressedKeys.has('KeyW') ? 1 : 0),
|
||||||
@@ -1094,12 +1098,26 @@ export function useMovementVectorRef(enabled = true) {
|
|||||||
const updateMovement = () => {
|
const updateMovement = () => {
|
||||||
const gamepad = firstConnectedGamepad()
|
const gamepad = firstConnectedGamepad()
|
||||||
const stick = gamepad ? applyStickDeadzone(gamepad.axes[0] ?? 0, gamepad.axes[1] ?? 0) : { x: 0, y: 0 }
|
const stick = gamepad ? applyStickDeadzone(gamepad.axes[0] ?? 0, gamepad.axes[1] ?? 0) : { x: 0, y: 0 }
|
||||||
|
const native = performance.now() - nativeStickUpdatedAt <= NATIVE_STICK_TIMEOUT_MS
|
||||||
|
? applyStickDeadzone(nativeStick.x, nativeStick.y)
|
||||||
|
: { x: 0, y: 0 }
|
||||||
const keyboard = keyboardVector()
|
const keyboard = keyboardVector()
|
||||||
movementRef.current = Math.hypot(stick.x, stick.y) > 0
|
movementRef.current = Math.hypot(native.x, native.y) > 0
|
||||||
? stick
|
? native
|
||||||
: keyboard
|
: Math.hypot(stick.x, stick.y) > 0
|
||||||
|
? stick
|
||||||
|
: keyboard
|
||||||
frame = window.requestAnimationFrame(updateMovement)
|
frame = window.requestAnimationFrame(updateMovement)
|
||||||
}
|
}
|
||||||
|
const onNativeMotion = (event: Event) => {
|
||||||
|
const detail = (event as CustomEvent<Partial<MovementVector>>).detail
|
||||||
|
nativeStick = {
|
||||||
|
x: Number(detail.x) || 0,
|
||||||
|
y: Number(detail.y) || 0,
|
||||||
|
}
|
||||||
|
nativeStickUpdatedAt = performance.now()
|
||||||
|
document.documentElement.dataset.inputDevice = 'controller'
|
||||||
|
}
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (isTextInput(document.activeElement)) return
|
if (isTextInput(document.activeElement)) return
|
||||||
if (!['KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(event.code)) return
|
if (!['KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(event.code)) return
|
||||||
@@ -1113,12 +1131,14 @@ export function useMovementVectorRef(enabled = true) {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener(NATIVE_CONTROLLER_MOTION_EVENT, onNativeMotion)
|
||||||
window.addEventListener('keydown', onKeyDown)
|
window.addEventListener('keydown', onKeyDown)
|
||||||
window.addEventListener('keyup', onKeyUp)
|
window.addEventListener('keyup', onKeyUp)
|
||||||
frame = window.requestAnimationFrame(updateMovement)
|
frame = window.requestAnimationFrame(updateMovement)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(frame)
|
window.cancelAnimationFrame(frame)
|
||||||
|
window.removeEventListener(NATIVE_CONTROLLER_MOTION_EVENT, onNativeMotion)
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
window.removeEventListener('keyup', onKeyUp)
|
window.removeEventListener('keyup', onKeyUp)
|
||||||
movementRef.current = { x: 0, y: 0 }
|
movementRef.current = { x: 0, y: 0 }
|
||||||
|
|||||||
@@ -63,12 +63,17 @@ type BossArenaScreenProps = {
|
|||||||
|
|
||||||
export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
|
export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
|
||||||
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
||||||
|
const pvpRoguelike = roguelikeRun?.variant === 'pvp'
|
||||||
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId))
|
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId))
|
||||||
|
const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
|
||||||
|
pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
|
||||||
|
))
|
||||||
const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
|
const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
|
||||||
const [status, setStatus] = useState<ArenaStatus>('playing')
|
const [status, setStatus] = useState<ArenaStatus>('playing')
|
||||||
const [selectedOverlayAction, setSelectedOverlayAction] = useState<OverlayAction>('primary')
|
const [selectedOverlayAction, setSelectedOverlayAction] = useState<OverlayAction>('primary')
|
||||||
const [selectedPartyId, setSelectedPartyId] = useState<Iwt2EntityId>('player-healer')
|
const [selectedPartyId, setSelectedPartyId] = useState<Iwt2EntityId>('player-healer')
|
||||||
const stateRef = useRef(arenaState)
|
const stateRef = useRef(arenaState)
|
||||||
|
const opponentStateRef = useRef<Iwt2ArenaState | null>(opponentArenaState)
|
||||||
const statusRef = useRef(status)
|
const statusRef = useRef(status)
|
||||||
const selectedOverlayActionRef = useRef<OverlayAction>(selectedOverlayAction)
|
const selectedOverlayActionRef = useRef<OverlayAction>(selectedOverlayAction)
|
||||||
const selectedPartyIdRef = useRef<Iwt2EntityId>(selectedPartyId)
|
const selectedPartyIdRef = useRef<Iwt2EntityId>(selectedPartyId)
|
||||||
@@ -91,6 +96,16 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
stateRef.current = arenaState
|
stateRef.current = arenaState
|
||||||
}, [arenaState])
|
}, [arenaState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
opponentStateRef.current = opponentArenaState
|
||||||
|
}, [opponentArenaState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
|
||||||
|
opponentStateRef.current = nextOpponentState
|
||||||
|
setOpponentArenaState(nextOpponentState)
|
||||||
|
}, [bossId, pvpRoguelike])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
statusRef.current = status
|
statusRef.current = status
|
||||||
}, [status])
|
}, [status])
|
||||||
@@ -109,15 +124,18 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
|
|
||||||
const resetArena = useCallback(() => {
|
const resetArena = useCallback(() => {
|
||||||
const next = createInitialIwt2ArenaState(bossId)
|
const next = createInitialIwt2ArenaState(bossId)
|
||||||
|
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId) : null
|
||||||
killRecordedRef.current = false
|
killRecordedRef.current = false
|
||||||
abilityCooldownsRef.current = {}
|
abilityCooldownsRef.current = {}
|
||||||
lastHudSignatureRef.current = arenaHudSignature(next)
|
lastHudSignatureRef.current = arenaHudSignature(next)
|
||||||
stateRef.current = next
|
stateRef.current = next
|
||||||
|
opponentStateRef.current = nextOpponentState
|
||||||
setArenaState(next)
|
setArenaState(next)
|
||||||
|
setOpponentArenaState(nextOpponentState)
|
||||||
setAbilityCooldowns({})
|
setAbilityCooldowns({})
|
||||||
setSelectedOverlayAction('primary')
|
setSelectedOverlayAction('primary')
|
||||||
setStatus('playing')
|
setStatus('playing')
|
||||||
}, [bossId])
|
}, [bossId, pvpRoguelike])
|
||||||
|
|
||||||
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
|
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
|
||||||
setSelectedOverlayAction('primary')
|
setSelectedOverlayAction('primary')
|
||||||
@@ -240,6 +258,19 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
moveY: movement.y,
|
moveY: movement.y,
|
||||||
}, dtSeconds)
|
}, dtSeconds)
|
||||||
stateRef.current = next
|
stateRef.current = next
|
||||||
|
let nextOpponentState = opponentStateRef.current
|
||||||
|
if (pvpRoguelike && nextOpponentState) {
|
||||||
|
const opponentMovement = iwt2OpponentMovement(nextOpponentState)
|
||||||
|
nextOpponentState = tickIwt2Arena(
|
||||||
|
nextOpponentState,
|
||||||
|
{
|
||||||
|
moveX: opponentMovement.x,
|
||||||
|
moveY: opponentMovement.y,
|
||||||
|
},
|
||||||
|
dtSeconds,
|
||||||
|
)
|
||||||
|
opponentStateRef.current = nextOpponentState
|
||||||
|
}
|
||||||
|
|
||||||
if (next.boss.health <= 0 && !killRecordedRef.current) {
|
if (next.boss.health <= 0 && !killRecordedRef.current) {
|
||||||
killRecordedRef.current = true
|
killRecordedRef.current = true
|
||||||
@@ -260,10 +291,11 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
lastHudSignatureRef.current = hudSignature
|
lastHudSignatureRef.current = hudSignature
|
||||||
lastPublishTimeRef.current = next.time
|
lastPublishTimeRef.current = next.time
|
||||||
setArenaState(next)
|
setArenaState(next)
|
||||||
|
if (pvpRoguelike) setOpponentArenaState(nextOpponentState)
|
||||||
setAbilityCooldowns(abilityCooldownsRef.current)
|
setAbilityCooldowns(abilityCooldownsRef.current)
|
||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
}, [bossId, onSaveUpdated, showOverlay])
|
}, [bossId, onSaveUpdated, pvpRoguelike, showOverlay])
|
||||||
|
|
||||||
const targetBindings = directPartyTargeting
|
const targetBindings = directPartyTargeting
|
||||||
? IWT2_TARGET_ACTIONS.map((action) => activeBindings[action] ?? null)
|
? IWT2_TARGET_ACTIONS.map((action) => activeBindings[action] ?? null)
|
||||||
@@ -310,7 +342,9 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
controllerIconStyle,
|
controllerIconStyle,
|
||||||
directPartyTargeting,
|
directPartyTargeting,
|
||||||
modeLabel: modeLabel ?? 'Arena',
|
modeLabel: modeLabel ?? 'Arena',
|
||||||
|
opponentArenaState,
|
||||||
playerMana,
|
playerMana,
|
||||||
|
roguelikeRun,
|
||||||
selectedPartyId,
|
selectedPartyId,
|
||||||
status,
|
status,
|
||||||
}),
|
}),
|
||||||
@@ -323,7 +357,9 @@ export function BossArenaScreen({ bossId, modeLabel, save, onBack, onSaveUpdated
|
|||||||
controllerIconStyle,
|
controllerIconStyle,
|
||||||
directPartyTargeting,
|
directPartyTargeting,
|
||||||
modeLabel,
|
modeLabel,
|
||||||
|
opponentArenaState,
|
||||||
playerMana,
|
playerMana,
|
||||||
|
roguelikeRun,
|
||||||
selectedPartyId,
|
selectedPartyId,
|
||||||
status,
|
status,
|
||||||
],
|
],
|
||||||
@@ -555,7 +591,9 @@ function buildIwt2DualScreenCombatState({
|
|||||||
controllerIconStyle,
|
controllerIconStyle,
|
||||||
directPartyTargeting,
|
directPartyTargeting,
|
||||||
modeLabel,
|
modeLabel,
|
||||||
|
opponentArenaState,
|
||||||
playerMana,
|
playerMana,
|
||||||
|
roguelikeRun,
|
||||||
selectedPartyId,
|
selectedPartyId,
|
||||||
status,
|
status,
|
||||||
}: {
|
}: {
|
||||||
@@ -567,11 +605,28 @@ function buildIwt2DualScreenCombatState({
|
|||||||
controllerIconStyle: DualScreenCombatState['controllerIconStyle']
|
controllerIconStyle: DualScreenCombatState['controllerIconStyle']
|
||||||
directPartyTargeting: boolean
|
directPartyTargeting: boolean
|
||||||
modeLabel: string
|
modeLabel: string
|
||||||
|
opponentArenaState: Iwt2ArenaState | null
|
||||||
playerMana: number
|
playerMana: number
|
||||||
|
roguelikeRun: BossArenaScreenProps['roguelikeRun'] | undefined
|
||||||
selectedPartyId: Iwt2EntityId
|
selectedPartyId: Iwt2EntityId
|
||||||
status: ArenaStatus
|
status: ArenaStatus
|
||||||
}): DualScreenCombatState {
|
}): DualScreenCombatState {
|
||||||
|
const opponentHealer = opponentArenaState?.party.find((member) => member.id === 'player-healer')
|
||||||
|
const pvpOpponentState = roguelikeRun?.variant === 'pvp' && opponentArenaState
|
||||||
|
? {
|
||||||
|
opponentBuffSummary: `Stage ${roguelikeRun.stage}`,
|
||||||
|
opponentClassName: `CPU Healer | ${formatRoguelikeContentType(roguelikeRun.contentType)}`,
|
||||||
|
opponentDebuffSummary: formatIwt2DebuffSummary(roguelikeRun.debuffs),
|
||||||
|
opponentEnemyHealth: opponentArenaState.boss.health,
|
||||||
|
opponentMaxResource: opponentHealer?.maxMana ?? 100,
|
||||||
|
opponentName: 'CPU Rival',
|
||||||
|
opponentParty: opponentArenaState.party.map((member) => toDualScreenPartyMember(member, true)),
|
||||||
|
opponentResource: opponentHealer?.mana ?? 100,
|
||||||
|
opponentResourceName: 'Mana',
|
||||||
|
}
|
||||||
|
: {}
|
||||||
return {
|
return {
|
||||||
|
...pvpOpponentState,
|
||||||
bindings,
|
bindings,
|
||||||
contentName: modeLabel,
|
contentName: modeLabel,
|
||||||
controllerIconStyle,
|
controllerIconStyle,
|
||||||
@@ -587,7 +642,7 @@ function buildIwt2DualScreenCombatState({
|
|||||||
encounterName: bossMetadata.name,
|
encounterName: bossMetadata.name,
|
||||||
floatingTexts: [],
|
floatingTexts: [],
|
||||||
maxResource: 100,
|
maxResource: 100,
|
||||||
party: arenaState.party.map(toDualScreenPartyMember),
|
party: arenaState.party.map((member) => toDualScreenPartyMember(member)),
|
||||||
partySize: arenaState.party.length,
|
partySize: arenaState.party.length,
|
||||||
paused: status === 'paused',
|
paused: status === 'paused',
|
||||||
playerIsAlive: (arenaState.party.find((member) => member.id === 'player-healer')?.health ?? 0) > 0,
|
playerIsAlive: (arenaState.party.find((member) => member.id === 'player-healer')?.health ?? 0) > 0,
|
||||||
@@ -601,7 +656,7 @@ function buildIwt2DualScreenCombatState({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number]): PartyMember {
|
function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number], opponent = false): PartyMember {
|
||||||
const metadata = IWT2_CLASS_METADATA[member.classId]
|
const metadata = IWT2_CLASS_METADATA[member.classId]
|
||||||
return {
|
return {
|
||||||
bounceHeals: [],
|
bounceHeals: [],
|
||||||
@@ -614,14 +669,19 @@ function toDualScreenPartyMember(member: Iwt2ArenaState['party'][number]): Party
|
|||||||
ticks: Math.ceil(effect.remainingSeconds),
|
ticks: Math.ceil(effect.remainingSeconds),
|
||||||
})),
|
})),
|
||||||
hotTicks: member.hotEffects.length,
|
hotTicks: member.hotEffects.length,
|
||||||
id: member.id,
|
id: opponent ? `opponent-${member.id}` : member.id,
|
||||||
maxHealth: member.maxHealth,
|
maxHealth: member.maxHealth,
|
||||||
name: metadata.name,
|
name: opponent ? opponentPartyName(metadata.name) : metadata.name,
|
||||||
role: toDualScreenRole(metadata.role),
|
role: toDualScreenRole(metadata.role),
|
||||||
shield: member.shield,
|
shield: member.shield,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function opponentPartyName(name: string): string {
|
||||||
|
if (name === 'Player Healer') return 'CPU Healer'
|
||||||
|
return `CPU ${name.replace(' Tank', '')}`
|
||||||
|
}
|
||||||
|
|
||||||
function toDualScreenRole(role: (typeof IWT2_CLASS_METADATA)[keyof typeof IWT2_CLASS_METADATA]['role']): Role {
|
function toDualScreenRole(role: (typeof IWT2_CLASS_METADATA)[keyof typeof IWT2_CLASS_METADATA]['role']): Role {
|
||||||
if (role === 'tank') return 'Tank'
|
if (role === 'tank') return 'Tank'
|
||||||
if (role === 'healer') return 'Healer'
|
if (role === 'healer') return 'Healer'
|
||||||
@@ -648,3 +708,31 @@ function toDualScreenSpell(
|
|||||||
slotIndex,
|
slotIndex,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function iwt2OpponentMovement(state: Iwt2ArenaState): MovementVector {
|
||||||
|
const healer = state.party.find((member) => member.id === 'player-healer')
|
||||||
|
if (!healer || healer.health <= 0) return { x: 0, y: 0 }
|
||||||
|
const orbitSeconds = Math.max(1, state.time)
|
||||||
|
return normalizedArenaMovement({
|
||||||
|
x: Math.cos(orbitSeconds * 0.75),
|
||||||
|
y: Math.sin(orbitSeconds * 0.75) * 0.55,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedArenaMovement(vector: MovementVector): MovementVector {
|
||||||
|
const magnitude = Math.hypot(vector.x, vector.y)
|
||||||
|
if (magnitude <= 1) return vector
|
||||||
|
return {
|
||||||
|
x: vector.x / magnitude,
|
||||||
|
y: vector.y / magnitude,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIwt2DebuffSummary(debuffs: readonly string[]): string {
|
||||||
|
if (debuffs.length === 0) return 'none'
|
||||||
|
return debuffs
|
||||||
|
.map((debuff) => debuff
|
||||||
|
.replace('opp-', '')
|
||||||
|
.replaceAll('-', ' '))
|
||||||
|
.join(', ')
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user