<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D日本庭園:大鬼決戦</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body { margin: 0; overflow: hidden; background: #000; touch-action: none; user-select: none; }
#ui-layer {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; display: none; flex-direction: column;
justify-content: space-between; padding: 20px;
color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
font-family: 'serif';
}
.gauge-container {
width: 200px; height: 15px; background: rgba(0,0,0,0.5);
border-radius: 10px; overflow: hidden; border: 2px solid #d4af37;
box-shadow: 0 0 15px rgba(212, 175, 55, 0.5);
}
#power-fill { width: 0%; height: 100%; background: linear-gradient(90deg, #d4af37, #ff4500); }
.overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85); display: flex; align-items: center;
justify-content: center; z-index: 200; pointer-events: auto;
}
.modal-content {
background: rgba(20, 20, 20, 0.95); color: #e0e0e0; padding: 40px;
border: 2px solid #b22222; border-radius: 2px; text-align: center;
box-shadow: 0 0 50px rgba(178, 34, 34, 0.3); max-width: 90%;
}
.char-card {
cursor: pointer; transition: all 0.3s;
border: 1px solid #444; padding: 20px; border-radius: 4px; background: rgba(255,255,255,0.05);
}
.char-card:hover { transform: translateY(-10px); border-color: #d4af37; background: rgba(212, 175, 55, 0.1); }
.btn {
background: #b22222; color: white; padding: 12px 40px;
border-radius: 2px; font-weight: bold; cursor: pointer; border: none;
display: inline-block; margin-top: 20px; letter-spacing: 0.2em;
}
#vignette {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; transition: opacity 0.3s;
box-shadow: inset 0 0 150px rgba(255, 0, 0, 0); z-index: 100;
}
.critical-flash { animation: flash-red 0.5s infinite alternate; }
@keyframes flash-red {
from { box-shadow: inset 0 0 100px rgba(255, 0, 0, 0.3); }
to { box-shadow: inset 0 0 200px rgba(255, 0, 0, 0.6); }
}
</style>
</head>
<body>
<div id="vignette"></div>
<div id="char-select-overlay" class="overlay">
<div class="modal-content">
<h1 class="text-4xl font-black mb-8 border-b border-red-900 pb-4 italic tracking-widest text-red-600">血戦:大鬼の庭園</h1>
<p class="mb-8 text-gray-400">大鬼を討つための器を選べ</p>
<div class="grid grid-cols-3 gap-6">
<div class="char-card" onclick="selectCharacter('swordsman')">
<div class="w-16 h-20 bg-blue-900 mx-auto mb-4 rounded relative border border-blue-400">
<div class="absolute top-1 w-8 h-8 bg-orange-200 rounded-full left-1/2 -translate-x-1/2"></div>
</div>
<p class="font-bold text-blue-400">剣士</p>
</div>
<div class="char-card" onclick="selectCharacter('shrine_maiden')">
<div class="w-16 h-20 bg-red-800 mx-auto mb-4 rounded relative border border-red-200">
<div class="absolute top-1 w-8 h-8 bg-orange-200 rounded-full left-1/2 -translate-x-1/2"></div>
</div>
<p class="font-bold text-red-400">巫女</p>
</div>
<div class="char-card" onclick="selectCharacter('ninja')">
<div class="w-16 h-20 bg-gray-900 mx-auto mb-4 rounded relative border border-purple-900">
<div class="absolute top-1 w-8 h-8 bg-gray-800 rounded-full left-1/2 -translate-x-1/2"></div>
</div>
<p class="font-bold text-purple-400">忍者</p>
</div>
</div>
</div>
</div>
<div id="ui-layer">
<div class="flex justify-between items-start w-full px-10 pt-5">
<div class="w-1/3">
<div id="player-name" class="text-lg font-bold tracking-tighter text-blue-400 uppercase">PLAYER</div>
<div class="w-full h-3 bg-gray-900 border border-gray-700 rounded-sm overflow-hidden">
<div id="player-hp" class="h-full bg-blue-500 w-full transition-all duration-300"></div>
</div>
</div>
<div class="w-2/5 text-right">
<div class="text-lg font-bold tracking-tighter text-red-500 uppercase">DAIONI - THE BOSS</div>
<div class="w-full h-4 bg-gray-900 border border-red-900 rounded-sm overflow-hidden relative">
<div id="oni-hp" class="h-full bg-gradient-to-r from-red-800 to-red-600 w-full transition-all duration-300"></div>
</div>
<div id="hp-text" class="text-[10px] text-red-400 font-bold mt-1">HP: 200 / 200</div>
</div>
</div>
<div class="flex flex-col items-center mb-16">
<div id="turn-indicator" class="text-4xl font-black mb-4 italic tracking-widest uppercase">BOSS BATTLE</div>
<div class="gauge-container">
<div id="power-fill"></div>
</div>
<div class="text-[10px] mt-2 font-bold text-gray-400 tracking-widest uppercase">Space / Mouse to Slay</div>
</div>
</div>
<div id="result-overlay" class="overlay" style="display: none;">
<div class="modal-content">
<h2 id="msg-title" class="text-5xl font-black mb-4 italic"></h2>
<p id="msg-body" class="mb-8 text-xl text-gray-400"></p>
<button class="btn" onclick="location.reload()">再戦に挑む</button>
</div>
</div>
<script>
let scene, camera, renderer, clock;
let player, oni, ground;
let balls = [];
let particles = [];
let screenShake = 0;
const characterData = {
swordsman: { name: "剣士", color: 0x001d3d, ballColor: 0x00b4d8, trail: 0x90e0ef, skin: 0xffdbac },
shrine_maiden: { name: "巫女", color: 0x780000, ballColor: 0xff4d6d, trail: 0xffb3c1, skin: 0xffdbac },
ninja: { name: "忍者", color: 0x1a1a1a, ballColor: 0x9d4edd, trail: 0xe0aaff, skin: 0x5d4037 }
};
const gameState = {
selectedChar: null,
currentTurn: 'player',
isCharging: false,
chargePower: 0,
playerHp: 100,
oniHpMax: 200,
oniHp: 200,
isGameOver: false,
mouse: new THREE.Vector2(),
gravity: -20.0
};
const UI = {
layer: document.getElementById('ui-layer'),
playerHp: document.getElementById('player-hp'),
playerName: document.getElementById('player-name'),
oniHp: document.getElementById('oni-hp'),
hpText: document.getElementById('hp-text'),
power: document.getElementById('power-fill'),
turn: document.getElementById('turn-indicator'),
resultOverlay: document.getElementById('result-overlay'),
charSelectOverlay: document.getElementById('char-select-overlay'),
msgTitle: document.getElementById('msg-title'),
msgBody: document.getElementById('msg-body'),
vignette: document.getElementById('vignette')
};
function selectCharacter(type) {
gameState.selectedChar = characterData[type];
UI.charSelectOverlay.style.display = 'none';
UI.layer.style.display = 'flex';
UI.playerName.innerText = gameState.selectedChar.name;
initGame();
}
function initGame() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510);
scene.fog = new THREE.Fog(0x050510, 30, 120);
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 12);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
clock = new THREE.Clock();
// Lights
scene.add(new THREE.AmbientLight(0x202020, 0.5));
const mainLight = new THREE.DirectionalLight(0xff0000, 0.6);
mainLight.position.set(15, 30, 15);
mainLight.castShadow = true;
scene.add(mainLight);
const bossLight = new THREE.PointLight(0xff0000, 2, 30);
bossLight.position.set(0, 5, -35);
scene.add(bossLight);
// Ground
const groundGeo = new THREE.PlaneGeometry(500, 500);
const groundMat = new THREE.MeshPhongMaterial({ color: 0x0a0a0a, shininess: 5 });
ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Characters
player = createHumanoid(gameState.selectedChar, false);
player.position.set(0, 0, 10);
scene.add(player);
// Create BOSS ONI
oni = createBossOni();
oni.position.set(0, 0, -40);
scene.add(oni);
addDecorations();
window.addEventListener('mousedown', startCharging);
window.addEventListener('mouseup', releaseCharging);
window.addEventListener('keydown', (e) => { if (e.code === 'Space') startCharging(); });
window.addEventListener('keyup', (e) => { if (e.code === 'Space') releaseCharging(); });
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('resize', onWindowResize);
animate();
}
function createHumanoid(data, isOni) {
const group = new THREE.Group();
const matBody = new THREE.MeshPhongMaterial({ color: data.color });
const matSkin = new THREE.MeshPhongMaterial({ color: data.skin });
const torso = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.8, 0.7), matBody);
torso.position.y = 1.9;
torso.castShadow = true;
group.add(torso);
const head = new THREE.Mesh(new THREE.BoxGeometry(0.85, 0.85, 0.85), matSkin);
head.position.y = 3.2;
head.castShadow = true;
group.add(head);
return group;
}
function createBossOni() {
const group = new THREE.Group();
const matBody = new THREE.MeshPhongMaterial({ color: 0x400000 });
const matSkin = new THREE.MeshPhongMaterial({ color: 0x111111 });
// Massive Torso
const torso = new THREE.Mesh(new THREE.BoxGeometry(4, 5, 2.5), matBody);
torso.position.y = 4.5;
torso.castShadow = true;
group.add(torso);
// Massive Head
const head = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), matSkin);
head.position.y = 8.5;
head.castShadow = true;
group.add(head);
// BOSS Horn (Massive One Horn)
const hGeo = new THREE.ConeGeometry(0.5, 3, 4);
const hMat = new THREE.MeshPhongMaterial({color: 0x000000, emissive: 0x330000});
const horn = new THREE.Mesh(hGeo, hMat);
horn.position.set(0, 11, 0.5);
horn.rotation.x = -0.3;
group.add(horn);
// Glowing Red Eyes
const eyeGeo = new THREE.SphereGeometry(0.3, 16, 16);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const e1 = new THREE.Mesh(eyeGeo, eyeMat); e1.position.set(-0.6, 8.8, 1.1); group.add(e1);
const e2 = new THREE.Mesh(eyeGeo, eyeMat); e2.position.set(0.6, 8.8, 1.1); group.add(e2);
// Eye Flare
const flare = new THREE.PointLight(0xff0000, 5, 15);
flare.position.set(0, 8.8, 1.5);
group.add(flare);
// Massive Arms
const armGeo = new THREE.BoxGeometry(1.2, 4, 1.2);
const armL = new THREE.Mesh(armGeo, matBody); armL.position.set(-2.8, 5, 0); group.add(armL);
const armR = new THREE.Mesh(armGeo, matBody); armR.position.set(2.8, 5, 0); group.add(armR);
return group;
}
function addDecorations() {
const torii = new THREE.Group();
const mat = new THREE.MeshPhongMaterial({ color: 0x220000 });
const p1 = new THREE.Mesh(new THREE.BoxGeometry(1.5, 25, 1.5), mat); p1.position.set(-15, 12.5, 0); torii.add(p1);
const p2 = new THREE.Mesh(new THREE.BoxGeometry(1.5, 25, 1.5), mat); p2.position.set(15, 12.5, 0); torii.add(p2);
const top = new THREE.Mesh(new THREE.BoxGeometry(40, 2, 2), mat); top.position.y = 25; torii.add(top);
torii.position.set(0, 0, -60);
scene.add(torii);
// Red Mist Ground
const circle = new THREE.Mesh(
new THREE.CircleGeometry(60, 32),
new THREE.MeshBasicMaterial({ color: 0x220000, transparent: true, opacity: 0.3 })
);
circle.rotation.x = -Math.PI/2;
circle.position.y = 0.02;
scene.add(circle);
}
function startCharging() {
if (gameState.isGameOver || gameState.currentTurn !== 'player' || balls.some(b => b.active) || gameState.isCharging) return;
gameState.isCharging = true;
gameState.chargePower = 0;
}
function releaseCharging() {
if (!gameState.isCharging) return;
gameState.isCharging = false;
const power = 20 + (gameState.chargePower / 100) * 50;
const aimX = (gameState.mouse.x) * 15;
throwBall(player.position.clone().add(new THREE.Vector3(0, 3, -1)), new THREE.Vector3(aimX, power * 0.35, -power), 'player');
screenShake = 0.4;
gameState.chargePower = 0;
updateUI();
}
function onMouseMove(event) {
gameState.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
gameState.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
function throwBall(pos, vel, owner) {
const isBoss = owner === 'oni';
const size = isBoss ? 1.5 : 0.6;
const geo = new THREE.SphereGeometry(size, 16, 16);
const color = owner === 'player' ? gameState.selectedChar.ballColor : 0xff0000;
const mat = new THREE.MeshStandardMaterial({
color, emissive: color, emissiveIntensity: isBoss ? 4 : 2, roughness: 0, metalness: 1
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
mesh.castShadow = true;
scene.add(mesh);
const light = new THREE.PointLight(color, isBoss ? 4 : 2, 15);
mesh.add(light);
balls.push({ mesh, velocity: vel, owner, active: true, life: 0, color });
}
function oniTurn() {
if (gameState.isGameOver) return;
setTimeout(() => {
const distZ = Math.abs(player.position.z - oni.position.z);
const vz = distZ * 1.2;
const vy = 12 + Math.random() * 12;
const vx = (player.position.x - oni.position.x) + (Math.random() - 0.5) * 8;
throwBall(oni.position.clone().add(new THREE.Vector3(0, 7, 3)), new THREE.Vector3(vx, vy, vz), 'oni');
screenShake = 1.0; // BOSS THROW SHAKE
}, 1000);
}
function update() {
if (!gameState.selectedChar) return;
const dt = clock.getDelta();
const time = clock.getElapsedTime();
if (gameState.isCharging) {
gameState.chargePower = Math.min(gameState.chargePower + 85 * dt, 100);
updateUI();
}
if (screenShake > 0) {
camera.position.x += (Math.random() - 0.5) * screenShake;
camera.position.y += (Math.random() - 0.5) * screenShake;
screenShake *= 0.92;
}
balls.forEach(ball => {
if (ball.active) {
if (Math.random() > 0.2) createParticle(ball.mesh.position, ball.color, 0.5);
}
});
balls.forEach((ball, idx) => {
if (!ball.active) return;
ball.velocity.y += gameState.gravity * dt;
ball.mesh.position.add(ball.velocity.clone().multiplyScalar(dt));
ball.life += dt;
if (ball.mesh.position.y < (ball.owner === 'oni' ? 1.5 : 0.6)) {
ball.mesh.position.y = (ball.owner === 'oni' ? 1.5 : 0.6);
ball.velocity.y *= -0.3;
if (Math.abs(ball.velocity.y) < 1.5) deactivateBall(ball);
}
const target = ball.owner === 'player' ? oni : player;
const hitRadius = ball.owner === 'player' ? 5.0 : 2.5; // Boss has bigger hit box
const targetBox = target.position.clone().add(new THREE.Vector3(0, ball.owner === 'player' ? 5 : 2, 0));
if (ball.mesh.position.distanceTo(targetBox) < hitRadius) {
if (ball.owner === 'player') {
gameState.oniHp -= 20;
} else {
gameState.playerHp -= 40; // Boss deals more damage
triggerDamageUI();
}
createImpact(ball.mesh.position, ball.color, ball.owner === 'oni' ? 80 : 40);
screenShake = ball.owner === 'oni' ? 1.5 : 0.8;
deactivateBall(ball);
checkGameOver();
}
if (ball.mesh.position.z < -120 || ball.mesh.position.z > 60 || ball.life > 6) deactivateBall(ball);
});
particles.forEach((p, idx) => {
p.mesh.position.add(p.vel);
p.life -= dt * 2;
p.mesh.scale.setScalar(p.life);
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(idx, 1);
}
});
// Camera behavior
let targetCamPos;
if (gameState.currentTurn === 'player') {
const zoom = gameState.isCharging ? 4 : 0;
targetCamPos = new THREE.Vector3(gameState.mouse.x * 3, 6, 20 - zoom);
} else {
targetCamPos = new THREE.Vector3(0, 10, -15);
}
camera.position.lerp(targetCamPos, 0.08);
camera.lookAt(0, 4, -15);
// Boss Aura
if (Math.random() > 0.5) createParticle(oni.position.clone().add(new THREE.Vector3((Math.random()-0.5)*8, Math.random()*10, (Math.random()-0.5)*4)), 0xff0000, 0.3);
}
function createParticle(pos, color, life = 1.0) {
const p = new THREE.Mesh(
new THREE.BoxGeometry(0.3, 0.3, 0.3),
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.8 })
);
p.position.copy(pos).add(new THREE.Vector3((Math.random()-0.5), (Math.random()-0.5), (Math.random()-0.5)));
scene.add(p);
particles.push({
mesh: p,
vel: new THREE.Vector3((Math.random()-0.5)*0.15, Math.random()*0.15, (Math.random()-0.5)*0.15),
life: life
});
}
function createImpact(pos, color, count) {
for (let i = 0; i < count; i++) {
const p = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), new THREE.MeshStandardMaterial({ color, emissive: color }));
p.position.copy(pos);
scene.add(p);
particles.push({
mesh: p,
vel: new THREE.Vector3((Math.random()-0.5)*1.5, (Math.random()-0.5)*1.5, (Math.random()-0.5)*1.5),
life: 2.0
});
}
}
function triggerDamageUI() {
UI.vignette.style.opacity = '1';
UI.vignette.style.boxShadow = 'inset 0 0 150px rgba(255, 0, 0, 0.9)';
setTimeout(() => { if (gameState.playerHp > 30) UI.vignette.style.opacity = '0'; }, 500);
}
function deactivateBall(ball) {
if (!ball.active) return;
ball.active = false;
setTimeout(() => {
scene.remove(ball.mesh);
balls = balls.filter(b => b !== ball);
if (balls.length === 0 && !gameState.isGameOver) switchTurn();
}, 200);
}
function switchTurn() {
gameState.currentTurn = gameState.currentTurn === 'player' ? 'oni' : 'player';
updateUI();
if (gameState.currentTurn === 'oni') oniTurn();
}
function checkGameOver() {
updateUI();
if (gameState.oniHp <= 0) endGame("大金星", "伝説の大鬼を討ち果たしたり!");
else if (gameState.playerHp <= 0) endGame("力及ばず", "大鬼の圧倒的な力の前に沈む...");
if (gameState.playerHp <= 30) UI.vignette.classList.add('critical-flash');
}
function endGame(title, body) {
gameState.isGameOver = true;
UI.msgTitle.innerText = title;
UI.msgBody.innerText = body;
UI.resultOverlay.style.display = 'flex';
}
function updateUI() {
UI.playerHp.style.width = gameState.playerHp + '%';
UI.oniHp.style.width = (gameState.oniHp / gameState.oniHpMax * 100) + '%';
UI.hpText.innerText = `HP: ${Math.max(0, gameState.oniHp)} / ${gameState.oniHpMax}`;
UI.power.style.width = gameState.chargePower + '%';
UI.turn.innerText = gameState.currentTurn === 'player' ? "好機!撃て!" : "回避せよ!";
UI.turn.style.color = gameState.currentTurn === 'player' ? "#00b4d8" : "#ff0000";
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
update();
if (renderer) renderer.render(scene, camera);
}
</script>
</body>
</html>
スペースキーでボールを投げれます