| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- (function () {
- 'use strict';
- const petImg = document.getElementById('pet-img');
- const petEl = document.getElementById('pet');
- const bubble = document.getElementById('bubble');
- let currentState = 'idle';
- let currentAnimation = null;
- let config = null;
- let lastConfig = null;
- let customImage = null;
- let customDataUrl = null;
- let runtimeAnimations = {};
- let animSig = null;
- let animPending = false;
- let bubbleTimer = null;
- let frameTimer = null;
- let frameIdx = 0;
- const DEFAULT_IMAGE = 'data:image/svg+xml;utf8,' +
- encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="120" height="140">' +
- '<text x="60" y="100" font-size="90" text-anchor="middle">🦜</text></svg>');
- // 离线兜底配置:未连后端(未登录 / 服务不可达)时也要有「状态→动作」映射,
- // 否则 setState 会退化成 .anim-<state> 而动画样式并不存在 → 宠物静止。
- const FALLBACK_CONFIG = {
- image_base_url: 'DefaultPets/xibao/',
- default_image: 'standing.png',
- actions: {
- idle: 'wings_idle',
- happy: 'wings_spread',
- busy: 'head_tilt',
- waitingForInteraction: 'hand_gesture',
- sleeping: 'breath_loop',
- celebrate: 'celebrate',
- sad: 'sad_animation'
- }
- };
- // 服务端状态超过该时长未到达即视为离线,进入「自主活动」模式。
- const SERVER_TRUST_MS = 90000;
- const LIFE_TICK_MS = 2800;
- const LIFE_CYCLE = ['idle', 'idle', 'busy', 'idle', 'happy', 'idle',
- 'waitingForInteraction', 'idle', 'sleeping', 'idle', 'celebrate'];
- const LIFE_BUBBLES = ['在想什么呢~', '陪我玩一会儿吧!', '今天也要加油哦~', '嗯…休息一下~', '你好呀!'];
- let lastServerStateAt = 0;
- let lifeIdx = 0;
- const BUBBLE_TEXTS = {
- happy: '完成了!继续保持~',
- busy: '有任务在等你呢~',
- worried: '任务要过期啦,快去看看!',
- excited: '打卡成功,太棒了!',
- remind: '该喝水啦~',
- tired: '有点累了呢…',
- sad: '要照顾好自己哦…',
- waitingForInteraction: '来和我互动一下吧!',
- miniGame: '我们一起玩吧!',
- sleeping: 'zZ…',
- custom: '自定义状态'
- };
- window.cfcPetAPI = {
- notifyState: function (stateJson) {
- const payload = typeof stateJson === 'string' ? JSON.parse(stateJson) : stateJson;
- lastServerStateAt = Date.now();
- lifeIdx = 0;
- setState(payload.state, payload.reason || null, payload.data || null);
- },
- setCustomState: function (stateName, dataJson) {
- const data = typeof dataJson === 'string' ? JSON.parse(dataJson) : dataJson;
- setState(stateName, 'custom', data);
- },
- loadConfig: function (configJson) {
- config = typeof configJson === 'string' ? JSON.parse(configJson) : configJson;
- applyImage(config);
- },
- setCustomImage: function (url, dataUrl) {
- customImage = url || null;
- customDataUrl = dataUrl || null;
- stopClip();
- if (customImage) {
- petImg.src = customImage;
- petImg.onload = function () { ensureAnimations(); };
- petImg.onerror = function () { petImg.src = DEFAULT_IMAGE; };
- } else if (lastConfig) {
- applyImage(lastConfig);
- } else {
- petImg.src = DEFAULT_IMAGE;
- ensureAnimations();
- }
- },
- showBubble: function (text, durationMs) {
- bubble.textContent = text;
- bubble.style.display = 'block';
- clearTimeout(bubbleTimer);
- if (durationMs) bubbleTimer = setTimeout(hideBubble, durationMs);
- },
- hideBubble: hideBubble,
- react: react,
- getState: function () { return currentState; }
- };
- function hideBubble() { bubble.style.display = 'none'; }
- function applyImage(cfg) {
- if (!cfg) return;
- stopClip();
- lastConfig = cfg;
- const src = customImage || (cfg.default_image ? cfg.image_base_url + cfg.default_image : null);
- if (!src) { petImg.src = DEFAULT_IMAGE; ensureAnimations(); return; }
- petImg.src = src;
- petImg.onload = function () { ensureAnimations(); };
- petImg.onerror = function () {
- petImg.src = DEFAULT_IMAGE;
- petImg.onload = function () { ensureAnimations(); };
- };
- }
- function setState(stateName, reason, data) {
- currentState = stateName;
- if (currentAnimation) { petEl.classList.remove(currentAnimation); currentAnimation = null; }
- const actionName = (config && config.actions && config.actions[stateName]) || stateName;
- const clipCfg = (config && config.animations && config.animations[actionName])
- || runtimeAnimations[actionName]
- || null;
- if (clipCfg && clipCfg.frames && clipCfg.frames.length) {
- playClip(clipCfg);
- } else {
- stopClip();
- applyImage(lastConfig);
- const animClass = 'anim-' + actionName;
- const styleEl = document.getElementById('pet-animations');
- const sheet = styleEl ? styleEl.sheet : null;
- const hasClass = sheet && Array.from(sheet.cssRules).some(r => r.selectorText === '.' + animClass);
- if (hasClass) { petEl.classList.add(animClass); currentAnimation = animClass; }
- }
- if (reason === 'drink_water') {
- bubble.textContent = '该喝水啦~';
- bubble.style.display = 'block';
- clearTimeout(bubbleTimer);
- bubbleTimer = setTimeout(hideBubble, 8000);
- } else if (reason === 'welcome') {
- bubble.textContent = '我来了!';
- bubble.style.display = 'block';
- clearTimeout(bubbleTimer);
- bubbleTimer = setTimeout(hideBubble, 3000);
- } else if (reason !== 'life' && BUBBLE_TEXTS[stateName]) {
- bubble.textContent = BUBBLE_TEXTS[stateName];
- bubble.style.display = 'block';
- clearTimeout(bubbleTimer);
- bubbleTimer = setTimeout(hideBubble, 5000);
- }
- }
- function baseSrc() {
- if (customImage) return customImage;
- if (lastConfig && lastConfig.default_image) return lastConfig.image_base_url + lastConfig.default_image;
- return DEFAULT_IMAGE;
- }
- // 生成动作帧所用的基础形象:优先用 data URL(避免 canvas 跨域污染)。
- function generationSrc() {
- if (customImage) return customDataUrl || customImage;
- if (lastConfig && lastConfig.default_image) return lastConfig.image_base_url + lastConfig.default_image;
- return DEFAULT_IMAGE;
- }
- // 本地程序化生成动作帧(provider 无关、无需 API Key)。
- // 成功后重新应用当前状态,使生成的动作立即生效。
- // 以「基础形象 + 动作映射」为签名,同一形象下不重复生成。
- function ensureAnimations() {
- if (!window.cfcPetGen) return;
- const src = generationSrc();
- if (!src) return;
- const cfg = lastConfig || { actions: {} };
- const sig = src + '|' + JSON.stringify(cfg.actions || {});
- if (animSig === sig && Object.keys(runtimeAnimations).length) return;
- if (animPending) return;
- animPending = true;
- window.cfcPetGen.generateForConfig(cfg, src).then(function (clips) {
- runtimeAnimations = clips || {};
- animSig = sig;
- animPending = false;
- setState(currentState, null, null);
- }).catch(function () {
- animPending = false;
- // 配置形象缺失(如默认图不存在)时,退回内置形象再生成一次,保证离线也有帧动画。
- if (src !== DEFAULT_IMAGE) {
- animPending = true;
- window.cfcPetGen.generateForConfig(cfg, DEFAULT_IMAGE).then(function (clips) {
- runtimeAnimations = clips || {};
- animSig = sig + '|fallback';
- animPending = false;
- setState(currentState, null, null);
- }).catch(function () { animPending = false; });
- }
- });
- }
- function stopClip() {
- if (frameTimer) { clearInterval(frameTimer); frameTimer = null; }
- }
- // 播放帧序列剪辑(provider 无关)。loop:false 播完回到基础形象。
- function playClip(clip) {
- stopClip();
- const base = (lastConfig && lastConfig.image_base_url) || '';
- const frames = clip.frames.map(function (f) {
- return /^(https?:|data:|\/)/.test(f) ? f : base + f;
- });
- const fps = clip.fps > 0 ? clip.fps : 12;
- frameIdx = 0;
- petImg.onerror = null;
- petImg.src = frames[0];
- frameTimer = setInterval(function () {
- frameIdx++;
- if (frameIdx >= frames.length) {
- if (clip.loop === false) {
- stopClip();
- petImg.src = baseSrc();
- return;
- }
- frameIdx = 0;
- }
- petImg.src = frames[frameIdx];
- }, 1000 / fps);
- }
- // 本地即时互动反馈(不依赖后端,也不改变当前状态):点击/抚摸时立即给出动作。
- function react(kind) {
- const cls = kind === 'pet' ? 'anim-clap' : 'anim-pet_touch';
- const ms = kind === 'pet' ? 1900 : 700;
- petEl.classList.remove(cls);
- void petEl.offsetWidth; // 强制回流,确保动画可重播
- petEl.classList.add(cls);
- setTimeout(function () { petEl.classList.remove(cls); }, ms);
- if (kind === 'pet') {
- bubble.textContent = '好痒~嘻嘻!';
- bubble.style.display = 'block';
- clearTimeout(bubbleTimer);
- bubbleTimer = setTimeout(hideBubble, 2000);
- }
- }
- // 自主活动循环:服务端状态长时间未到达(离线 / 未登录)时,按节奏切换轻量动作,
- // 让宠物始终保持「活着」的互动感;服务端状态恢复后自动让位。
- setInterval(function () {
- if (Date.now() - lastServerStateAt <= SERVER_TRUST_MS) { lifeIdx = 0; return; }
- lifeIdx = (lifeIdx + 1) % LIFE_CYCLE.length;
- const st = LIFE_CYCLE[lifeIdx];
- setState(st, 'life', null);
- if (st !== 'idle' && Math.random() < 0.5) {
- bubble.textContent = LIFE_BUBBLES[Math.floor(Math.random() * LIFE_BUBBLES.length)];
- bubble.style.display = 'block';
- clearTimeout(bubbleTimer);
- bubbleTimer = setTimeout(hideBubble, 3000);
- }
- }, LIFE_TICK_MS);
- // 启动即以兜底配置呈现,确保离线也具备「状态→动作」映射;服务端配置到达后覆盖。
- config = FALLBACK_CONFIG;
- lastConfig = FALLBACK_CONFIG;
- petImg.src = DEFAULT_IMAGE;
- petImg.onload = function () { ensureAnimations(); };
- setInterval(function () {
- if (document.getElementById('pet-img')) {
- petImg.classList.add('anim-blink_eye');
- setTimeout(() => petImg.classList.remove('anim-blink_eye'), 200);
- }
- }, 3500);
- })();
|