|
|
@@ -22,6 +22,31 @@
|
|
|
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: '有任务在等你呢~',
|
|
|
@@ -39,6 +64,8 @@
|
|
|
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) {
|
|
|
@@ -71,6 +98,7 @@
|
|
|
if (durationMs) bubbleTimer = setTimeout(hideBubble, durationMs);
|
|
|
},
|
|
|
hideBubble: hideBubble,
|
|
|
+ react: react,
|
|
|
getState: function () { return currentState; }
|
|
|
};
|
|
|
|
|
|
@@ -81,10 +109,13 @@
|
|
|
stopClip();
|
|
|
lastConfig = cfg;
|
|
|
const src = customImage || (cfg.default_image ? cfg.image_base_url + cfg.default_image : null);
|
|
|
- if (!src) return;
|
|
|
+ if (!src) { petImg.src = DEFAULT_IMAGE; ensureAnimations(); return; }
|
|
|
petImg.src = src;
|
|
|
petImg.onload = function () { ensureAnimations(); };
|
|
|
- petImg.onerror = function () { petImg.src = DEFAULT_IMAGE; };
|
|
|
+ petImg.onerror = function () {
|
|
|
+ petImg.src = DEFAULT_IMAGE;
|
|
|
+ petImg.onload = function () { ensureAnimations(); };
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
function setState(stateName, reason, data) {
|
|
|
@@ -118,7 +149,7 @@
|
|
|
bubble.style.display = 'block';
|
|
|
clearTimeout(bubbleTimer);
|
|
|
bubbleTimer = setTimeout(hideBubble, 3000);
|
|
|
- } else if (BUBBLE_TEXTS[stateName]) {
|
|
|
+ } else if (reason !== 'life' && BUBBLE_TEXTS[stateName]) {
|
|
|
bubble.textContent = BUBBLE_TEXTS[stateName];
|
|
|
bubble.style.display = 'block';
|
|
|
clearTimeout(bubbleTimer);
|
|
|
@@ -158,6 +189,16 @@
|
|
|
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; });
|
|
|
+ }
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -190,6 +231,40 @@
|
|
|
}, 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(); };
|
|
|
|