完成しましたこちらからdownloadできます!
カテゴリー: ゲーム開発
ToHeart3 – 未来の約束
『ToHeart3 – 未来の約束』完成しました。
『ToHeart』『ToHeart2』の世界とキャラクターたちを受け継ぎながら、新たな主人公、新たなヒロイン、そして新しい物語を描く長編ノベルゲームです。
テーマは、「思い出と未来、そしてもう一度交わす約束」。
懐かしい日常の中で再会する人たち。
新しく出会う仲間たち。
変わってしまったものと、それでも変わらなかった想い。
物語には『ToHeart』『ToHeart2』の歴代キャラクターが登場し、神岸あかり、柚原このみをはじめとしたキャラクターたちと、新たに生まれたオリジナルキャラクターが同じ世界で物語を紡いでいきます。
プレイヤーの選択によってキャラクターとの関係や物語が変化し、それぞれに長編の個別ルートを用意。
楽しい学園生活だけではなく、恋愛、友情、家族、別れ、再会、そして未来へ進んでいくことを描いた作品になっています。
主な特徴
- 『ToHeart』『ToHeart2』歴代キャラクターが登場
- 本作オリジナルキャラクターを追加
- キャラクターごとの立ち絵を実装
- 長編個別ルートを収録
- 選択肢によって変化するストーリー
- 複数のエンディング
- 日常・恋愛・青春・感動を中心とした物語
- 過去作品を知っている人が楽しめる再会やクロスオーバー要素
- 初めてこの世界に触れる人でも楽しめる新規ストーリー
昔、大切だった人。
いつの間にか離れてしまった人。
まだ出会っていない人。
そして、忘れたくない約束。
時間が流れても、人の想いは完全には消えない。
これは、過去を懐かしむだけの物語ではありません。
あの日の思い出を抱えたまま、それでも未来へ進んでいく人たちの物語です。
――いつか交わした約束の、その先へ。
『ToHeart3 – 未来の約束』
ここから、新しい物語が始まります。
アンリアルエンジン6の新機能
大きな新要素はこのあたりです。
- Verseが本格的なゲームプログラミング言語になる
- UE6ではゲームプレイのプログラミングモデルをVerseへ移行。
- PythonやC#に近い感覚を意識した言語です。
- C++とも連携します。
- 将来的には巨大なオンラインゲームを複数サーバーへ自動分散する構想まであります。
- Scene Graphという新しいゲームプレイフレームワーク
- Verseベースでゼロから設計。
- オブジェクトやゲーム機能を「再利用できるコンポーネント」として扱いやすくする方向。
- EpicはこれをUE6の新しいゲーム制作基盤にしようとしています。
- UE5とUEFNが統合される
- これがかなり大きいです。
- UE5のAAAゲーム制作能力+Fortnite/UEFNで培ったライブゲーム開発環境を、1つのUnreal Engine 6にまとめます。
- 普通のPC/PS5/Xbox向けゲームとして出すことも、Fortnite向けに出すことも、自前のオンライン環境に出すことも想定されています。
- AI開発支援がエンジン側に入る
- MCP(Model Context Protocol)対応。
- EpicはClaude、Geminiなどとの統合を明言しています。
- コード生成だけではなく、制作パイプラインそのものをAIで効率化する方向です。
- ゲーム間でコンテンツやコードを持ち運べる方向へ
- glTFやUSDなどのオープン標準を重視。
- コンテンツ・コード・ゲーム内経済などを、ゲームやエンジンをまたいで相互運用できる仕組みを目指しています。
そして気になるBlueprintですが、ここはかなり重要です。
UE6初期版ではActorとBlueprintは残ります。
いきなりBlueprintが消えるわけではありません。ただしEpicは、Scene Graph+Verseが十分成熟した段階で、Actor/Blueprintを最終的には非推奨にする計画を明言しています。その際は既存プロジェクトを新方式へ移す変換ツールも用意する方針です。
なので今Blueprintを勉強していることが無駄になるわけではありません。UE5プロジェクトについてもEpicはUE6へ「ハードな断絶を強制しない」方針です。
リリース予定
UE6 Early Access:2027年末予定
その後、
正式版:Early Accessから12〜18か月後
なので、順調なら正式版は2028年末〜2029年前半あたりになります。現在のUE5.8は「予定上最後のUE5メジャーリリース」で、必要なら5.9を出す可能性も残されています。
ゲーム開発という観点では、UE5:Blueprint中心 → UE6:Verse+Scene Graph+AI支援中心という移行が一番大きそうです。特に一人開発ではAI統合の部分がかなり面白いところです。
star_breaker_stg_stage5_power_barrage.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>STAR BREAKER - 5 STAGE STG</title>
<style>
:root{--cyan:#55efff;--pink:#ff4fd8;--yellow:#ffe66d;--bg:#02040e}
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#02040e;color:#fff;font-family:system-ui,-apple-system,"Yu Gothic UI",sans-serif}
body{display:grid;place-items:center;background:radial-gradient(circle at 50% 20%,#17265e,#050817 50%,#010208)}
#wrap{position:relative;width:min(100vw,900px);height:min(100vh,960px);aspect-ratio:3/4;overflow:hidden;border:1px solid #55efff55;box-shadow:0 0 36px #55efff22;background:#02040e}
canvas{display:block;width:100%;height:100%;touch-action:none;cursor:crosshair}
#hud{position:absolute;inset:0;pointer-events:none;font-weight:900;text-shadow:0 0 8px #55efff}
.top{position:absolute;top:12px;left:16px;right:16px;display:flex;justify-content:space-between;gap:12px;font-size:clamp(12px,2vw,19px)}
#stageLabel{position:absolute;top:45px;left:0;right:0;text-align:center;color:#bff8ff;letter-spacing:.22em}
#bossWrap{display:none;position:absolute;top:73px;left:9%;width:82%}
#bossName{text-align:center;font-size:13px;margin-bottom:5px;color:#ffd4f5}
.bar{height:13px;border:1px solid #ffffffaa;background:#ffffff12}
#bossHp{height:100%;width:100%;background:linear-gradient(90deg,#ff4fd8,#ff9a3d)}
.overlay{position:absolute;inset:0;display:grid;place-items:center;background:linear-gradient(#02040e33,#02040ecc);z-index:5}
.panel{width:min(88%,650px);padding:clamp(22px,5vw,44px);text-align:center;background:#081027e8;border:1px solid #55efff88;box-shadow:0 0 28px #55efff33;backdrop-filter:blur(7px)}
h1{margin:0 0 10px;font-size:clamp(38px,8vw,76px);line-height:.9;color:#fff;text-shadow:0 0 9px #fff,0 0 22px #55efff,4px 4px 0 #ff4fd8}
.sub{color:#8deeff;letter-spacing:.25em;margin-bottom:22px;font-weight:800}
#message{line-height:1.8;color:#ddebff}
button{margin:20px 7px 0;padding:14px 28px;border:0;border-radius:999px;color:#fff;font-weight:900;font-size:17px;cursor:pointer;background:linear-gradient(90deg,#55efff,#7277ff,#ff4fd8);box-shadow:0 0 18px #55efff66}
button.secondary{background:#252c46;box-shadow:none;border:1px solid #ffffff44}
button:hover{filter:brightness(1.15);transform:scale(1.03)}
.small{margin-top:18px;padding-top:14px;border-top:1px solid #ffffff22;color:#b7c9dd;font-size:13px;line-height:1.7}
#mobile{position:absolute;left:0;right:0;bottom:12px;display:none;justify-content:space-between;padding:0 14px;pointer-events:none;z-index:4}
.group{display:flex;gap:8px;pointer-events:auto}.touch{width:58px;height:58px;margin:0;padding:0;border-radius:50%;opacity:.62;font-size:20px}
@media(pointer:coarse){#mobile{display:flex}}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="720" height="960"></canvas>
<div id="hud">
<div class="top">
<div>SCORE <span id="score">0000000</span></div>
<div>HI <span id="high">0000000</span></div>
<div>POWER <span id="power">1</span></div>
</div>
<div id="stageLabel">STAGE 1 / 5</div>
<div id="bossWrap">
<div id="bossName">BOSS</div>
<div class="bar"><div id="bossHp"></div></div>
</div>
</div>
<div id="overlay" class="overlay">
<div class="panel">
<h1 id="title">STAR<br>BREAKER</h1>
<div class="sub" id="subtitle">5 STAGE MISSION</div>
<div id="message">全5ステージを突破し、最終要塞を破壊せよ。<br>敵弾に一発でも当たると爆発してゲームオーバー。<br>POWERアイテムでショットを最大5段階まで強化。</div>
<div id="buttons">
<button id="startBtn">GAME START</button>
</div>
<div class="small">PC:WASD / 矢印キー / マウスドラッグで移動 | P:一時停止<br>スマホ:画面ドラッグ、または下の移動ボタン</div>
</div>
</div>
<div id="mobile">
<div class="group"><button class="touch" data-key="left">◀</button><button class="touch" data-key="right">▶</button></div>
<div class="group"><button class="touch" data-key="up">▲</button><button class="touch" data-key="down">▼</button></div>
</div>
</div>
<script>
(()=>{
"use strict";
const canvas=document.getElementById("game"),ctx=canvas.getContext("2d");
const W=720,H=960;
const scoreEl=document.getElementById("score"),highEl=document.getElementById("high"),powerEl=document.getElementById("power");
const stageLabel=document.getElementById("stageLabel"),bossWrap=document.getElementById("bossWrap"),bossName=document.getElementById("bossName"),bossHp=document.getElementById("bossHp");
const overlay=document.getElementById("overlay"),title=document.getElementById("title"),subtitle=document.getElementById("subtitle"),message=document.getElementById("message"),buttons=document.getElementById("buttons");
let running=false,paused=false,dead=false,last=0,time=0,score=0,high=Number(localStorage.getItem("starBreaker5High")||0);
let stage=1,stageTime=0,waveTimer=0,wavesSpawned=0,bossActive=false,transition=false,shake=0,flash=0;
let pointer=false;
const keys={},bullets=[],enemyBullets=[],enemies=[],particles=[],powerups=[],stars=[];
const stageData=[
{name:"ORBITAL FRONT",duration:23,boss:"IRON TALON",hp:260,bg:["#07112c","#02040e"]},
{name:"CRIMSON NEBULA",duration:25,boss:"SCARLET MANTA",hp:380,bg:["#2b071c","#07030b"]},
{name:"ASTEROID BELT",duration:27,boss:"GRAVITY GOLEM",hp:520,bg:["#241b10","#050403"]},
{name:"VOID TEMPLE",duration:29,boss:"ABYSS SERAPH",hp:700,bg:["#16072a","#030109"]},
{name:"FINAL FORTRESS",duration:32,boss:"OMEGA EMPRESS",hp:980,bg:["#051b26","#010408"]}
];
const player={x:W/2,y:H-120,r:11,speed:410,cool:0,rate:.12,power:1,maxPower:5,inv:0,tilt:0};
for(let i=0;i<150;i++)stars.push({x:Math.random()*W,y:Math.random()*H,z:Math.random()*2.5+.5,a:Math.random()*.8+.2});
function clearAll(){bullets.length=enemyBullets.length=enemies.length=particles.length=powerups.length=0}
function updateHUD(){
scoreEl.textContent=String(Math.floor(score)).padStart(7,"0");
highEl.textContent=String(Math.floor(high)).padStart(7,"0");
powerEl.textContent=player.power>=player.maxPower?"MAX":player.power;
stageLabel.textContent=`STAGE ${stage} / 5 ${stageData[stage-1].name}`;
}
function reset(){
clearAll();stage=1;stageTime=0;waveTimer=0;wavesSpawned=0;bossActive=false;transition=false;dead=false;paused=false;
score=0;time=0;shake=0;flash=0;player.x=W/2;player.y=H-120;player.cool=0;player.power=1;player.inv=1;
bossWrap.style.display="none";updateHUD();
}
function begin(){
reset();running=true;overlay.style.display="none";last=performance.now();requestAnimationFrame(loop);
}
function addScore(n){
score+=n;if(score>high){high=score;localStorage.setItem("starBreaker5High",String(Math.floor(high)))}updateHUD();
}
function burst(x,y,color,count=25,speed=260){
for(let i=0;i<count;i++){const a=Math.random()*Math.PI*2,s=Math.random()*speed+40;
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:Math.random()*.7+.3,max:1,size:Math.random()*6+2,color});
}}
function shoot(){
let shots=[];
if(player.power===1)shots=[{a:0,x:0,d:1}];
if(player.power===2)shots=[{a:-.10,x:-7,d:1},{a:.10,x:7,d:1}];
if(player.power===3)shots=[{a:-.18,x:-10,d:1},{a:0,x:0,d:2},{a:.18,x:10,d:1}];
if(player.power===4)shots=[{a:-.25,x:-15,d:2},{a:-.08,x:-5,d:2},{a:.08,x:5,d:2},{a:.25,x:15,d:2}];
if(player.power>=5)shots=[
{a:-.31,x:-18,d:2},{a:-.16,x:-10,d:2},{a:0,x:0,d:3},
{a:.16,x:10,d:2},{a:.31,x:18,d:2}
];
shots.forEach(s=>bullets.push({
x:player.x+s.x,y:player.y-20,
vx:Math.sin(s.a)*230,vy:-820,
r:s.d>=3?7:5,dmg:s.d,power:player.power
}));
player.rate=Math.max(.065,.13-player.power*.013);
}
function spawnEnemy(type,x,y){
const s=stage;
if(type==="scout")enemies.push({type,x,y,r:19,hp:1+s,maxHp:1+s,vx:(Math.random()-.5)*130,vy:105+s*11,age:0,shot:1.2+Math.random()});
if(type==="zig")enemies.push({type,x,y,r:22,hp:3+s,maxHp:3+s,vx:0,vy:80+s*7,age:Math.random()*5,shot:.9+Math.random()});
if(type==="tank")enemies.push({type,x,y,r:32,hp:11+s*4,maxHp:11+s*4,vx:0,vy:42+s*3,age:0,shot:.7});
}
function spawnWave(){
wavesSpawned++;
const pattern=(wavesSpawned+stage)%4;
if(pattern===0){for(let i=0;i<7;i++)spawnEnemy("scout",75+i*95,-40-i*34)}
if(pattern===1){for(let i=0;i<6;i++)spawnEnemy("zig",90+i*108,-45-i*70)}
if(pattern===2){spawnEnemy("tank",W/2,-70);for(let i=0;i<4;i++)spawnEnemy("scout",120+i*160,-160-i*40)}
if(pattern===3){for(let i=0;i<3;i++){spawnEnemy("zig",170+i*190,-60-i*80);spawnEnemy("scout",90+i*250,-190-i*50)}}
}
function spawnBoss(){
bossActive=true;enemyBullets.length=0;
const d=stageData[stage-1];
enemies.push({type:"boss",x:W/2,y:-130,r:82,hp:d.hp,maxHp:d.hp,age:0,shot:.6,stage,vy:48});
bossName.textContent=d.boss;bossHp.style.width="100%";bossWrap.style.display="block";
}
function addEnemyBullet(x,y,a,speed,r=7,color="#ff8ae7"){
enemyBullets.push({x,y,vx:Math.cos(a)*speed,vy:Math.sin(a)*speed,r,color});
}
function enemyShoot(e){
const aim=Math.atan2(player.y-e.y,player.x-e.x);
const base=145+stage*18;
if(e.type==="scout"){
if(stage<=2){
for(let i=-1;i<=1;i++)addEnemyBullet(e.x,e.y,aim+i*.12,base,6,"#ff8ae7");
}else{
for(let i=-2;i<=2;i++)addEnemyBullet(e.x,e.y,aim+i*.13,base+12,6,"#ff8ae7");
}
}
if(e.type==="zig"){
const count=5+stage;
for(let i=0;i<count;i++){
const a=(i/(count-1)-.5)*1.15+Math.PI/2;
addEnemyBullet(e.x,e.y,a,145+stage*12,6,"#9d8cff");
}
addEnemyBullet(e.x,e.y,aim,210+stage*8,5,"#ffffff");
}
if(e.type==="tank"){
const n=10+stage*2;
for(let i=0;i<n;i++){
const a=i*Math.PI*2/n+e.age*.32;
addEnemyBullet(e.x,e.y,a,125+stage*10,7,"#ffb25f");
}
for(let i=-2;i<=2;i++)addEnemyBullet(e.x,e.y,aim+i*.15,190+stage*10,6,"#fff0a8");
}
if(e.type==="boss"){
const ratio=e.hp/e.maxHp;
if(e.stage===1){
for(let ring=0;ring<2;ring++){
for(let i=-5;i<=5;i++){
const a=aim+i*.12+(ring?0.055:0);
addEnemyBullet(e.x,e.y+35,a,195+ring*45,7,ring?"#ffd1f4":"#ff78d8");
}
}
}
if(e.stage===2){
const n=22;
for(let i=0;i<n;i++){
const a=i*Math.PI*2/n+e.age*.62;
addEnemyBullet(e.x,e.y,a,155+(i%2)*42,7,i%2?"#ff7ac8":"#a884ff");
}
for(let i=-2;i<=2;i++)addEnemyBullet(e.x,e.y+25,aim+i*.13,235,6,"#ffffff");
}
if(e.stage===3){
for(let ring=0;ring<3;ring++){
const n=12+ring*4;
for(let i=0;i<n;i++){
const a=i*Math.PI*2/n+e.age*(ring%2?-.42:.42);
addEnemyBullet(e.x,e.y,a,115+ring*42,8-ring,"#ffb35f");
}
}
}
if(e.stage===4){
for(let side=-1;side<=1;side+=2){
for(let i=0;i<9;i++){
const a=aim+side*(.08+i*.085)+Math.sin(e.age*1.8)*.18;
addEnemyBullet(e.x+side*45,e.y+20,a,205+i*6,6,"#b477ff");
}
}
if(ratio<.5){
for(let i=0;i<18;i++){
const a=i*Math.PI/9-e.age*.55;
addEnemyBullet(e.x,e.y,a,175,7,"#ff70dd");
}
}
}
if(e.stage===5){
const n=ratio>.6?28:38;
for(let i=0;i<n;i++){
const a=i*Math.PI*2/n+e.age*.78;
addEnemyBullet(e.x,e.y,a,150+(i%4)*28,7,i%2?"#ff4fd8":"#65eaff");
}
for(let i=-5;i<=5;i++)addEnemyBullet(e.x,e.y+42,aim+i*.095,250,6,"#fff2a8");
if(ratio<.45){
for(let i=0;i<20;i++){
const a=i*Math.PI/10-e.age*1.05;
addEnemyBullet(e.x,e.y,a,225,5,"#ffffff");
}
}
}
}
}
function collide(a,b){const dx=a.x-b.x,dy=a.y-b.y;return dx*dx+dy*dy<(a.r+b.r)*(a.r+b.r)}
function gameOver(){
if(dead)return;dead=true;running=false;shake=34;flash=.75;
burst(player.x,player.y,"#ff4fd8",120,430);burst(player.x,player.y,"#ffe66d",80,360);
draw();
setTimeout(()=>{
title.innerHTML="GAME<br>OVER";subtitle.textContent="SHIP DESTROYED";
message.innerHTML=`敵弾に命中し、機体が爆発した。<br>SCORE:<b>${Math.floor(score).toLocaleString()}</b> STAGE:<b>${stage}</b>`;
buttons.innerHTML='<button id="retryBtn">リトライ</button><button id="quitBtn" class="secondary">やめる</button>';
overlay.style.display="grid";
document.getElementById("retryBtn").addEventListener("click",begin);
document.getElementById("quitBtn").addEventListener("click",quitGame);
},650);
}
function quitGame(){
clearAll();dead=false;bossWrap.style.display="none";
title.innerHTML="STAR<br>BREAKER";subtitle.textContent="MISSION ABORTED";
message.innerHTML="ゲームを終了しました。<br>もう一度遊ぶときはスタートを押してください。";
buttons.innerHTML='<button id="againBtn">GAME START</button>';
document.getElementById("againBtn").addEventListener("click",begin);
draw();
}
function stageClear(){
transition=true;bossActive=false;bossWrap.style.display="none";enemyBullets.length=0;clearAll();
addScore(2500*stage);
if(stage===5){running=false;title.innerHTML="ALL<br>CLEAR";subtitle.textContent="OMEGA FORTRESS DESTROYED";
message.innerHTML=`全5ステージ制覇!<br>FINAL SCORE:<b>${Math.floor(score).toLocaleString()}</b>`;
buttons.innerHTML='<button id="retryAll">もう一度遊ぶ</button><button id="quitAll" class="secondary">やめる</button>';
overlay.style.display="grid";
document.getElementById("retryAll").addEventListener("click",begin);
document.getElementById("quitAll").addEventListener("click",quitGame);
return;
}
running=false;title.innerHTML=`STAGE ${stage}<br>CLEAR`;subtitle.textContent=stageData[stage-1].boss+" DESTROYED";
message.innerHTML=`次は STAGE ${stage+1}「${stageData[stage].name}」`;
buttons.innerHTML='<button id="nextStage">NEXT STAGE</button>';
overlay.style.display="grid";
document.getElementById("nextStage").addEventListener("click",()=>{
stage++;stageTime=0;waveTimer=0;wavesSpawned=0;transition=false;player.x=W/2;player.y=H-120;player.inv=1.5;
updateHUD();overlay.style.display="none";running=true;last=performance.now();requestAnimationFrame(loop);
});
}
function update(dt){
time+=dt;stageTime+=dt;if(player.inv>0)player.inv-=dt;if(shake>0)shake=Math.max(0,shake-dt*55);if(flash>0)flash=Math.max(0,flash-dt*1.7);
stars.forEach(s=>{s.y+=(55+s.z*65+stage*8)*dt;if(s.y>H){s.y=-5;s.x=Math.random()*W}});
let dx=0,dy=0;if(keys.ArrowLeft||keys.a||keys.left)dx--;if(keys.ArrowRight||keys.d||keys.right)dx++;if(keys.ArrowUp||keys.w||keys.up)dy--;if(keys.ArrowDown||keys.s||keys.down)dy++;
const l=Math.hypot(dx,dy)||1;dx/=l;dy/=l;player.x+=dx*player.speed*dt;player.y+=dy*player.speed*dt;player.x=Math.max(25,Math.min(W-25,player.x));player.y=Math.max(95,Math.min(H-35,player.y));player.tilt+=(dx*.45-player.tilt)*Math.min(1,dt*10);
player.cool-=dt;if(player.cool<=0){shoot();player.cool=player.rate}
if(!bossActive){
waveTimer-=dt;if(waveTimer<=0){spawnWave();waveTimer=Math.max(2.5,4.6-stage*.28)}
if(stageTime>=stageData[stage-1].duration)spawnBoss();
}
for(let i=bullets.length-1;i>=0;i--){const b=bullets[i];b.x+=b.vx*dt;b.y+=b.vy*dt;if(b.y<-30||b.x<-30||b.x>W+30)bullets.splice(i,1)}
for(let i=enemyBullets.length-1;i>=0;i--){const b=enemyBullets[i];b.x+=b.vx*dt;b.y+=b.vy*dt;if(b.y>H+50||b.y<-60||b.x<-60||b.x>W+60){enemyBullets.splice(i,1);continue}if(player.inv<=0&&collide(b,player)){enemyBullets.splice(i,1);gameOver();return}}
for(let i=enemies.length-1;i>=0;i--){const e=enemies[i];e.age+=dt;
if(e.type==="scout"){e.x+=e.vx*dt;e.y+=e.vy*dt;if(e.x<30||e.x>W-30)e.vx*=-1}
if(e.type==="zig"){e.y+=e.vy*dt;e.x+=Math.sin(e.age*2.5)*125*dt}
if(e.type==="tank"){e.y+=e.vy*dt;e.x=W/2+Math.sin(e.age*.85)*220}
if(e.type==="boss"){if(e.y<165)e.y+=e.vy*dt;else e.x=W/2+Math.sin(e.age*(.55+e.stage*.06))*245;bossHp.style.width=Math.max(0,e.hp/e.maxHp*100)+"%"}
e.shot-=dt;if(e.shot<=0&&e.y>0){enemyShoot(e);e.shot=e.type==="boss"?Math.max(.22,.62-stage*.065):e.type==="tank"?1.08:e.type==="zig"?.92:1.12}
for(let j=bullets.length-1;j>=0;j--){const b=bullets[j];if(collide(b,e)){e.hp-=b.dmg;bullets.splice(j,1);burst(b.x,b.y,"#ffe66d",4,80);
if(e.hp<=0){const boss=e.type==="boss";burst(e.x,e.y,boss?"#ff4fd8":"#55efff",boss?130:28,boss?430:230);shake=boss?30:7;
addScore(boss?5000*stage:e.type==="tank"?550:e.type==="zig"?220:100);
if(!boss){
const dropRate=e.type==="tank"?.60:e.type==="zig"?.32:.20;
if(Math.random()<dropRate)powerups.push({x:e.x,y:e.y,r:15,vy:82,spin:0});
}
enemies.splice(i,1);if(boss){enemyBullets.length=0;setTimeout(stageClear,550)}break;
}}}
if(i<enemies.length&&e.y>H+100)enemies.splice(i,1);
else if(i<enemies.length&&player.inv<=0&&collide(e,player)){gameOver();return}
}
for(let i=powerups.length-1;i>=0;i--){const p=powerups[i];p.y+=p.vy*dt;p.spin+=dt*4;if(collide(p,player)){player.power=Math.min(player.maxPower,player.power+1);addScore(player.power>=player.maxPower?800:400);burst(p.x,p.y,"#ffe66d",20,190);powerups.splice(i,1);updateHUD()}else if(p.y>H+30)powerups.splice(i,1)}
for(let i=particles.length-1;i>=0;i--){const p=particles[i];p.x+=p.vx*dt;p.y+=p.vy*dt;p.vx*=.984;p.vy*=.984;p.life-=dt;if(p.life<=0)particles.splice(i,1)}
}
function drawShip(){
ctx.save();ctx.translate(player.x,player.y);ctx.rotate(player.tilt);if(player.inv>0&&Math.floor(player.inv*13)%2===0)ctx.globalAlpha=.3;
ctx.shadowBlur=22;ctx.shadowColor="#55efff";ctx.fillStyle="#e9fdff";ctx.beginPath();ctx.moveTo(0,-27);ctx.lineTo(18,19);ctx.lineTo(0,10);ctx.lineTo(-18,19);ctx.closePath();ctx.fill();
ctx.fillStyle="#ff4fd8";ctx.beginPath();ctx.moveTo(0,-13);ctx.lineTo(7,11);ctx.lineTo(-7,11);ctx.closePath();ctx.fill();
ctx.shadowColor="#ff9a3d";ctx.fillStyle="#fff";ctx.beginPath();ctx.moveTo(-7,15);ctx.lineTo(0,35+Math.random()*9);ctx.lineTo(7,15);ctx.closePath();ctx.fill();ctx.restore();
}
function drawEnemy(e){
ctx.save();ctx.translate(e.x,e.y);
if(e.type==="boss"){ctx.rotate(Math.sin(e.age*.8)*.07);ctx.shadowBlur=28;ctx.shadowColor="#ff4fd8";ctx.fillStyle=`hsl(${305-e.stage*18} 65% 22%)`;ctx.beginPath();ctx.moveTo(0,-72);ctx.lineTo(96,-18);ctx.lineTo(65,60);ctx.lineTo(0,36);ctx.lineTo(-65,60);ctx.lineTo(-96,-18);ctx.closePath();ctx.fill();ctx.strokeStyle="#ff76df";ctx.lineWidth=5;ctx.stroke();ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(0,-4,25,0,Math.PI*2);ctx.fill();ctx.fillStyle="#ff4fd8";ctx.beginPath();ctx.arc(0,-4,11,0,Math.PI*2);ctx.fill()}
else{const c=e.type==="tank"?"#ff9a3d":e.type==="zig"?"#a873ff":"#55efff";ctx.shadowBlur=16;ctx.shadowColor=c;ctx.fillStyle=c;if(e.type==="tank"){ctx.beginPath();ctx.roundRect(-29,-24,58,48,12);ctx.fill();ctx.fillStyle="#fff";ctx.fillRect(-8,-31,16,24)}else{ctx.rotate(Math.PI);ctx.beginPath();ctx.moveTo(0,-24);ctx.lineTo(22,18);ctx.lineTo(0,8);ctx.lineTo(-22,18);ctx.closePath();ctx.fill();ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(0,2,6,0,Math.PI*2);ctx.fill()}}
ctx.restore();
}
function draw(){
const bg=stageData[Math.max(0,stage-1)].bg,g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,bg[0]);g.addColorStop(1,bg[1]);ctx.fillStyle=g;ctx.fillRect(0,0,W,H);
ctx.save();if(shake>0)ctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);
stars.forEach(s=>{ctx.globalAlpha=s.a;ctx.fillStyle="#fff";ctx.fillRect(s.x,s.y,s.z,s.z*3)});ctx.globalAlpha=1;
for(const b of bullets){ctx.shadowBlur=15;ctx.shadowColor="#55efff";ctx.fillStyle="#efffff";ctx.fillRect(b.x-3,b.y-13,6,24)}ctx.shadowBlur=0;
for(const b of enemyBullets){ctx.shadowBlur=14;ctx.shadowColor=b.color||"#ff4fd8";ctx.fillStyle=b.color||"#ff8ae7";ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fill()}ctx.shadowBlur=0;
enemies.forEach(drawEnemy);
powerups.forEach(p=>{ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.spin);ctx.shadowBlur=18;ctx.shadowColor="#ffe66d";ctx.fillStyle="#ffe66d";ctx.beginPath();for(let i=0;i<8;i++){const a=i*Math.PI/4,r=i%2?7:15;ctx.lineTo(Math.cos(a)*r,Math.sin(a)*r)}ctx.closePath();ctx.fill();ctx.restore()});
particles.forEach(p=>{ctx.globalAlpha=Math.max(0,p.life/p.max);ctx.fillStyle=p.color;ctx.fillRect(p.x,p.y,p.size,p.size)});ctx.globalAlpha=1;
if(!dead)drawShip();
if(paused){ctx.fillStyle="#0009";ctx.fillRect(0,0,W,H);ctx.fillStyle="#fff";ctx.textAlign="center";ctx.font="bold 54px system-ui";ctx.fillText("PAUSE",W/2,H/2)}
if(flash>0){ctx.globalAlpha=flash;ctx.fillStyle="#fff";ctx.fillRect(0,0,W,H);ctx.globalAlpha=1}
ctx.restore();
}
function loop(now){if(!running)return;const dt=Math.min(.033,(now-last)/1000);last=now;if(!paused)update(dt);draw();if(running)requestAnimationFrame(loop)}
function setPointer(e){const r=canvas.getBoundingClientRect();player.x=(e.clientX-r.left)/r.width*W;player.y=(e.clientY-r.top)/r.height*H}
addEventListener("keydown",e=>{keys[e.key]=true;keys[e.key.toLowerCase()]=true;if(e.key.toLowerCase()==="p"&&running)paused=!paused;if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"," "].includes(e.key))e.preventDefault()});
addEventListener("keyup",e=>{keys[e.key]=false;keys[e.key.toLowerCase()]=false});
canvas.addEventListener("pointerdown",e=>{pointer=true;setPointer(e)});canvas.addEventListener("pointermove",e=>{if(pointer)setPointer(e)});addEventListener("pointerup",()=>pointer=false);
document.querySelectorAll(".touch").forEach(b=>{const k=b.dataset.key;b.addEventListener("pointerdown",e=>{e.preventDefault();keys[k]=true});b.addEventListener("pointerup",()=>keys[k]=false);b.addEventListener("pointercancel",()=>keys[k]=false)});
document.getElementById("startBtn").addEventListener("click",begin);
highEl.textContent=String(high).padStart(7,"0");updateHUD();draw();
})();
</script>
</body>
</html>
「Gジェネレーション クロスブースト」ゲーム企画書
「Gジェネレーション クロスブースト」ゲーム企画書
タイトル
SDガンダム ジージェネレーション クロスブースト
ジャンル
- ガンダム戦略シミュレーションRPG
- 育成 × クロスオーバー × リアルタイム演出
コンセプト
歴代ガンダム作品の世界を越え、
異なる時代・勢力・機体・パイロットが交差する――
「限界突破(クロスブースト)」 をテーマにした
新世代Gジェネレーション。
従来のターン制戦略に加え、
- ブースト覚醒
- 合体戦術
- リンク必殺技
- 巨大ボスレイド
- AI戦艦支援
などを搭載し、
“育成の気持ちよさ” と “戦闘演出の熱さ” を極限まで強化する。
世界観
宇宙世紀、アナザー、未来世紀、厄祭戦…
全ガンダム世界を繋ぐ謎の次元崩壊
「クロスブレイク」 が発生。
時空が混ざり合い、
- アムロとキラ
- 刹那とヒイロ
- バナージとシーブック
など、本来出会わない戦士たちが共闘する。
しかしその裏では、
全時代を支配しようとする
謎の存在「ゼロ・オリジン」が動き始めていた。
ゲームシステム
1. クロスブーストシステム
本作最大の特徴。
戦闘中にテンションMAXで発動。
機体性能が大幅上昇し、
- 攻撃回数増加
- MAP兵器強化
- EN消費軽減
- 演出変化
- 専用BGM
などが発生。
例
機動戦士ガンダムSEED DESTINY
- ストライクフリーダムガンダム
→ スーパードラグーン全展開
機動戦士ガンダム00
- ダブルオーライザー
→ TRANS-AM BURST発動
機動戦士ガンダムUC
- ユニコーンガンダム
→ 覚醒モード変化
2. クロスリンク必殺技
特定キャラ同士で超演出発動。
例
アムロ × カミーユ × ジュドー
「宇宙世紀の光」
キラ × 刹那
「未来を切り開く力」
ヒイロ × 刹那
「ゼロとイノベイター」
超高火力+特殊カットイン。
3. 超大型レイドボス
最大8部隊で挑む。
レイド候補
- デビルガンダム最終形態
- ネオ・ジオング
- ELSクアンタ暴走体
- ターンX完全覚醒
- グレートジオング
- オリジナル巨大MA
4. 戦艦AI支援システム
艦長によって能力が変化。
例
オットー艦長
- EN回復
- 命中支援
- UC系強化
ブライト艦長
- 全体士気アップ
- 回避率上昇
マリュー艦長
- SEED系強化
5. SSP・ULT進化システム
既存のSP化をさらに進化。
SSP
- 限界突破
- 新武装追加
- カットイン追加
ULT
最終覚醒段階。
- 機体外観変化
- 専用BGM
- EX必殺技
- 特殊台詞追加
ストーリーモード
Chapter 1
「崩壊する宇宙世紀」
Chapter 2
「アナザー世界侵食」
Chapter 3
「鉄血とSEEDの衝突」
Chapter 4
「ELS襲来」
Chapter 5
「∀の黒歴史」
Final
「全てを超えるガンダム」
オリジナル主人公
男主人公
レイン・クロス
- 次元適応能力を持つ青年
女主人公
セリア・ノヴァ
- AIと共鳴する少女
プレイヤー選択式。
オリジナル機体
クロスガンダム・ゼロ
特徴:
- 全作品技術融合
- フェーズ変形
- 光の翼
- GNドライブ
- サイコミュ
- 月光蝶
ULT形態:
「クロスガンダム・インフィニティ」
PvP・オンライン
マスターリーグ
- 全国ランキング
- AI防衛編成
ギルド戦
- 20vs20
ワールドレイド
全ユーザー共闘。
ガシャシステム
Ver.シリーズ
Ver.1
宇宙世紀
Ver.2
アナザー系
Ver.3
神話級限定
ハーフアニバーサリー案
目玉機体候補
- ターンエーガンダム
- ターンX
- V2アサルトバスターガンダム
- クロスボーン・ガンダムX1フルクロス
- ダブルオークアンタ
UIイメージ
ホーム画面
- 3D機体表示
- ライブ背景
- AIナビキャラ
編成画面
- ドラッグ編成
- タグ自動表示
- 相性可視化
戦闘画面
- フル3D演出
- カメラズーム
- 必殺技シネマ
BGM
- 原作曲アレンジ
- オーケストラ版
- 覚醒専用BGM
ターゲット層
- Gジェネファン
- ガンダムファン
- 育成ゲーム好き
- 戦略RPG好き
- ソシャゲユーザー
キャッチコピー
「限界を超えろ、全てのガンダム。」
「時代を越えた覚醒が始まる。」
「クロスしろ、運命を。」
Unity 2D Action Game development
STGGAME.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bullet Hell STG Game</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #1f2b5c, #070915 70%);
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, sans-serif;
color: white;
overflow: hidden;
}
.wrap { text-align: center; }
h1 {
margin: 0 0 10px;
font-size: 28px;
letter-spacing: 0.08em;
}
canvas {
background: #050816;
border: 3px solid #ffffff33;
border-radius: 16px;
box-shadow: 0 20px 80px #000a;
display: block;
}
.info {
margin-top: 10px;
color: #dce6ff;
font-size: 14px;
}
.panel {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
background: #0008;
border: 1px solid #fff2;
padding: 8px 16px;
border-radius: 999px;
backdrop-filter: blur(8px);
font-weight: 700;
}
</style>
</head>
<body>
<div class="panel">
<div>Score: <span id="score">0</span></div>
<div>HP: <span id="hp">5</span></div>
<div>Power: <span id="power">1</span></div>
</div>
<div class="wrap">
<h1>BULLET STORM</h1>
<canvas id="game" width="480" height="640"></canvas>
<div class="info">移動: WASD / 方向キー ショット: Space パワーアップを取ると弾が強化 リスタート: Enter</div>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const scoreEl = document.getElementById("score");
const hpEl = document.getElementById("hp");
const powerEl = document.getElementById("power");
const keys = {};
const player = {
x: canvas.width / 2,
y: canvas.height - 70,
w: 34,
h: 42,
speed: 5,
hp: 5,
power: 1,
shotCooldown: 0,
invincible: 0
};
let bullets = [];
let enemyBullets = [];
let enemies = [];
let items = [];
let particles = [];
let stars = [];
let score = 0;
let enemyTimer = 0;
let itemTimer = 0;
let gameOver = false;
let boss = null;
let bossTimer = 0;
let frame = 0;
for (let i = 0; i < 100; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 2 + 0.5,
speed: Math.random() * 2 + 1
});
}
window.addEventListener("keydown", (e) => {
keys[e.key.toLowerCase()] = true;
if (gameOver && e.key === "Enter") restart();
});
window.addEventListener("keyup", (e) => {
keys[e.key.toLowerCase()] = false;
});
function restart() {
player.x = canvas.width / 2;
player.y = canvas.height - 70;
player.hp = 5;
player.power = 1;
player.shotCooldown = 0;
player.invincible = 0;
bullets = [];
enemyBullets = [];
enemies = [];
items = [];
particles = [];
score = 0;
enemyTimer = 0;
itemTimer = 0;
frame = 0;
gameOver = false;
updateUI();
}
function updateUI() {
scoreEl.textContent = score;
hpEl.textContent = player.hp;
powerEl.textContent = player.power;
}
function addPlayerBullet(x, y, vx, vy, power = 1) {
bullets.push({ x, y, w: 6, h: 16, vx, vy, power });
}
function shoot() {
if (player.power === 1) {
addPlayerBullet(player.x, player.y - 25, 0, -10);
} else if (player.power === 2) {
addPlayerBullet(player.x - 9, player.y - 25, 0, -10);
addPlayerBullet(player.x + 9, player.y - 25, 0, -10);
} else if (player.power === 3) {
addPlayerBullet(player.x, player.y - 28, 0, -11);
addPlayerBullet(player.x - 16, player.y - 20, -1.2, -10);
addPlayerBullet(player.x + 16, player.y - 20, 1.2, -10);
} else {
addPlayerBullet(player.x, player.y - 30, 0, -12, 2);
addPlayerBullet(player.x - 14, player.y - 24, -0.8, -11);
addPlayerBullet(player.x + 14, player.y - 24, 0.8, -11);
addPlayerBullet(player.x - 24, player.y - 10, -1.7, -10);
addPlayerBullet(player.x + 24, player.y - 10, 1.7, -10);
}
player.shotCooldown = Math.max(4, 10 - player.power);
}
function spawnEnemy() {
if (boss) return;
const size = Math.random() * 16 + 28;
enemies.push({
x: Math.random() * (canvas.width - size) + size / 2,
y: -size,
w: size,
h: size,
speed: Math.random() * 1.4 + 1.4,
hp: size > 38 ? 4 : 2,
shotTimer: Math.floor(Math.random() * 50),
type: Math.random() < 0.35 ? "spread" : "normal"
});
}
function spawnPowerItem(x = Math.random() * (canvas.width - 40) + 20, y = -20) {
items.push({
x,
y,
w: 24,
h: 24,
speed: 2.2,
type: "power"
});
}
function enemyShoot(enemy) {
if (enemy.type === "spread") {
for (let i = -2; i <= 2; i++) {
enemyBullets.push({
x: enemy.x,
y: enemy.y + enemy.h / 2,
w: 8,
h: 8,
vx: i * 1.1,
vy: 3.2
});
}
} else {
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const len = Math.hypot(dx, dy) || 1;
enemyBullets.push({
x: enemy.x,
y: enemy.y + enemy.h / 2,
w: 8,
h: 8,
vx: dx / len * 3.2,
vy: dy / len * 3.2
});
}
}
function createExplosion(x, y) {
for (let i = 0; i < 16; i++) {
particles.push({
x,
y,
vx: (Math.random() - 0.5) * 7,
vy: (Math.random() - 0.5) * 7,
life: 24,
r: Math.random() * 4 + 2
});
}
}
function isHit(a, b) {
return (
a.x - a.w / 2 < b.x + b.w / 2 &&
a.x + a.w / 2 > b.x - b.w / 2 &&
a.y - a.h / 2 < b.y + b.h / 2 &&
a.y + a.h / 2 > b.y - b.h / 2
);
}
function damagePlayer() {
if (player.invincible > 0) return;
player.hp--;
player.power = Math.max(1, player.power - 1);
player.invincible = 80;
createExplosion(player.x, player.y);
updateUI();
if (player.hp <= 0) gameOver = true;
}
function update() {
if (gameOver) return;
frame++;
// boss spawn
bossTimer++;
if (!boss && bossTimer > 2000) {
boss = {
x: canvas.width / 2,
y: 120,
w: 120,
h: 120,
hp: 200,
maxHp: 200,
shotTimer: 0
};
}
if (keys["arrowleft"] || keys["a"]) player.x -= player.speed;
if (keys["arrowright"] || keys["d"]) player.x += player.speed;
if (keys["arrowup"] || keys["w"]) player.y -= player.speed;
if (keys["arrowdown"] || keys["s"]) player.y += player.speed;
player.x = Math.max(player.w / 2, Math.min(canvas.width - player.w / 2, player.x));
player.y = Math.max(player.h / 2, Math.min(canvas.height - player.h / 2, player.y));
if (player.invincible > 0) player.invincible--;
if (player.shotCooldown > 0) player.shotCooldown--;
if ((keys[" "] || keys["space"]) && player.shotCooldown <= 0) shoot();
bullets.forEach((b) => {
b.x += b.vx;
b.y += b.vy;
});
bullets = bullets.filter((b) => b.y > -30 && b.x > -30 && b.x < canvas.width + 30);
enemyBullets.forEach((b) => {
b.x += b.vx;
b.y += b.vy;
});
enemyBullets = enemyBullets.filter((b) => b.y < canvas.height + 30 && b.x > -30 && b.x < canvas.width + 30);
enemyTimer++;
if (enemyTimer > Math.max(20, 40 - Math.floor(score / 1000))) {
spawnEnemy();
enemyTimer = 0;
}
itemTimer++;
if (itemTimer > 520) {
spawnPowerItem();
itemTimer = 0;
}
enemies.forEach((e) => {
e.y += e.speed;
e.shotTimer++;
if (e.shotTimer > 70) {
enemyShoot(e);
e.shotTimer = 0;
}
});
// boss behavior
if (boss) {
boss.shotTimer++;
if (boss.shotTimer % 40 === 0) {
for (let i = 0; i < 20; i++) {
const angle = (Math.PI * 2 / 20) * i + frame * 0.02;
enemyBullets.push({
x: boss.x,
y: boss.y,
w: 10,
h: 10,
vx: Math.cos(angle) * 3,
vy: Math.sin(angle) * 3
});
}
}
}
items.forEach((item) => {
item.y += item.speed;
item.x += Math.sin((frame + item.y) * 0.04) * 0.8;
});
items = items.filter((item) => item.y < canvas.height + 40);
for (let i = items.length - 1; i >= 0; i--) {
if (isHit(player, items[i])) {
player.power = Math.min(4, player.power + 1);
score += 300;
createExplosion(items[i].x, items[i].y);
items.splice(i, 1);
updateUI();
}
}
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
if (isHit(player, e)) {
createExplosion(e.x, e.y);
enemies.splice(i, 1);
damagePlayer();
continue;
}
if (e.y > canvas.height + 50) {
enemies.splice(i, 1);
damagePlayer();
}
}
for (let i = enemyBullets.length - 1; i >= 0; i--) {
if (isHit(player, enemyBullets[i])) {
enemyBullets.splice(i, 1);
damagePlayer();
}
}
for (let i = enemies.length - 1; i >= 0; i--) {
for (let j = bullets.length - 1; j >= 0; j--) {
if (isHit(enemies[i], bullets[j])) {
enemies[i].hp -= bullets[j].power;
bullets.splice(j, 1);
if (enemies[i].hp <= 0) {
const drop = Math.random() < 0.18;
if (drop) spawnPowerItem(enemies[i].x, enemies[i].y);
createExplosion(enemies[i].x, enemies[i].y);
enemies.splice(i, 1);
score += 100;
updateUI();
}
break;
}
}
}
particles.forEach((p) => {
p.x += p.vx;
p.y += p.vy;
p.life--;
});
particles = particles.filter((p) => p.life > 0);
stars.forEach((s) => {
s.y += s.speed;
if (s.y > canvas.height) {
s.y = 0;
s.x = Math.random() * canvas.width;
}
});
}
function drawPlayer() {
if (player.invincible > 0 && Math.floor(frame / 5) % 2 === 0) return;
ctx.save();
ctx.translate(player.x, player.y);
// body
const grad = ctx.createLinearGradient(0, -20, 0, 30);
grad.addColorStop(0, "#7df9ff");
grad.addColorStop(1, "#0077ff");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, -26);
ctx.lineTo(20, 22);
ctx.lineTo(0, 12);
ctx.lineTo(-20, 22);
ctx.closePath();
ctx.fill();
// cockpit
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(0, -6, 6, 0, Math.PI * 2);
ctx.fill();
// wings glow
ctx.fillStyle = "#00eaff";
ctx.globalAlpha = 0.5;
ctx.fillRect(-24, 0, 8, 10);
ctx.fillRect(16, 0, 8, 10);
ctx.globalAlpha = 1;
// engine flame
ctx.fillStyle = "#ffcf5a";
ctx.beginPath();
ctx.moveTo(-8, 24);
ctx.lineTo(0, 40 + Math.random() * 6);
ctx.lineTo(8, 24);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawEnemy(e) {
ctx.save();
ctx.translate(e.x, e.y);
// core
const grad = ctx.createRadialGradient(0, 0, 4, 0, 0, e.w / 2);
grad.addColorStop(0, "#fff");
grad.addColorStop(1, e.type === "spread" ? "#ff00cc" : "#ff0000");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, e.w / 2, 0, Math.PI * 2);
ctx.fill();
// spikes
ctx.strokeStyle = "#fff";
ctx.lineWidth = 2;
for (let i = 0; i < 6; i++) {
const angle = (Math.PI * 2 / 6) * i + frame * 0.01;
ctx.beginPath();
ctx.moveTo(Math.cos(angle) * 6, Math.sin(angle) * 6);
ctx.lineTo(Math.cos(angle) * (e.w / 2 + 8), Math.sin(angle) * (e.w / 2 + 8));
ctx.stroke();
}
ctx.restore();
}
function drawPowerItem(item) {
ctx.save();
ctx.translate(item.x, item.y);
ctx.rotate(frame * 0.05);
ctx.fillStyle = "#68ff7a";
ctx.fillRect(-12, -12, 24, 24);
ctx.fillStyle = "#052";
ctx.font = "bold 18px system-ui";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("P", 0, 1);
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#050816";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ffffff";
stars.forEach((s) => {
ctx.globalAlpha = 0.4 + Math.random() * 0.5;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
bullets.forEach((b) => {
ctx.fillStyle = b.power >= 2 ? "#fff36a" : "#8ffcff";
ctx.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
});
enemyBullets.forEach((b) => {
ctx.fillStyle = "#ff9a3b";
ctx.beginPath();
ctx.arc(b.x, b.y, b.w / 2, 0, Math.PI * 2);
ctx.fill();
});
items.forEach(drawPowerItem);
enemies.forEach(drawEnemy);
// draw boss
if (boss) {
ctx.save();
ctx.translate(boss.x, boss.y);
const grad = ctx.createRadialGradient(0, 0, 10, 0, 0, boss.w / 2);
grad.addColorStop(0, "#fff");
grad.addColorStop(1, "#ff00aa");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, boss.w / 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// HP bar
ctx.fillStyle = "#000";
ctx.fillRect(80, 20, 320, 16);
ctx.fillStyle = "#ff0066";
ctx.fillRect(80, 20, 320 * (boss.hp / boss.maxHp), 16);
}
drawPlayer();
particles.forEach((p) => {
ctx.globalAlpha = p.life / 24;
ctx.fillStyle = "#ffd35a";
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
if (gameOver) {
ctx.fillStyle = "#000b";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = "bold 42px system-ui";
ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2 - 30);
ctx.font = "20px system-ui";
ctx.fillText("Score: " + score, canvas.width / 2, canvas.height / 2 + 10);
ctx.fillText("Enterでリスタート", canvas.width / 2, canvas.height / 2 + 48);
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
updateUI();
loop();
</script>
</body>
</html>
横スクロールSTG公開!
ゲーム開発(UE5)ElderChroniclepart1

敵のHPバーの実装をやった
Aran Red Fantasy.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Aran Red Fantasy - Ultimate</title>
<style>
/* ===============================
基本CSSスタイル
=============================== */
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
}
header, footer, nav {
background-color: #005ce6;
color: #fff;
text-align: center;
padding: 10px;
}
header h1, footer .container { margin: 0; }
nav a {
color: #fff;
text-decoration: none;
margin: 0 8px;
padding: 5px 8px;
display: inline-block;
}
nav a:hover { background-color: #004bb5; border-radius: 4px; }
nav a.active { background-color: #003a8c; border-radius: 4px; }
main { padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
.button {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
border-radius: 5px;
}
.button:hover { background-color: #45a049; }
.disabled { opacity: 0.6; cursor: default; }
.muted { color:#667; font-size: 13px; }
/* カード風 */
.card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
.card h3 { margin-top: 0; }
/* プログレスバー */
.progress-bar {
background-color: #ddd;
border-radius: 5px;
height: 20px;
width: 100%;
margin-bottom: 10px;
}
.progress {
background-color: #4CAF50;
height: 100%;
border-radius: 5px;
width: 0%;
}
/* インベントリアイテム表示 */
.inventory-item {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
display: inline-block;
margin: 5px;
padding: 10px;
min-width: 120px;
text-align: center;
cursor: pointer;
transition: background-color 0.2s;
user-select: none;
}
.inventory-item:hover { background-color: #eef; }
/* メッセージ表示 */
.message {
background-color: #fff8dd;
border: 1px solid #f5c666;
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
white-space: pre-wrap;
}
/* モーダル */
.modal-bg {
position: fixed;
top: 0; left: 0;
width:100%; height:100%;
background: rgba(0,0,0,.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.modal {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
max-width: 520px;
width: 92%;
}
.modal h2 { margin-top: 0; }
.modal img { max-width: 100%; height: auto; border-radius: 6px; }
/* キャラクター表示 */
#character-image {
max-width: 420px;
width: 100%;
height: auto;
margin: 20px auto;
display: block;
border-radius: 8px;
border: 1px solid #ddd;
background: #fff;
}
/* バトル用スタイル */
.battle-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.enemy-card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 250px;
padding: 16px;
text-align: center;
}
/* メッセージログ */
.log {
background-color: #eef;
border: 1px solid #bbe;
border-radius: 5px;
padding: 10px;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
white-space: pre-wrap;
}
/* ロケーションボタン */
#location-buttons button { margin-right: 10px; }
/* スキル一覧 */
.skill-list { list-style: none; padding: 0; }
.skill-list li { margin: 5px 0; }
/* クエストログ */
#quest-log-list { list-style: none; padding: 0; }
#quest-log-list li { margin: 4px 0; }
/* 実績一覧 */
#achievement-list { list-style: none; padding: 0; }
#achievement-list li { margin: 5px 0; }
/* ===============================
アートギャラリー
=============================== */
.gallery-grid{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
}
.gallery-item{
background:#fff;
border:1px solid #ddd;
border-radius:8px;
overflow:hidden;
box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
display:flex;
flex-direction:column;
}
.gallery-item img{
width:100%;
height:auto;
display:block;
background:#fff;
}
.gallery-meta{
padding:10px;
display:flex;
align-items:center;
justify-content:space-between;
gap:8px;
flex-wrap:wrap;
}
.badge{
display:inline-block;
padding:4px 8px;
border-radius:999px;
font-size:12px;
background:#eef;
border:1px solid #bbe;
color:#223;
}
.badge.owned{
background:#e9ffe9;
border-color:#9fd49f;
color:#1c5a1c;
}
.badge.rarity-ur{
background:#fff2cc;
border-color:#f3d27a;
color:#6b4b00;
}
.badge.rarity-ssr{
background:#e8f0ff;
border-color:#9fb7ff;
color:#133a7a;
}
.gallery-actions{
display:flex;
gap:8px;
flex-wrap:wrap;
padding: 0 10px 12px;
}
/* ガチャUI */
.gacha-row{
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
flex-wrap:wrap;
}
.gacha-result{
margin-top:10px;
display:flex;
gap:10px;
flex-wrap:wrap;
align-items:flex-start;
}
.gacha-card{
width: 220px;
background:#fff;
border:1px solid #ddd;
border-radius:10px;
overflow:hidden;
box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
}
.gacha-card img{ width:100%; display:block; }
.gacha-card .p{ padding:10px; }
</style>
</head>
<body>
<!-- ===============================
ヘッダー
=============================== -->
<header>
<h1>Aran Red Fantasy - Ultimate</h1>
</header>
<!-- ===============================
ナビゲーション
=============================== -->
<nav>
<div class="container">
<a href="#" id="home-link" onclick="showPage('home')">Home</a>
<a href="#" id="quests-link" onclick="showPage('quests')">Quests</a>
<a href="#" id="items-link" onclick="showPage('items')">Items</a>
<a href="#" id="friends-link" onclick="showPage('friends')">Companions</a>
<a href="#" id="character-link" onclick="showPage('character')">Character</a>
<a href="#" id="art-link" onclick="showPage('art')">Art Gallery</a>
<a href="#" id="battle-link" onclick="showPage('battle')">Battle</a>
<a href="#" id="store-link" onclick="showPage('store')">Store</a>
<a href="#" id="craft-link" onclick="showPage('craft')">Craft</a>
<a href="#" id="skills-link" onclick="showPage('skills')">Skills</a>
<a href="#" id="questlog-link" onclick="showPage('questlog')">QuestLog</a>
<a href="#" id="achievements-link" onclick="showPage('achievements')">Achievements</a>
</div>
</nav>
<!-- ===============================
メインコンテンツ
=============================== -->
<main>
<div class="container" id="content">
<!-- ===============================
Home
=============================== -->
<div id="home">
<h2>Welcome to Aran Red Fantasy!</h2>
<p>Explore the world, complete quests, craft items, recruit companions, and unlock achievements!</p>
<div id="home-message"></div>
<button class="button" onclick="showLoginModal()">Log In / Change User</button>
<button class="button" onclick="logout()">Logout (Reset All Data)</button>
<br/><br/>
<!-- BGM(継続再生対応:audioはページ外に置く) -->
<div class="card">
<h3>BGM</h3>
<p class="muted">※ 最初の1回だけ「Enable BGM」を押してください(ブラウザの自動再生ブロック対策)。以後はページ切替しても継続します。</p>
<button class="button" id="bgm-enable-btn" onclick="enableBGM()">Enable BGM (First Click)</button>
<button class="button" onclick="toggleMusic()">Toggle Music</button>
<div class="muted" id="bgm-status">Status: Off</div>
</div>
<!-- ★ガチャ(SSR/UR追加) -->
<div class="card">
<h3>Art Gacha</h3>
<div class="gacha-row">
<div class="muted">
Cost: <strong>10 Gold</strong> / pull<br/>
SSR/URが出ます。引いたアートは自動で所持になり、アートギャラリーに追加されます。
</div>
<div>
<button class="button" onclick="pullArtGacha(1)">Pull x1</button>
<button class="button" onclick="pullArtGacha(10)">Pull x10</button>
</div>
</div>
<div class="muted" id="gacha-status">—</div>
<div class="gacha-result" id="gacha-result"></div>
</div>
<!-- ランダムイベント/天候表示 -->
<div id="weather-display"></div>
<button class="button" onclick="triggerRandomEvent()">Check Random Event</button>
<!-- 昼夜サイクル -->
<div id="day-night-display"></div>
<button class="button" onclick="advanceTime()">Pass Time (+6h)</button>
<!-- 宿屋で休息 -->
<h3>Inn</h3>
<button class="button" onclick="restAtInn()">Rest at Inn (10 Gold)</button>
<!-- ロケーション移動 -->
<div id="location-section" class="card">
<h3>Locations</h3>
<div id="location-buttons">
<button class="button" onclick="moveLocation('Town')">Move to Town</button>
<button class="button" onclick="moveLocation('Forest')">Move to Forest</button>
<button class="button" onclick="moveLocation('Dungeon')">Move to Dungeon</button>
<button class="button" onclick="moveLocation('Mountain')">Move to Mountain</button>
</div>
<p>Current Location: <span id="current-location">Town</span></p>
<div class="log" id="location-log"></div>
</div>
</div>
<!-- ===============================
Quests
=============================== -->
<div id="quests" style="display: none;">
<h2>Quests</h2>
<h3>Main Quests</h3>
<div class="card" id="dragon-quest">
<h4>Defeat the Dragon</h4>
<p>A fierce dragon has appeared near the village! Defeat it to save the locals.</p>
<div class="progress-bar">
<div class="progress" id="dragon-progress"></div>
</div>
<p>Reward: 100 Gold, 100 XP, Dragon Scale</p>
<button class="button" onclick="startQuest('dragon')">Start Quest</button>
</div>
<div class="card" id="final-quest" style="display: none;">
<h4>The Ancient Evil (Final)</h4>
<p>The final threat emerges after you've proven your strength! Vanquish it!</p>
<div class="progress-bar">
<div class="progress" id="final-progress"></div>
</div>
<p>Reward: 200 Gold, 200 XP, Legendary Relic</p>
<button class="button" onclick="startQuest('final')">Start Quest</button>
</div>
<h3>Side Quests</h3>
<div class="card" id="crystal-quest">
<h4>Collect Magic Crystals</h4>
<p>Gather magical crystals scattered around the forest. Watch out for monsters!</p>
<div class="progress-bar">
<div class="progress" id="crystal-progress"></div>
</div>
<p>Reward: 50 Gold, 50 XP, Magic Crystal</p>
<button class="button" onclick="startQuest('crystal')">Start Quest</button>
</div>
<div class="card" id="orc-quest">
<h4>Eliminate the Orc Bandits</h4>
<p>A group of orc bandits is attacking travelers. Defeat them to restore peace!</p>
<div class="progress-bar">
<div class="progress" id="orc-progress"></div>
</div>
<p>Reward: 80 Gold, 70 XP, Orc Tusk</p>
<button class="button" onclick="startQuest('orc')">Start Quest</button>
</div>
</div>
<!-- ===============================
Items
=============================== -->
<div id="items" style="display: none;">
<h2>Inventory</h2>
<p>Click an item to use/equip/sell it (if applicable).</p>
<div id="inventory"></div>
</div>
<!-- ===============================
Companions
=============================== -->
<div id="friends" style="display: none;">
<h2>Companions</h2>
<p>Hire companions who fight alongside you!</p>
<input type="text" id="friendName" placeholder="Companion name" />
<button class="button" onclick="hireCompanion()">Hire Companion</button>
<h3>Your Companions</h3>
<ul id="companion-list"></ul>
<p class="muted">* Each companion has its own level, HP, and Attack. They also gain XP when you do.</p>
</div>
<!-- ===============================
Character
=============================== -->
<div id="character" style="display: none;">
<h2>Character</h2>
<img src="a.png" alt="Character" id="character-image"/>
<p>Name: <span id="character-name"></span></p>
<p>Level: <span id="character-level"></span></p>
<p>HP: <span id="character-hp"></span> / <span id="character-maxhp"></span></p>
<p>XP: <span id="character-xp"></span> / <span id="character-nextLevelXp"></span></p>
<p>Gold: <span id="character-gold"></span></p>
<p>Attack: <span id="character-attack"></span></p>
<p>Defense: <span id="character-defense"></span></p>
<p>Skill Points: <span id="character-skillpoints"></span></p>
<p>Active Buffs/Debuffs: <span id="character-buffs">None</span></p>
<p>Special Items: <span id="character-items">None</span></p>
<p class="muted">Portrait changes when you buy or pull an art (Store / Gacha).</p>
</div>
<!-- ===============================
Art Gallery
=============================== -->
<div id="art" style="display: none;">
<h2>アートギャラリー</h2>
<p class="muted">所持済みのアートは「Set as Character Art」でキャラクター画像に設定できます。ガチャでも入手できます。</p>
<div class="card">
<h3>Your Art Collection</h3>
<div id="art-collection-summary" class="muted"></div>
</div>
<div class="gallery-grid" id="art-gallery-grid"></div>
</div>
<!-- ===============================
Battle
=============================== -->
<div id="battle" style="display: none;">
<h2>Battle Arena</h2>
<p>Choose an enemy to fight or wait for random encounters in the wild!</p>
<div class="battle-container">
<div class="enemy-card">
<h3>Slime</h3>
<p>HP: 30</p>
<p>Attack: 1-3</p>
<p>Reward: 10 Gold, 10 XP</p>
<button class="button" onclick="startBattle('slime')">Fight Slime</button>
</div>
<div class="enemy-card">
<h3>Goblin</h3>
<p>HP: 50</p>
<p>Attack: 2-5</p>
<p>Reward: 20 Gold, 20 XP</p>
<button class="button" onclick="startBattle('goblin')">Fight Goblin</button>
</div>
<div class="enemy-card">
<h3>Orc Warrior</h3>
<p>HP: 80</p>
<p>Attack: 5-8</p>
<p>Reward: 40 Gold, 40 XP</p>
<button class="button" onclick="startBattle('orcEnemy')">Fight Orc</button>
</div>
</div>
<div class="log" id="battle-log"></div>
</div>
<!-- ===============================
Store
=============================== -->
<div id="store" style="display: none;">
<h2>Store</h2>
<p>Use your gold to purchase or sell items!</p>
<div class="card">
<h3>Buy Items</h3>
<div>
<h4>Minor Health Potion (20 Gold)</h4>
<button class="button" onclick="buyItem('Minor Health Potion')">Buy</button>
</div>
<div>
<h4>Major Health Potion (50 Gold)</h4>
<button class="button" onclick="buyItem('Major Health Potion')">Buy</button>
</div>
<div>
<h4>Iron Sword (80 Gold)</h4>
<button class="button" onclick="buyItem('Iron Sword')">Buy</button>
</div>
<div>
<h4>Steel Armor (100 Gold)</h4>
<button class="button" onclick="buyItem('Steel Armor')">Buy</button>
</div>
<div>
<h4>Lucky Ring (120 Gold)</h4>
<button class="button" onclick="buyItem('Lucky Ring')">Buy</button>
</div>
</div>
<!-- アート購入:購入するとキャラクター絵が変わる -->
<div class="card">
<h3>Art Shop(購入でキャラクター画像が変わる)</h3>
<p class="muted">Buy an art → it becomes “Owned” and you can set it anytime. (Gacha also adds Owned.)</p>
<div id="art-shop-list"></div>
</div>
<div class="card">
<h3>Sell Items</h3>
<p>Click an item in your inventory to sell it, if possible.</p>
<p class="muted">(You can't sell special quest items or currently equipped gear.)</p>
</div>
</div>
<!-- ===============================
Craft
=============================== -->
<div id="craft" style="display: none;">
<h2>Item Crafting</h2>
<p>Combine items to create something new!</p>
<div class="card">
<h3>Example Recipes</h3>
<ul>
<li>Dragon Scale + Orc Tusk => Dragon Tusk Lance (Weapon)</li>
<li>Magic Crystal + Magic Crystal => Greater Crystal (Special)</li>
</ul>
<p>Select any two items from your inventory to craft (if a valid recipe exists).</p>
</div>
<p>Currently Selected: <span id="craft-selection">None</span></p>
<button class="button" id="craft-button" onclick="attemptCraft()" disabled>Craft</button>
</div>
<!-- ===============================
Skills
=============================== -->
<div id="skills" style="display: none;">
<h2>Skills</h2>
<p>Use skill points to learn or upgrade skills!</p>
<p>You have <span id="skill-point-display"></span> skill points.</p>
<ul class="skill-list" id="skill-list"></ul>
</div>
<!-- ===============================
QuestLog
=============================== -->
<div id="questlog" style="display: none;">
<h2>Quest Log</h2>
<ul id="quest-log-list"></ul>
</div>
<!-- ===============================
Achievements
=============================== -->
<div id="achievements" style="display: none;">
<h2>Achievements</h2>
<ul id="achievement-list"></ul>
</div>
</div>
</main>
<!-- ===============================
フッター
=============================== -->
<footer>
<div class="container">
© 2025 Aran Red Fantasy
</div>
</footer>
<!-- ===============================
ログインモーダル
=============================== -->
<div class="modal-bg" id="login-modal-bg" style="display: none;">
<div class="modal">
<h2>Enter Your Name</h2>
<input type="text" id="loginName" placeholder="Your name" />
<br/><br/>
<button class="button" onclick="confirmLogin()">Login</button>
<button class="button" onclick="closeLoginModal()">Cancel</button>
</div>
</div>
<!-- ===============================
アイテム使用モーダル
=============================== -->
<div class="modal-bg" id="item-modal-bg" style="display: none;">
<div class="modal">
<h2 id="item-modal-title">Use/Equip Item</h2>
<p id="item-modal-description"></p>
<button class="button" onclick="confirmItemUse()">Use/Equip</button>
<button class="button" onclick="closeItemModal()">Cancel</button>
</div>
</div>
<!-- ===============================
アートプレビューモーダル
=============================== -->
<div class="modal-bg" id="art-modal-bg" style="display: none;">
<div class="modal">
<h2 id="art-modal-title">Art Preview</h2>
<img id="art-modal-img" alt="Art Preview" />
<p class="muted" id="art-modal-desc"></p>
<div style="margin-top:10px;">
<button class="button" id="art-modal-set-btn" onclick="confirmSetPortrait()">Set as Character Art</button>
<button class="button" onclick="closeArtModal()">Close</button>
</div>
</div>
</div>
<!-- ===============================
BGM本体(継続再生のためページ切替の外に置く)
=============================== -->
<audio id="bgm" loop preload="auto" playsinline>
<source src="http://tyosuke20xx.com/fjordnosundakaze.mp3" type="audio/mpeg">
</audio>
<!-- ===============================
JavaScript
=============================== -->
<script>
// -------------------------------------------
// ページ切り替え
// -------------------------------------------
function showPage(page) {
const pages = [
"home", "quests", "items", "friends", "character",
"art",
"battle", "store", "craft", "skills", "questlog", "achievements"
];
pages.forEach(p => {
const pageElement = document.getElementById(p);
const linkElement = document.getElementById(p + '-link');
if (!pageElement) return;
if (p === page) {
pageElement.style.display = "block";
if (linkElement) linkElement.classList.add("active");
} else {
pageElement.style.display = "none";
if (linkElement) linkElement.classList.remove("active");
}
});
if (page === "skills") refreshSkillList();
if (page === "questlog") updateQuestLog();
if (page === "achievements") updateAchievementList();
if (page === "art") renderArtGallery();
if (page === "store") renderArtShop();
}
// -------------------------------------------
// ローカルストレージキー
// -------------------------------------------
const LS_KEY_USER = "ARF_Username_Ultimate";
const LS_KEY_CHARACTER = "ARF_Character_Ultimate";
const LS_KEY_INVENTORY = "ARF_Inventory_Ultimate";
const LS_KEY_COMPANIONS = "ARF_Companions_Ultimate";
const LS_KEY_QUESTS = "ARF_Quests_Ultimate";
const LS_KEY_SKILLS = "ARF_Skills_Ultimate";
const LS_KEY_DAYTIME = "ARF_Daytime_Ultimate";
const LS_KEY_WEATHER = "ARF_Weather_Ultimate";
const LS_KEY_ACHIEVEMENT = "ARF_Achievement_Ultimate";
// BGM状態
const LS_KEY_BGM = "ARF_BGM_STATE_Ultimate";
// -------------------------------------------
// ★アート定義(SSR1〜SSR3 + UR1〜UR10)
// -------------------------------------------
const ART_LIST = [
// SSR(追加)
{ key:"SSR1", name:"SSR Art 1", url:"http://tyosuke20xx.com/SSR1.png", cost: 20, rarity:"SSR" },
{ key:"SSR2", name:"SSR Art 2", url:"http://tyosuke20xx.com/SSR2.png", cost: 20, rarity:"SSR" },
{ key:"SSR3", name:"SSR Art 3", url:"http://tyosuke20xx.com/SSR3.png", cost: 20, rarity:"SSR" },
// UR
{ key:"UR1", name:"UR Art 1", url:"http://tyosuke20xx.com/UR1.png", cost: 30, rarity:"UR" },
{ key:"UR2", name:"UR Art 2", url:"http://tyosuke20xx.com/UR2.png", cost: 30, rarity:"UR" },
{ key:"UR3", name:"UR Art 3", url:"http://tyosuke20xx.com/UR3.png", cost: 30, rarity:"UR" },
{ key:"UR4", name:"UR Art 4", url:"http://tyosuke20xx.com/UR4.png", cost: 30, rarity:"UR" },
{ key:"UR5", name:"UR Art 5", url:"http://tyosuke20xx.com/UR5.png", cost: 30, rarity:"UR" },
{ key:"UR6", name:"UR Art 6", url:"http://tyosuke20xx.com/UR6.png", cost: 30, rarity:"UR" },
{ key:"UR7", name:"UR Art 7", url:"http://tyosuke20xx.com/UR7.png", cost: 30, rarity:"UR" },
{ key:"UR8", name:"UR Art 8", url:"http://tyosuke20xx.com/UR8.png", cost: 30, rarity:"UR" },
{ key:"UR9", name:"UR Art 9", url:"http://tyosuke20xx.com/UR9.png", cost: 30, rarity:"UR" },
{ key:"UR10", name:"UR Art 10", url:"http://tyosuke20xx.com/UR10.png", cost: 30, rarity:"UR" }
];
// -------------------------------------------
// ★ガチャ設定(SSR/UR抽選)
// -------------------------------------------
const GACHA_COST = 10; // 1回10G
const GACHA_RATE_UR = 10; // UR 10%
const GACHA_RATE_SSR = 90; // SSR 90%(残り)
// -------------------------------------------
// キャラクター情報
// -------------------------------------------
let character = {
name: "Adventurer",
level: 1,
hp: 50,
maxHp: 50,
xp: 0,
nextLevelXp: 100,
gold: 0,
attack: 5,
defense: 2,
skillPoints: 0,
location: "Town",
specialItems: [],
buffs: [],
ownedArtKeys: [],
portraitUrl: "a.png"
};
// -------------------------------------------
// クエスト情報
// -------------------------------------------
let mainQuests = {
dragon: {
name: "Defeat the Dragon",
progress: 0,
reward: { gold: 100, xp: 100, items: ["Dragon Scale"] },
isRunning: false,
isCompleted: false,
unlockNext: "final",
locked: false
},
final: {
name: "The Ancient Evil",
progress: 0,
reward: { gold: 200, xp: 200, items: ["Legendary Relic"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: true
}
};
let sideQuests = {
crystal: {
name: "Collect Magic Crystals",
progress: 0,
reward: { gold: 50, xp: 50, items: ["Magic Crystal"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: false
},
orc: {
name: "Eliminate the Orc Bandits",
progress: 0,
reward: { gold: 80, xp: 70, items: ["Orc Tusk"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: false
}
};
function getAllQuests() {
return { ...mainQuests, ...sideQuests };
}
// -------------------------------------------
// インベントリ
// -------------------------------------------
let inventory = [];
// -------------------------------------------
// 仲間
// -------------------------------------------
let companions = [];
// -------------------------------------------
// スキル
// -------------------------------------------
let skills = {
Fireball: {
name: "Fireball",
level: 0,
maxLevel: 3,
cost: 1,
description: "Deal extra magic damage in battle"
},
Heal: {
name: "Heal",
level: 0,
maxLevel: 3,
cost: 1,
description: "Restores some HP at the start of battle"
}
};
// -------------------------------------------
// バトル用エネミー
// -------------------------------------------
const enemies = {
slime: {
name: "Slime",
hp: 30,
attackMin: 1,
attackMax: 3,
rewardGold: 10,
rewardXp: 10
},
goblin: {
name: "Goblin",
hp: 50,
attackMin: 2,
attackMax: 5,
rewardGold: 20,
rewardXp: 20
},
orcEnemy: {
name: "Orc Warrior",
hp: 80,
attackMin: 5,
attackMax: 8,
rewardGold: 40,
rewardXp: 40
}
};
// -------------------------------------------
// ストアアイテム
// -------------------------------------------
const storeItems = {
"Minor Health Potion": { name: "Minor Health Potion", type: "potion", heal: 20, cost: 20 },
"Major Health Potion": { name: "Major Health Potion", type: "potion", heal: 50, cost: 50 },
"Iron Sword": { name: "Iron Sword", type: "weapon", attack: 5, cost: 80, equipped: false },
"Steel Armor": { name: "Steel Armor", type: "armor", defense: 5, cost: 100, equipped: false },
"Lucky Ring": { name: "Lucky Ring", type: "accessory", attack: 1, defense: 1, cost: 120, equipped: false }
};
// -------------------------------------------
// クラフト用レシピ
// -------------------------------------------
const craftRecipes = [
{
components: ["Dragon Scale", "Orc Tusk"].sort(),
result: { name: "Dragon Tusk Lance", type: "weapon", attack: 10, equipped: false }
},
{
components: ["Magic Crystal", "Magic Crystal"].sort(),
result: { name: "Greater Crystal", type: "special" }
}
];
// -------------------------------------------
// 昼夜 & 天候
// -------------------------------------------
let currentHour = 12;
let currentWeather = "Sunny";
const possibleWeathers = ["Sunny","Rainy","Storm","Cloudy"];
// -------------------------------------------
// 実績
// -------------------------------------------
let achievements = {
firstKill: { name: "First Blood", description: "Defeat your first enemy.", isUnlocked: false },
level5: { name: "Rising Hero", description: "Reach Level 5.", isUnlocked: false },
quest3: { name: "Quest Hunter", description: "Complete 3 Quests.", isUnlocked: false }
};
// -------------------------------------------
// onload
// -------------------------------------------
window.onload = function() {
loadLocalData();
// BGM(継続再生 & 状態保存)
loadBgmState();
wireBgmAutoSave();
updateEnableBtn();
setBgmStatus(isMusicPlaying ? "On (will resume)" : "Off");
resumeBgmOnNextUserActionIfNeeded();
updateCharacterInfo();
updateQuestVisibility();
updateInventoryDisplay();
updateCompanionList();
updateDayNightDisplay();
updateWeatherDisplay();
updateAchievementList();
renderArtShop();
renderArtGallery();
showPage('home');
document.getElementById("current-location").textContent = character.location;
};
// -------------------------------------------
// ローカルストレージ: 読込/保存/リセット
// -------------------------------------------
function loadLocalData() {
let storedName = localStorage.getItem(LS_KEY_USER);
if (storedName) character.name = storedName;
let storedChar = localStorage.getItem(LS_KEY_CHARACTER);
if (storedChar) {
try {
const parsed = JSON.parse(storedChar);
character = { ...character, ...parsed };
} catch(e) {}
}
if (!Array.isArray(character.ownedArtKeys)) character.ownedArtKeys = [];
if (!character.portraitUrl) character.portraitUrl = "a.png";
let storedInv = localStorage.getItem(LS_KEY_INVENTORY);
if (storedInv) { try { inventory = JSON.parse(storedInv); } catch(e) {} }
let storedComp = localStorage.getItem(LS_KEY_COMPANIONS);
if (storedComp) { try { companions = JSON.parse(storedComp); } catch(e) {} }
let storedMQ = localStorage.getItem(LS_KEY_QUESTS+"_main");
if (storedMQ) { try { mainQuests = JSON.parse(storedMQ); } catch(e) {} }
let storedSQ = localStorage.getItem(LS_KEY_QUESTS+"_side");
if (storedSQ) { try { sideQuests = JSON.parse(storedSQ); } catch(e) {} }
let storedSkills = localStorage.getItem(LS_KEY_SKILLS);
if (storedSkills) { try { skills = JSON.parse(storedSkills); } catch(e) {} }
let storedHour = localStorage.getItem(LS_KEY_DAYTIME+"_hour");
if (storedHour) currentHour = parseInt(storedHour, 10);
let storedWeather = localStorage.getItem(LS_KEY_WEATHER);
if (storedWeather) currentWeather = storedWeather;
let storedAchv = localStorage.getItem(LS_KEY_ACHIEVEMENT);
if (storedAchv) { try { achievements = JSON.parse(storedAchv); } catch(e) {} }
}
function saveLocalData() {
localStorage.setItem(LS_KEY_USER, character.name);
localStorage.setItem(LS_KEY_CHARACTER, JSON.stringify(character));
localStorage.setItem(LS_KEY_INVENTORY, JSON.stringify(inventory));
localStorage.setItem(LS_KEY_COMPANIONS, JSON.stringify(companions));
localStorage.setItem(LS_KEY_QUESTS+"_main", JSON.stringify(mainQuests));
localStorage.setItem(LS_KEY_QUESTS+"_side", JSON.stringify(sideQuests));
localStorage.setItem(LS_KEY_SKILLS, JSON.stringify(skills));
localStorage.setItem(LS_KEY_DAYTIME+"_hour", currentHour.toString());
localStorage.setItem(LS_KEY_WEATHER, currentWeather);
localStorage.setItem(LS_KEY_ACHIEVEMENT, JSON.stringify(achievements));
}
function logout() {
if (!confirm("All data will be cleared. Are you sure?")) return;
localStorage.clear();
location.reload();
}
// -------------------------------------------
// ログインモーダル
// -------------------------------------------
function showLoginModal() {
document.getElementById("login-modal-bg").style.display = "flex";
}
function closeLoginModal() {
document.getElementById("login-modal-bg").style.display = "none";
}
function confirmLogin() {
const inputName = document.getElementById("loginName").value.trim();
if (inputName) {
character.name = inputName;
saveLocalData();
updateCharacterInfo();
}
closeLoginModal();
}
// -------------------------------------------
// キャラクター情報表示更新
// -------------------------------------------
function updateCharacterInfo() {
document.getElementById("character-name").textContent = character.name;
document.getElementById("character-level").textContent = character.level;
document.getElementById("character-hp").textContent = character.hp;
document.getElementById("character-maxhp").textContent = character.maxHp;
document.getElementById("character-xp").textContent = character.xp;
document.getElementById("character-nextLevelXp").textContent = character.nextLevelXp;
document.getElementById("character-gold").textContent = character.gold;
document.getElementById("character-attack").textContent = character.attack;
document.getElementById("character-defense").textContent = character.defense;
document.getElementById("character-skillpoints").textContent = character.skillPoints;
const img = document.getElementById("character-image");
if (img) img.src = character.portraitUrl || "a.png";
if (character.specialItems.length > 0) {
document.getElementById("character-items").textContent = character.specialItems.join(", ");
} else {
document.getElementById("character-items").textContent = "None";
}
if (character.buffs.length > 0) {
document.getElementById("character-buffs").textContent = character.buffs.map(b => b.name).join(", ");
} else {
document.getElementById("character-buffs").textContent = "None";
}
saveLocalData();
checkAchievements();
}
// -------------------------------------------
// レベルアップ
// -------------------------------------------
function addXp(amount) {
character.xp += amount;
while (character.xp >= character.nextLevelXp) {
character.level++;
character.xp -= character.nextLevelXp;
character.nextLevelXp = character.level * 100;
character.maxHp += 20;
character.hp = character.maxHp;
character.attack += 1;
character.defense += 1;
character.skillPoints += 1;
showHomeMessage(`Level up! Now Level ${character.level} (+1 Skill Point).`);
for (let c of companions) {
c.level++;
c.hp = c.maxHp;
c.attack++;
}
}
updateCharacterInfo();
}
// -------------------------------------------
// バフ/デバフ
// -------------------------------------------
function addBuff(buffObj) {
character.buffs.push(buffObj);
updateCharacterInfo();
}
function processBuffsEachTurn(logElm) {
for (let i = character.buffs.length - 1; i >= 0; i--) {
const b = character.buffs[i];
if (b.effectType === "dot") {
character.hp -= b.effectValue;
if (character.hp < 0) character.hp = 0;
logMessage(logElm, `[${b.name}] You take ${b.effectValue} damage! (HP: ${character.hp})`);
}
b.turns--;
if (b.turns <= 0) {
logMessage(logElm, `[${b.name}] effect ended.`);
character.buffs.splice(i, 1);
}
}
}
// -------------------------------------------
// ホームメッセージ
// -------------------------------------------
function showHomeMessage(msg) {
const homeMessage = document.getElementById('home-message');
homeMessage.innerHTML = `<div class="message">${msg}</div>`;
}
// -------------------------------------------
// 昼夜
// -------------------------------------------
function updateDayNightDisplay() {
let dnElm = document.getElementById("day-night-display");
let hourStr = (currentHour < 10) ? "0"+currentHour : currentHour;
let isNight = (currentHour >= 18 || currentHour < 6);
let dayNight = isNight ? "Night" : "Day";
dnElm.innerHTML = `<p>Time: ${hourStr}:00 (${dayNight})</p>`;
}
function advanceTime() {
currentHour += 6;
if (currentHour >= 24) currentHour -= 24;
saveLocalData();
updateDayNightDisplay();
showHomeMessage("Time passes by...");
}
// -------------------------------------------
// 天候
// -------------------------------------------
function updateWeatherDisplay() {
const wElm = document.getElementById("weather-display");
wElm.innerHTML = `<p>Weather: ${currentWeather}</p>`;
}
function changeWeatherRandom() {
currentWeather = possibleWeathers[Math.floor(Math.random() * possibleWeathers.length)];
updateWeatherDisplay();
saveLocalData();
}
// -------------------------------------------
// ランダムイベント
// -------------------------------------------
function triggerRandomEvent() {
const randomRoll = Math.random();
let msg = "";
if (randomRoll < 0.2) {
msg = "A traveling merchant appears, offering rare goods (not yet implemented).";
} else if (randomRoll < 0.4) {
changeWeatherRandom();
msg = `The weather suddenly changes to ${currentWeather}!`;
} else if (randomRoll < 0.6) {
addBuff({ name: 'Poison', turns: 3, effectType: 'dot', effectValue: 3 });
msg = "You stepped on a poisonous trap! You are now poisoned.";
} else {
msg = "Nothing special happens.";
}
showHomeMessage(msg);
}
// -------------------------------------------
// 宿屋
// -------------------------------------------
function restAtInn() {
if (character.gold < 10) {
showHomeMessage("Not enough gold to rest at the inn!");
return;
}
character.gold -= 10;
character.hp = character.maxHp;
for (let c of companions) c.hp = c.maxHp;
showHomeMessage("You and your companions rest at the inn and recover full HP.");
updateCharacterInfo();
}
// -------------------------------------------
// ロケーション移動 + ランダムエンカウント
// -------------------------------------------
function moveLocation(newLocation) {
character.location = newLocation;
document.getElementById("current-location").textContent = newLocation;
saveLocalData();
const logElm = document.getElementById('location-log');
logElm.textContent = `You moved to ${newLocation}.`;
let encounterChance = 0;
if (newLocation === "Town") encounterChance = 0;
else if (newLocation === "Forest") encounterChance = 40;
else if (newLocation === "Dungeon") encounterChance = 70;
else if (newLocation === "Mountain") encounterChance = 50;
const roll = Math.random() * 100;
if (roll < encounterChance) {
const enemyKeys = Object.keys(enemies);
const randEnemyKey = enemyKeys[Math.floor(Math.random() * enemyKeys.length)];
logElm.textContent += `\nA wild ${enemies[randEnemyKey].name} appears!`;
startBattle(randEnemyKey);
}
}
// -------------------------------------------
// 仲間の雇用
// -------------------------------------------
function hireCompanion() {
const input = document.getElementById('friendName');
let name = input.value.trim();
if (!name) return;
let newCompanion = { name, level: 1, hp: 30, maxHp: 30, attack: 2 };
companions.push(newCompanion);
input.value = "";
updateCompanionList();
saveLocalData();
showHomeMessage(`${name} joined your party!`);
}
function updateCompanionList() {
const listElm = document.getElementById('companion-list');
listElm.innerHTML = "";
companions.forEach(c => {
const li = document.createElement("li");
li.textContent = `${c.name} (Lv ${c.level}, HP ${c.hp}/${c.maxHp}, ATK ${c.attack})`;
listElm.appendChild(li);
});
}
// -------------------------------------------
// インベントリ表示
// -------------------------------------------
let selectedItemIndex = null;
let selectedForCraft = [];
function updateInventoryDisplay() {
const invElm = document.getElementById('inventory');
invElm.innerHTML = "";
if (inventory.length === 0) {
invElm.innerHTML = "<p>Your inventory is empty.</p>";
return;
}
inventory.forEach((item, index) => {
const div = document.createElement("div");
div.className = "inventory-item";
div.textContent = item.name;
if (item.equipped) div.style.border = "2px solid #4CAF50";
div.onclick = () => onInventoryItemClick(index);
invElm.appendChild(div);
});
}
function onInventoryItemClick(index) {
if (document.getElementById("craft").style.display === "block") {
toggleCraftSelection(index);
return;
}
selectedItemIndex = index;
const item = inventory[index];
const modalTitle = document.getElementById("item-modal-title");
const modalDesc = document.getElementById("item-modal-description");
if (item.type === "potion") {
modalTitle.textContent = `Use ${item.name}?`;
modalDesc.textContent = `This potion restores ${item.heal} HP.`;
} else if (item.type === "weapon") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Weapon (+${item.attack} Attack).`;
} else if (item.type === "armor") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Armor (+${item.defense} Defense).`;
} else if (item.type === "accessory") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Accessory (+${item.attack} ATK, +${item.defense} DEF).`;
} else {
modalTitle.textContent = item.name;
modalDesc.textContent = "A special item. No direct use/equip.";
}
if (canSellItem(item)) {
modalDesc.textContent += `\n(Sell price: ${sellPrice(item)} Gold)`;
}
document.getElementById("item-modal-bg").style.display = "flex";
}
function closeItemModal() {
document.getElementById("item-modal-bg").style.display = "none";
selectedItemIndex = null;
}
function confirmItemUse() {
if (selectedItemIndex === null) return;
const item = inventory[selectedItemIndex];
if (item.type === "potion") {
character.hp += item.heal;
if (character.hp > character.maxHp) character.hp = character.maxHp;
inventory.splice(selectedItemIndex, 1);
showHomeMessage(`${item.name} used! You recovered ${item.heal} HP.`);
}
else if (item.type === "weapon") {
unequipItem("weapon");
item.equipped = true;
character.attack += item.attack;
showHomeMessage(`${item.name} equipped. (+${item.attack} Attack)`);
}
else if (item.type === "armor") {
unequipItem("armor");
item.equipped = true;
character.defense += item.defense;
showHomeMessage(`${item.name} equipped. (+${item.defense} Defense)`);
}
else if (item.type === "accessory") {
unequipItem("accessory");
item.equipped = true;
character.attack += item.attack;
character.defense += item.defense;
showHomeMessage(`${item.name} equipped. (+${item.attack} ATK, +${item.defense} DEF)`);
}
else {
if (canSellItem(item)) {
let price = sellPrice(item);
character.gold += price;
inventory.splice(selectedItemIndex, 1);
showHomeMessage(`You sold ${item.name} for ${price} Gold.`);
} else {
showHomeMessage(`You can't use ${item.name} right now.`);
}
}
updateCharacterInfo();
updateInventoryDisplay();
closeItemModal();
}
function unequipItem(type) {
for (let i = 0; i < inventory.length; i++) {
let it = inventory[i];
if (it.type === type && it.equipped) {
it.equipped = false;
if (type === "weapon") character.attack -= it.attack;
else if (type === "armor") character.defense -= it.defense;
else if (type === "accessory") { character.attack -= it.attack; character.defense -= it.defense; }
}
}
}
function canSellItem(item) {
if (item.equipped) return false;
if (item.type === "special") return false;
return !["weapon","armor","accessory","potion"].includes(item.type) ? true : false;
}
function sellPrice(item) { return 30; }
// -------------------------------------------
// クエストUI
// -------------------------------------------
function updateQuestVisibility() {
const finalQuestCard = document.getElementById("final-quest");
finalQuestCard.style.display = (!mainQuests.final.locked) ? "block" : "none";
}
function startQuest(questKey) {
let q = mainQuests[questKey] || sideQuests[questKey];
if (!q) return;
if (q.isRunning || q.isCompleted) return;
if (q.locked) {
showHomeMessage("This quest is locked. Complete the previous quest first!");
return;
}
q.isRunning = true;
q.progress = 0;
updateProgressBar(questKey);
let progressInterval = setInterval(() => {
q.progress += 5;
if (q.progress > 100) q.progress = 100;
updateProgressBar(questKey);
if (q.progress === 100) {
clearInterval(progressInterval);
completeQuest(questKey);
}
}, 400);
}
function completeQuest(questKey) {
let q = mainQuests[questKey] || sideQuests[questKey];
q.isRunning = false;
q.isCompleted = true;
character.gold += q.reward.gold;
addXp(q.reward.xp);
q.reward.items.forEach(it => character.specialItems.push(it));
showHomeMessage(`${q.name} completed! You got ${q.reward.gold} Gold, ${q.reward.xp} XP, and ${q.reward.items.join(", ")}.`);
if (q.unlockNext) {
if (mainQuests[q.unlockNext]) mainQuests[q.unlockNext].locked = false;
else if (sideQuests[q.unlockNext]) sideQuests[q.unlockNext].locked = false;
}
updateQuestVisibility();
updateCharacterInfo();
saveLocalData();
}
function updateProgressBar(questKey) {
const bar = document.getElementById(questKey + '-progress');
let q = mainQuests[questKey] || sideQuests[questKey];
if (bar) bar.style.width = q.progress + '%';
}
// -------------------------------------------
// バトル
// -------------------------------------------
function startBattle(enemyKey) {
const enemyDef = enemies[enemyKey];
if (!enemyDef) return;
const logElm = document.getElementById('battle-log');
logElm.innerHTML = `A wild ${enemyDef.name} appears! (HP: ${enemyDef.hp})`;
if (skills.Heal.level > 0) {
const healAmount = skills.Heal.level * 10;
character.hp += healAmount;
if (character.hp > character.maxHp) character.hp = character.maxHp;
logMessage(logElm, `[Skill: Heal Lv${skills.Heal.level}] You healed ${healAmount} HP!`);
updateCharacterInfo();
}
let enemyHp = enemyDef.hp;
let battleInterval = setInterval(() => {
processBuffsEachTurn(logElm);
if (character.hp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, "You have been defeated...");
saveLocalData();
return;
}
let baseDamage = getRandomInt(character.attack - 2, character.attack + 2);
if (baseDamage < 1) baseDamage = 1;
if (skills.Fireball.level > 0) {
let extra = skills.Fireball.level * 2;
baseDamage += extra;
logMessage(logElm, `[Fireball Lv${skills.Fireball.level}] Extra ${extra} magic damage!`);
}
let totalCompanionDamage = 0;
companions.forEach(c => { if (c.hp > 0) totalCompanionDamage += c.attack; });
let totalDamage = baseDamage + totalCompanionDamage;
enemyHp -= totalDamage;
logMessage(logElm, `You (and companions) deal ${totalDamage} damage! (Enemy HP: ${Math.max(enemyHp, 0)})`);
if (enemyHp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, `You defeated the ${enemyDef.name}!`);
character.gold += enemyDef.rewardGold;
addXp(enemyDef.rewardXp);
updateCompanionXP(enemyDef.rewardXp);
updateCharacterInfo();
saveLocalData();
achievements.firstKill.isUnlocked = true;
updateAchievementList();
return;
}
let eAtk = getRandomInt(enemyDef.attackMin, enemyDef.attackMax);
let dmgToPlayer = eAtk - character.defense;
if (dmgToPlayer < 1) dmgToPlayer = 1;
character.hp -= dmgToPlayer;
if (character.hp < 0) character.hp = 0;
logMessage(logElm, `The ${enemyDef.name} hits you for ${dmgToPlayer}. (Your HP: ${character.hp})`);
updateCharacterInfo();
if (character.hp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, "You have been defeated...");
saveLocalData();
return;
}
}, 800);
}
function logMessage(logElm, msg) {
const p = document.createElement("p");
p.textContent = msg;
logElm.appendChild(p);
logElm.scrollTop = logElm.scrollHeight;
}
function updateCompanionXP(amount) {
for (let c of companions) {
c.level += Math.floor(amount/50);
c.maxHp += 5;
c.hp = c.maxHp;
c.attack += 1;
}
updateCompanionList();
}
// -------------------------------------------
// ストア購入
// -------------------------------------------
function buyItem(itemKey) {
const itemDef = storeItems[itemKey];
if (!itemDef) return;
if (character.gold < itemDef.cost) {
showHomeMessage(`You don't have enough gold to buy ${itemDef.name}.`);
return;
}
character.gold -= itemDef.cost;
let newItem = JSON.parse(JSON.stringify(itemDef));
if (["weapon","armor","accessory"].includes(newItem.type)) newItem.equipped = false;
inventory.push(newItem);
showHomeMessage(`You bought ${newItem.name}!`);
updateCharacterInfo();
updateInventoryDisplay();
}
// -------------------------------------------
// アート所持/購入/設定
// -------------------------------------------
function isArtOwned(artKey) {
return character.ownedArtKeys.includes(artKey);
}
function grantArt(artKey, setAsPortrait=false) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return false;
if (!isArtOwned(art.key)) {
character.ownedArtKeys.push(art.key);
}
if (setAsPortrait) {
character.portraitUrl = art.url;
}
saveLocalData();
updateCharacterInfo();
renderArtShop();
renderArtGallery();
return true;
}
function buyArt(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
if (isArtOwned(art.key)) {
showHomeMessage(`You already own ${art.name}.`);
return;
}
if (character.gold < art.cost) {
showHomeMessage(`Not enough gold to buy ${art.name}. Need ${art.cost} Gold.`);
return;
}
character.gold -= art.cost;
// 購入したら即キャラ絵変更
grantArt(art.key, true);
showHomeMessage(`Purchased ${art.name}! Character portrait changed.`);
}
function setPortraitFromArt(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
if (!isArtOwned(art.key)) {
showHomeMessage("You don't own this art yet. Buy it in Store or pull it from Gacha.");
return;
}
character.portraitUrl = art.url;
saveLocalData();
updateCharacterInfo();
showHomeMessage(`Character portrait set to ${art.name}.`);
}
// Storeのアート一覧描画
function renderArtShop() {
const wrap = document.getElementById("art-shop-list");
if (!wrap) return;
wrap.innerHTML = "";
ART_LIST.forEach(a => {
const owned = isArtOwned(a.key);
const rarityBadge = a.rarity === "UR"
? `<span class="badge rarity-ur">UR</span>`
: `<span class="badge rarity-ssr">SSR</span>`;
const row = document.createElement("div");
row.style.marginBottom = "10px";
row.innerHTML = `
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;">
<div>
<strong>${a.name}</strong>
${rarityBadge}
<span class="muted">(${a.cost} Gold)</span>
${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
</div>
<div>
<button class="button" onclick="buyArt('${a.key}')" ${owned ? 'disabled class="button disabled"' : ''} ${owned ? 'disabled' : ''}>Buy</button>
<button class="button" onclick="openArtModal('${a.key}')">Preview</button>
</div>
</div>
`;
wrap.appendChild(row);
});
}
// ギャラリー描画
function renderArtGallery() {
const grid = document.getElementById("art-gallery-grid");
const sum = document.getElementById("art-collection-summary");
if (!grid || !sum) return;
const ownedCount = character.ownedArtKeys.length;
sum.textContent = `Owned: ${ownedCount} / ${ART_LIST.length}`;
grid.innerHTML = "";
ART_LIST.forEach(a => {
const owned = isArtOwned(a.key);
const item = document.createElement("div");
item.className = "gallery-item";
const img = document.createElement("img");
img.src = a.url;
img.alt = a.name;
img.loading = "lazy";
img.style.cursor = "pointer";
img.onclick = () => openArtModal(a.key);
const meta = document.createElement("div");
meta.className = "gallery-meta";
meta.innerHTML = `
<div>
<strong>${a.name}</strong><br/>
<span class="muted">${a.cost} Gold</span>
</div>
<div style="display:flex; gap:6px; align-items:center;">
${a.rarity === "UR"
? `<span class="badge rarity-ur">UR</span>`
: `<span class="badge rarity-ssr">SSR</span>`
}
${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
</div>
`;
const actions = document.createElement("div");
actions.className = "gallery-actions";
const btnPreview = document.createElement("button");
btnPreview.className = "button";
btnPreview.textContent = "Preview";
btnPreview.onclick = () => openArtModal(a.key);
const btnSet = document.createElement("button");
btnSet.className = "button";
btnSet.textContent = "Set as Character Art";
btnSet.disabled = !owned;
if (!owned) btnSet.classList.add("disabled");
btnSet.onclick = () => setPortraitFromArt(a.key);
actions.appendChild(btnPreview);
actions.appendChild(btnSet);
item.appendChild(img);
item.appendChild(meta);
item.appendChild(actions);
grid.appendChild(item);
});
}
// -------------------------------------------
// ★ガチャ(SSR1〜SSR3/UR1〜UR10から抽選)
// - 引いたアートはOwnedに追加
// - 1枚目だけは演出的にキャラ絵も即変更(setAsPortrait=true)
// -------------------------------------------
function pullArtGacha(times) {
const totalCost = GACHA_COST * times;
const status = document.getElementById("gacha-status");
const resultWrap = document.getElementById("gacha-result");
resultWrap.innerHTML = "";
if (character.gold < totalCost) {
status.textContent = `Not enough gold. Need ${totalCost} Gold.`;
showHomeMessage(`Not enough gold for gacha. Need ${totalCost} Gold.`);
return;
}
character.gold -= totalCost;
let pulled = [];
for (let i=0; i<times; i++) {
const art = rollOneArt();
pulled.push(art);
// 1枚目だけ即ポートレート変更(継続仕様)
grantArt(art.key, i === 0);
}
updateCharacterInfo();
status.textContent = `Pulled ${times} time(s). Cost ${totalCost} Gold.`;
// 表示
pulled.forEach((a, idx) => {
const card = document.createElement("div");
card.className = "gacha-card";
card.innerHTML = `
<img src="${a.url}" alt="${a.name}">
<div class="p">
<strong>${a.name}</strong><br/>
<span class="muted">${a.rarity}</span>
${isArtOwned(a.key) ? `<span class="badge owned" style="margin-left:6px;">Owned</span>` : ``}
</div>
`;
card.onclick = () => openArtModal(a.key);
resultWrap.appendChild(card);
});
showHomeMessage(`Gacha result: ${pulled.map(a => a.rarity + " " + a.key).join(", ")}`);
saveLocalData();
renderArtShop();
renderArtGallery();
}
function rollOneArt() {
const r = Math.random() * 100;
let rarity = (r < GACHA_RATE_UR) ? "UR" : "SSR";
const pool = ART_LIST.filter(a => a.rarity === rarity);
// 念のため
if (pool.length === 0) return ART_LIST[Math.floor(Math.random() * ART_LIST.length)];
return pool[Math.floor(Math.random() * pool.length)];
}
// -------------------------------------------
// アートプレビュー・モーダル
// -------------------------------------------
let pendingArtKey = null;
function openArtModal(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
pendingArtKey = artKey;
document.getElementById("art-modal-title").textContent = `${art.name} (${art.rarity})`;
document.getElementById("art-modal-img").src = art.url;
const owned = isArtOwned(art.key);
document.getElementById("art-modal-desc").textContent =
owned ? "Owned: You can set this as your character art." : "Not owned: Buy it in Store or pull it from Gacha.";
const setBtn = document.getElementById("art-modal-set-btn");
setBtn.disabled = !owned;
if (!owned) setBtn.classList.add("disabled");
else setBtn.classList.remove("disabled");
document.getElementById("art-modal-bg").style.display = "flex";
}
function closeArtModal() {
document.getElementById("art-modal-bg").style.display = "none";
pendingArtKey = null;
}
function confirmSetPortrait() {
if (!pendingArtKey) return;
setPortraitFromArt(pendingArtKey);
closeArtModal();
}
// -------------------------------------------
// クラフト関連
// -------------------------------------------
function toggleCraftSelection(invIndex) {
const item = inventory[invIndex];
if (selectedForCraft.includes(invIndex)) {
selectedForCraft = selectedForCraft.filter(i => i !== invIndex);
} else {
if (selectedForCraft.length >= 2) {
showHomeMessage("You can only select up to 2 items for crafting.");
return;
}
selectedForCraft.push(invIndex);
}
updateCraftSelectionDisplay();
}
function updateCraftSelectionDisplay() {
let names = selectedForCraft.map(i => inventory[i].name);
if (names.length === 0) names.push("None");
document.getElementById("craft-selection").textContent = names.join(" & ");
document.getElementById("craft-button").disabled = (selectedForCraft.length < 2);
}
function attemptCraft() {
if (selectedForCraft.length < 2) return;
let itemA = inventory[selectedForCraft[0]];
let itemB = inventory[selectedForCraft[1]];
let combo = [itemA.name, itemB.name].sort();
let craftedItem = null;
for (let r of craftRecipes) {
if (r.components[0] === combo[0] && r.components[1] === combo[1]) {
craftedItem = r.result;
break;
}
}
if (!craftedItem) {
showHomeMessage("No valid recipe found for these items.");
selectedForCraft = [];
updateCraftSelectionDisplay();
return;
}
let idxA = Math.max(selectedForCraft[0], selectedForCraft[1]);
let idxB = Math.min(selectedForCraft[0], selectedForCraft[1]);
inventory.splice(idxA, 1);
inventory.splice(idxB, 1);
inventory.push(craftedItem);
showHomeMessage(`You crafted: ${craftedItem.name}!`);
selectedForCraft = [];
updateCraftSelectionDisplay();
updateInventoryDisplay();
saveLocalData();
}
// -------------------------------------------
// スキル
// -------------------------------------------
function refreshSkillList() {
document.getElementById("skill-point-display").textContent = character.skillPoints;
const listElm = document.getElementById("skill-list");
listElm.innerHTML = "";
for (let sKey in skills) {
let sk = skills[sKey];
let li = document.createElement("li");
li.innerHTML = `
<strong>${sk.name} (Lv${sk.level}/${sk.maxLevel})</strong>
- ${sk.description}
${
sk.level < sk.maxLevel
? `(<button onclick="learnSkill('${sKey}')">Upgrade (cost ${sk.cost})</button>)`
: ''
}
`;
listElm.appendChild(li);
}
}
function learnSkill(skillKey) {
let skill = skills[skillKey];
if (!skill) return;
if (skill.level >= skill.maxLevel) {
showHomeMessage(`${skill.name} is already at max level.`);
return;
}
if (character.skillPoints < skill.cost) {
showHomeMessage(`Not enough skill points to upgrade ${skill.name}.`);
return;
}
character.skillPoints -= skill.cost;
skill.level++;
showHomeMessage(`You upgraded ${skill.name} to level ${skill.level}.`);
updateCharacterInfo();
refreshSkillList();
}
// -------------------------------------------
// クエストログ
// -------------------------------------------
function updateQuestLog() {
const logElm = document.getElementById("quest-log-list");
logElm.innerHTML = "";
let allQ = getAllQuests();
for (let key in allQ) {
let q = allQ[key];
let status = q.isCompleted ? "Completed" : (q.isRunning ? "In Progress" : "Not Started");
let li = document.createElement("li");
li.textContent = `${q.name}: ${status}`;
logElm.appendChild(li);
}
}
// -------------------------------------------
// 実績
// -------------------------------------------
function checkAchievements() {
if (character.level >= 5) achievements.level5.isUnlocked = true;
let completedCount = 0;
let allQ = getAllQuests();
for (let key in allQ) if (allQ[key].isCompleted) completedCount++;
if (completedCount >= 3) achievements.quest3.isUnlocked = true;
saveLocalData();
updateAchievementList();
}
function updateAchievementList() {
const listElm = document.getElementById("achievement-list");
listElm.innerHTML = "";
for (let aKey in achievements) {
let a = achievements[aKey];
let status = a.isUnlocked ? "Unlocked" : "Locked";
let li = document.createElement("li");
li.textContent = `${a.name} - ${a.description} [${status}]`;
listElm.appendChild(li);
}
}
// -------------------------------------------
// BGM(継続再生 & 状態保存 & 復帰)
// -------------------------------------------
let isMusicPlaying = false;
let bgmUnlocked = false;
let wantAutoResume = false;
function setBgmStatus(text) {
const s = document.getElementById("bgm-status");
if (s) s.textContent = "Status: " + text;
}
function saveBgmState() {
const audio = document.getElementById("bgm");
if (!audio) return;
const state = {
unlocked: bgmUnlocked,
playing: isMusicPlaying,
volume: audio.volume,
time: audio.currentTime
};
localStorage.setItem(LS_KEY_BGM, JSON.stringify(state));
}
function loadBgmState() {
const audio = document.getElementById("bgm");
if (!audio) return;
const raw = localStorage.getItem(LS_KEY_BGM);
if (!raw) return;
try {
const st = JSON.parse(raw);
bgmUnlocked = !!st.unlocked;
isMusicPlaying = !!st.playing;
wantAutoResume = isMusicPlaying;
if (typeof st.volume === "number") audio.volume = st.volume;
if (typeof st.time === "number") {
audio.addEventListener("loadedmetadata", () => {
try { audio.currentTime = Math.max(0, st.time); } catch(e) {}
}, { once: true });
}
} catch(e) {}
}
function wireBgmAutoSave() {
const audio = document.getElementById("bgm");
if (!audio) return;
audio.addEventListener("play", () => { isMusicPlaying = true; saveBgmState(); });
audio.addEventListener("pause", () => { isMusicPlaying = false; saveBgmState(); });
audio.addEventListener("volumechange", saveBgmState);
let lastSave = 0;
audio.addEventListener("timeupdate", () => {
const now = Date.now();
if (now - lastSave > 4000) {
lastSave = now;
saveBgmState();
}
});
}
function updateEnableBtn() {
const btn = document.getElementById("bgm-enable-btn");
if (!btn) return;
if (bgmUnlocked) {
btn.textContent = "BGM Enabled";
btn.classList.add("disabled");
btn.disabled = true;
} else {
btn.textContent = "Enable BGM (First Click)";
btn.classList.remove("disabled");
btn.disabled = false;
}
}
function resumeBgmOnNextUserActionIfNeeded() {
if (!bgmUnlocked || !wantAutoResume) return;
const audio = document.getElementById("bgm");
if (!audio) return;
const resumeOnce = () => {
audio.play().then(() => {
isMusicPlaying = true;
wantAutoResume = false;
setBgmStatus("On (Resumed)");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked (Enable again)");
});
};
document.addEventListener("pointerdown", resumeOnce, { once: true });
document.addEventListener("keydown", resumeOnce, { once: true });
}
function enableBGM() {
const audio = document.getElementById("bgm");
if (!audio) return;
audio.volume = 0.6;
audio.play().then(() => {
bgmUnlocked = true;
isMusicPlaying = true;
wantAutoResume = false;
updateEnableBtn();
setBgmStatus("On");
showHomeMessage("BGM Enabled & Playing");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked (Click again)");
showHomeMessage("BGM blocked by browser. Click Enable BGM again.");
});
}
function toggleMusic() {
const audio = document.getElementById("bgm");
if (!audio) return;
if (!bgmUnlocked) {
showHomeMessage("First, click 'Enable BGM (First Click)'.");
setBgmStatus("Locked");
return;
}
if (!isMusicPlaying) {
audio.play().then(() => {
isMusicPlaying = true;
setBgmStatus("On");
showHomeMessage("Music On");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked");
showHomeMessage("Music could not be played (browser block).");
});
} else {
audio.pause();
isMusicPlaying = false;
setBgmStatus("Off");
showHomeMessage("Music Off");
saveBgmState();
}
}
// -------------------------------------------
// 汎用ランダム整数
// -------------------------------------------
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
</script>
</body>
</html>

