Initial Commit
This commit is contained in:
706
index.js
Normal file
706
index.js
Normal file
@@ -0,0 +1,706 @@
|
||||
'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.65,
|
||||
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.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 modeToClip(mode) {
|
||||
if (mode === 'sleep') {
|
||||
return 'sleep';
|
||||
}
|
||||
if (mode === 'inspect') {
|
||||
return 'inspect';
|
||||
}
|
||||
if (mode === 'chase-cursor' || mode === 'chase-toy') {
|
||||
return 'chase';
|
||||
}
|
||||
if (mode === 'walk') {
|
||||
return 'walk';
|
||||
}
|
||||
return 'idle';
|
||||
}
|
||||
|
||||
function getClipDuration(petType, clipName, reducedMotion) {
|
||||
const petAsset = getPetAsset(petType);
|
||||
const clip = petAsset.clips[clipName];
|
||||
if (!clip) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
const fps = reducedMotion ? Math.max(clip.fps * 0.6, 1) : clip.fps;
|
||||
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 clipName = pet.clip || 'idle';
|
||||
const clip = petAsset.clips[clipName] || petAsset.clips.idle;
|
||||
const fps = reducedMotion ? Math.max(clip.fps * 0.6, 1) : clip.fps;
|
||||
const duration = 1000 / fps;
|
||||
const elapsed = now - (pet.clipStartedAt || now);
|
||||
const rawIndex = clip.frames.length === 1 ? 0 : Math.floor(elapsed / duration);
|
||||
const safeIndex = ((rawIndex % clip.frames.length) + clip.frames.length) % clip.frames.length;
|
||||
return clip.frames[safeIndex] || petAsset.clips.idle.frames[0];
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
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 {
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
}
|
||||
.hyperpets-frame svg {
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
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';
|
||||
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,
|
||||
zOffset: Math.round(Math.random() * 6),
|
||||
clip: 'idle',
|
||||
clipStartedAt: now,
|
||||
transitionTo: null
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
width: 640,
|
||||
height: 220,
|
||||
active: this.props.isTermActive !== false,
|
||||
pets,
|
||||
toys: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._mounted = true;
|
||||
this._container = this.props.termRef || null;
|
||||
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,
|
||||
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;
|
||||
|
||||
if (!active) {
|
||||
next.mode = 'sleep';
|
||||
next.sleepUntil = now + 5000;
|
||||
next.mood = 'calm';
|
||||
next.targetX = null;
|
||||
next.attentionUntil = 0;
|
||||
} 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.targetX = cursorTarget;
|
||||
next.mode = 'inspect';
|
||||
next.mood = 'curious';
|
||||
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) {
|
||||
next.mode = 'sleep';
|
||||
next.sleepUntil = now + 3500 + Math.random() * 5000;
|
||||
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;
|
||||
} else {
|
||||
next.mode = 'idle';
|
||||
next.targetX = null;
|
||||
next.attentionUntil = 0;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (typeof next.targetX === 'number') {
|
||||
const dx = next.targetX - next.x;
|
||||
if (Math.abs(dx) < 4) {
|
||||
next.targetX = null;
|
||||
next.attentionUntil = 0;
|
||||
next.mode = 'idle';
|
||||
} 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 (next.mode === 'walk') {
|
||||
next.mode = 'idle';
|
||||
}
|
||||
|
||||
if (next.x <= 12 || next.x >= laneMax) {
|
||||
next.facing *= -1;
|
||||
}
|
||||
|
||||
const desiredClip = modeToClip(next.mode);
|
||||
const animationState = resolveAnimationState(next, desiredClip, now, runtimeConfig.reducedMotion);
|
||||
next.clip = animationState.clip;
|
||||
next.clipStartedAt = animationState.clipStartedAt;
|
||||
next.transitionTo = animationState.transitionTo;
|
||||
|
||||
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 frame = getAnimatedFrame(pet, runtimeConfig.reducedMotion, Date.now());
|
||||
const className = [
|
||||
'hyperpets-pet',
|
||||
pet.mood === 'excited' ? 'excited' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
key: pet.id,
|
||||
className,
|
||||
style: {
|
||||
left: `${pet.x}px`,
|
||||
transform: `scaleX(${pet.facing}) translateZ(0)`
|
||||
}
|
||||
},
|
||||
React.createElement(
|
||||
'div',
|
||||
{
|
||||
className: 'hyperpets-frame',
|
||||
dangerouslySetInnerHTML: { __html: frame.svg }
|
||||
}
|
||||
),
|
||||
pet.mode === 'sleep'
|
||||
? React.createElement('div', {
|
||||
className: 'hyperpets-z',
|
||||
style: { left: '30px', bottom: `${26 + pet.zOffset}px` }
|
||||
}, 'z')
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!runtimeConfig.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: 'hyperpets-layer' },
|
||||
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
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user