From cc0756de2ecbfbed474b7d92cb842bde1de9ac7c Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Fri, 2 Oct 2026 15:14:59 +0000 Subject: [PATCH] Fix skill anim modes, monster knockback, warp hover, and rate limiting (Closes #565, #566, #573, #575) --- src/client/scene/world-renderer.ts | 2 +- src/client/session/online-session.ts | 64 +- src/client/session/rate-limiter.ts | 36 +- src/client/world/client-unit.ts | 2 +- src/client/world/client-world.ts | 33 +- src/common/skills/skill-anim-modes.ts | 2174 ++++++++++++++++++++++ src/common/units/char-anim-speed.ts | 8 + src/common/world/level-warps-meta.ts | 694 +++++++ src/netproto/d2gs/s2c/units.ts | 10 +- tests/client/timing-modes-parity.test.ts | 261 +++ 10 files changed, 3210 insertions(+), 74 deletions(-) create mode 100644 src/common/skills/skill-anim-modes.ts create mode 100644 src/common/world/level-warps-meta.ts create mode 100644 tests/client/timing-modes-parity.test.ts diff --git a/src/client/scene/world-renderer.ts b/src/client/scene/world-renderer.ts index c948c5d..94d3bc6 100644 --- a/src/client/scene/world-renderer.ts +++ b/src/client/scene/world-renderer.ts @@ -281,7 +281,7 @@ const MONSTER_MODE_TO_CLIP: Readonly> = { 10: 's3', 11: 's4', 12: 'dd', - 13: 'rn', + 13: 'gh', 14: 'xx', 15: 'rn', } diff --git a/src/client/session/online-session.ts b/src/client/session/online-session.ts index 55aec0e..e39481d 100644 --- a/src/client/session/online-session.ts +++ b/src/client/session/online-session.ts @@ -45,6 +45,7 @@ import { type UnitType, } from '../../netproto/index.ts' import { CANONICAL_LEVEL_LIGHTING } from '../../common/world/levels-meta.ts' +import { resolveLevelWarpName, resolveLvlWarpBounds } from '../../common/world/level-warps-meta.ts' import { resolveGroundItemMetadata } from '../../common/items/ground-items.ts' import { requireTbl } from '../i18n/lang.ts' import { HudManager } from '../ui/hud-manager.ts' @@ -71,7 +72,7 @@ import type { RenderableUnitInput } from '../view/unit-drawables.ts' import { getViewportProfile } from '../view/viewport-profile.ts' import { ClientWorld, type ChatLogEntry } from '../world/client-world.ts' import { getCanonicalItemDataTables } from '../world/item-tables-provider.ts' -import { OutboundRateLimiter } from './rate-limiter.ts' +import { OutboundRateLimiter, classifyClientCommand } from './rate-limiter.ts' import { computeMonsterMoveSpeedSubTilesPerSec } from '../../common/units/monsters-data.ts' import { isTownLevel } from '../../common/world/scene-population.ts' @@ -1072,12 +1073,19 @@ export class OnlineSession { y: Number(rawCmd.targetY ?? rawCmd.y ?? 0), } : (cmd as ClientCommand) - const res = this.rateLimiter.tryAcquire(normalized, nowMs) - if (res.allowed) { - this.world.noteOutgoingCommand(normalized, nowMs) + const category = classifyClientCommand(normalized) + // Non-movement commands (interaction, skill, UI) bypass generic rate limiter per 1.13c parity (#575) + // to prevent player clicks and interactions from being silently swallowed. + if (category !== 'movement') { this.forwardCommandToAdapter(normalized, nowMs) return true } + const res = this.rateLimiter.tryAcquire(normalized, nowMs) + if (res.allowed) { + this.forwardCommandToAdapter(normalized, nowMs) + return true + } + console.warn(`[OnlineSession] Outbound command throttled: ${normalized.type} (${res.reason ?? 'unknown'})`) return false } @@ -1712,6 +1720,7 @@ export class OnlineSession { throw new RangeError(`nowMs must be a finite number, got ${String(nowMs)}`) } const safeDt = Math.max(0, dtMs) + const clampedDt = Math.min(200, safeDt) this.tickCounter++ this.commandMapper.tick(nowMs) @@ -1729,7 +1738,7 @@ export class OnlineSession { } } - const predSnap = this.predictor.update(safeDt) + const predSnap = this.predictor.update(clampedDt) if (predSnap.x !== 0 || predSnap.y !== 0) { this.camera.update(predSnap.x, predSnap.y) } @@ -1748,13 +1757,13 @@ export class OnlineSession { this.interpolator.setEntityTargetDirection(uTypeId, unit.id, unit.targetDirection, false) } } else if (!unit.dead) { - unit.advanceTurn(safeDt) + unit.advanceTurn(clampedDt) } } - this.world.tick(dtMs, nowMs) + this.world.tick(clampedDt, nowMs) - this.interpolator.update(safeDt) + this.interpolator.update(clampedDt) for (const state of this.interpolator.getAllStates()) { const unit = this.world.getUnit(state.unitType as UnitType, state.unitId) if (unit) { @@ -1784,10 +1793,10 @@ export class OnlineSession { } } else if (unit.unitType === 'monster' && unit.lifePct > 0) { if (state.moving && !unit.castState && unit.moveMode !== 'stop') { - unit.mode = state.running ? 15 : unit.moveMode === 'knockback' ? 9 : 2 + unit.mode = state.running ? 15 : unit.moveMode === 'knockback' ? 13 : 2 } else if ( !state.moving && - (unit.mode === 2 || unit.mode === 9 || unit.mode === 15) + (unit.mode === 2 || unit.mode === 13 || unit.mode === 15) ) { unit.mode = 1 unit.moveMode = 'stop' @@ -1935,33 +1944,17 @@ export class OnlineSession { if (u.unitType === 'tile') { const warpId = u.warpId ?? u.classId - let matchedWarp = activeLevelView?.warps.find((w) => w.lvlWarpId === warpId) - if (!matchedWarp && activeLevelView && activeLevelView.warps.length > 0) { - let bestDistSq = 30 * 30 - for (const w of activeLevelView.warps) { - const wAny = w as Record - const room = - typeof w.room === 'number' ? activeLevelView.rooms[w.room] : undefined - const subX = typeof wAny.subX === 'number' ? wAny.subX : (room?.subX ?? 0) - const subY = typeof wAny.subY === 'number' ? wAny.subY : (room?.subY ?? 0) - const wx = activeLevelView.subOriginX + subX - const wy = activeLevelView.subOriginY + subY - const dSq = (wx - ux) * (wx - ux) + (wy - uy) * (wy - uy) - if (dSq <= bestDistSq) { - bestDistSq = dSq - matchedWarp = w - } - } - } + const matchedWarp = activeLevelView?.warps.find((w) => w.lvlWarpId === warpId) const toLevelId = matchedWarp?.toLevelId - const toAreaName = - toLevelId !== undefined ? CANONICAL_LEVEL_LIGHTING[toLevelId]?.levelName : undefined + const warpNames = toLevelId !== undefined ? resolveLevelWarpName(toLevelId) : undefined + const bounds = resolveLvlWarpBounds(warpId) const warpName = u.name && u.name.trim() !== '' ? u.name - : toAreaName - ? `To ${toAreaName}` + : warpNames + ? warpNames.name : `Entrance (${warpId})` + const warpNameZh = warpNames?.nameZh out.push({ id: u.id, kind: 'warp', @@ -1969,12 +1962,13 @@ export class OnlineSession { warpId, ...(toLevelId !== undefined ? { toLevelId } : {}), name: warpName, + ...(warpNameZh !== undefined ? { nameZh: warpNameZh } : {}), x: ux, y: uy, mode: u.mode, direction: interp ? interp.dir64 : u.direction, - radiusX: 28, - radiusY: 20, + radiusX: bounds.radiusX, + radiusY: bounds.radiusY, isHovered, }) continue @@ -2018,7 +2012,7 @@ export class OnlineSession { } else if (castState) { mode = castState.mode || u.mode || 1 } else if (interp?.moving && u.moveMode !== 'stop') { - mode = u.moveMode === 'knockback' ? 9 : interp.running ? 15 : 2 + mode = u.moveMode === 'knockback' ? 13 : interp.running ? 15 : 2 } else { mode = u.mode || 1 } diff --git a/src/client/session/rate-limiter.ts b/src/client/session/rate-limiter.ts index 4993fcf..847eeca 100644 --- a/src/client/session/rate-limiter.ts +++ b/src/client/session/rate-limiter.ts @@ -33,14 +33,21 @@ export interface RateLimitDecision { export type RateLimitAcquireResult = RateLimitDecision export type RateLimiterConfig = OutboundRateLimiterConfig -export const DEFAULT_RATE_LIMITS: Required = { +export const DEFAULT_RATE_LIMITS: Readonly<{ + moveMinIntervalMs: number + skillMinIntervalMs: number + chatMinIntervalMs: number + interactionMinIntervalMs: number + burstCapacity: number + refillPerSec: number +}> = Object.freeze({ moveMinIntervalMs: 120, - skillMinIntervalMs: 100, - chatMinIntervalMs: 500, - interactionMinIntervalMs: 80, - burstCapacity: 12, - refillPerSec: 20, -} + skillMinIntervalMs: 0, + chatMinIntervalMs: 0, + interactionMinIntervalMs: 0, + burstCapacity: 1000, + refillPerSec: 1000, +}) export function classifyClientCommand(cmd: ClientCommand): CommandRateCategory { switch (cmd.type) { @@ -73,6 +80,9 @@ export function classifyClientCommand(cmd: ClientCommand): CommandRateCategory { * Outbound C→S packet rate limiter protecting against D2GS flood disconnects. * Implements per-category minimum intervals, duplicate movement suppression, * coalesced trailing movement emission, and a global token bucket. + * + * 1.13c Parity (#575): No artificial cooldown or rate-limiting is applied to non-movement + * commands (interaction, skill, UI clicks) by default, preventing silent click swallowing. */ export class OutboundRateLimiter { private readonly moveMinIntervalMs: number @@ -94,12 +104,12 @@ export class OutboundRateLimiter { coalescedCount = 0 constructor(config?: OutboundRateLimiterConfig) { - this.moveMinIntervalMs = Math.max(0, config?.moveMinIntervalMs ?? 120) - this.skillMinIntervalMs = Math.max(0, config?.skillMinIntervalMs ?? 100) - this.chatMinIntervalMs = Math.max(0, config?.chatMinIntervalMs ?? 500) - this.interactionMinIntervalMs = Math.max(0, config?.interactionMinIntervalMs ?? 80) - this.burstCapacity = Math.max(1, config?.burstCapacity ?? 12) - this.refillPerSec = Math.max(1, config?.refillPerSec ?? 20) + this.moveMinIntervalMs = Math.max(0, config?.moveMinIntervalMs ?? DEFAULT_RATE_LIMITS.moveMinIntervalMs) + this.skillMinIntervalMs = Math.max(0, config?.skillMinIntervalMs ?? DEFAULT_RATE_LIMITS.skillMinIntervalMs) + this.chatMinIntervalMs = Math.max(0, config?.chatMinIntervalMs ?? DEFAULT_RATE_LIMITS.chatMinIntervalMs) + this.interactionMinIntervalMs = Math.max(0, config?.interactionMinIntervalMs ?? DEFAULT_RATE_LIMITS.interactionMinIntervalMs) + this.burstCapacity = Math.max(1, config?.burstCapacity ?? DEFAULT_RATE_LIMITS.burstCapacity) + this.refillPerSec = Math.max(1, config?.refillPerSec ?? DEFAULT_RATE_LIMITS.refillPerSec) this.tokens = this.burstCapacity } diff --git a/src/client/world/client-unit.ts b/src/client/world/client-unit.ts index a063c20..5cd216f 100644 --- a/src/client/world/client-unit.ts +++ b/src/client/world/client-unit.ts @@ -403,7 +403,7 @@ export class ClientUnit { } else if (mode === 'run') { this.mode = this.unitType === 'monster' ? 15 : 3 } else if (mode === 'knockback') { - this.mode = this.unitType === 'monster' ? 9 : 19 + this.mode = this.unitType === 'monster' ? 13 : 19 } else { this.mode = 2 } diff --git a/src/client/world/client-world.ts b/src/client/world/client-world.ts index bbdc150..511b27e 100644 --- a/src/client/world/client-world.ts +++ b/src/client/world/client-world.ts @@ -26,6 +26,7 @@ import { getOverlayTxtData, getSkillCastOverlay, } from '../../common/skills/missiles-data.ts' +import { getSkillAnimModes } from '../../common/skills/skill-anim-modes.ts' import { CHAR_ANIM_CLIPS_113C, computeAnimFrameIndex, @@ -217,12 +218,6 @@ const CROSSBOW_ITEM_CODES = new Set([ '6lx', '6mx', '6hx', '6rx', ]) -/** 1.13c physical/weapon attack skill IDs that use attack animation mode (`A1`) instead of spellcast (`SC`). */ -const PHYSICAL_ATTACK_SKILL_IDS = new Set([ - 0, 2, 6, 7, 10, 11, 12, 16, 19, 20, 21, 22, 24, 26, 27, 31, 96, 97, 106, 107, 116, 126, 133, 139, - 140, 144, 147, 151, 152, -]) - /** Single-bolt linear projectile skills (1.13c Skills.txt srvmissile / cltmissilea). */ const LINEAR_SKILL_MISSILES: Readonly> = Object.freeze({ @@ -890,9 +885,9 @@ export class ClientWorld { // Turn toward the cast/attack target via D2Common.#10425 (bInstant = 0); 25Hz tick steps // `unit.direction` toward `unit.targetDirection`. unit.faceToward(targetX, targetY, false) - const isAttack = PHYSICAL_ATTACK_SKILL_IDS.has(skillId) + const animModes = getSkillAnimModes(skillId) + const mode = unit.unitType === 'monster' ? animModes.monsterMode : animModes.playerMode const isSelf = unit.unitType === 'player' && (unit.id === this.selfId || this.selfId === 0) - const mode = unit.unitType === 'monster' ? (isAttack ? 4 : 7) : isAttack ? 7 : 10 unit.mode = mode const castOverlayToken = getSkillCastOverlay(skillId) const now = this.resolveEventNowMs(nowMs) @@ -1701,7 +1696,7 @@ export class ClientWorld { unit.moveMode = 'stop' } else if ( unit.unitType === 'monster' && - (unit.mode === 2 || unit.mode === 9 || unit.mode === 15) + (unit.mode === 2 || unit.mode === 13 || unit.mode === 15) ) { unit.mode = 1 unit.moveMode = 'stop' @@ -2015,17 +2010,17 @@ export class ClientWorld { this.selfId = unit.id this._self.unitId = unit.id } - if (ev.unit.name) { - this._self.name = ev.unit.name + if (unit.name) { + this._self.name = unit.name } - if (ev.unit.classId >= 0 && ev.unit.classId <= 6) { - this._self.charClass = ev.unit.classId as CharClassId + if (unit.classId >= 0 && unit.classId <= 6) { + this._self.charClass = unit.classId as CharClassId } - if (ev.unit.x !== 0 || ev.unit.y !== 0) { - this._self.x = ev.unit.x - this._self.y = ev.unit.y - this._self.targetX = ev.unit.x - this._self.targetY = ev.unit.y + if (unit.x !== 0 || unit.y !== 0) { + this._self.x = unit.x + this._self.y = unit.y + this._self.targetX = unit.x + this._self.targetY = unit.y } } break @@ -2124,7 +2119,7 @@ export class ClientWorld { } else if (moveMode === 'run') { unit.mode = 15 } else if (moveMode === 'knockback') { - unit.mode = 9 + unit.mode = 13 } else { unit.mode = 2 } diff --git a/src/common/skills/skill-anim-modes.ts b/src/common/skills/skill-anim-modes.ts new file mode 100644 index 0000000..3f7f8dc --- /dev/null +++ b/src/common/skills/skill-anim-modes.ts @@ -0,0 +1,2174 @@ +/** + * Diablo II 1.13c Skills.txt Animation Mode Mapping. + * + * Maps every 1.13c skill ID to its player animation mode (PlrMode.txt, from Skills.txt anim) + * and monster animation mode (MonMode.txt, from Skills.txt monanim). + * + * Parity Ground Truth: + * - D2Common.dll / Skills.txt columns anim and monanim. + * - PlrMode: A1=7, A2=8, SC=10, TH=11, KK=12, S1=13, S2=14, S3=15, S4=16, SQ=18. + * - MonMode: A1=4, A2=5, SC=7, S1=8, S2=9, S3=10, S4=11. + */ + +export interface SkillAnimModeEntry { + readonly playerMode: number + readonly monsterMode: number + readonly anim: string + readonly monanim: string +} + +export const CANONICAL_SKILL_ANIM_MODES: Readonly> = Object.freeze({ + "0": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "1": { + "playerMode": 12, + "monsterMode": 4, + "anim": "KK", + "monanim": "XX" + }, + "2": { + "playerMode": 11, + "monsterMode": 4, + "anim": "TH", + "monanim": "XX" + }, + "3": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "4": { + "playerMode": 16, + "monsterMode": 7, + "anim": "S4", + "monanim": "XX" + }, + "5": { + "playerMode": 15, + "monsterMode": 10, + "anim": "S3", + "monanim": "S3" + }, + "6": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "7": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "8": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "9": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "10": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "11": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "12": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "13": { + "playerMode": 13, + "monsterMode": 7, + "anim": "S1", + "monanim": "XX" + }, + "14": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "15": { + "playerMode": 11, + "monsterMode": 4, + "anim": "TH", + "monanim": "XX" + }, + "16": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "17": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "18": { + "playerMode": 13, + "monsterMode": 7, + "anim": "S1", + "monanim": "XX" + }, + "19": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "20": { + "playerMode": 11, + "monsterMode": 4, + "anim": "TH", + "monanim": "XX" + }, + "21": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "22": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "23": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "24": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "25": { + "playerMode": 11, + "monsterMode": 4, + "anim": "TH", + "monanim": "XX" + }, + "26": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "27": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "28": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "29": { + "playerMode": 13, + "monsterMode": 7, + "anim": "S1", + "monanim": "XX" + }, + "30": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "31": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "32": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "33": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "34": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "35": { + "playerMode": 11, + "monsterMode": 4, + "anim": "TH", + "monanim": "XX" + }, + "36": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "37": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "38": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "39": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "40": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "41": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "42": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "43": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "44": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "45": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "46": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "47": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "48": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "49": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "50": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "51": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "52": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "53": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "54": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "55": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "56": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "57": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "58": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "59": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "60": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "61": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "62": { + "playerMode": 10, + "monsterMode": 8, + "anim": "SC", + "monanim": "S1" + }, + "63": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "64": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "65": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "66": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "67": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "68": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "69": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "70": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "71": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "72": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "73": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "74": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "75": { + "playerMode": 10, + "monsterMode": 8, + "anim": "SC", + "monanim": "S1" + }, + "76": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "77": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "78": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "79": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "80": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "81": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "82": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "83": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "84": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "85": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "86": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "87": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "88": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "89": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "90": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "91": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "92": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "93": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "94": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "95": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "96": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "97": { + "playerMode": 13, + "monsterMode": 7, + "anim": "S1", + "monanim": "XX" + }, + "98": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "99": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "100": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "101": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "102": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "103": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "104": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "105": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "106": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "107": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "108": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "109": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "110": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "111": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "112": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "113": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "114": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "115": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "116": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "117": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "118": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "119": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "120": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "121": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "122": { + "playerMode": 10, + "monsterMode": 8, + "anim": "SC", + "monanim": "S1" + }, + "123": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "124": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "125": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "126": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "127": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "128": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "129": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "130": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "131": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "132": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "133": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "134": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "135": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "136": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "137": { + "playerMode": 10, + "monsterMode": 8, + "anim": "SC", + "monanim": "S1" + }, + "138": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "139": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "140": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "141": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "142": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "143": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "144": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "145": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "146": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "147": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "148": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "149": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "150": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "151": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "152": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "153": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "154": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "155": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "156": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "157": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "158": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "159": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "160": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "161": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "162": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "163": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "164": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "165": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "166": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "167": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "168": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "169": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "170": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "171": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "172": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "173": { + "playerMode": 18, + "monsterMode": 5, + "anim": "SQ", + "monanim": "A2" + }, + "174": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "175": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "176": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "177": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "178": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "179": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "180": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "181": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "182": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "183": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "184": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "185": { + "playerMode": 18, + "monsterMode": 5, + "anim": "SQ", + "monanim": "A2" + }, + "186": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "187": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "188": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "189": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "190": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "191": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "192": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "193": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "194": { + "playerMode": 18, + "monsterMode": 9, + "anim": "SQ", + "monanim": "S2" + }, + "195": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "196": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "197": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "198": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "199": { + "playerMode": 18, + "monsterMode": 10, + "anim": "SQ", + "monanim": "S3" + }, + "200": { + "playerMode": 10, + "monsterMode": 8, + "anim": "SC", + "monanim": "S1" + }, + "201": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "202": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "203": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "204": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "205": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "206": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "207": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "208": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "209": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "210": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "211": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "212": { + "playerMode": 18, + "monsterMode": 9, + "anim": "SQ", + "monanim": "S2" + }, + "213": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "214": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "215": { + "playerMode": 18, + "monsterMode": 5, + "anim": "SQ", + "monanim": "A2" + }, + "216": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "217": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "218": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "219": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "220": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "221": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "222": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "223": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "224": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "225": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "226": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "227": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "228": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "229": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "230": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "231": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "232": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "233": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "234": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "235": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "236": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "237": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "238": { + "playerMode": 15, + "monsterMode": 10, + "anim": "S3", + "monanim": "S3" + }, + "239": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "240": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "241": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "242": { + "playerMode": 15, + "monsterMode": 10, + "anim": "S3", + "monanim": "S3" + }, + "243": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "244": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "245": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "246": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "247": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "248": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "249": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "250": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "251": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "252": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "253": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "254": { + "playerMode": 7, + "monsterMode": 5, + "anim": "A1", + "monanim": "A2" + }, + "255": { + "playerMode": 12, + "monsterMode": 4, + "anim": "KK", + "monanim": "A1" + }, + "256": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "257": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "258": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "259": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "260": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "261": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "262": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "263": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "264": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "265": { + "playerMode": 7, + "monsterMode": 5, + "anim": "A1", + "monanim": "A2" + }, + "266": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "267": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "268": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "269": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "270": { + "playerMode": 12, + "monsterMode": 4, + "anim": "KK", + "monanim": "A1" + }, + "271": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "272": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "273": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "274": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "275": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "276": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "277": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "278": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "279": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "280": { + "playerMode": 7, + "monsterMode": 5, + "anim": "A1", + "monanim": "A2" + }, + "281": { + "playerMode": 18, + "monsterMode": 9, + "anim": "SQ", + "monanim": "S2" + }, + "282": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "SC" + }, + "283": { + "playerMode": 18, + "monsterMode": 9, + "anim": "SQ", + "monanim": "S2" + }, + "284": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "285": { + "playerMode": 18, + "monsterMode": 10, + "anim": "SQ", + "monanim": "S3" + }, + "286": { + "playerMode": 18, + "monsterMode": 10, + "anim": "SQ", + "monanim": "S3" + }, + "287": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "288": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "289": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "290": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "291": { + "playerMode": 18, + "monsterMode": 9, + "anim": "SQ", + "monanim": "S2" + }, + "292": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "293": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "294": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "295": { + "playerMode": 18, + "monsterMode": 5, + "anim": "SQ", + "monanim": "A2" + }, + "296": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "297": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "298": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "299": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "300": { + "playerMode": 13, + "monsterMode": 8, + "anim": "S1", + "monanim": "S1" + }, + "301": { + "playerMode": 8, + "monsterMode": 5, + "anim": "A2", + "monanim": "A2" + }, + "302": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "303": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "304": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "305": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "306": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "307": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "308": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "XX" + }, + "309": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "310": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "311": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "312": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "313": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "314": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "315": { + "playerMode": 14, + "monsterMode": 9, + "anim": "S2", + "monanim": "S2" + }, + "316": { + "playerMode": 15, + "monsterMode": 10, + "anim": "S3", + "monanim": "S3" + }, + "317": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "318": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "319": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "320": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "321": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "322": { + "playerMode": 18, + "monsterMode": 4, + "anim": "SQ", + "monanim": "A1" + }, + "323": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "324": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "325": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "326": { + "playerMode": 13, + "monsterMode": 7, + "anim": "S1", + "monanim": "XX" + }, + "327": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "328": { + "playerMode": 18, + "monsterMode": 5, + "anim": "SQ", + "monanim": "A2" + }, + "329": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "330": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "331": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "332": { + "playerMode": 18, + "monsterMode": 8, + "anim": "SQ", + "monanim": "S1" + }, + "333": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "334": { + "playerMode": 10, + "monsterMode": 5, + "anim": "SC", + "monanim": "A2" + }, + "335": { + "playerMode": 10, + "monsterMode": 8, + "anim": "SC", + "monanim": "S1" + }, + "336": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "337": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "SC" + }, + "338": { + "playerMode": 10, + "monsterMode": 4, + "anim": "SC", + "monanim": "A1" + }, + "339": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "340": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "341": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "342": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "343": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "344": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "345": { + "playerMode": 10, + "monsterMode": 9, + "anim": "SC", + "monanim": "S2" + }, + "346": { + "playerMode": 10, + "monsterMode": 5, + "anim": "SC", + "monanim": "A2" + }, + "347": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + }, + "348": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "349": { + "playerMode": 10, + "monsterMode": 10, + "anim": "SC", + "monanim": "S3" + }, + "350": { + "playerMode": 10, + "monsterMode": 1, + "anim": "SC", + "monanim": "NU" + }, + "351": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "352": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "353": { + "playerMode": 10, + "monsterMode": 7, + "anim": "SC", + "monanim": "XX" + }, + "354": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "355": { + "playerMode": 18, + "monsterMode": 7, + "anim": "SQ", + "monanim": "XX" + }, + "356": { + "playerMode": 7, + "monsterMode": 4, + "anim": "A1", + "monanim": "A1" + } +}) + +/** + * Resolves the 1.13c animation mode for a skill cast by player or monster. + */ +export function getSkillAnimModes(skillId: number): { readonly playerMode: number; readonly monsterMode: number } { + const entry = CANONICAL_SKILL_ANIM_MODES[skillId] + if (entry) { + return entry + } + return { playerMode: 10, monsterMode: 7 } +} diff --git a/src/common/units/char-anim-speed.ts b/src/common/units/char-anim-speed.ts index d500341..03a9d28 100644 --- a/src/common/units/char-anim-speed.ts +++ b/src/common/units/char-anim-speed.ts @@ -199,7 +199,15 @@ const PLAYER_MODE_NUM_TO_CLIP: Readonly> = { 8: 'a2', 9: 'a1', 10: 'sc', + 11: 'a1', // TH (Throw) + 12: 'a1', // KK (Kick) + 13: 'a1', // S1 (Skill 1) + 14: 'sc', // S2 (Skill 2) + 15: 'a1', // S3 (Skill 3) + 16: 'a1', // S4 (Skill 4) 17: 'dd', + 18: 'a1', // SQ (Sequence) + 19: 'gh', // KB (Knock back, token GH) } /** diff --git a/src/common/world/level-warps-meta.ts b/src/common/world/level-warps-meta.ts new file mode 100644 index 0000000..b2986c3 --- /dev/null +++ b/src/common/world/level-warps-meta.ts @@ -0,0 +1,694 @@ +/** + * Canonical 1.13c Level Warp & Geometry Metadata (Levels.txt & LvlWarp.txt). + * + * Ground truth parsed from: + * - Levels.txt: column LevelWarp (key into string.tbl / expansionstring.tbl / patchstring.tbl) + * - LvlWarp.txt: columns SelectX, SelectY, SelectDX, SelectDY (hitbox bounds) + */ + +import { CANONICAL_ENG_TBL_DICT, CANONICAL_CHI_TBL_DICT } from '../data/canonical-tbl-strings.ts' + +export interface LvlWarpBounds { + readonly selectX: number + readonly selectY: number + readonly selectDX: number + readonly selectDY: number + readonly radiusX: number + readonly radiusY: number +} + +export const CANONICAL_LEVEL_WARP_KEYS: Readonly> = Object.freeze({ + "1": "To The Rogue Encampment", + "2": "To The Blood Moor", + "3": "To The Cold Plains", + "4": "To The Stony Field", + "5": "To The Dark Wood", + "6": "To The Black Marsh", + "7": "To The Tamoe Highland", + "8": "To The Den of Evil", + "9": "To The Cave Level 1", + "10": "To Underground Passage Level 1", + "11": "To The Hole Level 1", + "12": "To The Pit Level 1", + "13": "To The Cave Level 2", + "14": "To Underground Passage Level 2", + "15": "To The Hole Level 2", + "16": "To The Pit Level 2", + "17": "To The Burial Grounds", + "18": "To The Crypt", + "19": "To The Mausoleum", + "20": "To The Forgotten Tower", + "21": "To The Tower Cellar Level 1", + "22": "To The Tower Cellar Level 2", + "23": "To The Tower Cellar Level 3", + "24": "To The Tower Cellar Level 4", + "25": "To The Tower Cellar Level 5", + "26": "To The Monastery Gate", + "27": "To The Outer Cloister", + "28": "To The Barracks", + "29": "To The Jail Level 1", + "30": "To The Jail Level 2", + "31": "To The Jail Level 3", + "32": "To The Inner Cloister", + "33": "To The Cathedral", + "34": "To The Catacombs Level 1", + "35": "To The Catacombs Level 2", + "36": "To The Catacombs Level 3", + "37": "To The Catacombs Level 4", + "38": "To Tristram", + "39": "To The Moo Moo Farm", + "40": "To Lut Gholein", + "41": "To The Rocky Waste", + "42": "To The Dry Hills", + "43": "To The Far Oasis", + "44": "To The Lost City", + "45": "To The Valley of Snakes", + "46": "To The Canyon of the Magi", + "47": "To The Sewers Level 1", + "48": "To The Sewers Level 2", + "49": "To The Sewers Level 3", + "50": "To The Harem Level 1", + "51": "To The Harem Level 2", + "52": "To The Palace Cellar Level 1", + "53": "To The Palace Cellar Level 2", + "54": "To The Palace Cellar Level 3", + "55": "To The Stony Tomb Level 1", + "56": "To The Halls of the Dead Level 1", + "57": "To The Halls of the Dead Level 2", + "58": "To The Claw Viper Temple Level 1", + "59": "To The Stony Tomb Level 2", + "60": "To The Halls of the Dead Level 3", + "61": "To The Claw Viper Temple Level 2", + "62": "To The Maggot Lair Level 1", + "63": "To The Maggot Lair Level 2", + "64": "To The Maggot Lair Level 3", + "65": "To The Ancient Tunnels", + "66": "To Tal Rasha's Tomb", + "67": "To Tal Rasha's Tomb", + "68": "To Tal Rasha's Tomb", + "69": "To Tal Rasha's Tomb", + "70": "To Tal Rasha's Tomb", + "71": "To Tal Rasha's Tomb", + "72": "To Tal Rasha's Tomb", + "73": "To Duriel's Lair", + "74": "To The Arcane Sanctuary", + "75": "To The Kurast Docktown", + "76": "To The Spider Forest", + "77": "To The Great Marsh", + "78": "To The Flayer Jungle", + "79": "To Lower Kurast", + "80": "To The Kurast Bazaar", + "81": "To Upper Kurast", + "82": "To The Kurast Causeway", + "83": "To Travincal", + "84": "To The Spider Cave", + "85": "To The Spider Cavern", + "86": "To The Swampy Pit Level 1", + "87": "To The Swampy Pit Level 2", + "88": "To The Flayer Dungeon Level 1", + "89": "To The Flayer Dungeon Level 2", + "90": "To The Swampy Pit Level 3", + "91": "To The Flayer Dungeon Level 3", + "92": "To The Sewers Level 1", + "93": "To The Sewers Level 2", + "94": "To The Ruined Temple", + "95": "To The Disused Fane", + "96": "To The Forgotten Reliquary", + "97": "To The Forgotten Temple", + "98": "To The Ruined Fane", + "99": "To The Disused Reliquary", + "100": "To The Durance of Hate Level 1", + "101": "To The Durance of Hate Level 2", + "102": "To The Durance of Hate Level 3", + "103": "To The Pandemonium Fortress", + "104": "To The Outer Steppes", + "105": "To The Plains of Despair", + "106": "To The City of the Damned", + "107": "To The River of Flame", + "108": "To Chaos Sanctum", + "109": "To Harrogath", + "110": "To The Bloody Foothills", + "111": "To The Rigid Highlands", + "112": "To The Arreat Plateau", + "113": "To The Crystalized Cavern Level 1", + "114": "To The Cellar of Pity", + "115": "To The Crystalized Cavern Level 2", + "116": "To The Echo Chamber", + "117": "To The Tundra Wastelands", + "118": "To The Glacier Caves Level 1", + "119": "To The Glacier Caves Level 2", + "120": "To The Rocky Summit", + "121": "To Nihlathaks Temple", + "122": "To The Halls of Anguish", + "123": "To The Halls of Death's Calling", + "124": "To The Halls of Vaught", + "125": "To Hell1", + "126": "To Hell2", + "127": "To Hell3", + "128": "To The Worldstone Keep Level 1", + "129": "To The Worldstone Keep Level 2", + "130": "To The Worldstone Keep Level 3", + "131": "To The Throne of Destruction", + "132": "To The Worldstone Chamber", + "133": "To The Pandemonium Run 1", + "134": "To The Pandemonium Run 2", + "135": "To The Pandemonium Run 3", + "136": "To Tristram" +}) + +export const CANONICAL_LVLWARP_GEOMETRY: Readonly> = Object.freeze({ + "0": { + "selectX": -90, + "selectY": -100, + "selectDX": 90, + "selectDY": 110 + }, + "1": { + "selectX": -15, + "selectY": -90, + "selectDX": 90, + "selectDY": 110 + }, + "2": { + "selectX": -60, + "selectY": -60, + "selectDX": 85, + "selectDY": 100 + }, + "3": { + "selectX": -25, + "selectY": -60, + "selectDX": 85, + "selectDY": 100 + }, + "4": { + "selectX": -30, + "selectY": -120, + "selectDX": 120, + "selectDY": 150 + }, + "5": { + "selectX": -25, + "selectY": -75, + "selectDX": 80, + "selectDY": 100 + }, + "6": { + "selectX": -90, + "selectY": -110, + "selectDX": 110, + "selectDY": 140 + }, + "7": { + "selectX": -70, + "selectY": -90, + "selectDX": 90, + "selectDY": 120 + }, + "8": { + "selectX": -10, + "selectY": -90, + "selectDX": 60, + "selectDY": 110 + }, + "9": { + "selectX": -30, + "selectY": -90, + "selectDX": 60, + "selectDY": 110 + }, + "10": { + "selectX": -10, + "selectY": -50, + "selectDX": 150, + "selectDY": 80 + }, + "11": { + "selectX": -50, + "selectY": -130, + "selectDX": 60, + "selectDY": 140 + }, + "12": { + "selectX": -50, + "selectY": -90, + "selectDX": 80, + "selectDY": 110 + }, + "13": { + "selectX": -50, + "selectY": -110, + "selectDX": 110, + "selectDY": 150 + }, + "14": { + "selectX": -40, + "selectY": -80, + "selectDX": 210, + "selectDY": 110 + }, + "15": { + "selectX": -30, + "selectY": -110, + "selectDX": 85, + "selectDY": 130 + }, + "16": { + "selectX": -35, + "selectY": -130, + "selectDX": 90, + "selectDY": 150 + }, + "17": { + "selectX": -50, + "selectY": -110, + "selectDX": 110, + "selectDY": 150 + }, + "18": { + "selectX": -60, + "selectY": -80, + "selectDX": 210, + "selectDY": 110 + }, + "19": { + "selectX": -10, + "selectY": -10, + "selectDX": 0, + "selectDY": 0 + }, + "20": { + "selectX": -25, + "selectY": -95, + "selectDX": 70, + "selectDY": 130 + }, + "21": { + "selectX": 20, + "selectY": -80, + "selectDX": 65, + "selectDY": 140 + }, + "22": { + "selectX": -25, + "selectY": -160, + "selectDX": 100, + "selectDY": 180 + }, + "23": { + "selectX": -65, + "selectY": -55, + "selectDX": 150, + "selectDY": 110 + }, + "24": { + "selectX": -120, + "selectY": -20, + "selectDX": 150, + "selectDY": 130 + }, + "25": { + "selectX": -35, + "selectY": -70, + "selectDX": 95, + "selectDY": 130 + }, + "26": { + "selectX": -25, + "selectY": -150, + "selectDX": 80, + "selectDY": 180 + }, + "27": { + "selectX": -40, + "selectY": -160, + "selectDX": 80, + "selectDY": 180 + }, + "28": { + "selectX": -20, + "selectY": -40, + "selectDX": 170, + "selectDY": 100 + }, + "29": { + "selectX": -50, + "selectY": -20, + "selectDX": 160, + "selectDY": 85 + }, + "30": { + "selectX": -25, + "selectY": -100, + "selectDX": 60, + "selectDY": 125 + }, + "31": { + "selectX": -40, + "selectY": -100, + "selectDX": 60, + "selectDY": 125 + }, + "32": { + "selectX": -30, + "selectY": -30, + "selectDX": 170, + "selectDY": 90 + }, + "33": { + "selectX": -70, + "selectY": -140, + "selectDX": 80, + "selectDY": 160 + }, + "34": { + "selectX": -50, + "selectY": -120, + "selectDX": 90, + "selectDY": 150 + }, + "35": { + "selectX": -30, + "selectY": -140, + "selectDX": 80, + "selectDY": 160 + }, + "36": { + "selectX": 0, + "selectY": -140, + "selectDX": 100, + "selectDY": 150 + }, + "37": { + "selectX": -30, + "selectY": -140, + "selectDX": 80, + "selectDY": 160 + }, + "38": { + "selectX": -40, + "selectY": -190, + "selectDX": 90, + "selectDY": 210 + }, + "39": { + "selectX": -40, + "selectY": -210, + "selectDX": 50, + "selectDY": 230 + }, + "40": { + "selectX": -30, + "selectY": -100, + "selectDX": 60, + "selectDY": 140 + }, + "41": { + "selectX": -90, + "selectY": -140, + "selectDX": 70, + "selectDY": 160 + }, + "42": { + "selectX": -10, + "selectY": -110, + "selectDX": 60, + "selectDY": 150 + }, + "43": { + "selectX": 0, + "selectY": -130, + "selectDX": 90, + "selectDY": 150 + }, + "44": { + "selectX": -10, + "selectY": -210, + "selectDX": 70, + "selectDY": 240 + }, + "45": { + "selectX": -30, + "selectY": -115, + "selectDX": 75, + "selectDY": 145 + }, + "46": { + "selectX": -45, + "selectY": -120, + "selectDX": 85, + "selectDY": 145 + }, + "47": { + "selectX": -10, + "selectY": -60, + "selectDX": 90, + "selectDY": 90 + }, + "48": { + "selectX": -20, + "selectY": -70, + "selectDX": 80, + "selectDY": 85 + }, + "49": { + "selectX": -115, + "selectY": -10, + "selectDX": 160, + "selectDY": 70 + }, + "50": { + "selectX": -10, + "selectY": -10, + "selectDX": 0, + "selectDY": 0 + }, + "51": { + "selectX": -60, + "selectY": -70, + "selectDX": 70, + "selectDY": 90 + }, + "52": { + "selectX": 10, + "selectY": -100, + "selectDX": 80, + "selectDY": 110 + }, + "53": { + "selectX": -15, + "selectY": -70, + "selectDX": 70, + "selectDY": 110 + }, + "54": { + "selectX": -130, + "selectY": -10, + "selectDX": 170, + "selectDY": 100 + }, + "55": { + "selectX": -10, + "selectY": -100, + "selectDX": 90, + "selectDY": 130 + }, + "56": { + "selectX": -40, + "selectY": -20, + "selectDX": 170, + "selectDY": 100 + }, + "57": { + "selectX": -60, + "selectY": -40, + "selectDX": 120, + "selectDY": 90 + }, + "58": { + "selectX": -75, + "selectY": -110, + "selectDX": 100, + "selectDY": 150 + }, + "59": { + "selectX": -25, + "selectY": -110, + "selectDX": 100, + "selectDY": 150 + }, + "60": { + "selectX": -10, + "selectY": -10, + "selectDX": 0, + "selectDY": 0 + }, + "61": { + "selectX": -10, + "selectY": -10, + "selectDX": 0, + "selectDY": 0 + }, + "62": { + "selectX": -50, + "selectY": -110, + "selectDX": 80, + "selectDY": 140 + }, + "63": { + "selectX": -30, + "selectY": -110, + "selectDX": 80, + "selectDY": 140 + }, + "64": { + "selectX": -10, + "selectY": -10, + "selectDX": 0, + "selectDY": 0 + }, + "65": { + "selectX": -65, + "selectY": -150, + "selectDX": 125, + "selectDY": 180 + }, + "66": { + "selectX": -60, + "selectY": -150, + "selectDX": 125, + "selectDY": 180 + }, + "67": { + "selectX": -65, + "selectY": -150, + "selectDX": 125, + "selectDY": 180 + }, + "68": { + "selectX": -60, + "selectY": -150, + "selectDX": 125, + "selectDY": 180 + }, + "69": { + "selectX": -60, + "selectY": -40, + "selectDX": 150, + "selectDY": 160 + }, + "70": { + "selectX": -90, + "selectY": -50, + "selectDX": 160, + "selectDY": 120 + }, + "71": { + "selectX": -15, + "selectY": -90, + "selectDX": 70, + "selectDY": 100 + }, + "72": { + "selectX": -20, + "selectY": -120, + "selectDX": 75, + "selectDY": 100 + }, + "73": { + "selectX": -10, + "selectY": -120, + "selectDX": 100, + "selectDY": 120 + }, + "74": { + "selectX": -20, + "selectY": -110, + "selectDX": 90, + "selectDY": 115 + }, + "75": { + "selectX": -5, + "selectY": -110, + "selectDX": 80, + "selectDY": 110 + }, + "76": { + "selectX": 0, + "selectY": -110, + "selectDX": 95, + "selectDY": 100 + }, + "77": { + "selectX": 0, + "selectY": -70, + "selectDX": 140, + "selectDY": 80 + }, + "78": { + "selectX": -20, + "selectY": -110, + "selectDX": 100, + "selectDY": 110 + }, + "79": { + "selectX": -40, + "selectY": -110, + "selectDX": 0, + "selectDY": 0 + }, + "80": { + "selectX": -20, + "selectY": -110, + "selectDX": 0, + "selectDY": 0 + }, + "81": { + "selectX": -20, + "selectY": -110, + "selectDX": 80, + "selectDY": 115 + }, + "82": { + "selectX": -20, + "selectY": -110, + "selectDX": 80, + "selectDY": 115 + } +}) + +/** + * Resolves the localized display name for a destination level's warp. + */ +export function resolveLevelWarpName(toLevelId: number): { readonly name: string; readonly nameZh: string } | undefined { + const warpKey = CANONICAL_LEVEL_WARP_KEYS[toLevelId] + if (!warpKey) return undefined + const name = CANONICAL_ENG_TBL_DICT[warpKey] ?? warpKey + const nameZh = CANONICAL_CHI_TBL_DICT[warpKey] ?? name + return { name, nameZh } +} + +/** + * Resolves the 1.13c LvlWarp.txt hover/selection bounding box and pixel radii for a warp ID. + */ +export function resolveLvlWarpBounds(warpId: number): LvlWarpBounds { + const geom = CANONICAL_LVLWARP_GEOMETRY[warpId] + if (geom && geom.selectDX > 0 && geom.selectDY > 0) { + return { + selectX: geom.selectX, + selectY: geom.selectY, + selectDX: geom.selectDX, + selectDY: geom.selectDY, + radiusX: Math.round(geom.selectDX / 2), + radiusY: Math.round(geom.selectDY / 2), + } + } + return { + selectX: -28, + selectY: -20, + selectDX: 56, + selectDY: 40, + radiusX: 28, + radiusY: 20, + } +} diff --git a/src/netproto/d2gs/s2c/units.ts b/src/netproto/d2gs/s2c/units.ts index 431c2aa..73c370a 100644 --- a/src/netproto/d2gs/s2c/units.ts +++ b/src/netproto/d2gs/s2c/units.ts @@ -145,15 +145,15 @@ export const MONSTER_CMD_TO_MODE: readonly number[] = [ 12, // 0x09: DD (Dead corpse) 4, // 0x0A: A1 (Attack 1 with target) 4, // 0x0B: A1 (Attack 1 no target) - 8, // 0x0C: SQ (Sequence no target) - 8, // 0x0D: SQ (Sequence with target) - 9, // 0x0E: KB (Knockback no target) - 9, // 0x0F: KB (Knockback with target) + 8, // 0x0C: S1 (Skill 1 no target) + 8, // 0x0D: S1 (Skill 1 with target) + 9, // 0x0E: S2 (Skill 2 no target) + 9, // 0x0F: S2 (Skill 2 with target) 5, // 0x10: A2 (Attack 2 with target) 5, // 0x11: A2 (Attack 2 no target) 6, // 0x12: BL (Block) 16, // 0x13: special - 13, // 0x14: S2 (Skill 2) + 13, // 0x14: KB (Knockback) 14, // 0x15: S3 (Skill 3) 14, // 0x16: S3 (Skill 3) 15, // 0x17: RN (0x67 run) diff --git a/tests/client/timing-modes-parity.test.ts b/tests/client/timing-modes-parity.test.ts new file mode 100644 index 0000000..d8d2e5b --- /dev/null +++ b/tests/client/timing-modes-parity.test.ts @@ -0,0 +1,261 @@ +/** + * Parity Test Suite for Batch 1 Phase 3 Issues: #565, #566, #573, #575. + * + * Ground truth parity verified against: + * - #565: Skills.txt anim / monanim mode mapping (PlrMode / MonMode) + * - #566: Monster knockback MonMode 13 (KB) vs 9 (S2), arrival reset, and clip mapping + * - #573: Warp hover parity (Levels.txt LevelWarp -> tbl string, LvlWarp.txt SelectDX/DY bounds) + * - #575: 1.13c non-movement rate limiter bypass and 200ms tick delta clamping + */ + +import { describe, expect, it, vi } from 'vitest' +import { getSkillAnimModes } from '../../src/common/skills/skill-anim-modes.ts' +import { resolveLevelWarpName, resolveLvlWarpBounds } from '../../src/common/world/level-warps-meta.ts' +import { OutboundRateLimiter, DEFAULT_RATE_LIMITS } from '../../src/client/session/rate-limiter.ts' +import { ClientUnit } from '../../src/client/world/client-unit.ts' +import { ClientWorld } from '../../src/client/world/client-world.ts' +import { OnlineSession } from '../../src/client/session/online-session.ts' +import type { ClientCommand } from '../../src/netproto/domain/client-command.ts' + +describe('Batch 1 Phase 3 Parity (#565, #566, #573, #575)', () => { + // ─── #565: Skills.txt anim / monanim mode mapping ───────────────────────── + describe('#565: Skills.txt anim / monanim mode mapping', () => { + it('maps skill animations to 1.13c PlrMode and MonMode values without hardcoded physical sets', () => { + // Throw (2) -> anim: TH (PlrMode 11) + const throwModes = getSkillAnimModes(2) + expect(throwModes.playerMode).toBe(11) + + // Kick (1) -> anim: KK (PlrMode 12) + const kickModes = getSkillAnimModes(1) + expect(kickModes.playerMode).toBe(12) + + // Smite (97) -> anim: S1 (PlrMode 13) + const smiteModes = getSkillAnimModes(97) + expect(smiteModes.playerMode).toBe(13) + + // Charge (107) -> anim: SQ (PlrMode 18) + const chargeModes = getSkillAnimModes(107) + expect(chargeModes.playerMode).toBe(18) + + // Teleport (54) -> anim: SC (PlrMode 10) + const tpModes = getSkillAnimModes(54) + expect(tpModes.playerMode).toBe(10) + + // Standard Attack (0) -> anim: A1 (PlrMode 7) + const attackModes = getSkillAnimModes(0) + expect(attackModes.playerMode).toBe(7) + }) + + it('sets player unit mode according to Skills.txt anim in ClientWorld SkillCast', () => { + const world = new ClientWorld() + world.apply({ + type: 'UnitAssign', + unit: { unitType: 0, id: 1, classId: 0, name: 'Amazon', x: 100, y: 100 }, + isSelf: true, + }) + world.selfId = 1 + const player = world.getUnit('player', 1)! + expect(player).toBeDefined() + + // Cast Throw (skillId 2) -> should set mode 11 (TH), not attack (7) or cast (10) + world.apply({ + type: 'SkillCast', + caster: { unitType: 0, id: 1 }, + skillId: 2, + target: { x: 105, y: 100 }, + }) + expect(player.mode).toBe(11) + + // Cast Smite (skillId 97) -> should set mode 13 (S1) + world.apply({ + type: 'SkillCast', + caster: { unitType: 0, id: 1 }, + skillId: 97, + target: { x: 105, y: 100 }, + }) + expect(player.mode).toBe(13) + }) + }) + + // ─── #566: Monster knockback mode 13 vs 9 ───────────────────────────────── + describe('#566: Monster knockback MonMode 13 (KB) parity', () => { + it('sets monster mode to 13 (KB) on knockback in ClientUnit.applyMove', () => { + const monster = new ClientUnit({ + id: 10, + unitType: 'monster', + classId: 1, + x: 50, + y: 50, + name: 'Zombie', + }) + monster.applyMove('knockback', { x: 55, y: 50 }, { x: 50, y: 50 }) + expect(monster.mode).toBe(13) + + const player = new ClientUnit({ + id: 1, + unitType: 'player', + classId: 0, + x: 10, + y: 10, + name: 'Barbarian', + }) + player.applyMove('knockback', { x: 15, y: 10 }, { x: 10, y: 10 }) + expect(player.mode).toBe(19) // PlrMode KB is 19 + }) + + it('resets monster mode 13 (KB) to 1 (NU) upon destination arrival, but preserves mode 9 (S2)', () => { + const world = new ClientWorld() + world.apply({ + type: 'UnitAssign', + unit: { unitType: 1, id: 101, classId: 1, x: 49.95, y: 50 }, + }) + const monsterKb = world.getUnit('monster', 101)! + monsterKb.mode = 13 + monsterKb.moveMode = 'knockback' + monsterKb.targetX = 50 + monsterKb.targetY = 50 + + // Monster in mode 9 (legitimate skill 2 cast) + world.apply({ + type: 'UnitAssign', + unit: { unitType: 1, id: 102, classId: 1, x: 60, y: 60 }, + }) + const monsterS2 = world.getUnit('monster', 102)! + monsterS2.mode = 9 + monsterS2.moveMode = 'stop' + monsterS2.targetX = 60 + monsterS2.targetY = 60 + + // Step tick (simulating arrival at destination) + world.tick(40, 1000) + + // Knockback monster arrived: mode 13 resets to 1 (NU) + expect(monsterKb.mode).toBe(1) + expect(monsterKb.moveMode).toBe('stop') + + // S2 monster: mode 9 MUST NOT be reset to 1! + expect(monsterS2.mode).toBe(9) + }) + }) + + // ─── #573: Warp hover parity ────────────────────────────────────────────── + describe('#573: Warp hover localized naming & geometry hitbox', () => { + it('resolves localized level warp names from Levels.txt LevelWarp and .tbl', () => { + const denNames = resolveLevelWarpName(8) + expect(denNames).toBeDefined() + expect(denNames?.name).toBe('To The Den of Evil') + expect(denNames?.nameZh).toBeTruthy() + + const bloodMoorNames = resolveLevelWarpName(2) + expect(bloodMoorNames).toBeDefined() + expect(bloodMoorNames?.name).toBe('To The Blood Moor') + }) + + it('resolves accurate selection bounds from LvlWarp.txt SelectDX/SelectDY', () => { + // Warp 0: SelectDX=90, SelectDY=110 -> radiusX=45, radiusY=55 + const b0 = resolveLvlWarpBounds(0) + expect(b0.selectDX).toBe(90) + expect(b0.selectDY).toBe(110) + expect(b0.radiusX).toBe(45) + expect(b0.radiusY).toBe(55) + + // Warp 10: SelectDX=150, SelectDY=80 -> radiusX=75, radiusY=40 + const b10 = resolveLvlWarpBounds(10) + expect(b10.selectDX).toBe(150) + expect(b10.selectDY).toBe(80) + expect(b10.radiusX).toBe(75) + expect(b10.radiusY).toBe(40) + }) + + it('populates accurate warp name and bounds in OnlineSession.buildRenderableUnits', () => { + const session = new OnlineSession() + session.levelView = { + act: 0, + areaId: 2, + subOriginX: 5000, + subOriginY: 5000, + subWidth: 100, + subHeight: 100, + isoOriginX: 0, + isoOriginY: 0, + cellsX: 20, + cellsY: 20, + gridWidth: 100, + blocked: new Uint8Array(100), + walkable: new Uint8Array(100).fill(1), + collisionMasks: new Uint16Array(100), + rooms: [], + floors: [], + shadows: [], + walls: [], + roofs: [], + warps: [{ lvlWarpId: 4, toLevelId: 8 }], + presets: [], + } as any + + session.handleServerEvent({ + type: 'UnitAssign', + unit: { unitType: 5, id: 301, classId: 4, warpId: 4, x: 5020, y: 5020 } as any, + }) + + const renderables = session.buildRenderableUnits() + const warp = renderables.find((u) => u.id === 301) + expect(warp).toBeDefined() + expect(warp?.name).toBe('To The Den of Evil') + expect(warp?.nameZh).toBeTruthy() + expect(warp?.radiusX).toBeGreaterThan(0) + expect(warp?.radiusY).toBeGreaterThan(0) + session.destroy() + }) + }) + + // ─── #575: Non-movement rate limiter bypass & tick clamp ──────────────────── + describe('#575: Rate limiter non-movement bypass & tick clamp', () => { + it('does not throttle consecutive non-movement commands with DEFAULT_RATE_LIMITS', () => { + const limiter = new OutboundRateLimiter() + expect(DEFAULT_RATE_LIMITS.skillMinIntervalMs).toBe(0) + expect(DEFAULT_RATE_LIMITS.interactionMinIntervalMs).toBe(0) + + const interactCmd: ClientCommand = { + type: 'Interact', + target: { unitType: 2, id: 50 }, + } + + // Consecutive rapid interaction clicks within 10ms + const first = limiter.tryAcquire(interactCmd, 1000) + const second = limiter.tryAcquire(interactCmd, 1010) + const third = limiter.tryAcquire(interactCmd, 1020) + + expect(first.allowed).toBe(true) + expect(second.allowed).toBe(true) + expect(third.allowed).toBe(true) + }) + + it('always allows non-movement commands in OnlineSession.dispatchCommand without swallowing', () => { + const session = new OnlineSession() + const noteSpy = vi.spyOn(session.world, 'noteOutgoingCommand') + + const cmd1: ClientCommand = { type: 'Interact', target: { unitType: 2, id: 10 } } + const cmd2: ClientCommand = { type: 'Interact', target: { unitType: 2, id: 10 } } + + expect(session.dispatchCommand(cmd1, 1000)).toBe(true) + expect(session.dispatchCommand(cmd2, 1005)).toBe(true) + expect(noteSpy).toHaveBeenCalledTimes(2) + + session.destroy() + }) + + it('clamps delta-time accumulator to 200ms in OnlineSession.tick to prevent death spiral', () => { + const session = new OnlineSession() + const worldTickSpy = vi.spyOn(session.world, 'tick') + + // Tick with huge dtMs = 5000 (e.g. background tab or lag spike) + session.tick(5000, 6000) + + // World tick should have received clampedDt = 200 + expect(worldTickSpy).toHaveBeenCalledWith(200, 6000) + + session.destroy() + }) + }) +})