PageSourceSearch

https://araki-live.jp/apps/ex.baseball/

html araki-live.jp collected 2026-10-06 04:15:31 UTC 76,548 bytes, 1,607 lines download raw bytes

1<!DOCTYPE html>
2<html lang="ja">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
6<title>ARAKI EXTREME BASEBALL</title>
7<link rel="stylesheet" href="style.css">
8</head>
9<body>
10<div id="app">
11<div id="title" class="ov" style="display:flex">
12 <div class="title-bottom">
13  <p class="title-info" id="t-ms"></p>
14  <div class="title-stats">
15   <span class="title-info" id="t-best"></span>
16   <span class="title-sep">│</span>
17   <span class="title-info" id="t-pts"></span>
18  </div>
19  <div class="btn-row" style="margin-bottom:.15rem"><button class="btn" id="btn-play">⚾ PLAY BALL</button></div>
20  <div class="btn-row" style="gap:.3rem">
21   <button class="btn btn-s" id="btn-t-tree">🌟 SKILL TREE</button>
22   <button class="btn btn-s" id="btn-t-ach">🏆 実績</button>
23  </div>
24  <div class="btn-row" style="gap:.3rem;margin-top:.1rem">
25   <button class="btn btn-s" id="btn-t-status">📊 ステータス</button>
26   <button class="btn btn-s" id="btn-t-balls" style="display:none">👻 ボール一覧</button>
27  </div>
28  <div class="btn-row" style="gap:.3rem;margin-top:.1rem">
29   <button class="btn btn-s" id="btn-t-profile" style="display:none">📸 プロフィール</button>
30  </div>
31  <div class="btn-row" style="gap:.3rem;margin-top:.1rem">
32   <button class="btn btn-s" id="btn-t-sound">🔊 SOUND</button>
33   <button class="btn btn-danger" id="btn-t-reset">🗑 RESET</button>
34  </div>
35 </div>
36</div>
37<canvas id="gc"></canvas>
38<div id="hud">
39 <div><div class="bc" id="hud-bc">1 / 10</div><div class="cb" id="hud-cb"></div></div>
40 <div style="text-align:right"><div class="sc" id="hud-sc">0</div><div class="tp" id="hud-tp"></div></div>
41 <button class="quit-btn" id="btn-quit">✕</button>
42</div>
43<button class="snd-btn" id="snd-btn" style="display:flex">🔊</button>
44<div class="ch-banner" id="ch-banner" style="display:none"></div>
45<div class="ev-announce" id="ev-announce" style="display:none"></div>
46<div id="result" class="ov">
47 <h2>RESULT</h2>
48 <div class="r-sub" id="r-breakdown"></div>
49 <div class="r-total-label">EARNED pts</div>
50 <div class="fs" id="r-total">0</div>
51 <div class="nb" id="r-nb" style="display:none"></div>
52 <div class="sd" id="r-detail"></div>
53 <div class="r-ach-notify" id="r-ach-notify"></div>
54 <div class="btn-row"><button class="btn" id="btn-retry">RETRY</button></div>
55 <div class="btn-row" style="margin-top:.2rem">
56  <button class="btn btn-s" id="btn-r-tree">SKILL TREE</button>
57  <button class="btn btn-s" id="btn-r-top">🏠 TOP</button>
58 </div>
59</div>
60<div id="tree" class="ov">
61 <div class="tr-header"><div style="display:flex;align-items:center;justify-content:center;gap:.5rem"><h2>SKILL TREE</h2><button class="tr-btn unlock" id="tr-maxall" style="padding:.2rem .6rem;font-size:.55rem">全スキル最大</button></div><div class="tr-pts" id="tr-pts"></div></div>
62 <div class="tr-tabs" id="tr-tabs"></div>
63 <div class="tr-list" id="tr-list"></div>
64 <div class="tr-foot">
65  <button class="btn btn-s" id="tr-back" style="flex:1">BACK</button>
66 </div>
67</div>
68<div id="achieve" class="ov">
69 <div class="tr-header"><h2>ACHIEVEMENTS</h2><div class="tr-pts" id="ach-count"></div></div>
70 <div class="tr-list" id="ach-list"></div>
71 <div class="tr-foot"><button class="btn btn-s" id="ach-back" style="flex:1">BACK</button></div>
72</div>
73<div id="status" class="ov">
74 <div class="tr-header"><h2>STATUS</h2></div>
75 <div class="tr-list" id="stat-list"></div>
76 <div class="tr-foot"><button class="btn btn-s" id="stat-back" style="flex:1">BACK</button></div>
77</div>
78<div id="balllist" class="ov" style="display:none">
79 <div class="tr-header"><h2>BALL LIST</h2></div>
80 <div class="tr-list" id="bl-list"></div>
81 <div class="tr-foot"><button class="btn btn-s" id="bl-back" style="flex:1">BACK</button></div>
82</div>
83<div id="profile" class="ov" style="display:none">
84 <div class="tr-header"><h2>PROFILE CARD</h2><div class="pf-sub">SNSで自慢しよう!</div></div>
85 <div class="pf-body">
86  <div class="pf-name-row">
87   <label class="pf-label">PLAYER NAME</label>
88   <input type="text" class="pf-input" id="pf-name" maxlength="16" placeholder="名前を入力...">
89  </div>
90  <canvas id="pf-canvas" width="1080" height="1920"></canvas>
91  <div class="btn-row" style="gap:.4rem;margin-top:.5rem">
92   <button class="btn" id="pf-dl" style="font-size:.85rem;padding:.6rem 1.8rem">🔍 全画面表示</button>
93  </div>
94  <div class="pf-hint">全画面表示してスクショで保存しよう!</div>
95 </div>
96 <div class="tr-foot"><button class="btn btn-s" id="pf-back" style="flex:1">BACK</button></div>
97</div>
98<div id="story" class="ov">
99 <div class="st-bg" id="st-bg"></div>
100 <img class="st-chara" id="st-chara">
101 <div class="st-bottom">
102  <div class="st-text" id="st-text"></div>
103  <div class="st-skip" id="st-skip">TAP TO START</div>
104 </div>
105</div>
106<div id="ending" class="ov">
107 <div class="end-bg" id="end-bg"></div>
108 <div class="end-content">
109  <div class="end-text" id="end-text"></div>
110  <div class="end-title" id="end-title">GAME CLEAR</div>
111  <div class="end-skip" id="end-skip">TAP TO CLOSE</div>
112 </div>
113</div>
114<div id="confirm" class="ov">
115 <div class="cf-box">
116  <h3>DATA RESET</h3>
117  <p>全ての進捗・スキル・ポイント<br>実績・ステータスがリセットされます。<br>元に戻せません。</p>
118  <div class="btn-row">
119   <button class="btn btn-danger" id="cf-yes" style="font-size:.85rem;padding:.7rem 1.5rem">RESET</button>
120   <button class="btn btn-s" id="cf-no" style="font-size:.85rem;padding:.7rem 1.5rem">CANCEL</button>
121  </div>
122 </div>
123</div>
124
125<div id="sound-settings" class="ov">
126 <div class="ss-box">
127  <h3>🔊 SOUND</h3>
128  <div class="ss-row">
129   <div class="ss-label"><span>🎵 BGM</span><span class="ss-val" id="ss-bgm-val">100%</span></div>
130   <input type="range" class="ss-slider" id="ss-bgm" min="0" max="100" value="100">
131  </div>
132  <div class="ss-row">
133   <div class="ss-label"><span>🔔 効果音</span><span class="ss-val" id="ss-sfx-val">100%</span></div>
134   <input type="range" class="ss-slider" id="ss-sfx" min="0" max="100" value="100">
135  </div>
136  <div class="btn-row" style="margin-top:.5rem">
137   <button class="btn btn-s" id="ss-close" style="flex:1">CLOSE</button>
138  </div>
139 </div>
140</div>
141
142<script src="skills.js?v=23"></script>
vendor: 1 bytes, line 142
142
143<script>
144(()=>{
145// ============ Audio ============
146const AC=window.AudioContext||window.webkitAudioContext;let ac;
147let sfxVol=0.7,bgmVol=0.1;
148// mp3音源
149const sfxBatting=new Audio('batting.mp3');
150const sfxCatch=new Audio('catch.mp3');
151const sfxMotion=new Audio('Motion.mp3');
152const sfxEvent=new Audio('event.mp3');
153const sfxFoul=new Audio('foul.mp3');
154const sfxBtn=new Audio('btn.mp3');
155const sfxCharge=new Audio('charge.mp3');
156const bgmClear=new Audio('clear.mp3');
157// BGM
158const bgmOp=new Audio('op.mp3');bgmOp.loop=true;
159const bgmBattle=new Audio('bgm.mp3');bgmBattle.loop=true;
160let curBgm=null;
161function playBgm(bgm){
162 if(curBgm===bgm)return;
163 if(curBgm){curBgm.pause();curBgm.currentTime=0}
164 curBgm=bgm;curBgm.volume=bgmVol;curBgm.play().catch(()=>{});
165}
166function stopBgm(){if(curBgm){curBgm.pause();curBgm.currentTime=0;curBgm=null}}
167function updateBgmVol(){if(curBgm)curBgm.volume=bgmVol}
168function ea(){if(!ac)ac=new AC();if(ac.state==='suspended')ac.resume();if(curBgm&&curBgm.paused)curBgm.play().catch(()=>{})}
169function playSfx(audio){if(sfxVol<=0)return;try{const a=audio.cloneNode();a.volume=sfxVol;a.play().catch(()=>{})}catch(e){audio.currentTime=0;audio.volume=sfxVol;audio.play().catch(()=>{})}}
170function snd(t){if(sfxVol<=0)return;
171if(t==='homerun'||t==='hit'){playSfx(sfxBatting)}
172else if(t==='miss'){playSfx(sfxCatch)}
173else if(t==='foul'){playSfx(sfxFoul)}
174else if(t==='pitch'){playSfx(sfxMotion)}
175else if(t==='buy'){playSfx(sfxBtn)}
176else if(t==='charge'){playSfx(sfxCharge)}}
177
178// sound settings
179const sndBtn=document.getElementById('snd-btn');
180sndBtn.onclick=()=>{document.getElementById('sound-settings').style.display='flex'};
181// restore saved volumes
182try{const sv=JSON.parse(localStorage.getItem('aeb_sound')||'{}');if(sv.sfx!==undefined)sfxVol=sv.sfx;if(sv.bgm!==undefined)bgmVol=sv.bgm}catch(e){}
183const ssBgm=document.getElementById('ss-bgm');
184const ssSfx=document.getElementById('ss-sfx');
185ssBgm.value=Math.round(bgmVol*100);ssSfx.value=Math.round(sfxVol*100);
186document.getElementById('ss-bgm-val').textContent=ssBgm.value+'%';
187document.getElementById('ss-sfx-val').textContent=ssSfx.value+'%';
188ssBgm.oninput=()=>{bgmVol=ssBgm.value/100;document.getElementById('ss-bgm-val').textContent=ssBgm.value+'%';updateBgmVol();localStorage.setItem('aeb_sound',JSON.stringify({sfx:sfxVol,bgm:bgmVol}))};
189ssSfx.oninput=()=>{sfxVol=ssSfx.value/100;document.getElementById('ss-sfx-val').textContent=ssSfx.value+'%';localStorage.setItem('aeb_sound',JSON.stringify({sfx:sfxVol,bgm:bgmVol}))};
190document.getElementById('ss-close').onclick=()=>{document.getElementById('sound-settings').style.display='none'};
191
192// ============ Event Announce ============
193function showEventAnnounce(ev){
194 const el=document.getElementById('ev-announce');
195 el.innerHTML=`<span class="ev-icon">${ev.icon}</span><div class="ev-name" style="color:${ev.color}">${ev.name}</div><div class="ev-desc">${ev.desc}</div>`;
196 el.style.display='block';el.style.animation='none';el.offsetHeight;el.style.animation='evIn .5s ease-out';
197 setTimeout(()=>{el.style.display='none'}
197,1200);
198 playSfx(sfxEvent);
199}
200
201// ============ Images ============
202const imgs={};let imgLoaded=0;const imgTotal=18;
203function loadImg(k,src){const i=new Image();i.onload=()=>{imgs[k]=i;imgLoaded++};i.src=src}
204loadImg('top','TOP.png');loadImg('ball','ball.png');loadImg('bat','bat.png');loadImg('bomb','bomb.png');loadImg('stage','stage.png');
205loadImg('throw1','throw1.png');loadImg('throw2','throw2.png');loadImg('throw3','throw3.png');loadImg('throw4','throw4.png');loadImg('throw5','throw5.png');loadImg('throw6','throw6.png');
206loadImg('golden','Golden.png');loadImg('fire','fire.png');loadImg('diamond','diamond.png');loadImg('rainbow','rainbow.png');
207loadImg('ghost','ghost.png');loadImg('clear','clear.png');loadImg('profile','Profile.png');
208
209// ============ Constants ============
210const BASE_BALLS=10;
211const RES={
212 homerun:{pt:10000,label:'HOME RUN!!',color:'#FFD700',sz:3.0},
213 triple: {pt:7500, label:'TRIPLE!',   color:'#F472B6',sz:2.6},
214 double: {pt:5000, label:'DOUBLE!',   color:'#4ADE80',sz:2.4},
215 hit:    {pt:3000, label:'HIT!',      color:'#60A5FA',sz:1.9},
216 foul:   {pt:500,  label:'FOUL',      color:'#9CA3AF',sz:1.5},
217 miss:   {pt:0,    label:'MISS...',    color:'#EF4444',sz:1.5},
218};
219const PITCH=[
220 {name:'Straight',speed:[.014,.020],curveX:0,weight:50},
221 {name:'Changeup',speed:[.005,.008],curveX:0,weight:25},
222 {name:'Slider',  speed:[.009,.013],curveX:.6,weight:25},
223];
224
225// ============ Canvas ============
226const cv=document.getElementById('gc'),cx=cv.getContext('2d');
227const app=document.getElementById('app');
228let W,H;function rz(){W=cv.width=app.clientWidth;H=cv.height=app.clientHeight}addEventListener('resize',rz);rz();
229
230// ============ Save ============
231function ld(){
232 try{
233  const d=JSON.parse(localStorage.getItem('aeb_save')||'{}');
234  if(!d.ver||d.ver<2)return{best:d.best||0,pts:d.pts||0,skills:{},togOff:{},stats:{},ver:3};
235  return{best:d.best||0,pts:d.pts||0,skills:d.skills||{},togOff:d.togOff||{},stats:d.stats||{},ver:3};
236 }catch{return{best:0,pts:0,skills:{},togOff:{},stats:{},ver:3}}
237}
238function sv(s){localStorage.setItem('aeb_save',JSON.stringify(s))}
239let S=ld();
240
241// ============ Skill helpers (v3: effectFn/costFn/descFn) ============
242function skillData(id){return SKILL_TREE.find(s=>s.id===id)}
243function skillLv(id){return S.skills[id]||0}
244function active(id){
245 if(skillLv(id)<1)return false;
246 const sk=skillData(id);
247 if(sk&&sk.toggleable&&S.togOff[id])return false;
248 return true;
249}
250// effectFnで効果を取得
251function skillVal(id){
252 const sk=skillData(id);if(!sk)return null;
253 const lv=skillLv(id);if(lv<1)return null;
254 return sk.effectFn(lv);
255}
256// effective max level: hyperScale=trueのみハイパーボーナス
257function effectiveMaxLv(sk){
258 if(!sk.hyperScale)return sk.maxLv;
259 return sk.maxLv+getHyperLvBonus(S);
260}
261// 全スキルの効果を集約
262function getEffect(type,def){
263 let val=def;
264 SKILL_TREE.forEach(sk=>{
265  const lv=skillLv(sk.id);if(lv<1)return;
266  if(sk.toggleable&&S.togOff[sk.id])return;
267  const e=sk.effectFn(lv);if(!e)return;
268  if(e.type==='legend'){
269   if(type==='score_mult')val+=e.score_mult;
270   else if(type==='spot_add')val+=e.spot_add;
271   else if(type==='speed_mult')val*=e.speed_mult;
272   return;
273  }
274  if(e.type==='ultimate'){
275   if(type==='score_mult')val+=e.score_mult;
276   else if(type==='spot_add')val+=e.spot_add;
277   else if(type==='speed_up')val*=e.speed_up;
278   return;
279  }
280  if(e.type==='hyper')return;
281  if(e.type==='chaos'){
282   if(type==='score_mult')val+=e.score_mult;
283   if(type==='speed_up')val*=e.speed_up;
284   return;
285  }
286  if(e.type===type){
287   if(type==='score_mult'||type==='combo_add'||type==='spot_add')val+=e.value;
288   else if(type==='speed_mult')val*=e.value;
289   else if(type==='pts_mult')val=e.value;
290   else val=e.value;
291  }
292  if(type==='spot_add'&&e.type==='giant_spot')val+=e.value;
293 });
294 return val;
295}
296// costFnでコスト取得
297function getSkillCost(id){
298 const sk=skillData(id);if(!sk)return Infinity;
299 const lv=skillLv(id);
300 if(lv>=effectiveMaxLv(sk))return Infinity;
301 return sk.costFn(lv+1);
302}
303function calcMultiCost(id,count){
304 const sk=skillData(id);if(!sk)return Infinity;
305 let lv=skillLv(id),total=0;
306 for(let i=0;i<count;i++){if(lv+i+1>effectiveMaxLv(sk))return Infinity;total+=sk.costFn(lv+i+1)}
307 return total;
308}
309function canUnlock(id){
310 const sk=skillData(id);if(!sk)return false;
311 const lv=skillLv(id);
312 if(lv>=effectiveMaxLv(sk))return false;
313 if(S.pts<getSkillCost(id))return false;
314 if(sk.unlock)return sk.unlock(S);
315 if(sk.ring<=1)return true;
316 const prev=SKILL_TREE.find(s=>s.branch===sk.branch&&s.ring===sk.ring-1);
317 if(!prev)return true;
318 return skillLv(prev.id)>=prev.maxLv;
319}
320function unlockSkill(id){
321 if(!canUnlock(id))return false;
322 const cost=getSkillCost(id);
323 S.pts-=cost;
324 S.skills[id]=(S.skills[id]||0)+1;
325 sv(S);return true;
326}
327function isSkillAvailable(id){
328 const sk=skillData(id);if(!sk)return false;
329 if(sk.unlock)return sk.unlock(S);
330 if(sk.ring<=1)return true;
331 const prev=SKILL_TREE.find(s=>s.branch===sk.branch&&s.ring===sk.ring-1);
332 if(!prev)return true;
333 return skillLv(prev.id)>=prev.maxLv;
334}
335
336// ============ State ============
337let st='title',maxB=BASE_BALLS,curB=0,totSc=0,dspSc=0,combo=0,resList=[];
338let ballT=0,ballSpd=0,ballCX=0,pWait=0,curPitch=null,ballWX=0;
339let isBomb=false;
340let lastWasHR=false;
341let isCritical=false;
342let curEvent=null;        // 現在のピッチイベント
343let curChallenge=null;    // 現在のチャレンジ
344let maxCombo=0;           // ゲーム中の最大コンボ
345let eventBonusPts=0;      // イベントで稼いだボーナスpt
346let challengeCleared=false;
347let milestoneJustReached=null; // ゲーム中に到達したマイルストーン
348let spotX,spotY,spotR,hrZoneR=0;
349let particles=[],shake=0,flash=0,flashColor='255,255,255';
350let rLabel=null,batSw=0,hitBall=null,sPops=[],comText=null;
351let pitcherFrame=0,pitcherTimer=0,windupId=0;
352let pitcherDown=false,pitcherDownTimer=0,pitcherDownHR=false;
353let eventPreview=null,eventPreviewTimer=0; // イベント予告
354let jaganSlow=0,jaganUsed=false,jaganFreezeT=0; // 邪眼フリーズ
355// 魔王とお戯れモード
356let ghostMode=false;
357let ghostType='';     // 'pinball' or 'vanish'
358let ghostBounces=[];  // ピンボール用: [{x,y},...] 経由点
359let ghostVisible=true; // 消える魔球用
360
361function updSpot(){
362 spotX=W/2;spotY=H*.72;spotR=Math.min(W,H)*.07;
363 const add=getEffect('spot_add',0);
364 spotR*=(1+add);
365 const giantEff=skillVal('giant_bat');
366 const hrExtra=giantEff?giantEff.hrZone:0;
367 const goubakuEff=active('goubaku')?skillVal('goubaku'):null;
368 const goubakuAdd=goubakuEff?goubakuEff.hrZoneAdd:0;
369 // 明鏡止水: ミートエリア縮小
370 const meikyoEff=active('meikyo')?skillVal('meikyo'):null;
371 if(meikyoEff)spotR*=meikyoEff.spotScale;
372 hrZoneR=spotR*(HR_ZONE_BASE_RATIO+hrExtra+goubakuAdd);
373}
374
375// ============ Particles ============
376function spawn(x,y,n,c,sp){for(let i=0;i<n;i++){const a=Math.random()*Math.PI*2,v=sp*(.5+Math.random());particles.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v-sp*.3,life:1,decay:.015+Math.random()*.02,size:2+Math.random()*4,color:c})}}
377
378// ============ Pitch ============
379function pickPitch(){
380 let t=PITCH.map(p=>({...p}));
381 const slEff=skillVal('slow_rate');
382 if(slEff)t[1].weight+=slEff.value;
383 if(active('no_slider'))t[2].weight=0;
384 if(active('straight_only')){t[1].weight=0;t[2].weight=0;t[0].weight=100}
385 const tot=t.reduce((s,p)=>s+p.weight,0);let r=Math.random()*tot;
386 for(const p of t){r-=p.weight;if(r<=0)return p}return t[0];
387}
388
389function getBallSpeed(base){
390 let s=base;
391 const ahEff=skillVal('arm_heavy');
392 if(ahEff)s*=ahEff.value;
393 const legendEff=skillVal('legend');
394 if(legendEff)s*=legendEff.speed_mult;
395 const ultEff=skillVal('ultimate');
396 if(ultEff)s*=ultEff.speed_up;
397 const chaosEff=skillVal('chaos');
398 if(chaosEff)s*=chaosEff.speed_up;
399 return s;
400}
401
402// ============ Ball pos ============
403function bpos(){
404 const cxp=W/2,sY=H*.37,eY=H*.85,t=ballT,et=t*t;
405 let by,bx;
406 // ピンボール軌道
407 if(ghostMode&&ghostType==='pinball'&&ghostBounces.length>1){
408  const seg=ghostBounces.length-1;
409  // ballTは通常通り進む。1.0到達時にミートエリアに届くようにする
410  const progress=Math.min(t,1);
411  const idx=progress*seg;
412  const i=Math.min(Math.floor(idx),seg-1);
413  const frac=idx-i;
414  const p0=ghostBounces[i],p1=ghostBounces[i+1];
415  bx=p0.x+(p1.x-p0.x)*frac;
416  by=p0.y+(p1.y-p0.y)*frac;
417 }else{
418  by=sY+(eY-sY)*et;
419  bx=cxp+ballWX*et*W*.15;
420 }
421 // サイズ: ピンボールはprogressベースで計算
422 let sz;
423 if(ghostMode&&ghostType==='pinball'){
424  const p=Math.min(t,1);sz=3+20*p*p;
425 }else{sz=3+20*et}
426 const masaiEff=active('masai')?skillVal('masai'):null;
427 if(masaiEff)sz*=masaiEff.ballScale;
428 return{x:bx,y:by,size:sz};
429}
430// ゴーストボール経由点生成
431function generateGhostBounces(){
432 const pts=[];
433 const startX=W/2,startY=H*.37;
434 const endX=spotX,endY=spotY;
435 const bounceCount=3+Math.floor(Math.random()*3); // 3~5回跳ね返り
436 pts.push({x:startX,y:startY});
437 for(let i=0;i<bounceCount;i++){
438  const t=(i+1)/(bounceCount+1);
439  const midY=startY+(endY-startY)*t;
440  // 左右の画面端にランダムに跳ね返り
441  const side=Math.random()>.5?1:-1;
442  const margin=W*0.05;
443  const bx=side>0?W-margin:margin;
444  pts.push({x:bx,y:midY});
445 }
446 pts.push({x:endX,y:endY});
447 return pts;
448}
449
450// ============ Judge ============
451function judge(){
452 const b=bpos(),dx=b.x-spotX,dy=b.y-spotY,d=Math.sqrt(dx*dx+dy*dy),r=spotR;
453 let res;
454 if(d<hrZoneR)res='homerun';
455 else if(d<r*.35)res='homerun';
456 else if(d<r*.65)res=active('triple_hit')?'triple':'double';
457 else if(d<r*1.0)res='hit';
458 else if(d<r*1.5)res='foul';
459 else res='miss';
460 // レインボーボール: ミス・ファウル以外は全部HR
461 if(curEvent&&curEvent.allHR&&res!=='miss'&&res!=='foul')res='homerun';
462 return res;
463}
464
465// ============ Score calc ============
466function calcPts(type){
467 let base=RES[type].pt;
468 let cm=combo>1?combo:1;
469 const comboAdd=getEffect('combo_add',0);
470 if(comboAdd>0&&combo>1)cm+=comboAdd;
471 let pts=Math.floor(base*cm);
472 const scoreMult=getEffect('score_mult',0);
473 if(scoreMult>0)pts=Math.floor(pts*(1+scoreMult));
474 if(type==='homerun'){
475  const gsEff=skillVal('grand_slam');
476  if(gsEff)pts=Math.floor(pts*gsEff.value);
477 }
478 if(active('diamond_bat')){const dbEff=skillVal('diamond_bat');pts=Math.floor(pts*(dbEff?dbEff.value:2))}
479 if(lastWasHR&&active('chain_lightning')){
480  const clEff=skillVal('chain_lightning');
481  if(clEff)pts=Math.floor(pts*(1+clEff.value));
482 }
483 if(isCritical)pts=Math.floor(pts*2);
484 if(active('one_shot')){
485  const osEff=skillVal('one_shot');
486  if(osEff)pts=Math.floor(pts*osEff.value);
487 }
488 // カオスモードのスコア倍率
489 const chaosEff=skillVal('chaos');
490 if(chaosEff)pts=Math.floor(pts*(1+chaosEff.score_mult));
491 // 明鏡止水のスコア倍率
492 const meikyoEff=active('meikyo')?skillVal('meikyo'):null;
493 if(meikyoEff)pts=Math.floor(pts*meikyoEff.scoreMult);
494 // ランダムイベントのスコア倍率
495 if(curEvent&&curEvent.scoreMult&&type!=='miss')pts=Math.floor(pts*curEvent.scoreMult);
496 // EXTREME BASEBALL: 累計試合数%分スコアUP
497 if(active('extreme_bb')){const games=S.stats&&S.stats.games?S.stats.games:0;if(games>0)pts=Math.floor(pts*(1+games*0.01))}
498 // 魔王とお戯れモード: スコア10000倍
499 if(ghostMode&&type!=='miss')pts=Math.floor(pts*10000);
500 return pts;
501}
502
503// ============ Draw ============
504function draw(){
505 cx.save();
506 if(shake>0){cx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake)}
507 drawBg();
508 // ボール軌道付近を少し暗くして視認性UP(ピッチャー足元から開始、上端ぼかし)
509 if(st==='pitching'||st==='waiting'||st==='event_preview'||st==='windup'||st==='result_wait'){
510  const trkTop=H*.45,trkBot=H*.9,trkW=60,fadeH=H*.08;
511  cx.save();
512  // 横方向グラデーション(帯の本体)
513  const gd=cx.createLinearGradient(W/2-trkW,0,W/2+trkW,0);
514  gd.addColorStop(0,'rgba(0,0,0,0)');gd.addColorStop(.25,'rgba(0,0,0,.18)');gd.addColorStop(.5,'rgba(0,0,0,.22)');gd.addColorStop(.75,'rgba(0,0,0,.18)');gd.addColorStop(1,'rgba(0,0,0,0)');
515  // 上端フェードイン:縦方向alphaマスク
516  const am=cx.createLinearGradient(0,trkTop,0,trkTop+fadeH);
517  am.addColorStop(0,'rgba(0,0,0,0)');am.addColorStop(1,'rgba(0,0,0,1)');
518  // フェード部分
519  cx.fillStyle=gd;cx.globalAlpha=1;
520  // まず上端fadeHの部分を段階描画
521  const steps=8;
522  for(let i=0;i<steps;i++){
523   const t0=i/steps,t1=(i+1)/steps;
524   const y0=trkTop+fadeH*t0,y1=trkTop+fadeH*t1;
525   cx.globalAlpha=t0;
526   cx.fillRect(W/2-trkW,y0,trkW*2,y1-y0);
527  }
528  // 本体部分(フェード後〜下端)
529  cx.globalAlpha=1;
530  cx.fillRect(W/2-trkW,trkTop+fadeH,trkW*2,trkBot-trkTop-fadeH);
531  cx.restore();
532 }
533 drawPitcherImg();drawEventPreview();drawZone();drawSpot();
534 if(st==='pitching'){if(active('clairvoyance'))drawTrajectory();drawBallImg()}
535 if(batSw>0)drawSwing();
536 if(hitBall)drawHitBall();
537 drawParts();drawLabel();drawPops();
538 cx.restore();
539 if(flash>0){cx.fillStyle=`rgba(${flashColor},${flash})`;cx.fillRect(0,0,W,H)}
540}
541function drawBg(){
542 if(imgs.stage){
543  // cover方式: 画面全体を埋める
544  const img=imgs.stage,iw=img.width,ih=img.height;
545  const scale=Math.max(W/iw,H/ih);
546  const dw=iw*scale,dh=ih*scale;
547  cx.drawImage(img,(W-dw)/2,(H-dh)/2,dw,dh);
548 }else{
549  // フォールバック(画像未ロード時)
550  const g=cx.createLinearGradient(0,0,0,H);g.addColorStop(0,'#060618');g.addColorStop(.4,'#1a0a2a');g.addColorStop(1,'#0a0a0a');
551  cx.fillStyle=g;cx.fillRect(0,0,W,H);
552 }
553}
554function drawPitcherImg(){
555 const drawH=H*.18,py=H*.33-drawH*.3;
556 // HR→throw6, それ以外の結果→throw5
557 if(pitcherDown){
558  const img=pitcherDownHR?imgs.throw6:imgs.throw5;
559  if(!img)return;
560  const drawW=drawH*(img.width/img.height);
561  cx.drawImage(img,W/2-drawW/2,py,drawW,drawH);
562  return;
563 }
564 const frameKey='throw'+(pitcherFrame+1);
565 const img=imgs[frameKey];if(!img)return;
566 const drawW=drawH*(img.width/img.height);
567 cx.drawImage(img,W/2-drawW/2,py,drawW,drawH);
568}
569function drawEventPreview(){
570 if(!eventPreview||st!=='event_preview')return;
571 const al=Math.min(1,eventPreviewTimer/20);
572 const bounce=Math.sin(Date.now()*.008)*3;
573 const py=H*.33-H*.18*.3-20+bounce; // ピッチャー頭上
574 cx.save();cx.globalAlpha=al;cx.textAlign='center';
575 cx.font='1.5rem sans-serif';cx.fillText(eventPreview.icon,W/2,py);
576 cx.font='bold .65rem sans-serif';cx.fillStyle=eventPreview.color;
577 cx.fillText(eventPreview.name,W/2,py+22);
578 cx.restore();
579}
580function drawZone(){
581 if(st!=='pitching'&&st!=='waiting'&&st!=='event_preview'&&st!=='windup'&&st!=='result_wait')return;
582 const zw=100,zh=120;cx.strokeStyle='rgba(255,255,255,.08)';cx.lineWidth=1.5;cx.setLineDash([5,5]);
583 cx.strokeRect(spotX-zw/2,spotY-zh/2,zw,zh);cx.setLineDash([]);
584}
585function drawSpot(){
586 if(st!=='pitching'&&st!=='waiting'&&st!=='event_preview'&&st!=='windup'&&st!=='result_wait')return;
587 const p=1+Math.sin(Date.now()*.005)*.08,r=spotR*p;
588 const g=cx.createRadialGradient(spotX,spotY,r*.3,spotX,spotY,r*1.2);
589 g.addColorStop(0,'rgba(255,215,0,.25)');g.addColorStop(.6,'rgba(255,215,0,.08)');g.addColorStop(1,'rgba(255,215,0,0)');
590 cx.fillStyle=g;cx.beginPath();cx.arc(spotX,spotY,r*1.2,0,Math.PI*2);cx.fill();
591 cx.strokeStyle=`rgba(255,215,0,${.5+Math.sin(Date.now()*.004)*.15})`;cx.lineWidth=2.5;
592 cx.beginPath();cx.arc(spotX,spotY,r,0,Math.PI*2);cx.stroke();
593 cx.strokeStyle='rgba(255,215,0,.2)';cx.lineWidth=1;
594 cx.beginPath();cx.moveTo(spotX-r,spotY);cx.lineTo(spotX+r,spotY);cx.stroke();
595 cx.beginPath();cx.moveTo(spotX,spotY-r);cx.lineTo(spotX,spotY+r);cx.stroke();
596 cx.fillStyle='rgba(255,215,0,.5)';cx.beginPath();cx.arc(spotX,spotY,3,0,Math.PI*2);cx.fill();
597 if(hrZoneR>3){
598  const hr=hrZoneR*p;
599  cx.strokeStyle=`rgba(239,68,68,${.4+Math.sin(Date.now()*.006)*.15})`;cx.lineWidth=2;
600  cx.beginPath();cx.arc(spotX,spotY,hr,0,Math.PI*2);cx.stroke();
601  const hg=cx.createRadialGradient(spotX,spotY,0,spotX,spotY,hr);
602  hg.addColorStop(0,'rgba(239,68,68,.15)');hg.addColorStop(1,'rgba(239,68,68,.02)');
603  cx.fillStyle=hg;cx.beginPath();cx.arc(spotX,spotY,hr,0,Math.PI*2);cx.fill();
604 }
605}
606function drawTrajectory(){
607 if(ghostMode)return; // ゴーストモードでは軌道非表示
608 const steps=20;cx.strokeStyle='rgba(255,215,0,.1)';cx.lineWidth=1;cx.setLineDash([3,6]);cx.beginPath();
609 for(let i=0;i<=steps;i++){const t=i/steps,et=t*t;const sY=H*.37,eY=H*.85;const by=sY+(eY-sY)*et;const bx=W/2+ballWX*et*W*.15;
610  if(i===0)cx.moveTo(bx,by);else cx.lineTo(bx,by)}
611 cx.stroke();cx.setLineDash([]);
612 if(curPitch){cx.fillStyle='rgba(255,215,0,.4)';cx.font='600 .6rem sans-serif';cx.textAlign='center';
613  const label=curPitch.name==='Straight'?'速':curPitch.name==='Changeup'?'遅':'変';cx.fillText(label,W/2,H*.34)}
614}
615function drawBallImg(){
616 if(ballT<0)return;let{x,y,size}=bpos();
617 // 消える魔球: 消えてる間は描画しない(vanishタイプのみ)
618 if(ghostMode&&ghostType==='vanish'&&!ghostVisible&&jaganSlow<=0){return}
619 // 邪眼フリーズ中はグロー表示
620 if(jaganSlow>0){cx.save();cx.globalAlpha=.4+Math.sin(Date.now()*.02)*.2;const sg=cx.createRadialGradient(x,y,size*.5,x,y,size*3.5);sg.addColorStop(0,'rgba(128,0,255,.6)');sg.addColorStop(1,'rgba(0,0,0,0)');cx.fillStyle=sg;cx.beginPath();cx.arc(x,y,size*3.5,0,Math.PI*2);cx.fill();cx.restore()}
621 drawBallCore(x,y,size);
622 // イベントボールのグロー
623 if(curEvent&&!isBomb){
624  cx.save();cx.globalAlpha=.3+Math.sin(Date.now()*.01)*.15;
625  const glow=cx.createRadialGradient(x,y,size*.5,x,y,size*3);
626  glow.addColorStop(0,curEvent.glowColor||'rgba(255,255,255,.3)');glow.addColorStop(1,'rgba(0,0,0,0)');
627  cx.fillStyle=glow;cx.beginPath();cx.arc(x,y,size*3,0,Math.PI*2);cx.fill();cx.restore();
628 }
629}
630function drawBallCore(x,y,size){
631 // イベント玉の画像を選択
632 let img;
633 if(ghostMode){img=imgs.ghost}
634 else if(isBomb){img=imgs.bomb}
635 else if(curEvent&&imgs[curEvent.id]){img=imgs[curEvent.id]}
636 else{img=imgs.ball}
637 if(img){const sz=size*2;cx.save();
638  // ゴーストモード: 半透明+紫グロー
639  if(ghostMode){
640   cx.globalAlpha=0.5+Math.sin(Date.now()*.005)*.2;
641   cx.shadowColor='rgba(128,0,255,.8)';cx.shadowBlur=30+size*2;
642  }
643  else if(curEvent&&!isBomb){cx.shadowColor=curEvent.glowColor||'rgba(255,255,255,.8)';cx.shadowBlur=25+size*1.5}
644  else if(!isBomb){cx.shadowColor='rgba(255,255,255,.9)';cx.shadowBlur=15+size*1.5}
645  else{cx.shadowColor='rgba(255,50,0,.8)';cx.shadowBlur=20+size}
646  cx.drawImage(img,x-sz/2,y-sz/2,sz,sz);
647  // 明度ブースト
648  if(!isBomb&&!ghostMode){cx.globalCompositeOperation='lighter';cx.globalAlpha=.25;cx.drawImage(img,x-sz/2,y-sz/2,sz,sz);cx.globalAlpha=1;cx.globalCompositeOperation='source-over'}
649  cx.shadowBlur=0;cx.restore();
650 }else{cx.fillStyle='#fff';cx.beginPath();cx.arc(x,y,size,0,Math.PI*2);cx.fill()}
651 if(ballT>.15&&!ghostMode){cx.strokeStyle=`rgba(255,255,255,${.15*ballT})`;cx.lineWidth=1.5;
652  for(let i=0;i<3;i++){const ly=y-size-4-i*10,lx=x+(i-1)*3;cx.beginPath();cx.moveTo(lx,ly);cx.lineTo(lx,ly-20*ballT);cx.stroke()}}
653}
654function drawSwing(){
655 const prog=Math.min(batSw*3.5,1);const startA=Math.PI*1.2,endA=Math.PI*.1,curA=startA+(endA-startA)*prog;
656 const px=spotX+20,py=spotY+30;
657 if(imgs.bat){cx.save();cx.globalAlpha=.7*(1-batSw);cx.translate(px,py);cx.rotate(curA-Math.PI*.5);
658  const bLen=70;cx.drawImage(imgs.bat,-8,-bLen*.1,16,bLen);cx.restore()}
659 cx.strokeStyle=`rgba(255,215,0,${.4*(1-batSw)})`;cx.lineWidth=4;cx.lineCap='round';
660 cx.beginPath();cx.arc(px,py,55,startA,curA,true);cx.stroke();
661}
662function drawHitBall(){
663 if(!hitBall)return;const a=hitBall,sz=3+(1-a.t/a.mt)*3;
664 cx.strokeStyle=`rgba(255,255,255,${a.t/a.mt*.25})`;cx.lineWidth=1.5;cx.setLineDash([3,3]);
665 cx.beginPath();cx.moveTo(spotX,spotY);cx.lineTo(a.x,a.y);cx.stroke();cx.setLineDash([]);
666 if(imgs.ball){const s=sz*2.5;cx.drawImage(imgs.ball,a.x-s/2,a.y-s/2,s,s)}
667 else{cx.fillStyle='#fff';cx.beginPath();cx.arc(a.x,a.y,sz,0,Math.PI*2);cx.fill()}
668}
669function drawParts(){particles.forEach(p=>{cx.globalAlpha=p.life;cx.fillStyle=p.color;cx.beginPath();cx.arc(p.x,p.y,p.size*p.life,0,Math.PI*2);cx.fill()});cx.globalAlpha=1}
670function drawLabel(){
671 if(!rLabel||rLabel.t<=0)return;const r=rLabel,pg=1-r.t/r.mt;
672 const sc=pg<.12?(pg/.12)*1.4:(pg<.22?1.4-(pg-.12)/.1*.4:1);const al=pg>.65?1-(pg-.65)/.35:1;
673 cx.save();cx.translate(W/2,r.y);cx.scale(sc,sc);cx.globalAlpha=al;
674 cx.font=`900 ${r.sz}rem 'Helvetica Neue',Arial,sans-serif`;cx.textAlign='center';
675 cx.fillStyle='rgba(0,0,0,.5)';cx.fillText(r.tx,2,2);cx.fillStyle=r.c;cx.fillText(r.tx,0,0);
676 cx.restore();cx.globalAlpha=1;
677}
678function drawPops(){sPops.forEach(p=>{const pg=1-p.t/p.mt,al=pg>.6?1-(pg-.6)/.4:1,y=p.y-pg*50;
679 cx.globalAlpha=al;cx.fillStyle='#FFD700';cx.font='800 1.2rem sans-serif';cx.textAlign='center';cx.fillText(p.tx,p.x,y)});cx.globalAlpha=1}
680
681// ============ Effects ============
682function fx(type){
683 const hx=spotX,hy=spotY;
684 if(type==='homerun'){shake=22;flash=.6;flashColor='255,255,255';spawn(hx,hy,45,'#FFD700',9);spawn(hx,hy,20,'#FF6B35',7);snd('homerun');
685  hitBall={x:hx,y:hy,vx:(Math.random()-.5)*3,vy:-13,t:85,mt:85};if(navigator.vibrate)navigator.vibrate([50,30,100]);
686  pitcherDown=true;pitcherDownHR=true;pitcherDownTimer=80;
687 }else if(type==='triple'){shake=16;flash=.4;flashColor='244,114,182';spawn(hx,hy,35,'#F472B6',7);snd('hit');hitBall={x:hx,y:hy,vx:(Math.random()>.5?1:-1)*5,vy:-11,t:70,mt:70};if(navigator.vibrate)navigator.vibrate(50);pitcherDown=true;pitcherDownHR=false;pitcherDownTimer=55}
688 else if(type==='double'){shake=13;flash=.3;flashColor='255,255,255';spawn(hx,hy,28,'#4ADE80',6);snd('hit');hitBall={x:hx,y:hy,vx:(Math.random()>.5?1:-1)*4,vy:-9,t:65,mt:65};if(navigator.vibrate)navigator.vibrate(40);pitcherDown=true;pitcherDownHR=false;pitcherDownTimer=55}
689 else if(type==='hit'){shake=7;flash=.15;flashColor='255,255,255';spawn(hx,hy,16,'#60A5FA',4);snd('hit');hitBall={x:hx,y:hy,vx:(Math.random()-.5)*6,vy:-5,t:45,mt:45};if(navigator.vibrate)navigator.vibrate(20);pitcherDown=true;pitcherDownHR=false;pitcherDownTimer=45}
690 else if(type==='foul'){shake=3;spawn(hx,hy,8,'#9CA3AF',3);snd('foul');hitBall={x:hx,y:hy,vx:(Math.random()>.5?9:-9),vy:-3,t:30,mt:30};pitcherDown=true;pitcherDownHR=false;pitcherDownTimer=30}
691 else{snd('miss');pitcherDown=true;pitcherDownHR=false;pitcherDownTimer=40}
692 if(isCritical&&type!=='miss'&&type!=='foul'){flash=.4;flashColor='200,100,255';spawn(hx,hy,15,'#C084FC',6)}
693 if(lastWasHR&&active('chain_lightning')&&type!=='miss'){for(let i=0;i<4;i++){const lx=hx+(Math.random()-.5)*80;spawn(lx,hy-20-Math.random()*50,4,'#87CEEB',4)}}
694 const info=RES[type];rLabel={tx:info.label,c:info.color,sz:info.sz,t:75,mt:75,y:H*.46};batSw=.01;
695}
696
697// ============ Main Loop ============
698let lastFrameTime=0;
699let dtScale=1; // 60fps基準のフレーム補正倍率
700function upd(now){
701 try{
702 if(!now)now=performance.now();
703 if(lastFrameTime){const dt=now-lastFrameTime;dtScale=dt/(1000/60)}
704 lastFrameTime=now;
705 if(dtScale>3)dtScale=3; // 極端な遅延を防止
706 if(dspSc<totSc){dspSc+=Math.ceil((totSc-dspSc)*.15);if(dspSc>totSc)dspSc=totSc;document.getElementById('hud-sc').textContent=fmtS(dspSc)}
707 if(shake>0)shake*=.84;if(shake<.5)shake=0;
708 if(flash>0)flash*=.87;if(flash<.01)flash=0;
709 if(batSw>0){batSw+=.045*dtScale;if(batSw>1)batSw=0}
710 particles.forEach(p=>{p.x+=p.vx*dtScale;p.y+=p.vy*dtScale;p.vy+=.15*dtScale;p.life-=p.decay*dtScale});particles=particles.filter(p=>p.life>0);
711 if(hitBall){hitBall.x+=hitBall.vx*dtScale;hitBall.y+=hitBall.vy*dtScale;hitBall.vy+=.07*dtScale;hitBall.t-=dtScale;if(hitBall.t<=0)hitBall=null}
712 if(rLabel){rLabel.t-=dtScale;if(rLabel.t<=0)rLabel=null}
713 sPops.forEach(p=>p.t-=dtScale);sPops=sPops.filter(p=>p.t>0);
714 if(pitcherTimer>0){pitcherTimer-=dtScale}
715 if(pitcherDownTimer>0){pitcherDownTimer-=dtScale;if(pitcherDownTimer<=0){pitcherDown=false;pitcherFrame=0}}
716 if(st==='waiting'){pWait-=dtScale;if(pWait<=0){
717  curPitch=pickPitch();
718  const sp=curPitch.speed;ballSpd=getBallSpeed(sp[0]+Math.random()*(sp[1]-sp[0]));
719  ballCX=curPitch.curveX*(Math.random()>.5?1:-1);ballWX=ballCX;
720  // 魔王とお戯れモード判定
721  ghostMode=active('otawamure');
722  ghostVisible=true;ghostType='';
723  if(ghostMode){
724   ghostType=Math.random()<0.3?'pinball':'vanish';
725   ballSpd*=(0.1+Math.random()*0.1); // -80%~-90%ランダム
726   if(ghostType==='pinball'){
727    ghostBounces=generateGhostBounces();
728   }else{
729    ghostBounces=[];
730   }
731  }else{ghostBounces=[]}
732  // イベント判定を先にする(爆弾と独立)
733  curEvent=null;
734  if(!ghostMode){
735  const ultLv=skillLv('ultimate');
736  const chaosEff=skillVal('chaos');
737  curEvent=rollPitchEvent(ultLv);
738  if(!curEvent&&chaosEff&&chaosEff.allEvent){
739   // カオスモード: ファイヤーボール出現率大幅UP(40%)
740   if(Math.random()<0.4){curEvent=PITCH_EVENTS.find(e=>e.id==='fire')}
741   else{curEvent=PITCH_EVENTS[Math.floor(Math.random()*PITCH_EVENTS.length)]}
742  }
743  }
744  const bombEff=skillVal('bomb_ball');
745  isBomb=!curEvent&&!ghostMode&&bombEff&&Math.random()<bombEff.value;
746  if(curEvent){
747   if(curEvent.speedMult)ballSpd*=curEvent.speedMult;
748   // イベント予告表示 → 少し待ってから投球
749   eventPreview=curEvent;eventPreviewTimer=70;
750   showEventAnnounce(curEvent);
751   st='event_preview';
752  }else{
753   // 通常投球 - まず振りかぶりモーション
754   st='windup';
755   pitcherFrame=0;pitcherTimer=0;const wid=++windupId;
756   setTimeout(()=>{if(windupId!==wid||st!=='windup')return;pitcherFrame=1},500);
757   setTimeout(()=>{if(windupId!==wid||st!=='windup')return;pitcherFrame=2;snd('charge')},800);
758   setTimeout(()=>{if(windupId!==wid)return;pitcherFrame=3;st='pitching';ballT=0;jaganUsed=false;jaganSlow=0;snd('pitch')},2300);
759  }
760 }}
761 if(st==='event_preview'){eventPreviewTimer-=dtScale;if(eventPreviewTimer<=0){
762  eventPreview=null;st='windup';
763  pitcherFrame=0;pitcherTimer=0;const wid2=++windupId;
764  setTimeout(()=>{if(windupId!==wid2||st!=='windup')return;pitcherFrame=1},500);
765  setTimeout(()=>{if(windupId!==wid2||st!=='windup')return;pitcherFrame=2;snd('charge')},800);
766  setTimeout(()=>{if(windupId!==wid2)return;pitcherFrame=3;st='pitching';ballT=0;jaganUsed=false;jaganSlow=0;snd('pitch')},2300);
767 }}
768 if(st==='pitching'){
769  // 消える魔球: ランダム点滅(vanishのみ)
770  if(ghostMode&&ghostType==='vanish'){
771   if(Math.random()<0.08)ghostVisible=!ghostVisible;
772  }
773  // 邪眼解放: ミートエリア中心でボール完全停止(ゴーストモード中は無効)
774  if(jaganSlow>0){jaganSlow-=dtScale;ballT=jaganFreezeT} else {
775  ballT+=ballSpd*dtScale;
776  if(!ghostMode&&!jaganUsed&&active('jagan')){
777   const tTarget=Math.sqrt((spotY-H*.37)/(H*.85-H*.37));
778   if(ballT>=tTarget){
779    const jEff=skillVal('jagan');
780    const freezeTime=jEff?jEff.freezeTime:0.1;
781    jaganFreezeT=tTarget;ballT=tTarget;
782    jaganSlow=Math.round(Math.min(freezeTime,1.0)*60);jaganUsed=true;
783   }
784  }}
785  if(ballT>=1.1){
786  if(isBomb){const bombEff=skillVal('bomb_ball');const pen=bombEff?bombEff.penalty:0.05;const lost=Math.floor(S.pts*pen);S.pts=Math.max(0,S.pts-lost);sv(S);if(lost>0)sPops.push({tx:`-${fmtS(lost)} pts`,x:W/2,y:H*.35,t:60,mt:60})}
787  lastWasHR=false;regResult('miss');
788 }}
789 if(st==='result_wait'){pWait-=dtScale;if(pWait<=0)nextB()}
790 draw();
791 }catch(err){console.error('upd error:',err)}
792 requestAnimationFrame(upd);
793}
794
795// ============ Story ============
796let storyShown=false;
797function showStory(){
798 ea();
799 const stEl=document.getElementById('story');
800 document.getElementById('st-bg').style.backgroundImage="url('stage.png')";
801 document.getElementById('st-chara').src='throw2.png';
802 const lines=[
803  '世界は魔王<em>ボーク・ストライクス</em>に支配された',
804  '',
805  '希望は もはやひとつ',
806  '伝説の打者となり <em>ヘルスタジアム</em>へ向かえ',
807  '',
808  '運命のプレイボールが いま始まる'
809 ];
810 document.getElementById('st-text').innerHTML=lines.join('<br>');
811 document.getElementById('title').style.display='none';
812 stEl.style.display='flex';
813 const skip=()=>{stEl.removeEventListener('click',skip);stEl.style.display='none';storyShown=true;realStartGame()};
814 stEl.addEventListener('click',skip);
815}
816// ============ Ending ============
817function showEnding(){
818 ea();
819 stopBgm();
820 // ホワイトアウト用オーバーレイ
821 const endEl=document.getElementById('ending');
822 const endBg=document.getElementById('end-bg');
823 const endText=document.getElementById('end-text');
824 const endTitle=document.getElementById('end-title');
825 const endSkip=document.getElementById('end-skip');
826 endBg.style.backgroundImage="url('clear.png')";
827 endBg.style.opacity='0';
828 endText.className='end-text';
829 endTitle.className='end-title';
830 endSkip.className='end-skip';
831 const lines=[
832  '魔王<em>ボーク・ストライクス</em>は敗れた',
833  '<em>ヘルスタジアム</em>に響いていた支配の歓声は消え',
834  '長く続いた魔王の時代は いま終わりを告げた',
835  '',
836  '勝敗ですべてが決まる世界は崩れ去り',
837  '人々はようやく 本当の空を見上げる',
838  '野球は奪い合うものではなく',
839  '笑い合うためのものへと戻っていく',
840  '',
841  'スタジアムには歓声が戻り',
842  '子どもたちは自由にボールを追い',
843  '誰もが好きなようにバットを振るえる世界が帰ってきた',
844  '',
845  'いま 野球は',
846  '支配のためではなく',
847  '楽しむためにある',
848  '',
849  'すべては きみの一振りから始まった',
850  'そしてこの伝説は',
851  'これからも語り継がれていく',
852  '',
853  'ありがとう 勇敢なる打者よ',
854 ];
855 endText.innerHTML=lines.join('<br>');
856 // 全画面白→フェードイン
857 endEl.style.display='flex';endEl.style.background='#fff';
858 // clear.mp3を再生
859 const clearAudio=bgmClear.cloneNode();clearAudio.volume=bgmVol>0?Math.max(bgmVol,0.3):0.3;clearAudio.play().catch(()=>{});
860 setTimeout(()=>{endEl.style.transition='background 3s';endEl.style.background='#000'},500);
861 setTimeout(()=>{endBg.style.opacity='.4'},2000);
862 setTimeout(()=>{endText.classList.add('show')},3000);
863 setTimeout(()=>{endTitle.classList.add('show')},5000);
864 setTimeout(()=>{endSkip.classList.add('show')},7000);
865 // エンディングを見たフラグ
866 S.stats.endingSeen=true;sv(S);
867 const close=()=>{endEl.removeEventListener('click',close);
868  endEl.style.display='none';endEl.style.transition='';endEl.style.background='#000';
869  clearAudio.pause();clearAudio.currentTime=0;
870  playBgm(bgmOp);showTitle();document.getElementById('title').style.display='flex'};
871 // 7秒後からタップ可能
872 setTimeout(()=>{endEl.addEventListener('click',close)},7000);
873}
874
875function startGame(){
876 // 初回のみストーリー表示(一度もゲームをプレイしていない=stats.gamesが0)
877 const s=ld();
878 if(!storyShown&&(!s.stats||!s.stats.games||s.stats.games===0)){showStory();return}
879 realStartGame();
880}
881
882// ============ Game ============
883function realStartGame(){
884 ea();updSpot();playBgm(bgmBattle);
885 document.getElementById('title').style.display='none';
886 document.getElementById('result').style.display='none';
887 document.getElementById('tree').style.display='none';
888 document.getElementById('hud').style.display='flex';
889 if(active('one_shot'))maxB=3; else maxB=BASE_BALLS;
890 curB=0;totSc=0;dspSc=0;combo=0;maxCombo=0;resList=[];particles=[];sPops=[];rLabel=null;hitBall=null;comText=null;
891 lastWasHR=false;isCritical=false;curEvent=null;eventBonusPts=0;challengeCleared=false;milestoneJustReached=null;
892 pitcherDown=false;pitcherDownTimer=0;pitcherDownHR=false;jaganSlow=0;jaganUsed=false;pitcherFrame=0;
893 ghostMode=false;ghostType='';ghostBounces=[];ghostVisible=true;
894 // チャレンジ選出(アルティメットモード以降のみ)
895 if(skillLv('ultimate')>=1){
896  curChallenge=pickChallenge();
897  const chEl=document.getElementById('ch-banner');
898  chEl.innerHTML=`<span class="ch-icon">${curChallenge.icon}</span><span class="ch-name">CHALLENGE: ${curChallenge.name}</span><span class="ch-desc"> - ${curChallenge.desc}</span>`;
899  chEl.style.display='block';chEl.style.opacity='1';
900 }else{
901  curChallenge=null;
902  document.getElementById('ch-banner').style.display='none';
903 }
904 document.getElementById('hud-sc').textContent='0';document.getElementById('hud-cb').textContent='';
905 document.getElementById('hud-tp').textContent=fmtS(S.pts)+' pts';
906 updHUD();nextB();
907}
908function nextB(){if(curB>=maxB){showResult();return}st='waiting';pitcherFrame=0;pitcherDown=false;pitcherDownHR=false;++windupId;ballT=0;flash=0;flashColor='255,255,255';shake=0;ghostMode=false;ghostType='';ghostBounces=[];ghostVisible=true;pWait=30+Math.floor(Math.random()*40);updHUD()}
909function regResult(type){
910 if(type==='homerun'||type==='triple'||type==='double'||type==='hit'){combo++;
910if(combo>maxCombo)maxCombo=combo}else{
911  // ずっと俺のターン: ミス時にコンボ継続判定
912  const mtEff=skillVal('my_turn');
913  if(mtEff&&Math.random()<mtEff.chance){sPops.push({tx:'俺のターン!',x:W/2,y:H*.48,t:40,mt:40})}
914  else{combo=0}
915 }
916 isCritical=false;
917 if(active('critical')&&type!=='miss'&&type!=='foul'){const critEff=skillVal('critical');if(critEff&&Math.random()<critEff.value)isCritical=true}
918 let pts=calcPts(type);
919 if(isBomb&&type!=='miss'){totSc=Math.floor(totSc*2)}
920 totSc+=pts;const resEntry={type,pts};if(curEvent){resEntry.eventId=curEvent.id;if(curEvent.diamondPtsMult&&type==='homerun')resEntry.diamondPtsMult=curEvent.diamondPtsMult}resList.push(resEntry);curB++;
921 // ⑪スコア表示: ×コンボを先に表示
922 if(pts>0){let tx='';if(combo>1)tx+=`×${combo} `;tx+=`+${fmtS(pts)}`;if(isCritical)tx+=' CRIT!';if(curEvent&&type!=='miss')tx+=` ${curEvent.icon}`;sPops.push({tx,x:W/2,y:H*.56,t:50,mt:50})}
923 lastWasHR=(type==='homerun');fx(type);updHUD();
924 st='result_wait';pWait=(type==='homerun'?85:55)+60;
925}
926function updHUD(){
927 document.getElementById('hud-bc').textContent=`${Math.min(curB+1,maxB)} / ${maxB}`;
928 document.getElementById('hud-cb').textContent=combo>1?`${combo} COMBO!`:'';
929}
930function showResult(){
931 st='finished';document.getElementById('hud').style.display='none';
932 playBgm(bgmOp);
933 document.getElementById('ch-banner').style.display='none';
934 let earnPts=totSc;let ptsMult=getEffect('pts_mult',1);
935 // EXTREME BASEBALL: 累計HR/10をpt倍率に加算
936 if(active('extreme_bb')){const totalHR=S.stats&&S.stats.totalHR?S.stats.totalHR:0;ptsMult+=totalHR/10}
937 earnPts=Math.floor(earnPts*ptsMult);
938
939 // チャレンジ判定
940 challengeCleared=false;
941 let challengeBonus=0;
942 if(curChallenge){
943  const stats={maxCombo:maxCombo};
944  challengeCleared=curChallenge.check(resList,stats);
945  if(challengeCleared){challengeBonus=Math.floor(earnPts*(curChallenge.bonus-1));earnPts+=challengeBonus}
946 }
947
948 // ダイヤモンドボール: HR時に最終獲得ptに乗算
949 let hasDiamondHR=false;
950 resList.forEach(r=>{if(r.diamondPtsMult)hasDiamondHR=true});
951 if(hasDiamondHR){ptsMult*=3;earnPts=Math.floor(earnPts*3)}
952
953 // stats更新
954 if(!S.stats)S.stats={};
955 S.stats.games=(S.stats.games||0)+1;
956 S.stats.totalBalls=(S.stats.totalBalls||0)+resList.length;
957 S.stats.totalScore=(S.stats.totalScore||0)+totSc;
958 const hrCount=resList.filter(r=>r.type==='homerun').length;
959 const hitCount=resList.filter(r=>r.type!=='miss'&&r.type!=='foul').length;
960 S.stats.totalHR=(S.stats.totalHR||0)+hrCount;
961 S.stats.totalHits=(S.stats.totalHits||0)+hitCount;
962 S.stats.totalTriple=(S.stats.totalTriple||0)+resList.filter(r=>r.type==='triple').length;
963 S.stats.totalDouble=(S.stats.totalDouble||0)+resList.filter(r=>r.type==='double').length;
964 S.stats.totalSingle=(S.stats.totalSingle||0)+resList.filter(r=>r.type==='hit').length;
965 S.stats.totalFoul=(S.stats.totalFoul||0)+resList.filter(r=>r.type==='foul').length;
966 S.stats.totalMiss=(S.stats.totalMiss||0)+resList.filter(r=>r.type==='miss').length;
967 if(maxCombo>(S.stats.bestCombo||0))S.stats.bestCombo=maxCombo;
968 if(hrCount===maxB&&maxB>=3)S.stats.perfectGame=true;
969 if(hrCount===maxB&&maxB===3&&skillLv('chaos')>=9999&&active('one_shot'))S.stats.chaosMaxPerfect=true;
970 // 亡霊殺し: 全スキルMAX+ON + お戯れモード + 全球HR
971 if(hrCount===maxB&&active('otawamure')){
972  let allMax=true;SKILL_TREE.forEach(sk=>{
973   if(sk.id==='profile_card')return;
974   if(skillLv(sk.id)<(sk.hyperScale?effectiveMaxLv(sk):sk.maxLv))allMax=false;
975   if(sk.toggleable&&S.togOff[sk.id])allMax=false;
976  });if(allMax)S.stats.ghostPerfect=true;
977 }
978 if(challengeCleared)S.stats.challengeClears=(S.stats.challengeClears||0)+1;
979 // イベント実績
980 resList.forEach(r=>{
981  if(r.eventId==='rainbow'&&r.type!=='miss')S.stats.rainbowHit=true;
982  if(r.eventId==='fire'&&r.type!=='miss')S.stats.fireHit=true;
983 });
984 // 実績: 解除前の状態を記録
985 const prevAch={};ACHIEVEMENTS.forEach(a=>{prevAch[a.id]=a.check(S)});
986 // マイルストーンチェック(レベルアップ前のを記録)
987 const prevMs=getMilestone(S);
988 S.pts+=earnPts;if(totSc>S.best)S.best=totSc;sv(S);
989 const newMs=getMilestone(S);
990 milestoneJustReached=(newMs&&(!prevMs||newMs.lv>prevMs.lv))?newMs:null;
991
992 // 上段: スコアと倍率を小さく表示
993 let bkText=`${fmtS(totSc)}`;
994 if(Math.floor(ptsMult)>1)bkText+=`  ×${Math.floor(ptsMult)}`;
995 document.getElementById('r-breakdown').textContent=bkText;
996 // 最終獲得ptをデカデカ表示
997 document.getElementById('r-total').textContent=`+${fmtS(earnPts)}`;
998 const c={homerun:0,triple:0,double:0,hit:0,foul:0,miss:0};resList.forEach(r=>c[r.type]++);
999 let d='';if(c.homerun)d+=`HOME RUN  x${c.homerun}\n`;if(c.triple)d+=`TRIPLE  x${c.triple}\n`;if(c.double)d+=`DOUBLE  x${c.double}\n`;if(c.hit)d+=`HIT  x${c.hit}\n`;if(c.foul)d+=`FOUL  x${c.foul}\n`;if(c.miss)d+=`MISS  x${c.miss}\n`;
1000 if(maxCombo>1)d+=`MAX COMBO  x${maxCombo}\n`;
1001 // チャレンジ結果
1002 if(curChallenge){
1003  if(challengeCleared)d+=`\n${curChallenge.icon} CHALLENGE CLEAR! +${fmtS(challengeBonus)} pts\n`;
1004  else d+=`\n${curChallenge.icon} CHALLENGE FAILED...\n`;
1005 }
1006 // マイルストーン到達
1007 if(milestoneJustReached)d+=`\n${milestoneJustReached.icon} ${milestoneJustReached.title} 到達!\n`;
1008 document.getElementById('r-detail').textContent=d;
1009 if(totSc>=S.best){document.getElementById('r-nb').textContent='NEW BEST!';document.getElementById('r-nb').style.display='block'}else{document.getElementById('r-nb').style.display='none'}
1010 // ⑦ 実績解除通知
1011 const achEl=document.getElementById('r-ach-notify');achEl.innerHTML='';
1012 const newAchs=[];ACHIEVEMENTS.forEach(a=>{if(!prevAch[a.id]&&a.check(S))newAchs.push(a)});
1013 newAchs.forEach(a=>{
1014  const div=document.createElement('div');div.className='r-ach-item';
1015  div.textContent=`🏆 ${a.icon} ${a.name} の実績をクリア!`;
1016  if(a.id==='chaos_perfect'){div.textContent+=' 👉実績画面でタップ!';div.style.cursor='pointer';
1017   div.onclick=()=>{document.getElementById('result').style.display='none';openAch()}}
1018  achEl.appendChild(div);
1019 });
1020 document.getElementById('result').style.display='flex';
1021}
1022
1023// ============ Tap ============
1024function onTap(){ea();if(st==='pitching'){const type=judge();
1025 // 心眼: ファウル時やり直し(ノーカウント)
1026 if(type==='foul'&&active('shingan')){
1027  snd('miss');sPops.push({tx:'心眼!',x:W/2,y:H*.48,t:40,mt:40});
1028  st='waiting';pWait=30+Math.floor(Math.random()*20);return}
1029 regResult(type)}}
1030cv.addEventListener('touchstart',e=>{e.preventDefault();onTap()},{passive:false});
1031cv.addEventListener('mousedown',onTap);
1032
1033// ============ Skill Tree UI ============
1034let treeBranch='power';let treeFrom='title';
1035function openTree(from){treeFrom=from;renderTree();document.getElementById('tree').style.display='flex'}
1036document.getElementById('tr-maxall').onclick=()=>{
1037 let bought=0,changed=true;
1038 while(changed){changed=false;SKILL_TREE.forEach(sk=>{
1039  if(canUnlock(sk.id)){if(unlockSkill(sk.id)){bought++;changed=true}}
1040 })}
1041 if(bought>0){snd('buy');renderTree();updSpot()}
1042};
1043function closeTree(){
1044 document.getElementById('tree').style.display='none';
1045 if(treeFrom==='result')document.getElementById('result').style.display='flex';
1046 else{showTitle();document.getElementById('title').style.display='flex'}
1047}
1048function renderTree(){
1049 document.getElementById('tr-pts').textContent=fmtS(S.pts)+' pts';
1050 const tabEl=document.getElementById('tr-tabs');tabEl.innerHTML='';
1051 ['power','technique','wild','center'].forEach(bk=>{
1052  const b=BRANCHES[bk];const tab=document.createElement('div');
1053  tab.className='tr-tab'+(bk===treeBranch?' active':'');
1054  if(bk===treeBranch){tab.style.borderColor=b.color;tab.style.color=b.color;tab.style.background=b.bg}
1055  tab.textContent=b.icon+' '+b.name;tab.onclick=()=>{treeBranch=bk;renderTree()};tabEl.appendChild(tab);
1056 });
1057 const listEl=document.getElementById('tr-list');listEl.innerHTML='';
1058 const allSkills=SKILL_TREE.filter(s=>s.branch===treeBranch).sort((a,b)=>a.ring-b.ring);
1059 // カオス解放前のスキルは非表示(unlock条件がカオス依存かつLv0)
1060 const skills=allSkills.filter(sk=>{
1061  if(skillLv(sk.id)>=1)return true; // 既に取得済みなら常に表示
1062  if(!sk.unlock)return true; // unlock条件がなければ常に表示
1063  return sk.unlock(S); // 解放条件を満たしていれば表示
1064 });
1065 const branchColor=BRANCHES[treeBranch].color;
1066 const hyperBonus=getHyperLvBonus(S);
1067
1068 skills.forEach((sk,idx)=>{
1069  if(idx>0){
1070   const conn=document.createElement('div');conn.className='tr-conn';
1071   const prevSk=skills[idx-1];
1072   const prevMax=skillLv(prevSk.id)>=prevSk.maxLv;
1073   conn.style.background=prevMax?branchColor:'rgba(255,255,255,.1)';listEl.appendChild(conn);
1074  }
1075  const lv=skillLv(sk.id);
1076  const effMax=effectiveMaxLv(sk);
1077  const isMax=lv>=effMax;
1078  const avail=isSkillAvailable(sk.id);
1079  const canBuy=canUnlock(sk.id);
1080  const locked=lv===0&&!avail;
1081
1082  const node=document.createElement('div');
1083  node.className='tr-node'+(locked?' locked':'')+(isMax?' maxed':'');
1084  node.style.background=locked?'rgba(255,255,255,.02)':BRANCHES[treeBranch].bg;
1085
1086  const icon=document.createElement('div');icon.className='tr-icon';
1087  icon.style.borderColor=locked?'rgba(255,255,255,.1)':branchColor;
1088  if(isMax){icon.style.background='rgba(255,215,0,.2)';icon.style.borderColor='#FFD700';icon.textContent='👑'}
1089  else if(lv>0){icon.style.background=BRANCHES[treeBranch].bg;icon.textContent=BRANCHES[treeBranch].icon}
1090  else if(avail){icon.textContent=BRANCHES[treeBranch].icon}
1091  else{icon.textContent='🔒'}
1092  node.appendChild(icon);
1093
1094  const info=document.createElement('div');info.className='tr-info';
1095  const nameRow=document.createElement('div');nameRow.className='tr-name';nameRow.textContent=sk.name;
1096  // level badge
1097  if(effMax>1){
1098   const lvBadge=document.createElement('span');lvBadge.className='tr-lv';
1099   if(sk.hyperScale&&hyperBonus>0)lvBadge.textContent=`Lv ${lv}/${effMax}(+${hyperBonus})`;
1100   else lvBadge.textContent=`Lv ${lv}/${effMax}`;
1101   if(isMax){lvBadge.style.background='rgba(255,215,0,.2)';lvBadge.style.color='#FFD700'}
1102   nameRow.appendChild(lvBadge);
1103  }
1104  info.appendChild(nameRow);
1105
1106  // desc — 全てdescFnで動的生成
1107  const desc=document.createElement('div');desc.className='tr-desc';
1108  if(locked){desc.textContent='???'}
1109  else if(lv>0&&lv<effMax){
1110   desc.textContent=sk.descFn(lv)+' → 次: '+sk.descFn(lv+1);
1111  }else if(lv===0){
1112   desc.textContent=sk.descFn(1);
1113  }else{
1114   desc.textContent=sk.descFn(lv);
1115  }
1116  info.appendChild(desc);
1117
1118  // toggle for one_shot
1119  if(sk.toggleable&&lv>0){
1120   const togDiv=document.createElement('div');togDiv.className='tr-toggle';
1121   const togLabel=document.createElement('span');togLabel.className='tr-toggle-label';togLabel.textContent=S.togOff[sk.id]?'OFF':'ON';
1122   const togWrap=document.createElement('label');togWrap.className='tr-sw';
1123   togWrap.innerHTML=`<input type="checkbox" ${S.togOff[sk.id]?'':'checked'}><span class="sl"></span>`;
1124   togWrap.querySelector('input').onchange=e=>{
1125    if(e.target.checked){delete S.togOff[sk.id]}else{S.togOff[sk.id]=true}
1126    sv(S);renderTree();
1127   };
1128   togDiv.appendChild(togLabel);togDiv.appendChild(togWrap);info.appendChild(togDiv);
1129  }
1130
1131  // cost
1132  if(!isMax&&!locked){
1133   const costDiv=document.createElement('div');costDiv.className='tr-cost';
1134   const c=getSkillCost(sk.id);costDiv.textContent=fmtS(c)+' pt';
1135   if(S.pts<c)costDiv.style.color='#EF4444';info.appendChild(costDiv);
1136  }
1137  node.appendChild(info);
1138
1139  // ボタン列
1140  const btnCol=document.createElement('div');btnCol.style.cssText='display:flex;gap:4px;flex-shrink:0;flex-direction:column;align-items:center';
1141  // 倍率購入(ultimate/legend用)
1142  if((sk.id==='ultimate'||sk.id==='legend'||sk.id==='chaos')&&lv>0&&!isMax){
1143   const mults=sk.id==='chaos'?[1,10,100]:[1,5,10];
1144   mults.forEach(m=>{
1145    const mb=document.createElement('button');mb.className='tr-btn';
1146    const totalCost=calcMultiCost(sk.id,m);
1147    const canAfford=S.pts>=totalCost&&(lv+m<=effectiveMaxLv(sk));
1148    const actualM=Math.min(m,effectiveMaxLv(sk)-lv);
1149    if(canAfford&&actualM>0){
1150     mb.className+=' unlock';mb.textContent=`×${m}`;mb.style.cssText='padding:.25rem .5rem;font-size:.6rem;min-width:40px';
1151     mb.addEventListener('click',e=>{e.preventDefault();for(let i=0;i<actualM;i++){if(!unlockSkill(sk.id))break}snd('buy');renderTree();updSpot()});
1152    }else{
1153     mb.disabled=true;mb.textContent=`×${m}`;mb.style.cssText='padding:.25rem .5rem;font-size:.6rem;min-width:40px';
1154    }
1155    btnCol.appendChild(mb);
1156   });
1157  }else{
1158   const btn=document.createElement('button');btn.className='tr-btn';
1159   if(isMax){btn.className+=' maxed';btn.textContent='MAX';btn.disabled=true}
1160   else if(canBuy){btn.className+=' unlock';btn.textContent=lv>0?'LV UP':'UNLOCK';
1161    btn.addEventListener('click',e=>{e.preventDefault();if(unlockSkill(sk.id)){snd('buy');renderTree();updSpot()}});
1162   }else{btn.disabled=true;btn.textContent=locked?'🔒':lv>0?'LV UP':'UNLOCK'}
1163   btnCol.appendChild(btn);
1164  }
1165  // 「最大」ボタン(MAX未到達&ロック解除済み&購入可能な場合)
1166  if(!isMax&&!locked&&lv>=0){
1167   const maxBtn=document.createElement('button');maxBtn.className='tr-btn';
1168   maxBtn.style.cssText='padding:.25rem .5rem;font-size:.55rem;min-width:40px;background:#b8860b;color:#fff';
1169   // 最大何レベル買えるか計算
1170   let maxLvs=0,tempPts=S.pts,tmpLv=lv;
1171   while(tmpLv<effectiveMaxLv(sk)){
1172    const c=sk.costFn(tmpLv+1);
1173    if(tempPts<c)break;
1174    tempPts-=c;tmpLv++;maxLvs++;
1175   }
1176   if(maxLvs>1){
1177    maxBtn.className+=' unlock';maxBtn.textContent='最大';
1178    maxBtn.addEventListener('click',e=>{e.preventDefault();let bought=0;while(unlockSkill(sk.id))bought++;if(bought>0){snd('buy');renderTree();updSpot()}});
1179    btnCol.appendChild(maxBtn);
1180   }
1181  }
1182  node.appendChild(btnCol);
1183  listEl.appendChild(node);
1184 });
1185}
1186
1187// ============ Achievements UI ============
1188let achFrom='title';
1189function openAch(from){achFrom=from;renderAch();document.getElementById('achieve').style.display='flex'}
1190function closeAch(){
1191 document.getElementById('achieve').style.display='none';
1192 if(achFrom==='result')document.getElementById('result').style.display='flex';
1193 else{showTitle();document.getElementById('title').style.display='flex'}
1194}
1195function renderAch(){
1196 let unlocked=0;
1197 const list=document.getElementById('ach-list');list.innerHTML='';
1198 ACHIEVEMENTS.forEach(a=>{
1199  const done=a.check(S);if(done)unlocked++;
1200  const isHidden=a.hidden&&a.showCondition&&!a.showCondition(S)&&!done;
1201  const node=document.createElement('div');node.className='ach-node'+(done?' unlocked':'');
1202  // 魔王殺し解放済み→クリック可能
1203  if(done&&a.id==='chaos_perfect'){node.className+=' clickable';node.style.cursor='pointer';
1204   node.addEventListener('click',()=>{document.getElementById('achieve').style.display='none';showEnding()})}
1205  const icon=document.createElement('div');icon.className='ach-icon'+(done?'':' locked');icon.textContent=isHidden?'❓':a.icon;
1206  const info=document.createElement('div');info.className='ach-info';
1207  const dispName=isHidden?'???':a.name;
1208  const dispDesc=isHidden?'???':a.desc;
1209  info.innerHTML=`<div class="ach-name"${done?'':' style="color:#666"'}>${dispName}</div><div class="ach-desc"${done?'':' style="color:#555"'}>${dispDesc}</div>`;
1210  const chk=document.createElement('div');chk.className='ach-check';chk.textContent=done?'✅':'🔒';
1211  // 魔王殺し解放済み→▶️で再生を示唆
1212  if(done&&a.id==='chaos_perfect'){chk.textContent='▶️'}
1213  node.appendChild(icon);node.appendChild(info);node.appendChild(chk);list.appendChild(node);
1214 });
1215 document.getElementById('ach-count').textContent=`${unlocked} / ${ACHIEVEMENTS.length} 達成`;
1216}
1217document.getElementById('ach-back').onclick=closeAch;
1218
1219// ============ Status UI ============
1220function openStatus(){
1221 renderStatus();document.getElementById('status').style.display='flex';
1222}
1223function closeStatus(){
1224 document.getElementById('status').style.display='none';
1225 showTitle();document.getElementById('title').style.display='flex';
1226}
1227function renderStatus(){
1228 const list=document.getElementById('stat-list');list.innerHTML='';
1229 const st=S.stats||{};
1230 // --- 戦績セクション ---
1231 addStatSection(list,'⚾ 戦績',[
1232  ['🎮','累計試合数',fmt(st.games||0)],
1233  ['⚾','累計投球数',fmt(st.totalBalls||0)],
1234  ['📊','累計スコア',fmtS(st.totalScore||0)],
1235  ['🏆','ベストスコア',fmtS(S.best||0)],
1236  ['💰','所持ポイント',fmtS(S.pts||0)],
1237 ]);
1238 // --- 打撃セクション ---
1239 const tb=st.totalBalls||0;const th=st.totalHits||0;
1240 const avg=tb>0?(th/tb*100).toFixed(1)+'%':'--';
1241 addStatSection(list,'🏏 打撃成績',[
1242  ['💥','累計HR',fmt(st.totalHR||0)],
1243  ['🔺','累計TRIPLE',fmt(st.totalTriple||0)],
1244  ['🔸','累計DOUBLE',fmt(st.totalDouble||0)],
1245  ['🔹','累計HIT',fmt(st.totalSingle||0)],
1246  ['⚪','累計FOUL',fmt(st.totalFoul||0)],
1247  ['❌','累計MISS',fmt(st.totalMiss||0)],
1248  ['📈','打率(MISS/FOUL除外)',avg],
1249  ['🔗','最大コンボ',fmt(st.bestCombo||0)],
1250 ]);
1251 // --- 能力値セクション ---
1252 const scoreMult=getEffect('score_mult',0);
1253 const spotAdd=getEffect('spot_add',0);
1254 const speedMult=getEffect('speed_mult',1);
1255 const ptsMult=getEffect('pts_mult',1);
1256 const goubakuEff=active('goubaku')?skillVal('goubaku'):null;
1257 const goubakuAdd=goubakuEff?goubakuEff.hrZoneAdd:0;
1258 const giantEff=skillVal('giant_bat');
1259 const hrExtra=giantEff?giantEff.hrZone:0;
1260 const totalHrZone=((HR_ZONE_BASE_RATIO+hrExtra+goubakuAdd)*100).toFixed(0);
1261 const masaiEff=active('masai')?skillVal('masai'):null;
1262 const ballScale=masaiEff?((masaiEff.ballScale-1)*100).toFixed(0):'0';
1263 const jaganEff=active('jagan')?skillVal('jagan'):null;
1264 const slowTime=jaganEff?jaganEff.freezeTime.toFixed(1):'0';
1265 const critEff=active('critical')?skillVal('critical'):null;
1266 const critRate=critEff?((critEff.value)*100).toFixed(0):'0';
1267 const meikyoEff2=active('meikyo')?skillVal('meikyo'):null;
1268 const meikyoSpot=meikyoEff2?'-'+((1-meikyoEff2.spotScale)*100).toFixed(0)+'%':'--';
1269 const meikyoScore=meikyoEff2?'×'+meikyoEff2.scoreMult:'--';
1270 addStatSection(list,'⚡ 現在の能力値',[
1271  ['💪','スコア倍率','+'+(scoreMult*100).toFixed(0)+'%'],
1272  ['🎯','ミート拡大','+'+(spotAdd*100).toFixed(0)+'%'],
1273  ['🐢','球速変化',((1-speedMult)*100).toFixed(1)+'%減'],
1274  ['💰','pt倍率','×'+ptsMult],
1275  ['🔴','HRゾーン',totalHrZone+'%'],
1276  ['⚾','ボールサイズ','+'+ballScale+'%'],
1277  ['👁','邪眼停止時間',slowTime+'秒'],
1278  ['⚡','クリティカル率',critRate+'%'],
1279  ['🧘','明鏡止水ミート',meikyoSpot],
1280  ['🧘','明鏡止水スコア',meikyoScore],
1281  ['⚾','EX スコア加算',active('extreme_bb')?'+'+(S.stats&&S.stats.games?S.stats.games:0)+'%':'--'],
1282  ['⚾','EX pt倍率加算',active('extreme_bb')?'+'+(S.stats&&S.stats.totalHR?(S.stats.totalHR/10).toFixed(1):0):'--'],
1283 ]);
1284 // --- スキルLv ---
1285 const skCount=SKILL_TREE.filter(sk=>skillLv(sk.id)>=1).length;
1286 addStatSection(list,'🌟 スキル',[
1287  ['📖','習得スキル数',skCount+'/'+SKILL_TREE.length],
1288  ['🏅','チャレンジクリア',fmt(st.challengeClears||0)],
1289 ]);
1290}
1291function addStatSection(parent,title,rows){
1292 const sec=document.createElement('div');sec.className='stat-section';
1293 const h=document.createElement('div');h.className='stat-section-title';h.textContent=title;sec.appendChild(h);
1294 rows.forEach(r=>{
1295  const row=document.createElement('div');row.className='stat-row';
1296  row.innerHTML=`<span class="stat-label">${r[0]} ${r[1]}</span><span class="stat-value">${r[2]}</span>`;
1297  sec.appendChild(row);
1298 });
1299 parent.appendChild(sec);
1300}
1301function fmt(n){return n.toLocaleString()}
1302// 短縮スコア表記 (1K, 1M, 1B, 1T, 1aa, 1ab...)
1303function fmtS(n){
1304 if(n<0)return'-'+fmtS(-n);
1305 if(n<10000)return n.toLocaleString();
1306 const units=['','K','M','B','T'];
1307 // T以降: aa,ab,...az,ba,...zz
1308 function bigUnit(idx){
1309  if(idx<5)return units[idx];
1310  const i=idx-5;const a=String.fromCharCode(97+(i/26|0));const b=String.fromCharCode(97+i%26);return a+b;
1311 }
1312 let tier=0,v=n;
1313 while(v>=1000&&tier<30){v/=1000;tier++}
1314 return (tier>=1?(v>=100?v.toFixed(0):v>=10?v.toFixed(1):v.toFixed(2))+bigUnit(tier):n.toLocaleString());
1315}
1316document.getElementById('stat-back').onclick=closeStatus;
1317
1318// ============ Ball List UI ============
1319const BALL_LIST=[
1320 {img:'ball.png',name:'ノーマルボール',icon:'⚾',desc:'普通の球。魔王の投球の中ではいちばん優しい。ちなみに硬球。'},
1321 {img:'Golden.png',name:'ゴールデンボール',icon:'🌟',desc:'黄金に輝く特別な球。打てばスコアが50倍になる。ゴールデンなんだからそれぐらい当然だろう'},
1322 {img:'fire.png',name:'ファイヤーボール',icon:'🔥',desc:'灼熱の炎を纏い、正気じゃない速度で迫る魔球。でも速すぎてグラフィックが見えない'},
1323 {img:'diamond.png',name:'ダイヤモンドボール',icon:'💎',desc:'宝石のように美しく輝く球。ホームランで打ち返せば最終獲得ptが3倍になる。そんなことよりバットが折れるのでは?'},
1324 {img:'rainbow.png',name:'レインボーボール',icon:'🌈',desc:'七色の光を放つ奇跡の球。触れさえすれば、すべてがホームランになりスコア20倍。奇跡ってすごい'},
1325 {img:'ghost.png',name:'ゴーストボール【ピンボール】',icon:'👻',desc:'壁を跳ね返りながら不規則に迫る亡霊の球。ろくにデバッグしてないので打てない。打てたらすごい'},
1326 {img:'ghost.png',name:'ゴーストボール【消える魔球】',icon:'👻',desc:'ゆっくりと、しかし姿を消しながら迫る。勘で打つんだ。'},
1327];
1328function openBallList(){
1329 const list=document.getElementById('bl-list');list.innerHTML='';
1330 BALL_LIST.forEach(b=>{
1331  const card=document.createElement('div');card.className='bl-card';
1332  const img=document.createElement('img');img.className='bl-img';img.src=b.img;
1333  const info=document.createElement('div');info.className='bl-info';
1334  info.innerHTML=`<div class="bl-name">${b.name}</div><div class="bl-desc">${b.desc}</div>`;
1335  card.appendChild(img);card.appendChild(info);list.appendChild(card);
1336 });
1337 document.getElementById('title').style.display='none';
1338 document.getElementById('balllist').style.display='flex';
1339}
1340function closeBallList(){
1341 document.getElementById('balllist').style.display='none';
1342 showTitle();document.getElementById('title').style.display='flex';
1343}
1344document.getElementById('bl-back').onclick=closeBallList;
1345
1346// ============ Profile Card ============
1347function loadPlayerName(){try{return localStorage.getItem('aeb_pname')||''}catch(e){return''}}
1348function savePlayerName(n){try{localStorage.setItem('aeb_pname',n)}catch(e){}}
1349
1350function openProfile(){
1351 document.getElementById('title').style.display='none';
1352 document.getElementById('profile').style.display='flex';
1353 document.getElementById('pf-name').value=loadPlayerName()||'';
1354 generateProfileCard();
1355}
1356function closeProfile(){
1357 document.getElementById('profile').style.display='none';
1358 showTitle();document.getElementById('title').style.display='flex';
1359}
1360document.getElementById('pf-back').onclick=closeProfile;
1361document.getElementById('pf-name').addEventListener('input',function(){
1362 savePlayerName(this.value);generateProfileCard();
1363});
1364document.getElementById('pf-dl').onclick=function(){
1365 const canvas=document.getElementById('pf-canvas');
1366 // fullscreen overlay with the card image
1367 const ov=document.createElement('div');
1368 ov.style.cssText='position:fixed;inset:0;z-index:9999;background:#000;display:flex;align-items:center;justify-content:center;cursor:pointer';
1369 const img=new Image();img.src=canvas.toDataURL('image/png');
1370 img.style.cssText='max-width:100%;max-height:100%;object-fit:contain';
1371 ov.appendChild(img);
1372 ov.onclick=()=>ov.remove();
1373 document.body.appendChild(ov);
1374};
1375
1376function generateProfileCard(){
1377 const cv=document.getElementById('pf-canvas');
1378 const ctx=cv.getContext('2d');
1379 const W=1080,H=1920;cv.width=W;cv.height=H;
1380 const name=document.getElementById('pf-name').value||'NO NAME';
1381 const st=S.stats||{};
1382 const F='"Helvetica Neue",Arial,sans-serif';
1383
1384 // === BG ===
1385 const bg=ctx.createRadialGradient(W/2,H*0.25,0,W/2,H*0.25,W);
1386 bg.addColorStop(0,'#1a1035');bg.addColorStop(0.6,'#0d0d1f');bg.addColorStop(1,'#050510');
1387 ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);
1388
1389 // subtle grid
1390 ctx.save();ctx.globalAlpha=0.03;ctx.strokeStyle='#fff';ctx.lineWidth=1;
1391 for(let i=0;i<W;i+=60){ctx.beginPath();ctx.moveTo(i,0);ctx.lineTo(i,H);ctx.stroke()}
1392 for(let i=0;i<H;i+=60){ctx.beginPath();ctx.moveTo(0,i);ctx.lineTo(W,i);ctx.stroke()}
1393 ctx.restore();
1394
1395 // accent glow behind image
1396 const glow=ctx.createRadialGradient(W/2,400,0,W/2,400,600);
1397 glow.addColorStop(0,'rgba(255,215,0,0.08)');glow.addColorStop(0.5,'rgba(255,107,53,0.03)');glow.addColorStop(1,'transparent');
1398 ctx.fillStyle=glow;ctx.fillRect(0,0,W,900);
1399
1400 // top bar
1401 const bar=ctx.createLinearGradient(0,0,W,0);
1402 bar.addColorStop(0,'#FFD700');bar.addColorStop(0.4,'#FF8C00');bar.addColorStop(1,'#FF1744');
1403 ctx.fillStyle=bar;ctx.fillRect(0,0,W,6);
1404
1405 // diagonal lines
1406 ctx.save();ctx.globalAlpha=0.04;ctx.strokeStyle='#FFD700';ctx.lineWidth=1;
1407 for(let i=-H;i<W+H;i+=80){ctx.beginPath();ctx.moveTo(i,0);ctx.lineTo(i+H,H);ctx.stroke()}
1408 ctx.restore();
1409
1410 // === GAME TITLE ===
1411 ctx.textAlign='center';ctx.fillStyle='rgba(255,255,255,0.3)';
1412 ctx.font=`600 26px ${F}`;
1413 const ttl='ARAKI EXTREME BASEBALL';
1414 let tx=W/2-ctx.measureText(ttl).width*0.65;
1415 for(let i=0;i<ttl.length;i++){ctx.fillText(ttl[i],tx,54);tx+=ctx.measureText(ttl[i]).width+7}
1416
1417 // === PROFILE IMAGE (big, rectangular) ===
1418 let cy=80;
1419 if(imgs.profile){
1420  const img=imgs.profile;
1421  const iw=img.naturalWidth,ih=img.naturalHeight;
1422  const maxW=W-120,maxH=750;
1423  const scale=Math.min(maxW/iw,maxH/ih);
1424  const dw=iw*scale,dh=ih*scale;
1425  const ix=W/2-dw/2,iy=cy;
1426  // subtle shadow behind image
1427  ctx.shadowColor='rgba(0,0,0,0.5)';ctx.shadowBlur=40;ctx.shadowOffsetY=10;
1428  ctx.drawImage(img,ix,iy,dw,dh);
1429  ctx.shadowBlur=0;ctx.shadowOffsetY=0;
1430  // thin gold border
1431  ctx.strokeStyle='rgba(255,215,0,0.2)';ctx.lineWidth=2;
1432  ctx.strokeRect(ix-1,iy-1,dw+2,dh+2);
1433  cy=iy+dh+50;
1434 }
1435
1436 // === PLAYER NAME ===
1437 ctx.textAlign='center';
1438 ctx.shadowColor='rgba(255,215,0,0.5)';ctx.shadowBlur=50;
1439 const nameGrad=ctx.createLinearGradient(W*0.15,0,W*0.85,0);
1440 nameGrad.addColorStop(0,'#FFD700');nameGrad.addColorStop(0.5,'#FFF');nameGrad.addColorStop(1,'#FFD700');
1441 ctx.fillStyle=nameGrad;
1442 let nameSize=100;ctx.font=`900 ${nameSize}px ${F}`;
1443 let nm=name;while(ctx.measureText(nm).width>W-120&&nameSize>40){nameSize-=3;ctx.font=`900 ${nameSize}px ${F}`}
1444 ctx.fillText(nm,W/2,cy+nameSize*0.8);
1445 ctx.shadowBlur=0;
1446 cy+=nameSize+50;
1447
1448 // === DIVIDER ===
1449 const dv=ctx.createLinearGradient(80,0,W-80,0);
1450 dv.addColorStop(0,'transparent');dv.addColorStop(0.3,'rgba(255,215,0,0.3)');
1451 dv.addColorStop(0.7,'rgba(255,215,0,0.3)');dv.addColorStop(1,'transparent');
1452 ctx.fillStyle=dv;ctx.fillRect(80,cy,W-160,2);
1453 cy+=60;
1454
1455 // === STAT CARDS (2x2) ===
1456 const stats=[
1457  {label:'BEST SCORE',value:fmtS(S.best||0),color:'#FFD700'},
1458  {label:'GAMES',value:fmt(st.games||0),color:'#3B82F6'},
1459  {label:'HOME RUNS',value:fmt(st.totalHR||0),color:'#FF6B35'},
1460  {label:'HIT RATE',value:st.totalBalls>0?((st.totalHits||0)/st.totalBalls*100).toFixed(1)+'%':'--',color:'#4ADE80'},
1461 ];
1462 const cardW=440,cardH=170,gap=36;
1463 const gridX=(W-cardW*2-gap)/2;
1464 const gridY=cy;
1465 stats.forEach((s,i)=>{
1466  const col=i%2,row=Math.floor(i/2);
1467  const cx=gridX+col*(cardW+gap),ccy=gridY+row*(cardH+gap);
1468  // card bg
1469  ctx.fillStyle='rgba(255,255,255,0.03)';
1470  roundRect(ctx,cx,ccy,cardW,cardH,16);ctx.fill();
1471  // left accent
1472  const accG=ctx.createLinearGradient(cx,ccy,cx,ccy+cardH);
1473  accG.addColorStop(0,s.color);accG.addColorStop(1,'transparent');
1474  ctx.fillStyle=accG;roundRect(ctx,cx,ccy,5,cardH,2);ctx.fill();
1475  // border
1476  ctx.strokeStyle='rgba(255,255,255,0.06)';ctx.lineWidth=1;
1477  roundRect(ctx,cx,ccy,cardW,cardH,16);ctx.stroke();
1478  // label
1479  ctx.textAlign='left';ctx.fillStyle='rgba(255,255,255,0.45)';
1480  ctx.font=`700 22px ${F}`;ctx.fillText(s.label,cx+30,ccy+48);
1481  // value
1482  ctx.fillStyle=s.color;
1483  let valSize=64;ctx.font=`900 ${valSize}px ${F}`;
1484  if(ctx.measureText(s.value).width>cardW-60){valSize=48;ctx.font=`900 ${valSize}px ${F}`}
1485  ctx.fillText(s.value,cx+30,ccy+125);
1486 });
1487 cy=gridY+cardH*2+gap+50;
1488
1489 // === DIVIDER 2 ===
1490 const dv2=ctx.createLinearGradient(80,0,W-80,0);
1491 dv2.addColorStop(0,'transparent');dv2.addColorStop(0.3,'rgba(255,255,255,0.12)');
1492 dv2.addColorStop(0.7,'rgba(255,255,255,0.12)');dv2.addColorStop(1,'transparent');
1493 ctx.fillStyle=dv2;ctx.fillRect(80,cy,W-160,1);
1494 cy+=45;
1495
1496 // === SPECIAL BADGES ===
1497 const badges=[];
1498 if(st.perfectGame)badges.push({t:'PERFECT GAME',c:'#FFD700'});
1499 if(st.chaosMaxPerfect)badges.push({t:'DEMON SLAYER',c:'#FF1744'});
1500 if(st.ghostPerfect)badges.push({t:'GHOST SLAYER',c:'#E040FB'});
1501 if(st.rainbowHit)badges.push({t:'RAINBOW',c:'#FF69B4'});
1502 if(st.fireHit)badges.push({t:'INFERNO',c:'#FF4500'});
1503 if(badges.length>0){
1504  ctx.font=`800 22px ${F}`;
1505  let totalBW=0;badges.forEach(b=>{totalBW+=ctx.measureText(b.t).width+44});
1506  totalBW+=(badges.length-1)*14;
1507  let bx=W/2-totalBW/2;
1508  badges.forEach(b=>{
1509   const tw=ctx.measureText(b.t).width+44;
1510   ctx.fillStyle='rgba(0,0,0,0.4)';roundRect(ctx,bx,cy-20,tw,40,20);ctx.fill();
1511   ctx.strokeStyle=b.c;ctx.lineWidth=2;ctx.globalAlpha=0.7;
1512   roundRect(ctx,bx,cy-20,tw,40,20);ctx.stroke();ctx.globalAlpha=1;
1513   ctx.fillStyle=b.c;ctx.textAlign='center';ctx.fillText(b.t,bx+tw/2,cy+6);
1514   bx+=tw+14;
1515  });
1516 }
1517
1518 // === BOTTOM BAR ===
1519 const btm=ctx.createLinearGradient(0,0,W,0);
1520 btm.addColorStop(0,'#FFD700');btm.addColorStop(0.4,'#FF8C00');btm.addColorStop(1,'#FF1744');
1521 ctx.fillStyle=btm;ctx.fillRect(0,H-6,W,6);
1522
1523 // === FOOTER ===
1524 ctx.textAlign='center';ctx.fillStyle='rgba(255,255,255,0.15)';
1525 ctx.font=`600 18px ${F}`;
1526 ctx.fillText('ARAKI EXTREME BASEBALL   #ARAKIbaseball',W/2,H-22);
1527
1528 // corner accents
1529 ctx.save();ctx.globalAlpha=0.08;ctx.strokeStyle='#FFD700';ctx.lineWidth=2;
1530 ctx.beginPath();ctx.moveTo(24,60);ctx.lineTo(24,24);ctx.lineTo(60,24);ctx.stroke();
1531 ctx.beginPath();ctx.moveTo(W-60,24);ctx.lineTo(W-24,24);ctx.lineTo(W-24,60);ctx.stroke();
1532 ctx.beginPath();ctx.moveTo(24,H-60);ctx.lineTo(24,H-24);ctx.lineTo(60,H-24);ctx.stroke();
1533 ctx.beginPath();ctx.moveTo(W-60,H-24);ctx.lineTo(W-24,H-24);ctx.lineTo(W-24,H-60);ctx.stroke();
1534 ctx.restore();
1535}
1536
1537// roundRect helper
1538function roundRect(ctx,x,y,w,h,r){
1539 ctx.beginPath();ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);
1540 ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
1541 ctx.lineTo(x+r,y+h);ctx.quadraticCurveTo(x,y+h,x,y+h-r);
1542 ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath();
1543}
1544
1545// ============ Reset ============
1546function doReset(){
1547 localStorage.removeItem('aeb_save');
1548 S=ld();sv(S);
1549 document.getElementById('confirm').style.display='none';
1550 showTitle();document.getElementById('title').style.display='flex';
1551}
1552document.getElementById('cf-yes').onclick=doReset;
1553document.getElementById('cf-no').onclick=()=>{document.getElementById('confirm').style.display='none'};
1554
1555// ============ Title ============
1556function showTitle(){
1557 S=ld();playBgm(bgmOp);
1558 // マイルストーン表示
1559 const ms=getMilestone(S);const nextMs=getNextMilestone(S);
1560 const msEl=document.getElementById('t-ms');
1561 if(ms){
1562  msEl.innerHTML=`<span class="ms-icon">${ms.icon}</span> <span style="color:${ms.color};font-weight:800">${ms.title}</span>`;
1563  if(nextMs)msEl.innerHTML+=`<span style="color:rgba(255,255,255,.4);margin:0 .3rem">→</span><span style="color:rgba(255,255,255,.5)">${nextMs.icon}${nextMs.title} (Lv${nextMs.lv})</span>`;
1564 }else if(nextMs){
1565  msEl.innerHTML=`<span style="color:rgba(255,255,255,.5)">次の称号: ${nextMs.icon}${nextMs.title} (Lv${nextMs.lv})</span>`;
1566 }else{msEl.innerHTML=''}
1567 document.getElementById('t-best').innerHTML=S.best>0?`🏆 <span style="color:#FFD700">${fmtS(S.best)}</span>`:'';
1568 document.getElementById('t-pts').innerHTML=`💰 <span style="color:#FFD700">${fmtS(S.pts)}</span> pts`;
1569 // ボール一覧ボタン(お戯れモード解放後)
1570 document.getElementById('btn-t-balls').style.display=(S.skills.otawamure||0)>=1?'':'none';
1571 // プロフィールボタン(スキル解放後)
1572 document.getElementById('btn-t-profile').style.display=(S.skills.profile_card||0)>=1?'':'none';
1573}
1574
1575// ============ Buttons ============
1576// 全ボタンにクリック音
1577document.addEventListener('click',e=>{if(e.target.closest('.btn,.tr-btn,.snd-btn,.tr-tab'))snd('buy')});
1578document.getElementById('btn-play').onclick=startGame;
1579document.getElementById('btn-retry').onclick=startGame;
1580document.getElementById('btn-t-tree').onclick=()=>{document.getElementById('title').style.display='none';openTree('title')};
1581document.getElementById('btn-t-ach').onclick=()=>{document.getElementById('title').style.display='none';openAch('title')};
1582document.getElementById('btn-t-status').onclick=()=>{document.getElementById('title').style.display='none';openStatus()};
1583document.getElementById('btn-t-balls').onclick=()=>{openBallList()};
1584document.getElementById('btn-t-profile').onclick=()=>{openProfile()};
1585document.getElementById('btn-t-sound').onclick=()=>{ea();document.getElementById('sound-settings').style.display='flex'};
1586document.getElementById('btn-t-reset').onclick=()=>{document.getElementById('confirm').style.display='flex'};
1587document.getElementById('btn-r-tree').onclick=()=>{document.getElementById('result').style.display='none';openTree('result')};
1588document.getElementById('btn-r-top').onclick=()=>{document.getElementById('result').style.display='none';showTitle();document.getElementById('title').style.display='flex'};
1589// ③ プレイ中断ボタン - 結果破棄してTOPへ
1590document.getElementById('btn-quit').onclick=()=>{
1591 st='finished';document.getElementById('hud').style.display='none';
1592 document.getElementById('ch-banner').style.display='none';
1593 playBgm(bgmOp);showTitle();document.getElementById('title').style.display='flex';
1594};
1595document.getElementById('tr-back').onclick=closeTree;
1596
1597// ============ Prevent text selection on rapid tap ============
1598document.addEventListener('selectstart',e=>e.preventDefault());
1599document.addEventListener('contextmenu',e=>e.preventDefault());
1600
1601// ============ Init ============
1602updSpot();showTitle();requestAnimationFrame(upd);
1603})();
1604</script>
1604
1605</div>
1606</body>
1607</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.