'use strict'; const { getPetAsset, getAvailablePetTypes } = require('./assets/pets'); const DEFAULT_CONFIG = { enabled: true, petCount: 1, petTypes: ['cat'], maxToys: 1, toyCooldownMs: 20000, successToyChance: 0.35, chaseCursorChance: 0.04, walkSpeed: 0.45, animationSpeed: 0.5, sleepMinDurationMs: 30000, sleepMaxDurationMs: 180000, debugFrames: false, reducedMotion: false, onlyActivePane: true }; const listenersByUid = new Map(); const sessionStateByUid = new Map(); let runtimeConfig = DEFAULT_CONFIG; const ANSI_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g; const ERROR_RE = /\b(command not found|not recognized|no such file|error|exception|failed|traceback)\b/i; const PROMPT_RE = /(?:^|\n)[^\n]{0,80}(?:\$|%|>|❯|➜|λ)\s?$/; function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function normalizeConfig(input) { const next = Object.assign({}, DEFAULT_CONFIG, input || {}); const availablePetTypes = getAvailablePetTypes(); next.petCount = clamp(Number(next.petCount) || 1, 1, 2); next.maxToys = clamp(Number(next.maxToys) || 1, 0, 3); next.toyCooldownMs = Math.max(Number(next.toyCooldownMs) || DEFAULT_CONFIG.toyCooldownMs, 3000); next.successToyChance = clamp(Number(next.successToyChance), 0, 1); next.chaseCursorChance = clamp(Number(next.chaseCursorChance), 0, 1); next.walkSpeed = Math.max(Number(next.walkSpeed) || DEFAULT_CONFIG.walkSpeed, 0.2); next.animationSpeed = Math.max(Number(next.animationSpeed) || DEFAULT_CONFIG.animationSpeed, 0.15); next.sleepMinDurationMs = Math.max(Number(next.sleepMinDurationMs) || DEFAULT_CONFIG.sleepMinDurationMs, 1000); next.sleepMaxDurationMs = Math.max(Number(next.sleepMaxDurationMs) || DEFAULT_CONFIG.sleepMaxDurationMs, next.sleepMinDurationMs); next.debugFrames = Boolean(next.debugFrames); next.petTypes = Array.isArray(next.petTypes) && next.petTypes.length ? next.petTypes : DEFAULT_CONFIG.petTypes; next.petTypes = next.petTypes.filter((type) => availablePetTypes.includes(type)); if (next.petTypes.length === 0) { next.petTypes = DEFAULT_CONFIG.petTypes.slice(); } return next; } function findHyperPetsConfig(value, depth) { if (!value || typeof value !== 'object' || depth > 4) { return null; } if (value.hyperPets && typeof value.hyperPets === 'object') { return value.hyperPets; } const keys = Object.keys(value); for (let index = 0; index < keys.length; index += 1) { const nested = value[keys[index]]; if (nested && typeof nested === 'object') { const found = findHyperPetsConfig(nested, depth + 1); if (found) { return found; } } } return null; } function refreshRuntimeConfigFromRenderer() { try { if (typeof window === 'undefined') { return runtimeConfig; } const candidates = [ window.__hyperpetsRuntimeConfig, window.config, window.config && window.config.getConfig ? window.config.getConfig() : null, window.store && window.store.getState ? window.store.getState() : null ]; for (let index = 0; index < candidates.length; index += 1) { const found = findHyperPetsConfig(candidates[index], 0); if (found) { const normalized = normalizeConfig(found); runtimeConfig = normalized; window.__hyperpetsRuntimeConfig = normalized; return normalized; } } } catch (error) { console.warn('[hyperpets] failed to refresh renderer config', error); } return runtimeConfig; } function randomIdleClip() { const variants = ['idle0', 'idle1', 'idle2', 'idle3']; return variants[Math.floor(Math.random() * variants.length)]; } function randomSleepDuration() { const min = runtimeConfig.sleepMinDurationMs; const max = runtimeConfig.sleepMaxDurationMs; if (max <= min) { return min; } return min + Math.round(Math.random() * (max - min)); } function isIdleClipName(clipName) { return typeof clipName === 'string' && /^idle\d*$/.test(clipName); } function getDefaultClipName(petAsset) { if (petAsset.clips.idle0) { return 'idle0'; } if (petAsset.clips.idle) { return 'idle'; } return Object.keys(petAsset.clips)[0]; } function getEffectiveClipFps(clip, reducedMotion) { const baseFps = clip.fps * runtimeConfig.animationSpeed; return reducedMotion ? Math.max(baseFps * 0.6, 1) : Math.max(baseFps, 1); } function modeToClip(pet) { const mode = pet.mode; const facingRight = pet.facing >= 0; const petAsset = getPetAsset(pet.type); if (mode === 'sleep') { return 'sleep'; } if (mode === 'inspect') { return facingRight ? 'inspectRight' : 'inspectLeft'; } if (mode === 'chase-cursor' || mode === 'chase-toy') { return facingRight ? 'walkRight' : 'walkLeft'; } if (mode === 'walk') { return facingRight ? 'walkRight' : 'walkLeft'; } if (pet.idleClip && petAsset.clips[pet.idleClip]) { return pet.idleClip; } if (isIdleClipName(pet.clip) && petAsset.clips[pet.clip]) { return pet.clip; } return getDefaultClipName(petAsset); } function getClipDuration(petType, clipName, reducedMotion) { const petAsset = getPetAsset(petType); const clip = petAsset.clips[clipName]; if (!clip) { return 1000; } const fps = getEffectiveClipFps(clip, reducedMotion); return (clip.frames.length / fps) * 1000; } function resolveAnimationState(pet, desiredClip, now, reducedMotion) { const petAsset = getPetAsset(pet.type); const currentClip = pet.clip || desiredClip; const activeClip = petAsset.clips[currentClip] ? currentClip : desiredClip; const activeDef = petAsset.clips[activeClip]; const elapsed = now - (pet.clipStartedAt || now); const activeDuration = getClipDuration(pet.type, activeClip, reducedMotion); if (activeDef && activeDef.nextClip && elapsed >= activeDuration) { const nextClip = pet.transitionTo || activeDef.nextClip; return { clip: nextClip, clipStartedAt: now, transitionTo: null }; } if (activeClip === desiredClip) { return { clip: activeClip, clipStartedAt: pet.clipStartedAt || now, transitionTo: null }; } const transitionClip = petAsset.transitions[`${activeClip}->${desiredClip}`] || null; if (transitionClip) { return { clip: transitionClip, clipStartedAt: now, transitionTo: desiredClip }; } return { clip: desiredClip, clipStartedAt: now, transitionTo: null }; } function getAnimatedFrame(pet, reducedMotion, now) { const petAsset = getPetAsset(pet.type); const defaultClipName = getDefaultClipName(petAsset); const clipName = pet.clip || defaultClipName; const clip = petAsset.clips[clipName] || petAsset.clips[defaultClipName]; const fps = getEffectiveClipFps(clip, reducedMotion); const safeIndex = typeof pet.frameIndex === 'number' && clip.frames[pet.frameIndex] ? pet.frameIndex : 0; const frame = clip.frames[safeIndex] || petAsset.clips[defaultClipName].frames[0]; return { clipName, fps, frame, frameIndex: safeIndex, frameCount: clip.frames.length }; } function subscribe(uid, listener) { if (!uid) { return () => {}; } if (!listenersByUid.has(uid)) { listenersByUid.set(uid, new Set()); } const listeners = listenersByUid.get(uid); listeners.add(listener); return () => { listeners.delete(listener); if (listeners.size === 0) { listenersByUid.delete(uid); } }; } function emit(uid, event) { const listeners = listenersByUid.get(uid); if (!listeners) { return; } listeners.forEach((listener) => { try { listener(event); } catch (error) { console.error('[hyperpets] listener error', error); } }); } function stripAnsi(value) { return String(value || '').replace(ANSI_RE, ''); } function inspectSessionData(uid, data) { const clean = stripAnsi(data); const state = sessionStateByUid.get(uid) || { tail: '', promptSeen: false, lastPrompt: '', sawError: false, lastToyAt: 0 }; state.tail = (state.tail + clean).slice(-500); if (ERROR_RE.test(clean)) { state.sawError = true; emit(uid, { type: 'error-output' }); } const promptMatch = state.tail.match(PROMPT_RE); const promptText = promptMatch ? promptMatch[0] : ''; if (promptText && promptText !== state.lastPrompt) { if (state.promptSeen && !state.sawError) { const now = Date.now(); const ready = now - state.lastToyAt >= runtimeConfig.toyCooldownMs; if (ready && Math.random() <= runtimeConfig.successToyChance) { state.lastToyAt = now; emit(uid, { type: 'command-success' }); } } state.promptSeen = true; state.lastPrompt = promptText; state.sawError = false; } sessionStateByUid.set(uid, state); } exports.decorateConfig = (config) => { runtimeConfig = normalizeConfig(config.hyperPets); try { if (typeof window !== 'undefined') { window.__hyperpetsRuntimeConfig = runtimeConfig; } } catch (error) { // Ignore window access errors during non-renderer evaluation. } console.log('[hyperpets] decorateConfig', { input: config.hyperPets || null, normalized: runtimeConfig }); return Object.assign({}, config, { css: ` ${config.css || ''} .hyperpets-layer { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 12; } .hyperpets-floor { position: absolute; left: 0; right: 0; bottom: 6px; height: 42px; opacity: 0.12; background: linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.15) 100%); } .hyperpets-pet { position: absolute; bottom: 12px; transform-origin: center bottom; will-change: transform, left; } .hyperpets-toy { position: absolute; bottom: 10px; width: 12px; height: 12px; border-radius: 999px; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08); } .hyperpets-toy.ball { background: #d85f5f; } .hyperpets-toy.bone { width: 16px; height: 8px; border-radius: 999px; background: #dac7a2; } .hyperpets-pet.excited { filter: saturate(1.25); } .hyperpets-pet-svg { width: 48px; height: 32px; overflow: visible; } .hyperpets-frame { position: relative; overflow: hidden; transform-origin: center bottom; } .hyperpets-frame-debug { outline: 1px dashed rgba(80, 220, 255, 0.7); background-color: rgba(80, 220, 255, 0.06); } .hyperpets-frame svg { display: block; width: 100%; height: 100%; } .hyperpets-debug-label { position: absolute; left: -12px; bottom: 56px; min-width: 150px; padding: 4px 6px; border-radius: 6px; background: rgba(8, 12, 18, 0.86); color: #9ee7ff; font: 10px/1.35 Menlo, Monaco, monospace; white-space: pre; pointer-events: none; } .hyperpets-debug-hud { position: absolute; top: 8px; left: 8px; max-width: min(420px, calc(100% - 16px)); padding: 6px 8px; border-radius: 8px; background: rgba(8, 12, 18, 0.9); color: #9ee7ff; font: 11px/1.4 Menlo, Monaco, monospace; white-space: pre-wrap; pointer-events: none; z-index: 20; } .hyperpets-z { position: absolute; bottom: 28px; font-size: 10px; opacity: 0.45; color: rgba(255,255,255,0.75); } ` }); }; exports.middleware = (store) => (next) => (action) => { if (action && action.type === 'SESSION_ADD_DATA' && action.uid && action.data) { inspectSessionData(action.uid, action.data); } return next(action); }; exports.decorateTerm = (Term, { React }) => { class HyperPetsLayer extends React.Component { constructor(props) { super(props); this.state = this.buildInitialState(); this._mounted = false; this._unsubscribe = null; this._raf = null; this._lastFrameAt = 0; this._container = null; this._cursorFrame = null; this._mouseX = null; this._mouseInfluenceUntil = 0; this._resizeHandler = this.handleResize.bind(this); this._mouseHandler = this.handleMouseMove.bind(this); this._tick = this.tick.bind(this); this._debugFrameHistory = new Map(); this._debugLastLoggedToken = new Map(); } buildInitialState() { const config = runtimeConfig; const pets = []; for (let index = 0; index < config.petCount; index += 1) { const now = Date.now(); const type = config.petTypes[index % config.petTypes.length] || 'cat'; const petAsset = getPetAsset(type); const idleClip = petAsset.clips.idle0 ? randomIdleClip() : getDefaultClipName(petAsset); pets.push({ id: `pet-${index}`, type, x: 24 + index * 48, vx: 0.1 + Math.random() * 0.14, facing: 1, mode: 'idle', mood: 'calm', targetX: null, sleepUntil: 0, nextDecisionAt: now + 2200 + Math.random() * 1800, nextAttentionAt: now + 2500 + Math.random() * 2500, attentionUntil: 0, pendingMode: null, pendingModeAt: 0, pendingFacing: 1, idleClip, zOffset: Math.round(Math.random() * 6), clip: idleClip, clipStartedAt: now, frameIndex: 0, nextFrameAt: now + (1000 / getEffectiveClipFps(petAsset.clips[idleClip], runtimeConfig.reducedMotion)), transitionTo: null }); } return { width: 640, height: 220, active: this.props.isTermActive !== false, pets, toys: [] }; } componentDidMount() { this._mounted = true; this._container = this.props.termRef || null; refreshRuntimeConfigFromRenderer(); if (runtimeConfig.debugFrames) { console.log('[hyperpets] HyperPetsLayer mounted', { uid: this.props.uid, runtimeConfig }); } this.measure(); window.addEventListener('resize', this._resizeHandler); if (this._container) { this._container.addEventListener('mousemove', this._mouseHandler, { passive: true }); } this._unsubscribe = subscribe(this.props.uid, (event) => this.handlePetEvent(event)); this._raf = window.requestAnimationFrame(this._tick); } componentWillUnmount() { this._mounted = false; if (this._unsubscribe) { this._unsubscribe(); } if (this._raf) { window.cancelAnimationFrame(this._raf); } window.removeEventListener('resize', this._resizeHandler); if (this._container) { this._container.removeEventListener('mousemove', this._mouseHandler); } } componentDidUpdate(prevProps) { if (prevProps.termRef !== this.props.termRef) { if (this._container) { this._container.removeEventListener('mousemove', this._mouseHandler); } this._container = this.props.termRef || null; if (this._container) { this._container.addEventListener('mousemove', this._mouseHandler, { passive: true }); this.measure(); } } if (this.props.cursorFrame !== prevProps.cursorFrame) { this._cursorFrame = this.props.cursorFrame || null; } if (this.props.isTermActive !== prevProps.isTermActive) { this.setState({ active: this.props.isTermActive !== false }); } } handleResize() { this.measure(); } handleMouseMove(event) { if (!this._container) { return; } const bounds = this._container.getBoundingClientRect(); this._mouseX = clamp(event.clientX - bounds.left, 0, bounds.width); this._mouseInfluenceUntil = Date.now() + 2200; } handlePetEvent(event) { if (event.type === 'command-success') { this.spawnToy(); } else if (event.type === 'error-output') { this.setState((prevState) => ({ pets: prevState.pets.map((pet) => Object.assign({}, pet, { mood: 'alert', mode: pet.mode === 'sleep' ? 'idle' : pet.mode, sleepUntil: 0, attentionUntil: 0, pendingMode: null, pendingModeAt: 0, nextAttentionAt: Date.now() + 2200, nextDecisionAt: Date.now() + 1600 })) })); } } measure() { if (!this.props.termRef) { return; } const bounds = this.props.termRef.getBoundingClientRect(); this.setState({ width: Math.max(140, Math.round(bounds.width || 0)), height: Math.max(80, Math.round(bounds.height || 0)) }); } spawnToy() { if (!this._mounted || runtimeConfig.maxToys <= 0) { return; } this.setState((prevState) => { const width = prevState.width; const keptToyCount = Math.max(runtimeConfig.maxToys - 1, 0); const nextToys = keptToyCount === 0 ? [] : prevState.toys.slice(-keptToyCount); const kind = Math.random() > 0.5 ? 'ball' : 'bone'; const toy = { id: `toy-${Date.now()}-${Math.round(Math.random() * 9999)}`, kind, x: clamp(48 + Math.random() * (width - 96), 12, width - 20), expiresAt: Date.now() + 12000 }; const leadToy = toy; const pets = prevState.pets.map((pet, index) => { if (index > 0) { return pet; } return Object.assign({}, pet, { mode: 'chase-toy', mood: 'excited', targetX: leadToy.x, sleepUntil: 0, attentionUntil: Date.now() + 2400, nextAttentionAt: Date.now() + 5000, nextDecisionAt: Date.now() + 3200 }); }); return { toys: nextToys.concat(toy), pets }; }); } tick(timestamp) { if (!this._mounted) { return; } const dt = this._lastFrameAt ? Math.min(timestamp - this._lastFrameAt, 40) : 16; this._lastFrameAt = timestamp; const now = Date.now(); this.setState((prevState) => { const width = prevState.width; const active = runtimeConfig.onlyActivePane ? prevState.active : true; const liveToys = prevState.toys.filter((toy) => toy.expiresAt > now); const targetToy = liveToys[0] || null; const cursorTarget = this._cursorFrame ? this._cursorFrame.x : null; const mouseActive = this._mouseX != null && this._mouseInfluenceUntil > now; const pets = prevState.pets.map((pet) => { const next = Object.assign({}, pet); const laneMax = Math.max(18, width - 44); const speedScale = runtimeConfig.reducedMotion ? 0.45 : 1; const travel = next.vx * runtimeConfig.walkSpeed * dt * speedScale; const attentionActive = next.attentionUntil > now; const isMovingMode = next.mode === 'walk' || next.mode === 'chase-toy' || next.mode === 'chase-cursor'; if (!active) { next.mode = 'sleep'; next.sleepUntil = now + randomSleepDuration(); next.mood = 'calm'; next.targetX = null; next.attentionUntil = 0; next.pendingMode = null; next.pendingModeAt = 0; } else if (next.pendingMode && next.mode === 'idle' && now >= next.pendingModeAt) { next.mode = next.pendingMode; next.pendingMode = null; next.pendingModeAt = 0; next.targetX = null; next.facing = next.pendingFacing || next.facing; if (next.mode === 'sleep') { next.sleepUntil = now + randomSleepDuration(); next.attentionUntil = 0; } else if (next.mode === 'inspect') { next.attentionUntil = now + 1600 + Math.random() * 1400; next.nextDecisionAt = now + 1800 + Math.random() * 1200; } } else if (targetToy) { next.targetX = targetToy.x; next.mode = 'chase-toy'; next.mood = 'excited'; } else if (!attentionActive && now >= next.nextAttentionAt) { const shouldNoticeMouse = mouseActive && Math.random() < runtimeConfig.chaseCursorChance; const shouldNoticeCursor = cursorTarget != null && Math.random() < runtimeConfig.chaseCursorChance * 0.65; if (shouldNoticeMouse) { next.targetX = this._mouseX; next.mode = 'chase-cursor'; next.mood = 'curious'; next.attentionUntil = now + 1200 + Math.random() * 1200; next.nextAttentionAt = now + 5000 + Math.random() * 5000; next.nextDecisionAt = now + 1800 + Math.random() * 1200; } else if (shouldNoticeCursor) { next.pendingFacing = cursorTarget >= next.x ? 1 : -1; if (isMovingMode) { next.targetX = null; next.mode = 'idle'; next.pendingMode = 'inspect'; next.pendingModeAt = now + 900; next.attentionUntil = 0; next.nextDecisionAt = now + 900; } else { next.targetX = null; next.mode = 'inspect'; next.mood = 'curious'; next.facing = next.pendingFacing; next.attentionUntil = now + 900 + Math.random() * 900; next.nextAttentionAt = now + 4200 + Math.random() * 4200; next.nextDecisionAt = now + 1500 + Math.random() * 900; } } else { next.nextAttentionAt = now + 1800 + Math.random() * 2800; } } else if (now >= next.nextDecisionAt) { const roll = Math.random(); if (roll < 0.2) { if (isMovingMode) { next.mode = 'idle'; next.targetX = null; next.attentionUntil = 0; next.pendingMode = 'sleep'; next.pendingModeAt = now + 900; } else { next.mode = 'sleep'; next.sleepUntil = now + randomSleepDuration(); next.targetX = null; next.attentionUntil = 0; } } else if (roll < 0.78) { next.mode = 'walk'; next.targetX = clamp(24 + Math.random() * (width - 48), 16, laneMax); next.attentionUntil = now + 1200 + Math.random() * 1600; next.pendingMode = null; next.pendingModeAt = 0; } else { next.mode = 'idle'; next.targetX = null; next.attentionUntil = 0; next.pendingMode = null; next.pendingModeAt = 0; next.idleClip = randomIdleClip(); } next.mood = 'calm'; next.nextDecisionAt = now + 3200 + Math.random() * 3400; } if (next.mode === 'sleep' && now < next.sleepUntil) { return next; } if (next.mode === 'sleep' && now >= next.sleepUntil) { next.mode = 'idle'; next.nextDecisionAt = now + 1600 + Math.random() * 1600; next.nextAttentionAt = now + 2000 + Math.random() * 2000; next.pendingMode = null; next.pendingModeAt = 0; next.idleClip = randomIdleClip(); } if (typeof next.targetX === 'number' && (next.mode === 'walk' || next.mode === 'chase-toy' || next.mode === 'chase-cursor')) { const dx = next.targetX - next.x; if (Math.abs(dx) < 4) { next.targetX = null; next.attentionUntil = 0; next.mode = 'idle'; next.pendingMode = null; next.pendingModeAt = 0; next.idleClip = randomIdleClip(); } else { next.facing = dx >= 0 ? 1 : -1; next.x = clamp(next.x + Math.sign(dx) * travel, 12, laneMax); if (next.mode !== 'chase-toy' && next.mode !== 'inspect' && next.mode !== 'chase-cursor') { next.mode = 'walk'; } } } else if (typeof next.targetX === 'number') { next.targetX = null; } else if (next.mode === 'walk') { next.mode = 'idle'; } if (next.x <= 12 || next.x >= laneMax) { next.facing *= -1; } const desiredClip = modeToClip(next); const animationState = resolveAnimationState(next, desiredClip, now, runtimeConfig.reducedMotion); const previousClip = next.clip; next.clip = animationState.clip; next.clipStartedAt = animationState.clipStartedAt; next.transitionTo = animationState.transitionTo; const petAsset = getPetAsset(next.type); const defaultClipName = getDefaultClipName(petAsset); const activeClip = petAsset.clips[next.clip] || petAsset.clips[defaultClipName]; const frameDuration = 1000 / getEffectiveClipFps(activeClip, runtimeConfig.reducedMotion); if (previousClip !== next.clip) { next.frameIndex = 0; next.nextFrameAt = now + frameDuration; } else if (now >= (next.nextFrameAt || 0)) { const lastIndex = Math.max(activeClip.frames.length - 1, 0); const currentIndex = typeof next.frameIndex === 'number' ? next.frameIndex : 0; next.frameIndex = lastIndex === 0 ? 0 : currentIndex + 1; if (next.frameIndex > lastIndex) { next.frameIndex = activeClip.nextClip ? lastIndex : 0; } next.nextFrameAt = now + frameDuration; } return next; }); return { toys: liveToys, pets }; }); this._raf = window.requestAnimationFrame(this._tick); } renderToy(toy) { return React.createElement('div', { key: toy.id, className: `hyperpets-toy ${toy.kind}`, style: { left: `${toy.x}px` } }); } renderPet(pet) { const petAsset = getPetAsset(pet.type); const frameInfo = getAnimatedFrame(pet, runtimeConfig.reducedMotion, Date.now()); const frame = frameInfo.frame; const desiredFacing = pet.facing >= 0 ? 'right' : 'left'; const basisFacing = frame.facingBasis || 'left'; const flipX = desiredFacing !== basisFacing; const frameRow = frame.frameY != null ? Math.round(frame.frameY / frame.frameHeight) + 1 : null; const frameCol = frame.frameX != null ? Math.round(frame.frameX / frame.frameWidth) + 1 : null; const debugToken = frameRow != null && frameCol != null ? `${frameInfo.clipName}:${frameInfo.frameIndex + 1}/${frameInfo.frameCount}@r${frameRow}c${frameCol}` : `${frameInfo.clipName}:${frameInfo.frameIndex + 1}/${frameInfo.frameCount}@svg`; const existingHistory = this._debugFrameHistory.get(pet.id) || []; const nextHistory = existingHistory.length > 0 && existingHistory[existingHistory.length - 1] === debugToken ? existingHistory : existingHistory.concat(debugToken).slice(-5); this._debugFrameHistory.set(pet.id, nextHistory); if (runtimeConfig.debugFrames) { const previousToken = this._debugLastLoggedToken.get(pet.id); if (previousToken !== debugToken) { this._debugLastLoggedToken.set(pet.id, debugToken); console.log( `[hyperpets] ${pet.id} mode=${pet.mode} facing=${desiredFacing} basis=${basisFacing} clip=${frameInfo.clipName} frame=${frameInfo.frameIndex + 1}/${frameInfo.frameCount}` + (frameRow != null && frameCol != null ? ` cell=r${frameRow}c${frameCol}` : ' cell=svg') ); } } const className = [ 'hyperpets-pet', pet.mood === 'excited' ? 'excited' : '' ].filter(Boolean).join(' '); const frameNode = frame.svg ? React.createElement('div', { className: 'hyperpets-frame', style: { width: `${petAsset.width}px`, height: `${petAsset.height}px`, transform: `scaleX(${flipX ? -1 : 1}) translateZ(0)` }, dangerouslySetInnerHTML: { __html: frame.svg } }) : React.createElement('div', { className: `hyperpets-frame${runtimeConfig.debugFrames ? ' hyperpets-frame-debug' : ''}`, style: { width: `${petAsset.width}px`, height: `${petAsset.height}px`, transform: `scaleX(${flipX ? -1 : 1}) translateZ(0)`, backgroundImage: `url("${frame.sheetUrl}")`, backgroundRepeat: 'no-repeat', backgroundSize: `${(frame.sheetWidth / frame.frameWidth) * petAsset.width}px ${(frame.sheetHeight / frame.frameHeight) * petAsset.height}px`, backgroundPosition: `-${(frame.frameX / frame.frameWidth) * petAsset.width}px -${(frame.frameY / frame.frameHeight) * petAsset.height}px` } }); return React.createElement( 'div', { key: pet.id, className, style: { left: `${pet.x}px`, width: `${petAsset.width}px`, height: `${petAsset.height}px` } }, frameNode, runtimeConfig.debugFrames ? React.createElement( 'div', { className: 'hyperpets-debug-label' }, [ `${frameInfo.clipName} ${frameInfo.frameIndex + 1}/${frameInfo.frameCount}`, `face ${desiredFacing} basis ${basisFacing}`, frameRow != null && frameCol != null ? `cell r${frameRow} c${frameCol}` : 'svg-frame', `hist ${nextHistory.join(' | ')}` ].join('\n') ) : null, pet.mode === 'sleep' ? React.createElement('div', { className: 'hyperpets-z', style: { left: '30px', bottom: `${26 + pet.zOffset}px` } }, 'z') : null ); } render() { refreshRuntimeConfigFromRenderer(); if (!runtimeConfig.enabled) { return null; } const debugHud = runtimeConfig.debugFrames ? React.createElement( 'div', { className: 'hyperpets-debug-hud' }, this.state.pets.map((pet) => { const history = this._debugFrameHistory.get(pet.id) || []; return [ `${pet.id} mode=${pet.mode} facing=${pet.facing >= 0 ? 'right' : 'left'}`, history.length ? `hist ${history.join(' | ')}` : 'hist (waiting for frames)' ].join('\n'); }).join('\n\n') ) : null; return React.createElement( 'div', { className: 'hyperpets-layer' }, debugHud, React.createElement('div', { className: 'hyperpets-floor' }), this.state.toys.map((toy) => this.renderToy(toy)), this.state.pets.map((pet) => this.renderPet(pet)) ); } } return class HyperPetsTerm extends React.Component { constructor(props) { super(props); this.state = { termRef: null, cursorFrame: null }; this._lastCursorSyncAt = 0; this._lastCursorX = null; this.handleDecorated = this.handleDecorated.bind(this); this.handleCursorMove = this.handleCursorMove.bind(this); } handleDecorated(term) { if (this.props.onDecorated) { this.props.onDecorated(term); } this.setState({ termRef: term ? term.termRef : null }); } handleCursorMove(cursorFrame) { if (this.props.onCursorMove) { this.props.onCursorMove(cursorFrame); } const now = Date.now(); const x = cursorFrame && typeof cursorFrame.x === 'number' ? cursorFrame.x : null; const movedEnough = x == null || this._lastCursorX == null || Math.abs(x - this._lastCursorX) >= 10; const staleEnough = now - this._lastCursorSyncAt >= 180; if (movedEnough && staleEnough) { this._lastCursorSyncAt = now; this._lastCursorX = x; this.setState({ cursorFrame }); } } render() { const customChildren = [] .concat(this.props.customChildren || []) .concat(React.createElement(HyperPetsLayer, { key: 'hyperpets-layer', uid: this.props.uid, termRef: this.state.termRef, cursorFrame: this.state.cursorFrame, isTermActive: this.props.isTermActive })); return React.createElement(Term, Object.assign({}, this.props, { onDecorated: this.handleDecorated, onCursorMove: this.handleCursorMove, customChildren })); } }; };