mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 08:02:38 +00:00
Imported .codex-pet bundles played every animation at a flat 8 fps (~9x too fast). Mirror Codex's exact per-frame duration tables, render uneven holds as step-end keyframes, upgrade legacy-persisted pets at render, and add the Codex mascot's pointer interactions (hover, grab-and-hold on frame 0, horizontal-drag running). Hardened over several adversarial review rounds: honor explicit fps, start each row/pet from frame 0, scope drag to its pointer, and guard untrusted persisted data. Fixes #8729 Co-authored-by: nasagong <zinho2000@gachon.ac.kr>
112 lines
3.9 KiB
TypeScript
112 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
CODEX_PET_ANIMATIONS,
|
|
CODEX_PET_DEFAULT_COLUMNS,
|
|
CODEX_PET_DEFAULT_FPS,
|
|
applyCodexSpriteTimingDefaults,
|
|
type CustomPetSprite
|
|
} from './codex-pet-sprite-defaults'
|
|
|
|
function legacyCodexSprite(): CustomPetSprite {
|
|
return {
|
|
frameWidth: 192,
|
|
frameHeight: 208,
|
|
columns: 8,
|
|
rows: 9,
|
|
sheetWidth: 1536,
|
|
sheetHeight: 1872,
|
|
fps: CODEX_PET_DEFAULT_FPS,
|
|
defaultAnimation: 'idle',
|
|
// Pre-durations builds baked exactly this shape from the old defaults.
|
|
animations: Object.fromEntries(
|
|
Object.entries(CODEX_PET_ANIMATIONS).map(([name, anim]) => [
|
|
name,
|
|
{ row: anim.row, frames: anim.frames }
|
|
])
|
|
)
|
|
}
|
|
}
|
|
|
|
describe('CODEX_PET_ANIMATIONS', () => {
|
|
it('ships per-frame durations matching each frame count', () => {
|
|
for (const [name, anim] of Object.entries(CODEX_PET_ANIMATIONS)) {
|
|
expect(anim.frameDurationsMs, name).toBeDefined()
|
|
expect(anim.frameDurationsMs, name).toHaveLength(anim.frames)
|
|
for (const ms of anim.frameDurationsMs ?? []) {
|
|
expect(ms, name).toBeGreaterThan(0)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('keeps the Codex ambient idle cycle at 6.6 seconds', () => {
|
|
const total = (CODEX_PET_ANIMATIONS.idle.frameDurationsMs ?? []).reduce(
|
|
(sum, ms) => sum + ms,
|
|
0
|
|
)
|
|
expect(total).toBe(6600)
|
|
})
|
|
})
|
|
|
|
describe('applyCodexSpriteTimingDefaults', () => {
|
|
it('upgrades the legacy baked-default fingerprint with durations', () => {
|
|
const upgraded = applyCodexSpriteTimingDefaults(legacyCodexSprite())
|
|
expect(upgraded.animations).toEqual(CODEX_PET_ANIMATIONS)
|
|
expect(upgraded.animations?.idle.frameDurationsMs).toEqual([1680, 660, 660, 840, 840, 1920])
|
|
})
|
|
|
|
it('leaves hand-authored animation layouts untouched', () => {
|
|
const sprite = legacyCodexSprite()
|
|
sprite.animations = { blink: { row: 0, frames: 2 } }
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
|
|
it('leaves sprites with a non-default fps untouched', () => {
|
|
const sprite = { ...legacyCodexSprite(), fps: 4 }
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
|
|
it('leaves sprites that already declare durations untouched', () => {
|
|
const sprite = legacyCodexSprite()
|
|
sprite.animations = {
|
|
...sprite.animations,
|
|
idle: { row: 0, frames: 6, frameDurationsMs: [100, 100, 100, 100, 100, 100] }
|
|
}
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
|
|
it('leaves sprites without animations untouched', () => {
|
|
const sprite = { ...legacyCodexSprite(), animations: undefined }
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
|
|
it('leaves a hand-authored 8 fps sheet that only reuses the Codex row map untouched', () => {
|
|
// Same fps + nine Codex rows, but non-Codex frame geometry — must not retime.
|
|
const sprite: CustomPetSprite = {
|
|
...legacyCodexSprite(),
|
|
frameWidth: 32,
|
|
frameHeight: 32,
|
|
sheetWidth: CODEX_PET_DEFAULT_COLUMNS * 32,
|
|
sheetHeight: 9 * 32
|
|
}
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
|
|
it('leaves a sheet with a non-idle default animation untouched', () => {
|
|
const sprite = { ...legacyCodexSprite(), defaultAnimation: 'running' }
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
|
|
it('leaves a sheet with a non-Codex column count untouched', () => {
|
|
const sprite = { ...legacyCodexSprite(), columns: CODEX_PET_DEFAULT_COLUMNS + 2 }
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
|
|
it('does not throw on a corrupted animation entry that matches the outer fingerprint', () => {
|
|
const sprite = legacyCodexSprite()
|
|
// Persisted data is untrusted: a null value under a matching key must be a
|
|
// non-match, not a crash.
|
|
sprite.animations = { ...sprite.animations, idle: null as never }
|
|
expect(applyCodexSpriteTimingDefaults(sprite)).toBe(sprite)
|
|
})
|
|
})
|