-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathscript.js.del
More file actions
4338 lines (3921 loc) · 203 KB
/
script.js.del
File metadata and controls
4338 lines (3921 loc) · 203 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
/* script.js (主脚本) 已将大量基础定义拆分到 lib/constants.js, lib/utils.js, lib/models.js
这些文件必须在 HTML 中先于本文件加载。
*/
if(typeof GameState === 'undefined'){
console.warn('Warning: GameState is not defined. Ensure lib/models.js is loaded before script.js');
}
// 避免在脚本加载时使用未初始化的局部 `game`(会触发 TDZ)。
// 使用 `window.game` 作为全局持有者,并在需要时懒初始化。
if(typeof window.game === 'undefined' || !window.game){
try{ window.game = new GameState(); }catch(e){ /* 如果 GameState 不可用,保留为 undefined,稍后在 onload 中处理 */ }
}
// 局部引用始终通过 window.game 访问,避免在全局初始化顺序问题上抛错
let game = window.game;
// On load: no debug output. Keep initialization lightweight.
// Diagnostic helpers: write/read to multiple storage channels to help detect overwrite
// Minimal diagnostics: maintain a sessionStorage backup for cross-page recovery.
// This keeps recovery simple and avoids extra debug-only channels.
/* 每日/每次渲染随机一言 */
const QUOTES = [
"想想你的对手正在干什么",
"课间就是用来放松的?",
"没有天赋异禀的幸运,唯有水滴石穿的坚持",
"没有一步登天的幻想,唯有日积月累的付出",
"竞赛生没有特权",
"自律者出众,懒惰者出局"
];
/* =========== UI 辅助 =========== */
const $ = id => document.getElementById(id);
const currWeek = () => (game?.week) || 0;
function log(msg){
const el = $('log');
const wk = currWeek();
const text = `[周${wk}] ${msg}`;
if(el){ const p = document.createElement('div'); p.innerText = text; el.prepend(p); }
else { console.log(text); }
}
// 难度数值到标签的映射(更新为 7 个颜色等级:红/橙/黄/绿/蓝/紫/黑)
// 返回 HTML 字符串:带有 .diff-tag 和额外级别类(兼容旧的 diff-* 类)
function renderDifficultyTag(diff){
// 传入的 diff 可能为 0-100 的数值
const d = Number(diff) || 0;
// 将 0-100 划分为 7 个区间:0-14,15-29,30-44,45-59,60-74,75-89,90-100
// 并为每个区间返回一个语义标签与颜色类(diff-red, diff-orange, diff-yellow, diff-green, diff-blue, diff-purple, diff-black)
let label = '';
let cls = '';
if(d <= 14){ label = '入门'; cls = 'diff-red'; }
else if(d <= 39){ label = '普及-'; cls = 'diff-orange'; }
else if(d <= 54){ label = '普及/提高-'; cls = 'diff-yellow'; }
else if(d <= 79){ label = '普及+/提高'; cls = 'diff-green'; }
else if(d <= 94){ label = '提高+/省选-'; cls = 'diff-blue'; }
else if(d <= 110){ label = '省选/NOI-'; cls = 'diff-purple'; }
else { label = 'NOI+/CTSC'; cls = 'diff-black'; }
// 兼容旧的 class 名称:同时保留原有类别映射(以便其他地方可能使用旧类名)
// 额外附加原有 class 名称以便向后兼容
const legacy = (d <= 24) ? 'diff-beginner' : (d <= 34) ? 'diff-popular-low' : (d <= 44) ? 'diff-popular-high' : (d <= 64) ? 'diff-advanced-low' : (d <= 79) ? 'diff-provincial' : 'diff-noi';
// 包装为带背景的 tag(移除 .tag 类以避免被通用样式覆盖)
return `<span class="diff-tag ${cls} ${legacy}" title="难度: ${d}">${label}</span>`;
}
// 安全渲染:仅在页面具备主 UI 元素时调用 renderAll,避免在测试页面(缺少元素)时抛错
function safeRenderAll(){
try{
if(typeof window.renderAll === 'function' && document.getElementById('header-week')){
window.renderAll();
}
}catch(e){ console.error('safeRenderAll error', e); }
}
// 将事件推入突发事件卡片(并保留日志)
const recentEvents = [];
// 为每个事件生成唯一ID的计数器
let _eventIdCounter = 0;
function pushEvent(msg){
const wkDefault = currWeek();
const ev = (typeof msg === 'string')
? { name: null, description: msg, week: wkDefault }
: {
name: msg.name || null,
description: msg.description || msg.text || '',
week: msg.week || wkDefault,
options: msg.options || null, // 支持选项
eventId: msg.eventId || null // 用于区分同一事件的不同实例
};
// 为每个事件分配唯一的内部ID
ev._uid = ++_eventIdCounter;
log(`${ev.name ? ev.name + ':' : ''}${ev.description}`);
const key = `${ev.week}::${ev.name||''}::${ev.description||''}::${ev.eventId||''}`;
if(!recentEvents.some(r => `${r.week}::${r.name||''}::${r.description||''}::${r.eventId||''}` === key)){
recentEvents.unshift(ev);
if(recentEvents.length > 24) recentEvents.pop();
}
renderEventCards();
}
// 弱化人数影响的缩放函数:可以统一控制人数带来的放大效果
// 当前实现使用 sqrt(n) 再向下取整,至少返回 1(避免零人数导致乘积为0的场景)
function scaledPassCount(n){
n = Number(n) || 0;
if(n <= 0) return 0;
return Math.max(1, Math.floor(Math.sqrt(n)));
}
// ===== 状态快照与差异汇总工具 =====
function __createSnapshot(){
return {
budget: game.budget || 0,
reputation: game.reputation || 0,
students: game.students.map(s=>({
name: s.name,
// treat student as active unless explicitly set to false (backwards compatible)
active: (s && s.active !== false),
pressure: Number((s.pressure||0).toFixed(2)),
thinking: Number((s.thinking||0).toFixed(2)),
coding: Number((s.coding||0).toFixed(2)),
// 按维度保存知识,便于后来比较显示每个知识点的变化
knowledge_ds: Number((s.knowledge_ds||0).toFixed(2)),
knowledge_graph: Number((s.knowledge_graph||0).toFixed(2)),
knowledge_string: Number((s.knowledge_string||0).toFixed(2)),
knowledge_math: Number((s.knowledge_math||0).toFixed(2)),
knowledge_dp: Number((s.knowledge_dp||0).toFixed(2)),
// 保留总体知识总和(向后兼容)
knowledge: Number(s.getKnowledgeTotal?.() || ((s.knowledge_ds||0)+(s.knowledge_graph||0)+(s.knowledge_string||0)+(s.knowledge_math||0)+(s.knowledge_dp||0)))
}))
};
}
function __summarizeSnapshot(before, after, title, opts){
try{
opts = opts || {};
const parts = [];
const db = (after.budget||0) - (before.budget||0);
if(db !== 0) parts.push(`经费 ${db>0?'+':'-'}¥${Math.abs(db)}`);
const dr = (after.reputation||0) - (before.reputation||0);
if(dr !== 0) parts.push(`声誉 ${dr>0?'+':''}${dr}`);
const beforeMap = new Map(before.students.map(s => [s.name, s]));
const afterMap = new Map(after.students.map(s => [s.name, s]));
const added = [...afterMap.keys()].filter(n => !beforeMap.has(n));
if(added.length) parts.push(`加入: ${added.join('、')}`);
const removed = [...beforeMap.keys()].filter(n => !afterMap.has(n));
if(removed.length) parts.push(`退队: ${removed.join('、')}`);
const stuParts = [];
for(const [name, beforeS] of beforeMap){
const afterS = afterMap.get(name);
if(!afterS) continue;
const changes = [];
const dP = Number((afterS.pressure - beforeS.pressure).toFixed(2));
const dT = Number((afterS.thinking - beforeS.thinking).toFixed(2));
const dC = Number((afterS.coding - beforeS.coding).toFixed(2));
const dK = Number((afterS.knowledge - beforeS.knowledge).toFixed(2));
if(dP !== 0) changes.push(`压力 ${dP>0?'+':''}${dP}`);
if(dT !== 0) changes.push(`思维 ${dT>0?'+':''}${dT}`);
if(dC !== 0) changes.push(`编程 ${dC>0?'+':''}${dC}`);
// 逐维度显示知识点变化(仅在有变化时显示)
const dDS = Number(((afterS.knowledge_ds || 0) - (beforeS.knowledge_ds || 0)).toFixed(2));
const dGraph = Number(((afterS.knowledge_graph || 0) - (beforeS.knowledge_graph || 0)).toFixed(2));
const dStr = Number(((afterS.knowledge_string || 0) - (beforeS.knowledge_string || 0)).toFixed(2));
const dMath = Number(((afterS.knowledge_math || 0) - (beforeS.knowledge_math || 0)).toFixed(2));
const dDP = Number(((afterS.knowledge_dp || 0) - (beforeS.knowledge_dp || 0)).toFixed(2));
if(dDS !== 0) changes.push(`数据结构 ${dDS>0?'+':''}${dDS}`);
if(dGraph !== 0) changes.push(`图论 ${dGraph>0?'+':''}${dGraph}`);
if(dStr !== 0) changes.push(`字符串 ${dStr>0?'+':''}${dStr}`);
if(dMath !== 0) changes.push(`数学 ${dMath>0?'+':''}${dMath}`);
if(dDP !== 0) changes.push(`DP ${dDP>0?'+':''}${dDP}`);
// 如果总体知识有变化并且没有逐项展示(兼容旧逻辑),仍保留总体显示
if(dK !== 0 && !(dDS !==0 || dGraph !==0 || dStr !==0 || dMath !==0 || dDP !==0)) changes.push(`知识 ${dK>0?'+':''}${dK}`);
if(changes.length) stuParts.push(`${name}: ${changes.join(',')}`);
}
if(stuParts.length) parts.push(stuParts.join('; '));
const summary = parts.length ? parts.join('; ') : '无显著变化';
// 默认为推送事件卡片;当 opts.suppressPush 为 true 时仅返回汇总,不产生卡片
if (!opts.suppressPush) {
pushEvent({ name: title || '变动汇总', description: summary, week: currWeek() });
}
return summary;
}catch(e){ console.error('summarize error', e); return null; }
}
window.__createSnapshot = __createSnapshot;
window.__summarizeSnapshot = __summarizeSnapshot;
// 渲染所有突发事件卡到 #event-cards-container
function renderEventCards(){
const container = $('event-cards-container');
if(!container) return;
container.innerHTML = '';
if(recentEvents.length === 0){
// 当没有事件时保持容器为空(不显示占位卡片)
return;
}
const nowWeek = currWeek();
let shown = 0;
for(let i = 0; i < recentEvents.length; i++){
const ev = recentEvents[i];
if(ev.week && (nowWeek - ev.week) > 2) continue; // 只显示最近2周内
// 跳过已处理的事件(已点击选项的事件不应再显示)
if(ev._isHandled) continue;
const card = document.createElement('div');
// base classes for event card; add `event-required` when this event has options (needs user choice)
let cardClass = 'event-card event-active';
if (ev.options && ev.options.length > 0) {
cardClass += ' event-required';
}
card.className = cardClass;
// Prepare short summary (one-line) and full detail (preformatted)
const titleHtml = `<div class="card-title">${ev.name || '突发事件'}` +
`${(ev.options && ev.options.length > 0) ? '<span class="required-tag">未选择</span>' : ''}` +
`</div>`;
const descText = ev.description || '';
// Escape HTML in description to avoid injecting markup
const esc = (s) => String(s||'').replace(/[&<>"']/g, function(ch){return ({'&':'&','<':'<','>':'>','"':'"',"'":"'"})[ch];});
const shortDesc = (descText.length > 120) ? descText.slice(0, 118) + '…' : descText;
let cardHTML = '';
cardHTML += titleHtml;
cardHTML += `<div class="card-desc clamp" data-uid="${ev._uid}">${esc(shortDesc)}</div>`;
// hidden full detail area (pre-wrap for line breaks)
cardHTML += `<div class="event-detail" data-uid="${ev._uid}" style="display:none">${esc(descText)}</div>`;
// more / less toggle
if(descText && descText.length > 60){
cardHTML += `<button class="more-btn" data-action="toggle-detail" data-uid="${ev._uid}">更多</button>`;
}
// 如果有选项,添加选项按钮(保持原有行为)
if(ev.options && ev.options.length > 0){
cardHTML += '<div class="event-options" style="margin-top:10px; display:flex; gap:8px;">';
ev.options.forEach((opt, idx) => {
cardHTML += `<button class="btn event-choice-btn" data-event-uid="${ev._uid}" data-option-index="${idx}">${opt.label || `选项${idx+1}`}</button>`;
});
cardHTML += '</div>';
}
card.innerHTML = cardHTML;
container.appendChild(card);
if(++shown >= 6) break; // 最多显示6个
}
}
// 是否存在需要玩家选择但尚未处理的事件卡
function hasPendingRequiredEvents(){
try{
return recentEvents.some(ev => ev && ev.options && ev.options.length > 0 && !ev._isHandled);
}catch(e){ return false; }
}
// 事件卡的展开/折叠及更多按钮由容器的全局点击监听处理
// extend existing load handler to support toggle-detail
window.addEventListener('load', () => {
const container = $('event-cards-container');
if (!container) return;
// ensure event-detail animation styles are present
(function(){
if(!document.getElementById('event-detail-animations')){
const s = document.createElement('style');
s.id = 'event-detail-animations';
s.textContent = `
@keyframes et-slide-in-right { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: translateX(0); } }
@keyframes et-slide-out-right { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(24px); } }
.event-detail { display: none; }
.event-detail.visible { display: block; animation: et-slide-in-right 0.25s ease both; }
.event-detail.hiding { animation: et-slide-out-right 0.22s ease both; }
`;
document.head.appendChild(s);
}
})();
container.addEventListener('click', function(e){
const btn = e.target.closest('.more-btn');
if (!btn) return;
const uid = btn.dataset.uid ? parseInt(btn.dataset.uid, 10) : null;
if (!uid) return;
const detail = container.querySelector(`.event-detail[data-uid='${uid}']`);
const desc = container.querySelector(`.card-desc[data-uid='${uid}']`);
if (!detail || !desc) return;
// Use CSS classes + animation instead of immediate display toggling
if (detail.classList.contains('visible')){
// start hide animation
detail.classList.remove('visible');
detail.classList.add('hiding');
desc.classList.add('clamp');
btn.innerText = '更多';
const onAnimEnd = function(ev){
// only react to our animation
detail.classList.remove('hiding');
detail.style.display = 'none';
detail.removeEventListener('animationend', onAnimEnd);
};
detail.addEventListener('animationend', onAnimEnd);
} else {
// show and play enter animation
detail.style.display = 'block';
// force reflow so the animation runs
void detail.offsetWidth;
detail.classList.remove('hiding');
detail.classList.add('visible');
desc.classList.remove('clamp');
btn.innerText = '收起';
}
});
});
// 显示事件弹窗
function showEventModal(evt){
const title = evt?.name || '事件';
const desc = evt?.description || evt?.text || '暂无描述';
const weekInfo = `[周${evt?.week || currWeek()}] `;
showModal(`<h3>${weekInfo}${title}</h3><div class="small" style="margin-top:6px">${desc}</div><div class="modal-actions"><button class="btn" onclick="closeModal()">关闭</button></div>`);
}
// ✅ 重构:全局事件委托处理器
function handleEventChoice(event) {
const button = event.target.closest('.event-choice-btn');
if (!button) return;
const eventUid = parseInt(button.dataset.eventUid, 10);
const optionIndex = parseInt(button.dataset.optionIndex, 10);
if (isNaN(eventUid) || isNaN(optionIndex)) return;
// 通过唯一ID查找事件
const targetEvent = recentEvents.find(e => e._uid === eventUid);
if (!targetEvent || targetEvent._isHandled) return;
// 标记为已处理
targetEvent._isHandled = true;
// 禁用所有按钮
const card = button.closest('.event-card');
if (card) {
card.querySelectorAll('.event-choice-btn').forEach(btn => {
btn.disabled = true;
btn.classList.add('disabled');
});
}
// 执行选项效果
const option = targetEvent.options[optionIndex];
try {
if (option && typeof option.effect === 'function') {
option.effect();
}
} catch (err) {
console.error('执行事件选项效果时出错:', err);
}
// 强制清除一次性抑制标志
try {
if (game && game.suppressEventModalOnce) {
game.suppressEventModalOnce = false;
}
} catch (err) {}
// 重新渲染
renderEventCards();
safeRenderAll();
}
// ✅ 重构:在页面加载时绑定全局监听器
window.addEventListener('load', () => {
const container = $('event-cards-container');
if (container) {
container.addEventListener('click', handleEventChoice);
}
});
// 显示选择事件弹窗(现改为推送到信息卡片)
function showChoiceModal(evt){
const title = evt?.name || '选择事件';
const desc = evt?.description || '';
const options = evt?.options || [];
// 生成唯一的事件ID,避免重复推送相同事件
const eventId = `choice_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 直接推送到事件卡片而不是弹窗
pushEvent({
name: title,
description: desc,
week: evt?.week || currWeek(),
options: options,
eventId: eventId
});
}
// 开发期间用于测试的 `options` 数据已移除 — 该数据在运行时无引用,保留 showChoiceModal 功能。
/* 渲染:主页去数值化(不显示学生具体能力/压力数值) */
function renderAll(){
// 如果主 UI 元素不存在(例如在独立测试页面),安全退出以避免抛错
if(!document.getElementById('header-week')) return;
$('header-week').innerText = `第 ${currWeek()} 周`;
$('header-province').innerText = `省份: ${game.province_name} (${game.province_type})`;
const headerBudgetEl = $('header-budget');
if(headerBudgetEl) headerBudgetEl.innerText = `经费: ¥${game.budget}`;
// 当经费低于 20000 时添加警示类以高亮显示(红色),否则移除
try{
if(headerBudgetEl){
if(Number(game.budget) < 20000){ headerBudgetEl.classList.add('low-funds'); }
else { headerBudgetEl.classList.remove('low-funds'); }
}
}catch(e){ /* ignore */ }
$('header-reputation').innerText = `声誉: ${game.reputation}`;
$('info-week').innerText = currWeek();
// week info
const infoWeekEl = $('info-week'); if(infoWeekEl) infoWeekEl.innerText = currWeek();
// weather: populate both the sidebar detailed elements and the compact header elements (header-weather)
const tempText = game.temperature.toFixed(1) + "\u00b0C";
const weatherDesc = game.getWeatherDescription();
const infoTempEl = $('info-temp'); if(infoTempEl) infoTempEl.innerText = tempText;
const infoWeatherEl = $('info-weather'); if(infoWeatherEl) infoWeatherEl.innerText = weatherDesc;
const infoFutureEl = $('info-future-expense'); if(infoFutureEl) infoFutureEl.innerText = game.getFutureExpense();
// teaching points UI removed; avoid touching $('info-teach') which no longer exists
// 下场比赛单独面板渲染
const nextCompText = game.getNextCompetition();
const nextCompEl = $('next-comp'); if(nextCompEl) nextCompEl.innerText = nextCompText;
// fill compact header small summary
const headerNextSmall = $('header-next-comp-small'); if(headerNextSmall) headerNextSmall.innerText = nextCompText;
// also set header weather compact text
const headerWeatherText = $('header-weather-text'); if(headerWeatherText) headerWeatherText.innerText = weatherDesc;
const headerTempHeader = $('header-temp-header'); if(headerTempHeader) headerTempHeader.innerText = tempText;
// 随机一言
const q = QUOTES[ Math.floor(Math.random() * QUOTES.length) ];
$('daily-quote').innerText = q;
// 如果距离下场比赛 <=4周则高亮面板
let match = nextCompText.match(/还有(\d+)周/);
let weeksLeft = match ? parseInt(match[1],10) : null;
const panel = $('next-competition-panel');
if(weeksLeft !== null && weeksLeft <= 4){ panel.className = 'next-panel highlight'; }
else { panel.className = 'next-panel normal'; }
// 比赛时间轴按周次排序展示
const scheduleComps = competitions.slice().sort((a, b) => a.week - b.week);
$('comp-schedule').innerText = scheduleComps.map(c => `${c.week}:${c.name}`).join(" | ");
// comfort (舒适度显示更新)
const currentComfort = game.getComfort();
const comfortEl = $('comfort-val');
if(comfortEl) comfortEl.innerText = Math.floor(currentComfort);
// facilities
$('fac-computer').innerText = game.facilities.computer;
$('fac-library').innerText = game.facilities.library;
$('fac-ac').innerText = game.facilities.ac;
$('fac-dorm').innerText = game.facilities.dorm;
$('fac-canteen').innerText = game.facilities.canteen;
$('fac-maint').innerText = game.facilities.getMaintenanceCost();
// students: only show name, star-level (知识掌握 visual), pressure level (低/中/高), and small tags (生病 / 退队)
let out = '';
for(let s of game.students){
if(s && s.active === false) continue;
let pressureLevel = s.pressure < 35 ? "低" : s.pressure < 65 ? "中" : "高";
let pressureClass = s.pressure < 35 ? "pressure-low" : s.pressure < 65 ? "pressure-mid" : "pressure-high";
// 计算模糊资质与能力等级:思维能力 & 心理素质(确保为数字)
//const thinkingVal = Number(s.thinking || 0);
//const mentalVal = Number(s.mental || 0);
// let aptitudeVal = 0.5 * thinkingVal + 0.5 * mentalVal;
//let aptitudeGrade = getLetterGrade(Math.floor(aptitudeVal));
// 能力 = 各能力平均 + 各知识点方差加权
//let abilityAvg = Number(s.getAbilityAvg ? s.getAbilityAvg() : 0) || 0;
// 计算知识方差
//let kArr = [Number(s.knowledge_ds||0), Number(s.knowledge_graph||0), Number(s.knowledge_string||0), Number(s.knowledge_math||0), Number(s.knowledge_dp||0)];
//let kMean = kArr.reduce((a,v) => a+v, 0) / kArr.length;
//let variance = kArr.reduce((a,v) => a + Math.pow(v - kMean, 2), 0) / kArr.length;
//let varNorm = clamp(variance, 0, 100);
// 50% 能力平均 + 50% 知识方差
//let abilityVal = abilityAvg * 0.5 + varNorm * 0.5;
//let abilityGrade = getLetterGrade(Math.floor(abilityVal));
//const compRaw = Number(s.getComprehensiveAbility ? s.getComprehensiveAbility() : 0);
//const comp = isFinite(compRaw) ? Math.floor(compRaw) : 0;
// 生成天赋标签HTML
let talentsHtml = '';
if(s.talents && s.talents.size > 0){
const talentArray = Array.from(s.talents);
talentsHtml = talentArray.map(talentName => {
const talentInfo = window.TalentManager ? window.TalentManager.getTalentInfo(talentName) : { name: talentName, description: '暂无描述', color: '#2b6cb0' };
return `<span class="talent-tag" data-talent="${talentName}" style="background-color: ${talentInfo.color}20; color: ${talentInfo.color}; border-color: ${talentInfo.color}40;">
${talentName}
<span class="talent-tooltip">${talentInfo.description}</span>
</span>`;
}).join('');
}
out += `<div class="student-box">
<button class="evict-btn" data-idx="${game.students.indexOf(s)}" title="劝退">劝退</button>
<div class="student-header">
<div class="student-name">
${s.name}
${s.sick_weeks > 0 ? '<span class="warn">[生病]</span>' : ''}
</div>
<div class="student-status">
<span class="label-pill ${pressureClass}">压力: ${pressureLevel}</span>
</div>
</div>
<div class="student-details" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:6px;">
<span style="font-size:12px;color:#718096;font-weight:600;">知识</span>
<div class="knowledge-badges">
<span class="kb" title="数据结构: ${Math.floor(Number(s.knowledge_ds||0))}" data-grade="${getLetterGradeAbility(Math.floor(Number(s.knowledge_ds||0)))}">
DS ${getLetterGradeAbility(Math.floor(Number(s.knowledge_ds||0)))}
</span>
<span class="kb" title="图论: ${Math.floor(Number(s.knowledge_graph||0))}" data-grade="${getLetterGradeAbility(Math.floor(Number(s.knowledge_graph||0)))}">
图论 ${getLetterGradeAbility(Math.floor(Number(s.knowledge_graph||0)))}
</span>
<span class="kb" title="字符串: ${Math.floor(Number(s.knowledge_string||0))}" data-grade="${getLetterGradeAbility(Math.floor(Number(s.knowledge_string||0)))}">
字符串${getLetterGradeAbility(Math.floor(Number(s.knowledge_string||0)))}
</span>
<span class="kb" title="数学: ${Math.floor(Number(s.knowledge_math||0))}" data-grade="${getLetterGradeAbility(Math.floor(Number(s.knowledge_math||0)))}">
数学 ${getLetterGradeAbility(Math.floor(Number(s.knowledge_math||0)))}
</span>
<span class="kb" title="动态规划: ${Math.floor(Number(s.knowledge_dp||0))}" data-grade="${getLetterGradeAbility(Math.floor(Number(s.knowledge_dp||0)))}">
DP ${getLetterGradeAbility(Math.floor(Number(s.knowledge_dp||0)))}
</span>
<!-- 直接追加能力徽章到知识徽章组,保持统一样式 -->
<span class="kb ability" title="思维: ${Math.floor(Number(s.thinking||0))}" data-grade="${getLetterGradeAbility(Math.floor(Number(s.thinking||0)))}">思维${getLetterGradeAbility(Math.floor(Number(s.thinking||0)))}</span>
<span class="kb ability" title="代码: ${Math.floor(Number(s.coding||0))}" data-grade="${getLetterGradeAbility(Math.floor(Number(s.coding||0)))}">代码${getLetterGradeAbility(Math.floor(Number(s.coding||0)))}</span>
</div>
</div>
${talentsHtml ? `<div style="display:flex;align-items:center;gap:6px;"><span style="font-size:12px;color:#718096;font-weight:600;">天赋</span><div class="student-talents">${talentsHtml}</div></div>` : ''}
</div>
</div>`;
}
if(out==='') out = '<div class="muted">目前没有活跃学生</div>';
$('student-list').innerHTML = out;
// bind per-student evict buttons
document.querySelectorAll('#student-list .evict-btn').forEach(b=>{
b.onclick = (e) => {
const idx = parseInt(b.dataset.idx,10);
if(isNaN(idx)) return;
// confirm and evict single
if(game.reputation < EVICT_REPUTATION_COST){ alert('声誉不足,无法劝退'); return; }
if(!confirm(`确认劝退 ${game.students[idx].name}?将消耗声誉 ${EVICT_REPUTATION_COST}`)) return;
evictSingle(idx);
};
});
// render dynamic event cards
renderEventCards();
// 如果存在未处理的必选事件,禁用所有行动卡并引导玩家处理事件
try{
const pending = hasPendingRequiredEvents();
const actionCards = Array.from(document.querySelectorAll('.action-card'));
if(pending){
// 禁用并添加视觉提示
actionCards.forEach(ac => {
ac.classList.add('disabled');
ac.setAttribute('aria-disabled', 'true');
ac.setAttribute('tabindex', '-1');
// 保存原始 onclick 引用以便恢复(比保存字符串更可靠)
try{
if(typeof ac._origOnclickFn === 'undefined'){
ac._origOnclickFn = ac.onclick || null;
}
}catch(e){}
// 设置临时阻塞处理器
ac.onclick = (e) => { e && e.stopPropagation && e.stopPropagation(); e && e.preventDefault && e.preventDefault();
const msg = '存在未处理的事件卡片,请先在右侧事件区域选择处理后再进行行动。';
if(window.toastManager && typeof window.toastManager.show === 'function') window.toastManager.show(msg, 'warning'); else try{ alert(msg); }catch(e){}
// 将第一个未处理事件滚动进视图并轻微闪烁高亮
const container = $('event-cards-container');
if(container){
const firstPending = container.querySelector('.event-card.event-required');
if(firstPending){
try{ firstPending.scrollIntoView({ behavior: 'smooth', block: 'center' }); }catch(e){}
firstPending.classList.add('highlight-pending');
setTimeout(()=>{ firstPending.classList.remove('highlight-pending'); }, 1800);
}
}
};
});
// 高亮并滚动第一个未处理事件卡
const container = $('event-cards-container');
if(container){
const firstPending = container.querySelector('.event-card.event-required');
if(firstPending){ try{ firstPending.scrollIntoView({ behavior: 'smooth', block: 'center' }); }catch(e){}; firstPending.classList.add('highlight-pending'); setTimeout(()=>{ firstPending.classList.remove('highlight-pending'); }, 1800); }
}
} else {
// 恢复行动卡的交互
actionCards.forEach(ac => {
ac.classList.remove('disabled');
ac.removeAttribute('aria-disabled');
ac.setAttribute('tabindex', '0');
try{
// 如果我们之前保存了原始 onclick 引用,则恢复它
if(typeof ac._origOnclickFn !== 'undefined'){
try{ ac.onclick = ac._origOnclickFn; }catch(e){}
try{ delete ac._origOnclickFn; }catch(e){}
} else {
// 兼容旧逻辑:如果当前 onclick 是我们的阻塞函数(通过文本判断),则清除它
if(ac.onclick && ac.onclick.toString && ac.onclick.toString().includes('存在未处理的事件卡片')){
ac.onclick = null;
}
}
}catch(e){}
});
}
}catch(e){ /* ignore UI assist failures */ }
// Competition-week: 如果当前周有未完成的比赛,则注入 "参加比赛" 按钮
// 只处理尚未完成的比赛
let compNow = null;
const sortedComps = Array.isArray(competitions) ? competitions.slice().sort((a,b)=>a.week - b.week) : [];
for (let comp of sortedComps) {
if (comp.week === currWeek()) {
const half = (currWeek() > WEEKS_PER_HALF) ? 1 : 0;
const key = `${half}_${comp.name}_${comp.week}`;
if (!game.completedCompetitions || !game.completedCompetitions.has(key)) {
compNow = comp;
}
break;
}
}
// render competition action card
const actionContainer = document.querySelector('.action-cards');
if (compNow) {
if (!document.getElementById('comp-only-action')) {
const compCard = document.createElement('div');
// place the competition action as a regular action-card and add a highlight class
compCard.className = 'action-card comp-highlight';
compCard.id = 'comp-only-action'; compCard.setAttribute('role','button'); compCard.tabIndex = 0;
compCard.innerHTML = `<div class="card-title">参加比赛【${compNow.name}】</div>`;
compCard.onclick = () => {
// 使用新的比赛系统
if(typeof window.holdCompetitionModalNew === 'function'){
window.holdCompetitionModalNew(compNow);
} else {
holdCompetitionModal(compNow);
}
};
// 插入到事件容器之前,这样顺序与其他 action-card 保持一致(即在事件卡上方)
const eventContainer = document.getElementById('event-cards-container');
if(eventContainer && actionContainer.contains(eventContainer)){
actionContainer.insertBefore(compCard, eventContainer);
} else {
actionContainer.appendChild(compCard);
}
}
document.body.classList.add('comp-week');
} else {
document.body.classList.remove('comp-week');
const compCard = document.getElementById('comp-only-action');
if (compCard) compCard.remove();
}
}
// Returns competition object if this week has one, otherwise null
// Render a minimal UI that shows only the "参加比赛" button and auto-starts the competition.
/* 新的基于题目的训练函数 */
function trainStudentsWithTask(task, intensity) {
log(`开始做题训练:${task.name}(难度${task.difficulty},强度${intensity===1?'轻':intensity===2?'中':'重'})`);
const __before = typeof __createSnapshot === 'function' ? __createSnapshot() : null;
let weather_factor = game.getWeatherFactor();
let comfort = game.getComfort();
let comfort_factor = 1.0 + Math.max(0.0, (50 - comfort) / 100.0);
// 记录每个学生的训练结果
const trainingResults = [];
for(let s of game.students) {
if(!s || s.active === false) continue;
// 计算个人舒适度(考虑天气敏感和美食家天赋)
let personalComfort = comfort;
// 天气敏感:天气影响翻倍
if(s.talents && s.talents.has('天气敏感')){
const baseComfort = game.base_comfort;
const weatherEffect = comfort - baseComfort;
personalComfort = baseComfort + weatherEffect * 2;
personalComfort = Math.max(0, Math.min(100, personalComfort));
}
// 美食家:食堂效果翻倍
if(s.talents && s.talents.has('美食家')){
const canteenBonus = 3 * (game.facilities.canteen - 1);
personalComfort += canteenBonus; // 额外增加一次食堂效果
personalComfort = Math.max(0, Math.min(100, personalComfort));
}
s.comfort = personalComfort;
let sick_penalty = (s.sick_weeks > 0) ? 0.7 : 1.0;
// 计算学生能力(思维和编码平均)
const studentAbility = (s.thinking + s.coding) / 2.0;
// 使用题目库中的函数计算增幅倍数
const boostMultiplier = calculateBoostMultiplier(studentAbility, task.difficulty);
// 应用题目对学生的知识点提升
const results = applyTaskBoosts(s, task);
// 根据强度和设施调整知识增益
const libraryLevel = game.facilities.library;
let libraryBonus = 0;
if(libraryLevel === 1) libraryBonus = -0.20;
else if(libraryLevel === 2) libraryBonus = -0.05;
else if(libraryLevel === 3) libraryBonus = 0.10;
else if(libraryLevel === 4) libraryBonus = 0.12;
else if(libraryLevel === 5) libraryBonus = 0.14;
const libraryMultiplier = 1.0 + libraryBonus;
// 强度影响:轻=0.7, 中=1.0, 重=1.3
const intensityFactor = intensity === 1 ? 0.7 : intensity === 3 ? 1.3 : 1.0;
// 应用所有调整因子(使用新的题库增幅)
for(const boost of results.boosts) {
const additionalBoost = Math.floor(boost.actualAmount * (libraryMultiplier - 1.0) * intensityFactor * sick_penalty);
s.addKnowledge(boost.type, additionalBoost);
}
// 计算机影响能力提升
const computerLevel = game.facilities.computer;
let computerBonus = 0;
if(computerLevel === 1) computerBonus = -0.2;
else if(computerLevel === 2) computerBonus = 0;
else if(computerLevel === 3) computerBonus = 0.1;
else if(computerLevel === 4) computerBonus = 0.2;
else if(computerLevel === 5) computerBonus = 0.3;
const computerMultiplier = 1.0 + computerBonus;
// 能力提升:根据题目难度和学生能力
// 做题会同时提升思维和编码能力,但幅度较小
const abilityGainBase = boostMultiplier * intensityFactor * (1 - Math.min(0.6, s.pressure/200.0));
const thinkingGain = uniform(0.6, 1.5) * abilityGainBase * computerMultiplier * (typeof TRAINING_EFFECT_MULTIPLIER !== 'undefined' ? TRAINING_EFFECT_MULTIPLIER : 1.0);
const codingGain = uniform(1, 2.5) * abilityGainBase * computerMultiplier * (typeof TRAINING_EFFECT_MULTIPLIER !== 'undefined' ? TRAINING_EFFECT_MULTIPLIER : 1.0);
s.thinking += thinkingGain;
s.coding += codingGain;
s.thinking = (s.thinking || 0);
s.coding = (s.coding || 0);
// 压力计算
let base_pressure = (intensity===1) ? 15 : (intensity===2) ? 25 : 40;
// 难题会增加压力
const difficultyPressure = Math.max(0, (task.difficulty - studentAbility) * 0.2);
base_pressure += difficultyPressure;
if(intensity===3) base_pressure *= TRAINING_PRESSURE_MULTIPLIER_HEAVY;
else if(intensity===2) base_pressure *= TRAINING_PRESSURE_MULTIPLIER_MEDIUM;
let canteen_reduction = game.facilities.getCanteenPressureReduction();
let pressure_increase = base_pressure * weather_factor * canteen_reduction * comfort_factor;
if(s.sick_weeks > 0) pressure_increase += 10;
// 应用全局压力增加量增幅
pressure_increase *= (typeof PRESSURE_INCREASE_MULTIPLIER !== 'undefined' ? PRESSURE_INCREASE_MULTIPLIER : 1.0);
// 处理天赋对压力的影响
let finalPressureIncrease = pressure_increase;
try{
if(typeof s.triggerTalents === 'function'){
const talentResults = s.triggerTalents('pressure_change', {
source: 'task_training',
amount: pressure_increase,
task: task,
intensity: intensity
}) || [];
for(const r of talentResults){
if(!r || !r.result) continue;
const out = r.result;
if(typeof out === 'object'){
const act = out.action;
if(act === 'moyu_cancel_pressure'){
finalPressureIncrease = 0;
} else if(act === 'halve_pressure'){
finalPressureIncrease = finalPressureIncrease * 0.5;
} else if(act === 'double_pressure'){
finalPressureIncrease = finalPressureIncrease * 2.0;
}
}
}
}
}catch(e){ console.error('triggerTalents pressure_change', e); }
s.pressure += finalPressureIncrease;
// 记录训练结果用于日志
trainingResults.push({
name: s.name,
multiplier: boostMultiplier,
boosts: results.boosts
});
}
game.weeks_since_entertainment += 1;
// 输出详细的训练日志
log(`训练结束。题目:${task.name}`);
/*
// Toast 反馈
if (window.toastManager) {
window.toastManager.show(`训练完成:${task.name}`, 'success');
}
*/
// 训练结束后尝试为每位学生获得天赋(按训练强度放大/降低概率)
try{
if(typeof window !== 'undefined' && window.TalentManager && typeof window.TalentManager.tryAcquireTalent === 'function'){
for(let s of game.students){ if(s && s.active !== false) try{ window.TalentManager.tryAcquireTalent(s, (typeof intensity !== 'undefined' ? (intensity===3?0.8:(intensity===2?0.4:0.2)) : 0.4)); }catch(e){} }
}
}catch(e){ console.error('post-task-training tryAcquireTalent error', e); }
for(const result of trainingResults) {
const boostStrs = result.boosts.map(b => `${b.type}+${b.actualAmount}`).join(', ');
const effPercent = Math.round(result.multiplier * 100);
log(` ${result.name}: 效率${effPercent}% [${boostStrs}]`);
}
const __after = typeof __createSnapshot === 'function' ? __createSnapshot() : null;
if(__before && __after) __summarizeSnapshot(__before, __after, `做题训练:${task.name}`);
}
// 标记本周刚刚完成了一次训练,供事件系统检测(如构造题忘放checker)
try{ game.lastTrainingFinishedWeek = game.week; }catch(e){}
// 训练结束后立即检查随机事件(会触发依赖刚结束训练的事件)
try{ checkRandomEvents(); }catch(e){ console.error('post-training checkRandomEvents failed', e); }
// 辅助:为单个学生运行一次隐藏模拟赛,返回总分(0..400)
function simulateHiddenMockScore(s, diffIdx){
const knowledge_types = ["数据结构","图论","字符串","数学","动态规划"];
let total = 0;
for(let qi=0; qi<4; qi++){
const num_tags = uniformInt(1,3);
const selected = [];
while(selected.length < num_tags){
const idx = uniformInt(0,4);
if(!selected.includes(knowledge_types[idx])) selected.push(knowledge_types[idx]);
}
const totalK = selected.reduce((sum, t) => sum + s.getKnowledgeByType(t), 0);
const avgK = selected.length > 0 ? Math.floor(totalK / selected.length) : 0;
const ability_avg = s.getAbilityAvg();
const mental_idx = s.getMentalIndex();
const difficulty_proxy = MOCK_CONTEST_DIFF_VALUES[diffIdx] || 30;
const perf = sigmoid((ability_avg + avgK * 3.5) / 15.0);
const stability = mental_idx / 100.0;
const sigma = (100 - mental_idx) / 150.0 + 0.08;
const random_factor = normal(0, sigma);
const final_ratio = clamp(perf * stability * (1 + random_factor) * sigmoid((ability_avg + avgK * 3.5 - difficulty_proxy) / 10.0), 0, 1);
total += clampInt(Math.floor(final_ratio * 100 / 10) * 10, 0, 100);
}
return total;
}
// 计算外出集训费用(与人数呈一次函数关系)
function computeOutingCostQuadratic(difficulty_choice, province_choice, participantCount){
const DIFF_COST_PENALTY = {1:100, 2:300, 3:600};
const base = (difficulty_choice === 2) ? OUTFIT_BASE_COST_INTERMEDIATE :
(difficulty_choice === 3) ? OUTFIT_BASE_COST_ADVANCED :
OUTFIT_BASE_COST_BASIC;
const target = PROVINCES[province_choice] || {type: '普通省'};
let adjustedBase = base;
if (target.type === '强省') {
adjustedBase = Math.floor(adjustedBase * STRONG_PROVINCE_COST_MULTIPLIER);
} else if (target.type === '弱省') {
adjustedBase = Math.floor(adjustedBase * WEAK_PROVINCE_COST_MULTIPLIER);
}
const n = Math.max(0, Number(participantCount || 0));
const diffPenalty = DIFF_COST_PENALTY[difficulty_choice] || 100;
try {
const rep = (typeof game !== 'undefined' && game && typeof game.reputation === 'number')
? clamp(game.reputation, 0, 100)
: 0;
const raw = Math.max(0, Math.floor(adjustedBase + 18000 * n + diffPenalty));
const maxDiscount = (typeof OUTFIT_REPUTATION_DISCOUNT !== 'undefined') ? OUTFIT_REPUTATION_DISCOUNT : 0.30;
const multiplier = (typeof OUTFIT_REPUTATION_DISCOUNT_MULTIPLIER !== 'undefined') ? OUTFIT_REPUTATION_DISCOUNT_MULTIPLIER : 1.0;
// Apply multiplier to increase/decrease the reputation-based discount effect.
const discount = (rep / 100.0) * maxDiscount * multiplier;
const finalCost = Math.max(0, Math.floor(raw * (1.0 - discount)));
return finalCost;
} catch (e) {
console.error('computeOutingCostQuadratic error', e);
return Math.max(0, Math.floor(adjustedBase + 20000 * n + diffPenalty));
}
}
// 新的外出集训实现:仅对 selectedNames 的学生进行集训
function outingTrainingWithSelection(difficulty_choice, province_choice, selectedNames, inspireTalents){
const target = PROVINCES[province_choice];
const __before = typeof __createSnapshot === 'function' ? __createSnapshot() : null;
const selectedStudents = game.students.filter(s => s && s.active && selectedNames.includes(s.name));
const participantCount = selectedStudents.length;
let final_cost = computeOutingCostQuadratic(difficulty_choice, province_choice, participantCount);
// 计算天赋激发费用
inspireTalents = inspireTalents || [];
const talentInspireCost = inspireTalents.length * 12000;
final_cost += talentInspireCost;
// 在结算前询问每位参与学生的天赋,收集可能的开支减免(action: 'reduce_outing_cost')
try{
let totalReduction = 0;
const reductions = [];
for(let s of selectedStudents){
try{
// 首选 student.triggerTalents(实例方法),否则尝试全局 TalentManager
let results = null;
if(s && typeof s.triggerTalents === 'function'){
results = s.triggerTalents('outing_cost_calculate', { province: target.name, difficulty: difficulty_choice, participantCount });
} else if(typeof window !== 'undefined' && window.TalentManager && typeof window.TalentManager.handleStudentEvent === 'function'){
results = window.TalentManager.handleStudentEvent(s, 'outing_cost_calculate', { province: target.name, difficulty: difficulty_choice, participantCount });
}
if(Array.isArray(results)){
for(const r of results){
const res = r && r.result ? r.result : r;
if(res && res.action === 'reduce_outing_cost' && typeof res.amount === 'number'){
totalReduction += Number(res.amount) || 0;
reductions.push({ student: s.name, amount: Number(res.amount), message: res.message });
}
}
}
}catch(e){ console.error('outing cost talent check error for', s && s.name, e); }
}
if(totalReduction > 0){
const applied = Math.min(final_cost, Math.floor(totalReduction));
final_cost = Math.max(0, final_cost - applied);
// 通知事件面板(如果存在)和日志
try{ if(window.pushEvent) window.pushEvent({ name: '集训经费减免', description: `天赋导致集训费用减少 ¥${applied}(来自: ${reductions.map(r=>r.student+':'+'¥'+r.amount).join(', ')})`, week: game.week }); }catch(e){}
log(`集训经费减免:共 -¥${applied}(明细: ${reductions.map(r=>r.student+':'+'¥'+r.amount).join(', ')})`);
}
// 结束 try 大块
}catch(e){ console.error('collect outing cost reductions error', e); }
if(game.budget < final_cost){ alert("经费不足,无法外出集训!"); return; }
game.recordExpense(final_cost, `外出集训:${target.name}`);
log(`外出集训:${target.name} (${target.type}),难度:${difficulty_choice},参与人数:${participantCount},费用 ¥${final_cost}`);
// 隐藏模拟赛难度映射:基础班->入门级(0),提高班->普及级(1),冲刺班->NOI级(4)
const DIFFIDX_MAP = {1:0, 2:1, 3:4};
const diffIdxForHidden = DIFFIDX_MAP[difficulty_choice] || 0;
// 对每个选中学生进行处理并运行隐藏模拟赛
for(let s of selectedStudents){
// run hidden mock (4题, 240分钟 equivalent behavior) and get score 0..400
let hiddenScore = simulateHiddenMockScore(s, diffIdxForHidden);
// 标准集训增益基数(沿用原逻辑)
let knowledge_base = (difficulty_choice===2) ? OUTFIT_KNOWLEDGE_BASE_INTERMEDIATE : (difficulty_choice===3) ? OUTFIT_KNOWLEDGE_BASE_ADVANCED : OUTFIT_KNOWLEDGE_BASE_BASIC;
let ability_base = (difficulty_choice===2) ? OUTFIT_ABILITY_BASE_INTERMEDIATE : (difficulty_choice===3) ? OUTFIT_ABILITY_BASE_ADVANCED : OUTFIT_ABILITY_BASE_BASIC;
let pressure_gain = (difficulty_choice===2) ? OUTFIT_PRESSURE_INTERMEDIATE : (difficulty_choice===3) ? OUTFIT_PRESSURE_ADVANCED : OUTFIT_PRESSURE_BASIC;
// 基于省份训练质量修正
let knowledge_mult = target.trainingQuality || 1.0;
let ability_mult = target.trainingQuality || 1.0;
// 难度惩罚仍然保留(原要求),我们使用 difficulty constants 来影响收获倍率
const DIFF_GAIN_PENALTY = {1:1.0, 2:1.0, 3:1.0}; // 收获不直接降低,但之后按分数调整
// 计算原始增益
let knowledge_min = Math.floor(knowledge_base * knowledge_mult);
let knowledge_max = Math.floor(knowledge_base * knowledge_mult * 1.8);
let ability_min = ability_base * ability_mult;
let ability_max = ability_base * ability_mult * 2.0;
// 根据隐藏模拟赛分数决定是否触发“实力不匹配”惩罚
let scoreThreshold = 200; // 要求中的阈值
let mismatch = (hiddenScore < scoreThreshold);
// 计算增益与压力
let knowledge_modifier = 1.0;
let ability_modifier = 1.0;
let pressure_multiplier = 1.0;
if(mismatch){
knowledge_modifier = 0.2; // 收获减小
ability_modifier = 0.5;
pressure_multiplier = 2.0; // 压力乘2