-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpet.js
More file actions
4408 lines (3883 loc) · 175 KB
/
pet.js
File metadata and controls
4408 lines (3883 loc) · 175 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ========== 魔法生物宠物养成系统 - 核心数据管理与存储层 ==========
// ===== 全局状态 =====
let currentCharId = '';
let currentCreature = null;
// ===== localStorage 辅助函数 =====
function getStorageJSON(key, def) {
try { return JSON.parse(localStorage.getItem(key)) || def; } catch { return def; }
}
function setStorageJSON(key, val) {
localStorage.setItem(key, JSON.stringify(val));
}
// ===== pet_{charId}_ 前缀存储封装 =====
function petKey(charId, suffix) {
return 'pet_' + charId + '_' + suffix;
}
function getPetData(charId, suffix, def) {
return getStorageJSON(petKey(charId, suffix), def);
}
function setPetData(charId, suffix, val) {
setStorageJSON(petKey(charId, suffix), val);
}
// ===== Creature 核心数据管理 =====
/**
* 加载指定 CHAR 的 Creature 数据
* @param {string} charId
* @returns {object|null} CreatureData 或 null
*/
function loadCreature(charId) {
return getPetData(charId, 'creature', null);
}
/**
* 保存 Creature 数据到 localStorage
* @param {string} charId
* @param {object} data - CreatureData
*/
function saveCreature(charId, data) {
setPetData(charId, 'creature', data);
}
/**
* 创建新 Creature(蛋阶段),绑定到指定 CHAR
* @param {string} charId
* @returns {object} 新创建的 CreatureData
*/
function createCreature(charId) {
var now = Date.now();
var creature = {
id: 'creature_' + now + '_' + Math.random().toString(36).substr(2, 6),
charId: charId,
name: '',
createdAt: now,
growthStage: 'egg',
alive: true,
satiety: 100,
mood: 70,
hp: 100,
attributes: {
intelligence: 10,
stamina: 10,
charisma: 10,
creativity: 10
},
equippedAccessories: {
head: null,
body: null,
hand: null,
back: null,
effect: null
},
lastSatietyUpdate: now,
lastMoodUpdate: now,
lastHpUpdate: now,
lastEventCheck: now,
satietyZeroSince: null
};
saveCreature(charId, creature);
// 记录最后活跃时间
setPetData(charId, 'last_active', now);
currentCreature = creature;
return creature;
}
// ===== Creature 状态查询函数 =====
/**
* 检查 Creature 是否存活
* @param {object} creatureData
* @returns {boolean}
*/
function isCreatureAlive(creatureData) {
if (!creatureData) return false;
return creatureData.alive === true;
}
/**
* 获取 Creature 当前年龄(天数)
* @param {object} creatureData
* @returns {number} 年龄天数
*/
function getCreatureAge(creatureData) {
if (!creatureData || !creatureData.createdAt) return 0;
var elapsed = Date.now() - creatureData.createdAt;
return Math.floor(elapsed / (24 * 60 * 60 * 1000));
}
// ========== 初始化与 CHAR 切换逻辑 ==========
// ===== 阶段名称映射 =====
var STAGE_LABELS = {
egg: '蛋',
baby: '婴儿',
child: '幼年',
teen: '少年',
adult: '成年'
};
// ===== 心情表情映射 =====
function getMoodEmoji(mood) {
if (mood >= 80) return '😊';
if (mood >= 40) return '😐';
if (mood >= 20) return '😢';
return '😡';
}
// ===== 加载 CHAR 列表到选择器 =====
function loadCharList() {
var contacts = getStorageJSON('vibe_contacts', []);
var select = document.getElementById('charSelect');
select.innerHTML = '<option value="">选择CHAR</option>';
contacts.forEach(function(c) {
var opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.nickname || c.name;
select.appendChild(opt);
});
}
// ===== 加载积分余额显示 =====
function loadPoints() {
var data = getStorageJSON('alipay_points', {});
var charPoints = currentCharId ? (data[currentCharId] || 0) : 0;
document.getElementById('walletAmount').textContent = charPoints;
}
// ===== 视图切换 =====
function switchView(viewId) {
var views = document.querySelectorAll('.pet-view');
for (var i = 0; i < views.length; i++) {
views[i].style.display = 'none';
}
var target = document.getElementById('view-' + viewId);
if (target) {
target.style.display = '';
}
}
// ===== 渲染主页(增强版 - Task 2.3) =====
function renderHome() {
if (!currentCreature) return;
var c = currentCreature;
// 先检查阶段进化
var stageConfig = getStageConfig(c.charId);
var evolveResult = checkAndEvolve(c, stageConfig);
if (evolveResult.evolved) {
// 阶段进化时所有属性 +5 成长奖励(需求 17.7)
applyGrowthBonus(c);
saveCreature(c.charId, c);
showEvolutionAnimation(evolveResult.oldStage, evolveResult.newStage);
// 如果从蛋进化到婴儿,弹出命名对话框
if (evolveResult.oldStage === 'egg' && evolveResult.newStage === 'baby') {
showNamingModal();
}
}
// 渲染形象
renderCreatureImage(c);
// 名称
document.getElementById('creatureName').textContent = c.name || (STAGE_LABELS[c.growthStage] || '魔法生物');
// 年龄
var age = getCreatureAge(c);
document.getElementById('creatureAge').textContent = age + '天';
// 阶段标签
document.getElementById('creatureStageLabel').textContent = STAGE_LABELS[c.growthStage] || c.growthStage;
// 饱食度
var satiety = Math.max(0, Math.min(100, Math.round(c.satiety)));
document.getElementById('satietyBar').style.width = satiety + '%';
document.getElementById('satietyValue').textContent = satiety;
// 心情
var moodEmoji = getMoodEmoji(c.mood);
document.getElementById('moodEmoji').textContent = moodEmoji + ' 心情';
var mood = Math.max(0, Math.min(100, Math.round(c.mood)));
document.getElementById('moodBar').style.width = mood + '%';
document.getElementById('moodValue').textContent = mood;
// 生命值
var hp = Math.max(0, Math.min(100, Math.round(c.hp)));
document.getElementById('hpBar').style.width = hp + '%';
document.getElementById('hpValue').textContent = hp;
// HP 危险警告和虚弱状态(调用 updateHPWarning)
updateHPWarning(c);
// 检查饱食度持续为 0 的额外 HP 扣减
if (c.alive && c.satiety <= 0 && c.satietyZeroSince) {
var starvDmg = calculateStarvationDamage(c);
if (starvDmg > 0) {
// 计算上次已扣减的量,只扣增量
var lastStarvDmg = c._lastStarvDmg || 0;
var newDmg = starvDmg - lastStarvDmg;
if (newDmg > 0) {
updateHP(c, -newDmg);
c._lastStarvDmg = starvDmg;
saveCreature(c.charId, c);
// 重新渲染 HP 条
var hpAfter = Math.max(0, Math.min(100, Math.round(c.hp)));
document.getElementById('hpBar').style.width = hpAfter + '%';
document.getElementById('hpValue').textContent = hpAfter;
updateHPWarning(c);
}
}
} else {
c._lastStarvDmg = 0;
}
// 检查死亡
if (checkDeath(c)) {
triggerDeath(c.charId, c);
return;
}
// 进化进度条
var timeInfo = getTimeToNextStage(c, stageConfig);
var evolutionBar = document.getElementById('evolutionBar');
var evolutionValue = document.getElementById('evolutionValue');
if (c.growthStage === 'adult') {
evolutionBar.style.width = '100%';
evolutionValue.textContent = '已成年';
} else {
var progressPct = Math.round(timeInfo.progress * 100);
evolutionBar.style.width = progressPct + '%';
// 显示剩余时间
var remainHours = Math.ceil(timeInfo.remainingMs / (60 * 60 * 1000));
if (remainHours >= 24) {
var remainDays = Math.floor(remainHours / 24);
var remainH = remainHours % 24;
evolutionValue.textContent = remainDays + '天' + (remainH > 0 ? remainH + '时' : '');
} else {
evolutionValue.textContent = remainHours + '小时';
}
}
// 渲染属性面板
renderAttributes(c.attributes);
// 饥饿状态
var stageEl = document.getElementById('creatureStage');
if (stageEl) {
if (c.satiety <= 0 && c.alive) {
stageEl.classList.add('hungry-state');
} else {
stageEl.classList.remove('hungry-state');
}
// 死亡状态
if (!c.alive) {
stageEl.classList.add('dead-state');
} else {
stageEl.classList.remove('dead-state');
}
}
}
/**
* 渲染属性面板(数值列表形式)
* @param {object} attributes - { intelligence, stamina, charisma, creativity }
*/
function renderAttributes(attributes) {
if (!attributes) return;
var attrKeys = ['intelligence', 'stamina', 'charisma', 'creativity'];
var attrIds = ['attrIntelligence', 'attrStamina', 'attrCharisma', 'attrCreativity'];
for (var i = 0; i < attrKeys.length; i++) {
var el = document.getElementById(attrIds[i]);
if (!el) continue;
var val = Math.max(0, Math.min(100, attributes[attrKeys[i]] || 0));
var level = getAttrLevel(val);
el.innerHTML = val + ' <span class="attr-level" style="color:' + level.color + '">' + level.label + '</span>';
}
}
// ===== CHAR 切换回调 =====
function onCharChange() {
var select = document.getElementById('charSelect');
var charId = select.value;
currentCharId = charId;
var noCharEl = document.getElementById('noCharPlaceholder');
var contentEl = document.getElementById('petContent');
var hatchEl = document.getElementById('hatchEntry');
var homeEl = document.getElementById('view-home');
if (!charId) {
// 未选择 CHAR:显示占位,隐藏内容
noCharEl.style.display = '';
contentEl.style.display = 'none';
currentCreature = null;
return;
}
// 已选择 CHAR:隐藏占位,显示内容
noCharEl.style.display = 'none';
contentEl.style.display = '';
// 加载 Creature 数据
var creature = loadCreature(charId);
if (creature && isCreatureAlive(creature)) {
// 有活着的 Creature:隐藏孵化入口,显示主页
currentCreature = creature;
hatchEl.style.display = 'none';
homeEl.style.display = '';
switchView('home');
// 离线补算:计算离线时长并补算属性衰减(需求 15.1, 15.4, 15.5, 15.6)
var lastActive = getLastActive(charId);
if (lastActive) {
processOfflineTime(creature, lastActive).then(function(offlineResult) {
// 补算完成后重新渲染(可能触发了死亡)
currentCreature = creature;
renderHome();
// 离线事件补算:离线超过 4 小时时生成离线事件(需求 15.2, 15.3)
if (offlineResult.offlineHours >= 4 && creature.alive) {
processOfflineEvents(creature, offlineResult.offlineHours).then(function(offlineEvents) {
if (offlineEvents && offlineEvents.length > 0) {
showOfflineSummary(offlineEvents);
}
});
}
});
} else {
renderHome();
}
// 记录当前时间为最后活跃时间
recordLastActive(charId);
} else if (creature && !isCreatureAlive(creature)) {
// Creature 已死亡:显示主页(死亡状态)
currentCreature = creature;
hatchEl.style.display = 'none';
homeEl.style.display = '';
switchView('home');
renderHome();
} else {
// 无 Creature:显示孵化入口,隐藏主页
currentCreature = null;
hatchEl.style.display = '';
homeEl.style.display = 'none';
// 隐藏所有视图
var views = document.querySelectorAll('.pet-view');
for (var i = 0; i < views.length; i++) {
views[i].style.display = 'none';
}
}
// 加载积分显示
loadPoints();
}
// ===== 覆盖 createCreature 以更新 UI =====
var _originalCreateCreature = createCreature;
createCreature = function(charId) {
var creature = _originalCreateCreature(charId);
currentCharId = charId;
currentCreature = creature;
// 更新 UI:隐藏孵化入口,显示主页
var hatchEl = document.getElementById('hatchEntry');
var homeEl = document.getElementById('view-home');
if (hatchEl) hatchEl.style.display = 'none';
if (homeEl) homeEl.style.display = '';
switchView('home');
renderHome();
return creature;
};
// ===== 页面加载入口 =====
function initPet() {
loadCharList();
var select = document.getElementById('charSelect');
if (select) {
select.onchange = onCharChange;
}
}
// DOMContentLoaded 触发初始化
document.addEventListener('DOMContentLoaded', initPet);
// ========== 成长阶段引擎 ==========
// ===== 成长阶段顺序 =====
var GROWTH_STAGES = ['egg', 'baby', 'child', 'teen', 'adult'];
// ===== 默认阶段天数配置 =====
var DEFAULT_STAGE_CONFIG = {
egg: 1,
baby: 2,
child: 4,
teen: 7,
adult: Infinity
};
/**
* 获取阶段天数配置
* @param {string} charId
* @returns {object} StageConfig
*/
function getStageConfig(charId) {
var config = getPetData(charId, 'stage_config', null);
if (!config) return Object.assign({}, DEFAULT_STAGE_CONFIG);
// Ensure adult is always Infinity (JSON serializes Infinity as null)
var merged = Object.assign({}, DEFAULT_STAGE_CONFIG, config);
merged.adult = Infinity;
return merged;
}
/**
* 保存阶段天数配置
* @param {string} charId
* @param {object} config - StageConfig
*/
function saveStageConfig(charId, config) {
// Store a copy; adult will become null in JSON but getStageConfig handles it
setPetData(charId, 'stage_config', config);
}
/**
* 根据创建时间戳和配置计算当前成长阶段
* 阶段按天数累加:egg占 [0, egg), baby占 [egg, egg+baby), child占 [egg+baby, egg+baby+child), ...
* @param {object} creatureData
* @param {object} stageConfig
* @returns {string} 当前阶段名称
*/
function calculateGrowthStage(creatureData, stageConfig) {
if (!creatureData || !creatureData.createdAt) return 'egg';
var ageMs = Date.now() - creatureData.createdAt;
var ageDays = ageMs / (24 * 60 * 60 * 1000); // fractional days
var cumulative = 0;
for (var i = 0; i < GROWTH_STAGES.length; i++) {
var stage = GROWTH_STAGES[i];
var duration = stageConfig[stage];
// adult (or Infinity) means no upper bound
if (duration === Infinity || duration === null || duration === undefined) {
return stage;
}
cumulative += duration;
if (ageDays < cumulative) {
return stage;
}
}
// Fallback: if somehow past all stages, return adult
return 'adult';
}
/**
* 检查并执行阶段进化
* 比较当前 creature 的 growthStage 与根据年龄计算出的期望阶段,
* 如果不同则更新 creature 数据并保存。
* @param {object} creatureData
* @param {object} stageConfig
* @returns {object} { evolved: boolean, oldStage: string, newStage: string }
*/
function checkAndEvolve(creatureData, stageConfig) {
var oldStage = creatureData.growthStage || 'egg';
var newStage = calculateGrowthStage(creatureData, stageConfig);
if (newStage !== oldStage) {
creatureData.growthStage = newStage;
saveCreature(creatureData.charId, creatureData);
return { evolved: true, oldStage: oldStage, newStage: newStage };
}
return { evolved: false, oldStage: oldStage, newStage: newStage };
}
/**
* 计算到下一阶段的剩余时间
* @param {object} creatureData
* @param {object} stageConfig
* @returns {object} { remainingMs: number, totalMs: number, progress: number }
* - remainingMs: 距离下一阶段的剩余毫秒数(adult阶段返回0)
* - totalMs: 当前阶段的总毫秒数(adult阶段返回0)
* - progress: 当前阶段进度 0-1(adult阶段返回1)
*/
function getTimeToNextStage(creatureData, stageConfig) {
if (!creatureData || !creatureData.createdAt) {
return { remainingMs: 0, totalMs: 0, progress: 0 };
}
var now = Date.now();
var ageMs = now - creatureData.createdAt;
var msPerDay = 24 * 60 * 60 * 1000;
// Calculate the start time (in ms from creation) of each stage
var cumulative = 0;
var currentStage = creatureData.growthStage || calculateGrowthStage(creatureData, stageConfig);
for (var i = 0; i < GROWTH_STAGES.length; i++) {
var stage = GROWTH_STAGES[i];
var duration = stageConfig[stage];
if (stage === currentStage) {
// adult or infinite duration: already at final stage
if (duration === Infinity || duration === null || duration === undefined) {
return { remainingMs: 0, totalMs: 0, progress: 1 };
}
var stageStartMs = cumulative * msPerDay;
var stageTotalMs = duration * msPerDay;
var elapsedInStage = ageMs - stageStartMs;
var remaining = stageTotalMs - elapsedInStage;
if (remaining < 0) remaining = 0;
var progress = stageTotalMs > 0 ? Math.min(elapsedInStage / stageTotalMs, 1) : 1;
return {
remainingMs: remaining,
totalMs: stageTotalMs,
progress: progress
};
}
if (duration !== Infinity && duration !== null && duration !== undefined) {
cumulative += duration;
}
}
// Fallback
return { remainingMs: 0, totalMs: 0, progress: 1 };
}
// ========== 默认像素风形象与形象渲染 (Task 2.2) ==========
// ===== 默认像素风 SVG 形象 =====
var DEFAULT_IMAGES = {
// 蛋:椭圆形蛋,带裂纹装饰
egg: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='80' viewBox='0 0 64 80'%3E%3Cellipse cx='32' cy='44' rx='24' ry='32' fill='%23f5e6c8' stroke='%23c9a96e' stroke-width='2'/%3E%3Cellipse cx='32' cy='44' rx='20' ry='28' fill='%23faf0dc'/%3E%3Cpath d='M22 36 L28 42 L24 48' stroke='%23c9a96e' stroke-width='1.5' fill='none'/%3E%3Cpath d='M38 30 L42 38 L36 40' stroke='%23c9a96e' stroke-width='1.5' fill='none'/%3E%3Cellipse cx='26' cy='34' rx='3' ry='2' fill='%23fce4b8' opacity='0.6'/%3E%3Cellipse cx='40' cy='50' rx='4' ry='2' fill='%23fce4b8' opacity='0.5'/%3E%3C/svg%3E",
// 婴儿:小型圆润可爱生物
baby: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='34' r='22' fill='%23a78bfa'/%3E%3Ccircle cx='32' cy='34' r='18' fill='%23c4b5fd'/%3E%3Ccircle cx='24' cy='30' r='4' fill='white'/%3E%3Ccircle cx='40' cy='30' r='4' fill='white'/%3E%3Ccircle cx='25' cy='31' r='2' fill='%231a1a2e'/%3E%3Ccircle cx='41' cy='31' r='2' fill='%231a1a2e'/%3E%3Cellipse cx='32' cy='40' rx='3' ry='2' fill='%23e94560'/%3E%3Ccircle cx='20' cy='36' r='3' fill='%23f9a8d4' opacity='0.5'/%3E%3Ccircle cx='44' cy='36' r='3' fill='%23f9a8d4' opacity='0.5'/%3E%3Cellipse cx='24' cy='56' rx='6' ry='4' fill='%23a78bfa'/%3E%3Cellipse cx='40' cy='56' rx='6' ry='4' fill='%23a78bfa'/%3E%3C/svg%3E",
// 幼年:中型活泼生物,有小耳朵
child: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Cellipse cx='22' cy='16' rx='8' ry='12' fill='%237c3aed' transform='rotate(-15 22 16)'/%3E%3Cellipse cx='58' cy='16' rx='8' ry='12' fill='%237c3aed' transform='rotate(15 58 16)'/%3E%3Cellipse cx='40' cy='42' rx='26' ry='28' fill='%238b5cf6'/%3E%3Cellipse cx='40' cy='42' rx='22' ry='24' fill='%23a78bfa'/%3E%3Ccircle cx='30' cy='36' r='5' fill='white'/%3E%3Ccircle cx='50' cy='36' r='5' fill='white'/%3E%3Ccircle cx='31' cy='37' r='2.5' fill='%231a1a2e'/%3E%3Ccircle cx='51' cy='37' r='2.5' fill='%231a1a2e'/%3E%3Ccircle cx='32' cy='36' r='1' fill='white'/%3E%3Ccircle cx='52' cy='36' r='1' fill='white'/%3E%3Cpath d='M36 46 Q40 50 44 46' stroke='%23e94560' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3Ccircle cx='22' cy='42' r='4' fill='%23f9a8d4' opacity='0.4'/%3E%3Ccircle cx='58' cy='42' r='4' fill='%23f9a8d4' opacity='0.4'/%3E%3Cellipse cx='28' cy='68' rx='8' ry='5' fill='%238b5cf6'/%3E%3Cellipse cx='52' cy='68' rx='8' ry='5' fill='%238b5cf6'/%3E%3C/svg%3E",
// 少年:较大的少年形态,有翅膀轮廓
teen: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Cpath d='M12 40 Q4 28 16 20 Q24 16 28 28' fill='%236d28d9' opacity='0.7'/%3E%3Cpath d='M84 40 Q92 28 80 20 Q72 16 68 28' fill='%236d28d9' opacity='0.7'/%3E%3Cellipse cx='28' cy='14' rx='9' ry='14' fill='%237c3aed' transform='rotate(-10 28 14)'/%3E%3Cellipse cx='68' cy='14' rx='9' ry='14' fill='%237c3aed' transform='rotate(10 68 14)'/%3E%3Cellipse cx='48' cy='48' rx='28' ry='32' fill='%237c3aed'/%3E%3Cellipse cx='48' cy='48' rx='24' ry='28' fill='%238b5cf6'/%3E%3Ccircle cx='38' cy='42' r='5' fill='white'/%3E%3Ccircle cx='58' cy='42' r='5' fill='white'/%3E%3Ccircle cx='39' cy='43' r='2.5' fill='%231a1a2e'/%3E%3Ccircle cx='59' cy='43' r='2.5' fill='%231a1a2e'/%3E%3Ccircle cx='40' cy='42' r='1' fill='white'/%3E%3Ccircle cx='60' cy='42' r='1' fill='white'/%3E%3Cpath d='M43 54 Q48 58 53 54' stroke='%23e94560' stroke-width='2' fill='none' stroke-linecap='round'/%3E%3Ccircle cx='30' cy='48' r='4' fill='%23f9a8d4' opacity='0.35'/%3E%3Ccircle cx='66' cy='48' r='4' fill='%23f9a8d4' opacity='0.35'/%3E%3Cellipse cx='34' cy='78' rx='10' ry='6' fill='%237c3aed'/%3E%3Cellipse cx='62' cy='78' rx='10' ry='6' fill='%237c3aed'/%3E%3C/svg%3E",
// 成年:完整成年形态,有翅膀和光环
adult: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='112' height='112' viewBox='0 0 112 112'%3E%3Cellipse cx='56' cy='10' rx='20' ry='4' fill='none' stroke='%23fbbf24' stroke-width='2' opacity='0.6'/%3E%3Cpath d='M10 48 Q0 30 14 18 Q26 10 32 30' fill='%235b21b6' opacity='0.8'/%3E%3Cpath d='M102 48 Q112 30 98 18 Q86 10 80 30' fill='%235b21b6' opacity='0.8'/%3E%3Cpath d='M14 52 Q6 40 18 32 Q26 28 30 38' fill='%236d28d9' opacity='0.5'/%3E%3Cpath d='M98 52 Q106 40 94 32 Q86 28 82 38' fill='%236d28d9' opacity='0.5'/%3E%3Cellipse cx='32' cy='16' rx='10' ry='16' fill='%236d28d9' transform='rotate(-8 32 16)'/%3E%3Cellipse cx='80' cy='16' rx='10' ry='16' fill='%236d28d9' transform='rotate(8 80 16)'/%3E%3Cellipse cx='56' cy='54' rx='32' ry='36' fill='%236d28d9'/%3E%3Cellipse cx='56' cy='54' rx='28' ry='32' fill='%237c3aed'/%3E%3Ccircle cx='44' cy='46' r='6' fill='white'/%3E%3Ccircle cx='68' cy='46' r='6' fill='white'/%3E%3Ccircle cx='45' cy='47' r='3' fill='%231a1a2e'/%3E%3Ccircle cx='69' cy='47' r='3' fill='%231a1a2e'/%3E%3Ccircle cx='46' cy='46' r='1.2' fill='white'/%3E%3Ccircle cx='70' cy='46' r='1.2' fill='white'/%3E%3Cpath d='M50 60 Q56 66 62 60' stroke='%23e94560' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3Ccircle cx='34' cy='54' r='5' fill='%23f9a8d4' opacity='0.3'/%3E%3Ccircle cx='78' cy='54' r='5' fill='%23f9a8d4' opacity='0.3'/%3E%3Cellipse cx='40' cy='88' rx='12' ry='7' fill='%236d28d9'/%3E%3Cellipse cx='72' cy='88' rx='12' ry='7' fill='%236d28d9'/%3E%3C/svg%3E"
};
/**
* 获取当前阶段形象(自定义优先,否则默认)
* @param {object} creatureData
* @returns {string} 图片 URL(data URI 或 base64)
*/
function getCreatureImage(creatureData) {
if (!creatureData) return DEFAULT_IMAGES.egg;
var stage = creatureData.growthStage || 'egg';
// 检查是否有自定义形象
var customImages = getPetData(creatureData.charId, 'custom_images', {});
if (customImages[stage]) {
return customImages[stage];
}
return DEFAULT_IMAGES[stage] || DEFAULT_IMAGES.egg;
}
/**
* 渲染形象(基础 + 饰品贴纸叠加)
* @param {object} creatureData
*/
function renderCreatureImage(creatureData) {
if (!creatureData) return;
// 设置基础形象
var imgEl = document.getElementById('creatureImg');
if (imgEl) {
imgEl.src = getCreatureImage(creatureData);
imgEl.alt = creatureData.name || STAGE_LABELS[creatureData.growthStage] || '魔法生物';
}
// 渲染饰品贴纸叠加层
var accessoryLayer = document.getElementById('accessoryLayer');
if (!accessoryLayer) return;
accessoryLayer.innerHTML = '';
var slotImages = getPetData(creatureData.charId, 'slot_images', {});
var slotPositions = getPetData(creatureData.charId, 'slot_positions', {});
var slotOrder = ['back', 'body', 'head', 'hand', 'effect'];
for (var i = 0; i < slotOrder.length; i++) {
var slot = slotOrder[i];
var url = slotImages[slot];
if (!url) continue;
var pos = slotPositions[slot] || { x: 50, y: 50, scale: 50, rotate: 0 };
var accImg = document.createElement('img');
accImg.src = url;
accImg.className = 'accessory-sticker';
accImg.style.left = pos.x + '%';
accImg.style.top = pos.y + '%';
accImg.style.transform = 'translate(-50%,-50%) scale(' + (pos.scale / 50) + ') rotate(' + (pos.rotate || 0) + 'deg)';
accImg.style.zIndex = (i + 1) * 10;
accImg.onerror = function() { this.style.display = 'none'; };
accessoryLayer.appendChild(accImg);
}
}
// ========== 阶段进化动画与命名流程 (Task 2.4) ==========
// ===== 阶段进化 emoji 映射 =====
var STAGE_EMOJIS = {
egg: '🥚',
baby: '👶',
child: '🧒',
teen: '🧑',
adult: '🧙'
};
/**
* 显示阶段进化动画提示
* @param {string} oldStage - 旧阶段
* @param {string} newStage - 新阶段
*/
function showEvolutionAnimation(oldStage, newStage) {
var modal = document.getElementById('evolutionModal');
var animEl = document.getElementById('evolutionAnim');
var textEl = document.getElementById('evolutionText');
if (!modal || !animEl || !textEl) return;
var oldEmoji = STAGE_EMOJIS[oldStage] || '❓';
var newEmoji = STAGE_EMOJIS[newStage] || '✨';
var oldLabel = STAGE_LABELS[oldStage] || oldStage;
var newLabel = STAGE_LABELS[newStage] || newStage;
animEl.innerHTML = oldEmoji + ' → ' + newEmoji;
textEl.innerHTML = '🎉 恭喜!你的魔法生物从<strong>' + oldLabel + '</strong>进化为<strong>' + newLabel + '</strong>了!';
modal.style.display = '';
// 重新触发动画
animEl.style.animation = 'none';
animEl.offsetHeight; // force reflow
animEl.style.animation = '';
}
/**
* 显示命名对话框
*/
function showNamingModal() {
var modal = document.getElementById('namingModal');
var input = document.getElementById('namingInput');
if (!modal) return;
if (input) {
input.value = '';
input.placeholder = '1-12个字符';
}
modal.style.display = '';
if (input) input.focus();
}
/**
* 确认命名(从 pet.html 的命名对话框调用)
*/
function confirmNaming() {
var input = document.getElementById('namingInput');
if (!input) return;
var name = input.value.trim();
if (!name || name.length < 1) {
input.placeholder = '请输入名称(1-12个字符)';
input.classList.add('input-error');
setTimeout(function() { input.classList.remove('input-error'); }, 1000);
return;
}
if (name.length > 12) {
name = name.substring(0, 12);
}
// 保存名称
if (currentCreature) {
currentCreature.name = name;
saveCreature(currentCharId, currentCreature);
}
// 关闭对话框
var modal = document.getElementById('namingModal');
if (modal) modal.style.display = 'none';
// 刷新主页显示
renderHome();
}
/**
* 显示改名对话框(设置中使用)
*/
function showRenameDialog() {
var modal = document.getElementById('namingModal');
var input = document.getElementById('namingInput');
if (!modal) return;
if (input) {
input.value = currentCreature ? (currentCreature.name || '') : '';
input.placeholder = '输入新名称(1-12个字符)';
}
modal.style.display = '';
if (input) input.focus();
}
// ========== LLM 统一调用层 ==========
/**
* 获取当前 CHAR 的 API 配置
* 复用项目中 vibe_contacts + vibe_api_schemes 的配置模式
* @param {string} charId - CHAR ID(可选,默认使用 currentCharId)
* @returns {{ apiUrl: string, apiKey: string, model: string }}
*/
function getPetApiConfig(charId) {
var cid = charId || currentCharId;
var contact = getStorageJSON('vibe_contacts', []).find(function(c) {
return String(c.id) === String(cid);
});
var apiUrl, apiKey, model;
if (contact && contact.apiScheme) {
var schemes = getStorageJSON('vibe_api_schemes', []);
var scheme = schemes.find(function(s) { return s.id === contact.apiScheme; });
if (scheme) {
apiUrl = scheme.apiUrl;
apiKey = scheme.apiKey;
model = scheme.model;
}
}
if (!apiUrl) {
apiUrl = localStorage.getItem('apiUrl');
apiKey = localStorage.getItem('apiKey');
model = (contact && contact.model) || localStorage.getItem('selectedModel');
}
return { apiUrl: apiUrl, apiKey: apiKey, model: model };
}
/**
* 统一 LLM 调用函数
* 复用 OpenAI 兼容 API 调用模式,包含完整错误处理和降级逻辑
* @param {string} prompt - 用户提示词
* @param {string} [systemPrompt] - 系统提示词(可选)
* @param {number} [temperature] - 温度参数(可选,默认 0.7)
* @returns {Promise<string>} LLM 回复文本
*/
async function callPetLLM(prompt, systemPrompt, temperature) {
var config = getPetApiConfig();
// 检查 API 配置是否完整
if (!config.apiUrl || !config.apiKey || !config.model) {
throw new Error('API 配置不完整,请在设置中配置 API 地址、密钥和模型');
}
// 构建消息数组
var messages = [];
if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt });
}
messages.push({ role: 'user', content: prompt });
// 构建请求 URL,处理双斜杠问题
var url = (config.apiUrl + '/chat/completions').replace(/([^:]\/)\/+/g, '$1');
var resp;
try {
resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + config.apiKey
},
body: JSON.stringify({
model: config.model,
messages: messages,
temperature: temperature || 0.7
})
});
} catch (e) {
throw new Error('网络连接失败,请检查网络或API地址是否可用');
}
// HTTP 错误处理
if (!resp.ok) {
var detail = '';
try { detail = await resp.text(); } catch (_) {}
throw new Error('API请求失败(' + resp.status + '): ' + (detail || '未知错误'));
}
// 验证响应格式为 JSON
var contentType = resp.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
var text = await resp.text();
throw new Error('API返回了非JSON格式: ' + text.slice(0, 200));
}
// 解析响应数据
var data = await resp.json();
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
throw new Error('API返回格式异常: ' + JSON.stringify(data).slice(0, 200));
}
return data.choices[0].message.content.trim();
}
// ===== 积分系统读写联动 =====
// 直接读写 alipay_points localStorage 键,与支付宝积分系统共享积分池
// 检查积分余额是否足够支付 cost
function checkPoints(charId, cost) {
var data = getStorageJSON('alipay_points', {});
var balance = data[charId] || 0;
return balance >= cost;
}
// 扣除积分并更新钱包显示
function deductPoints(charId, amount) {
var data = getStorageJSON('alipay_points', {});
var balance = data[charId] || 0;
data[charId] = Math.max(0, balance - amount);
setStorageJSON('alipay_points', data);
// 更新页面上的钱包余额显示
var walletEl = document.getElementById('walletAmount');
if (walletEl) {
walletEl.textContent = data[charId];
}
}
// ===== 喂养系统与饱食度机制 =====
// 默认食物列表(不需要从背包获取,直接用积分购买)
var DEFAULT_FOODS = [
{ id: 'food_bread', name: '魔法面包', satiety: 10, cost: 5, type: 'food' },
{ id: 'food_milk', name: '星光牛奶', satiety: 15, cost: 8, type: 'food' },
{ id: 'food_fruit', name: '彩虹水果', satiety: 20, cost: 10, type: 'food' },
{ id: 'food_cake', name: '月光蛋糕', satiety: 30, cost: 15, type: 'food' },
{ id: 'food_feast', name: '精灵大餐', satiety: 50, cost: 20, type: 'food' }
];
/**
* 获取可用食物列表(默认食物 + 背包中的食物)
* @param {string} charId
* @returns {Array} 食物列表,每项含 { id, name, satiety, cost, type, source }
*/
function getAvailableFoods(charId) {
var foods = [];
// 添加默认食物(标记来源为 default)
for (var i = 0; i < DEFAULT_FOODS.length; i++) {
var f = DEFAULT_FOODS[i];
foods.push({
id: f.id,
name: f.name,
satiety: f.satiety,
cost: f.cost,
type: f.type,
source: 'default'
});
}
// 添加背包中的食物(标记来源为 inventory)
var inventory = getPetData(charId, 'inventory', []);
for (var j = 0; j < inventory.length; j++) {
var item = inventory[j];
if (item && item.type === 'food') {
foods.push({
id: item.id,
name: item.name,
satiety: item.satiety || 10,
cost: 0, // 背包食物已购买,无需再扣积分
type: item.type,
source: 'inventory',
inventoryIndex: j
});
}
}
return foods;
}
/**
* 计算饱食度衰减量(基础每小时 -5,体力越高衰减越慢)
* @param {number} lastUpdateTime - 上次更新饱食度的时间戳(毫秒)
* @returns {number} 应衰减的饱食度值(正数)
*/
function calculateHungerDecay(lastUpdateTime) {
if (!lastUpdateTime) return 0;
var now = Date.now();
var elapsed = now - lastUpdateTime;
if (elapsed <= 0) return 0;
var hours = elapsed / (60 * 60 * 1000);
// 体力减缓饥饿:每10点体力减少5%衰减,最多减50%
var staminaReduction = 1;
if (currentCreature && currentCreature.attributes) {
var stamina = currentCreature.attributes.stamina || 10;
staminaReduction = Math.max(0.5, 1 - (stamina / 100) * 0.5);
}
return Math.floor(hours * 5 * staminaReduction);
}
/**
* 更新饱食度(限制 0-100),同时追踪 satietyZeroSince
* @param {object} creatureData
* @param {number} delta - 变化量(正数增加,负数减少)
*/
function updateSatiety(creatureData, delta) {
if (!creatureData) return;
var oldSatiety = creatureData.satiety || 0;
var newSatiety = oldSatiety + delta;
// 限制在 0-100 范围
newSatiety = Math.max(0, Math.min(100, newSatiety));
creatureData.satiety = newSatiety;
creatureData.lastSatietyUpdate = Date.now();
// 追踪饱食度归零时间(用于死亡机制)
if (newSatiety <= 0 && oldSatiety > 0) {
// 刚降到0,记录时间
creatureData.satietyZeroSince = Date.now();
} else if (newSatiety > 0) {
// 恢复了,清除记录
creatureData.satietyZeroSince = null;
}
}
/**
* 执行喂食操作
* 检查存活→检查积分/背包→扣除积分/移除背包物品→增加饱食度→心情+3→HP+2→保存→更新UI
* @param {string} charId
* @param {object} foodItem - 食物对象 { id, name, satiety, cost, source, inventoryIndex? }
* @returns {object} { success: boolean, message: string }
*/
function feedCreature(charId, foodItem) {
// 检查 Creature 是否存活
var creature = currentCreature || loadCreature(charId);
if (!creature || !isCreatureAlive(creature)) {
return { success: false, message: '魔法生物已经不在了…' };
}
// 根据食物来源检查并扣除
if (foodItem.source === 'default') {
// 默认食物:需要扣积分
if (!checkPoints(charId, foodItem.cost)) {
return { success: false, message: '积分不足,无法购买' + foodItem.name + '(需要' + foodItem.cost + '积分)' };
}
deductPoints(charId, foodItem.cost);
} else if (foodItem.source === 'inventory') {
// 背包食物:从背包中移除
var inventory = getPetData(charId, 'inventory', []);
var idx = -1;
// 优先使用 inventoryIndex,否则按 id 查找
if (typeof foodItem.inventoryIndex === 'number' && foodItem.inventoryIndex >= 0 && foodItem.inventoryIndex < inventory.length) {
idx = foodItem.inventoryIndex;
} else {
for (var i = 0; i < inventory.length; i++) {
if (inventory[i] && inventory[i].id === foodItem.id) {
idx = i;
break;
}
}
}
if (idx < 0) {
return { success: false, message: '背包中没有找到' + foodItem.name };
}
inventory.splice(idx, 1);
setPetData(charId, 'inventory', inventory);
}
// 增加饱食度(创造力加成)
var satietyGain = foodItem.satiety || 10;
var feedBonus = getAttrFeedBonus(creature);
satietyGain = Math.round(satietyGain * feedBonus.satietyMult);
updateSatiety(creature, satietyGain);
// 心情值 +3(魅力加成)
var moodGain = Math.round(3 * feedBonus.moodMult);
updateMood(creature, moodGain);