Blood Eclipse
by FrozenGecko131555 lines59.4 KB🛠️ Three.js (3D graphics)
file:///data/user/0/com.foxdebug.acodefree/files/public/index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#000000" />
<title>Blood Eclipse</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; user-select:none; }
html,body { width:100%; height:100%; overflow:hidden; background:#000; font-family:'Georgia',serif; color:#c00; }
#game { position:fixed; inset:0; touch-action:none; }
canvas { display:block; }
#loading {
position:fixed; inset:0; background:#050000;
display:flex; flex-direction:column; align-items:center; justify-content:center;
z-index:100; transition:opacity 1s;
}
#startBtn {
padding:1em 2.5em; font-size:1.2em; letter-spacing:0.3em;
background:linear-gradient(#1a0000,#0a0000); color:#c22;
border:1px solid #500; border-radius:2px;
font-family:'Georgia',serif; cursor:pointer;
box-shadow:0 0 30px rgba(120,0,0,0.5), inset 0 0 20px rgba(60,0,0,0.5);
transition:all 0.3s;
}
#startBtn:active { color:#f33; border-color:#900; box-shadow:0 0 40px rgba(180,0,0,0.8), inset 0 0 30px rgba(120,0,0,0.6); }
#hud { position:fixed; inset:0; pointer-events:none; z-index:10; display:none; }
#joystick {
position:absolute; left:20px; bottom:30px;
width:130px; height:130px; border-radius:50%;
background:radial-gradient(circle, rgba(40,0,0,0.35), rgba(10,0,0,0.6));
border:2px solid rgba(120,20,20,0.5);
pointer-events:auto; touch-action:none;
box-shadow:0 0 20px rgba(80,0,0,0.6) inset;
}
#stick {
position:absolute; left:50%; top:50%; width:55px; height:55px; border-radius:50%;
background:radial-gradient(circle, rgba(200,40,40,0.85), rgba(80,0,0,0.9));
transform:translate(-50%,-50%);
box-shadow:0 0 15px rgba(200,0,0,0.7);
}
#attackBtn {
position:absolute; right:25px; bottom:45px;
width:110px; height:110px; border-radius:50%;
background:radial-gradient(circle, rgba(90,0,0,0.6), rgba(20,0,0,0.8));
border:2px solid rgba(180,30,30,0.7);
color:#d33; font-size:0.85em; letter-spacing:0.15em; font-weight:bold;
display:flex; align-items:center; justify-content:center;
pointer-events:auto; touch-action:none;
box-shadow:0 0 25px rgba(150,0,0,0.5), inset 0 0 20px rgba(80,0,0,0.5);
text-shadow:0 0 8px #800;
}
#attackBtn:active { background:radial-gradient(circle, rgba(180,20,20,0.8), rgba(60,0,0,0.9)); transform:scale(0.95); }
#attackBtn.disabled { opacity:0.5; }
#rageBtn {
position:absolute; right:60px; bottom:175px;
width:85px; height:85px; border-radius:50%;
background:radial-gradient(circle, rgba(120,0,0,0.7), rgba(20,0,0,0.9));
border:3px solid rgba(200,40,40,0.9);
color:#ff3300; font-size:0.8em; letter-spacing:0.15em; font-weight:bold;
display:flex; align-items:center; justify-content:center;
pointer-events:auto; touch-action:none;
box-shadow:0 0 30px rgba(200,0,0,0.6), inset 0 0 25px rgba(120,0,0,0.7);
text-shadow:0 0 10px #f00, 0 0 20px #800;
transition:all 0.3s;
animation: ragePulse 2s ease-in-out infinite;
}
#rageBtn:active { transform:scale(0.9); }
#rageBtn.on-cooldown {
background:radial-gradient(circle, rgba(40,20,20,0.5), rgba(10,0,0,0.8));
border-color:rgba(80,40,40,0.5);
color:#644; box-shadow:none; animation:none;
}
#rageBtn.active {
background:radial-gradient(circle, rgba(255,40,0,0.95), rgba(120,0,0,0.9));
border-color:#ff5522; color:#fff;
box-shadow:0 0 60px rgba(255,60,0,1), inset 0 0 40px rgba(255,80,0,0.9);
animation: rageActive 0.15s ease-in-out infinite alternate;
}
@keyframes ragePulse {
0%,100% { box-shadow:0 0 30px rgba(200,0,0,0.6), inset 0 0 25px rgba(120,0,0,0.7); }
50% { box-shadow:0 0 45px rgba(255,20,0,0.9), inset 0 0 30px rgba(180,0,0,0.9); }
}
@keyframes rageActive { from { transform:scale(1); } to { transform:scale(1.08); } }
#bloodOverlay {
position:absolute; inset:0; pointer-events:none;
background:radial-gradient(circle at 50% 50%, transparent 40%, rgba(60,0,0,0.35) 100%);
mix-blend-mode:multiply;
}
#hurtFlash {
position:absolute; inset:0; pointer-events:none; opacity:0;
background:radial-gradient(circle, transparent 30%, rgba(180,0,0,0.85));
transition:opacity 0.1s;
}
#rageOverlay {
position:absolute; inset:0; pointer-events:none; opacity:0;
background:radial-gradient(circle at 50% 50%, rgba(255,40,0,0.15) 0%, rgba(120,0,0,0.5) 70%, rgba(60,0,0,0.8) 100%);
transition:opacity 0.4s;
mix-blend-mode:screen;
}
#rageOverlay.on { opacity:1; animation: rageScreenPulse 0.4s ease-in-out infinite alternate; }
@keyframes rageScreenPulse {
from { filter:brightness(0.9) saturate(1); }
to { filter:brightness(1.3) saturate(1.4); }
}
#hpBarContainer {
position:absolute; top:20px; left:20px;
width:55%; max-width:260px; height:22px;
background: rgba(20,0,0,0.85);
border: 2px solid rgba(120,20,20,0.8);
border-radius: 4px;
box-shadow: 0 0 15px rgba(80,0,0,0.7) inset, 0 0 12px rgba(0,0,0,0.8);
overflow: hidden; pointer-events: none;
}
#hpBarFill {
height: 100%; width: 100%;
background: linear-gradient(180deg, #44ff44, #00aa00);
transition: width 0.35s ease-out, background 0.4s;
box-shadow: 0 0 10px rgba(0,255,0,0.6) inset;
}
#hpBarContainer::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(255,255,255,0.15), transparent 50%, rgba(0,0,0,0.3));
pointer-events: none;
}
#killsDisplay {
position:absolute; top: 52px; left: 20px;
color:#d33; font-size: 0.75em; letter-spacing: 0.15em;
text-shadow: 0 0 10px #500;
}
#rageInfo {
position:absolute; top: 75px; left: 20px;
color:#f60; font-size: 0.75em; letter-spacing: 0.15em;
font-weight: bold; text-shadow: 0 0 10px #f30;
}
#error {
position:fixed; inset:0; background:#000; color:#f33;
display:none; align-items:center; justify-content:center;
padding:20px; font-size:0.9em; text-align:center; z-index:200;
}
</style>
</head>
<body>
<div id="game"></div>
<div id="hud">
<div id="bloodOverlay"></div>
<div id="hurtFlash"></div>
<div id="rageOverlay"></div>
<div id="hpBarContainer"><div id="hpBarFill"></div></div>
<div id="killsDisplay">KILLS: <span id="kills">0</span></div>
<div id="rageInfo"></div>
<div id="joystick"><div id="stick"></div></div>
<div id="attackBtn">ATTACK</div>
<div id="rageBtn">RAGE</div>
</div>
<div id="loading">
<button id="startBtn">START</button>
</div>
<div id="error"></div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
function lerp(a, b, t) { return a + (b - a) * t; }
window.addEventListener('error', (e) => {
const el = document.getElementById('error');
el.style.display = 'flex';
el.textContent = 'Error: ' + (e.message || e.error);
});
const state = {
started: false,
hp: 100, hpMax: 100,
kills: 0, running: false,
};
const rageState = {
active: false, timer: 0, duration: 8.0,
cooldown: 0, cooldownMax: 15.0,
cameraZoom: 0, eyeIntensity: 0,
};
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.5;
document.getElementById('game').appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x120408);
scene.fog = new THREE.FogExp2(0x150404, 0.007);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1500);
camera.position.set(0, 12, 15);
const ambient = new THREE.AmbientLight(0x442222, 1.8);
scene.add(ambient);
const hemi = new THREE.HemisphereLight(0x663322, 0x220000, 1.4);
scene.add(hemi);
const rim = new THREE.DirectionalLight(0xaa4422, 1.4);
rim.position.set(-50, 40, -30);
scene.add(rim);
const eclipseLight = new THREE.DirectionalLight(0xffaa66, 2.8);
eclipseLight.position.set(120, 180, -260);
eclipseLight.target.position.set(0, 0, 0);
scene.add(eclipseLight.target);
eclipseLight.castShadow = true;
eclipseLight.shadow.mapSize.set(1024, 1024);
eclipseLight.shadow.camera.left = -120;
eclipseLight.shadow.camera.right = 120;
eclipseLight.shadow.camera.top = 120;
eclipseLight.shadow.camera.bottom = -120;
eclipseLight.shadow.camera.far = 400;
eclipseLight.shadow.bias = -0.0008;
scene.add(eclipseLight);
const bloodGlow = new THREE.PointLight(0x880000, 1.6, 45, 2);
bloodGlow.position.set(0, 0.5, 0);
scene.add(bloodGlow);
// =============== ECLIPSE (بزرگتر و درخشانتر) ===============
function buildEclipse() {
const g = new THREE.Group();
// Outer corona (بزرگ)
const coronaCanvas = document.createElement('canvas');
coronaCanvas.width = coronaCanvas.height = 512;
const cx = coronaCanvas.getContext('2d');
const grd = cx.createRadialGradient(256, 256, 30, 256, 256, 256);
grd.addColorStop(0, 'rgba(255,240,200,1.0)');
grd.addColorStop(0.15, 'rgba(255,180,90,0.95)');
grd.addColorStop(0.35, 'rgba(255,100,30,0.7)');
grd.addColorStop(0.6, 'rgba(180,30,0,0.4)');
grd.addColorStop(1, 'rgba(20,0,0,0)');
cx.fillStyle = grd;
cx.fillRect(0, 0, 512, 512);
const coronaTex = new THREE.CanvasTexture(coronaCanvas);
const corona = new THREE.Sprite(new THREE.SpriteMaterial({
map: coronaTex, transparent: true, blending: THREE.AdditiveBlending,
depthWrite: false, fog: false,
}));
corona.scale.set(280, 280, 1);
g.add(corona);
// Small inner bright glow
const innerCanvas = document.createElement('canvas');
innerCanvas.width = innerCanvas.height = 256;
const icx = innerCanvas.getContext('2d');
const igrd = icx.createRadialGradient(128, 128, 20, 128, 128, 128);
igrd.addColorStop(0, 'rgba(255,255,230,1.0)');
igrd.addColorStop(0.4, 'rgba(255,200,120,0.9)');
igrd.addColorStop(1, 'rgba(255,120,30,0)');
icx.fillStyle = igrd;
icx.fillRect(0, 0, 256, 256);
const innerTex = new THREE.CanvasTexture(innerCanvas);
const innerGlow = new THREE.Sprite(new THREE.SpriteMaterial({
map: innerTex, transparent: true, blending: THREE.AdditiveBlending,
depthWrite: false, fog: false,
}));
innerGlow.scale.set(80, 80, 1);
g.add(innerGlow);
// Dark disk (مرکز سیاه)
const disk = new THREE.Mesh(
new THREE.CircleGeometry(32, 64),
new THREE.MeshBasicMaterial({ color: 0x000000, fog: false })
);
g.add(disk);
// Thin bright ring
const ring = new THREE.Mesh(
new THREE.RingGeometry(32, 38, 96),
new THREE.MeshBasicMaterial({
color: 0xfff0cc, transparent: true, opacity: 1.0, fog: false,
blending: THREE.AdditiveBlending, side: THREE.DoubleSide,
})
);
g.add(ring);
// Wide outer thin ring
const ring2 = new THREE.Mesh(
new THREE.RingGeometry(40, 45, 96),
new THREE.MeshBasicMaterial({
color: 0xffaa55, transparent: true, opacity: 0.55, fog: false,
blending: THREE.AdditiveBlending, side: THREE.DoubleSide,
})
);
g.add(ring2);
g.position.set(100, 320, -500);
g.lookAt(0, 0, 0);
return g;
}
const eclipseGroup = buildEclipse();
scene.add(eclipseGroup);
// Floating faces
function buildFloatingFace() {
const canvas = document.createElement('canvas');
canvas.width = 256; canvas.height = 256;
const cx = canvas.getContext('2d');
cx.fillStyle = '#5a0808';
cx.beginPath(); cx.ellipse(128, 128, 95, 120, 0, 0, Math.PI*2); cx.fill();
const g2 = cx.createRadialGradient(100, 100, 20, 128, 128, 140);
g2.addColorStop(0, 'rgba(140,20,20,0.6)');
g2.addColorStop(1, 'rgba(20,0,0,0.9)');
cx.fillStyle = g2;
cx.beginPath(); cx.ellipse(128, 128, 95, 120, 0, 0, Math.PI*2); cx.fill();
cx.fillStyle = '#000';
cx.beginPath(); cx.ellipse(90, 105, 22, 14, -0.1, 0, Math.PI*2); cx.fill();
cx.beginPath(); cx.ellipse(166, 105, 22, 14, 0.1, 0, Math.PI*2); cx.fill();
cx.fillStyle = '#ff1a1a';
cx.beginPath(); cx.arc(90, 105, 6, 0, Math.PI*2); cx.fill();
cx.beginPath(); cx.arc(166, 105, 6, 0, Math.PI*2); cx.fill();
cx.fillStyle = '#0a0000';
cx.beginPath(); cx.ellipse(128, 180, 45, 22, 0, 0, Math.PI*2); cx.fill();
cx.fillStyle = '#7a1010';
for (let i = 0; i < 6; i++) {
const x = 92 + i * 14;
cx.fillRect(x, 165, 6, 12);
cx.fillRect(x, 190, 6, 12);
}
cx.strokeStyle = 'rgba(0,0,0,0.6)';
cx.lineWidth = 2;
for (let i = 0; i < 12; i++) {
cx.beginPath(); cx.moveTo(128, 128);
const a = (i / 12) * Math.PI * 2;
cx.lineTo(128 + Math.cos(a)*90, 128 + Math.sin(a)*110);
cx.stroke();
}
return new THREE.CanvasTexture(canvas);
}
function spawnSkyFaces(count = 24) {
const group = new THREE.Group();
const tex = buildFloatingFace();
const geo = new THREE.PlaneGeometry(1, 1);
for (let i = 0; i < count; i++) {
const mat = new THREE.MeshBasicMaterial({
map: tex, transparent: true, opacity: 0.6 + Math.random()*0.35,
depthWrite: false, fog: false, side: THREE.DoubleSide, color: 0xee4444,
});
const face = new THREE.Mesh(geo, mat);
const angle = Math.random() * Math.PI * 2;
const dist = 150 + Math.random() * 250;
const height = 80 + Math.random() * 150;
face.position.set(Math.cos(angle) * dist, height, Math.sin(angle) * dist - 60);
const s = 20 + Math.random() * 50;
face.scale.set(s, s * 1.2, 1);
face.lookAt(0, height * 0.4, 0);
group.add(face);
}
return group;
}
const skyFaces = spawnSkyFaces(24);
scene.add(skyFaces);
// Terrain
function buildTerrain() {
const size = 260, segs = 96;
const geo = new THREE.PlaneGeometry(size, size, segs, segs);
geo.rotateX(-Math.PI / 2);
const pos = geo.attributes.position;
function heightAt(x, z) {
let h = 0;
h += Math.sin(x * 0.035) * Math.cos(z * 0.032) * 3.2;
h += Math.sin(x * 0.09 + 1.3) * Math.cos(z * 0.07 - 0.5) * 1.4;
h += Math.sin(x * 0.21 + z * 0.18) * 0.5;
const d = Math.sqrt(x*x + z*z);
h -= Math.max(0, 8 - d * 0.15) * 0.6;
return h;
}
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), z = pos.getZ(i);
pos.setY(i, heightAt(x, z));
}
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({
color: 0x3a1010, roughness: 0.9, metalness: 0.05,
emissive: 0x220505, emissiveIntensity: 0.5,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.receiveShadow = true;
mesh.userData.heightAt = heightAt;
return mesh;
}
const terrain = buildTerrain();
scene.add(terrain);
// Blood
function buildBloodSurface() {
const size = 260, segs = 128;
const geo = new THREE.PlaneGeometry(size, size, segs, segs);
geo.rotateX(-Math.PI / 2);
const mat = new THREE.MeshStandardMaterial({
color: 0x5a0808, emissive: 0x3a0000, emissiveIntensity: 0.7,
roughness: 0.4, metalness: 0.6, transparent: true, opacity: 0.92,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.renderOrder = 2;
return mesh;
}
const bloodSurface = buildBloodSurface();
scene.add(bloodSurface);
(function syncBloodToTerrain() {
const pos = bloodSurface.geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), z = pos.getZ(i);
pos.setY(i, terrain.userData.heightAt(x, z) + 0.15);
}
pos.needsUpdate = true;
bloodSurface.geometry.computeVertexNormals();
})();
// Ripples
const MAX_RIPPLES = 12;
const ripples = [];
const rippleGeo = new THREE.RingGeometry(0.4, 0.55, 24);
rippleGeo.rotateX(-Math.PI / 2);
for (let i = 0; i < MAX_RIPPLES; i++) {
const mat = new THREE.MeshBasicMaterial({
color: 0xff2222, transparent: true, opacity: 0,
side: THREE.DoubleSide, depthWrite: false, blending: THREE.AdditiveBlending,
});
const r = new THREE.Mesh(rippleGeo, mat);
r.visible = false;
scene.add(r);
ripples.push({ mesh: r, t: 0, active: false });
}
function spawnRipple(x, y, z, strength = 1) {
for (const r of ripples) {
if (!r.active) {
r.active = true; r.t = 0;
r.maxScale = 1.5 + strength * 2.2;
r.mesh.position.set(x, y + 0.18, z);
r.mesh.scale.set(1, 1, 1);
r.mesh.material.opacity = 0.85 * strength;
r.mesh.visible = true;
return;
}
}
}
function updateRipples(dt) {
for (const r of ripples) {
if (!r.active) continue;
r.t += dt;
const k = r.t / 1.1;
if (k >= 1) { r.active = false; r.mesh.visible = false; r.mesh.material.opacity = 0; continue; }
const s = 1 + k * r.maxScale;
r.mesh.scale.set(s, 1, s);
r.mesh.material.opacity = (1 - k) * 0.85;
}
}
// Giant face mountains
function buildGiantFaceMountain(scale = 1) {
const g = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({
color: 0x4a0808, roughness: 0.95, metalness: 0.1,
emissive: 0x2a0000, emissiveIntensity: 0.7, flatShading: true,
});
const head = new THREE.Mesh(new THREE.IcosahedronGeometry(10, 2), mat);
head.scale.set(1.3, 1.5, 1.0); head.position.y = 12;
head.castShadow = true; head.receiveShadow = true;
g.add(head);
const browMat = new THREE.MeshStandardMaterial({ color: 0x220000, roughness: 1 });
const browL = new THREE.Mesh(new THREE.BoxGeometry(5, 1.6, 3), browMat);
browL.position.set(-4, 15, 7); browL.rotation.z = 0.35; g.add(browL);
const browR = browL.clone(); browR.position.x = 4; browR.rotation.z = -0.35; g.add(browR);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
const eyeL = new THREE.Mesh(new THREE.SphereGeometry(1.6, 12, 12), eyeMat);
eyeL.position.set(-4, 12.5, 8); g.add(eyeL);
const eyeR = eyeL.clone(); eyeR.position.x = 4; g.add(eyeR);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });
const glowL = new THREE.Mesh(new THREE.SphereGeometry(0.8, 12, 12), glowMat);
glowL.position.set(-4, 12.5, 8.7); g.add(glowL);
const glowR = glowL.clone(); glowR.position.x = 4; g.add(glowR);
const nose = new THREE.Mesh(new THREE.ConeGeometry(1.5, 5, 6), browMat);
nose.position.set(0, 9.5, 8.5); nose.rotation.x = Math.PI / 2; g.add(nose);
const mouth = new THREE.Mesh(new THREE.BoxGeometry(9, 2.2, 3), new THREE.MeshBasicMaterial({ color: 0x080000 }));
mouth.position.set(0, 3.5, 7.5); g.add(mouth);
const toothMat = new THREE.MeshStandardMaterial({ color: 0x5a2020, roughness: 0.9 });
for (let i = 0; i < 7; i++) {
const t = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1.6, 5), toothMat);
t.position.set(-3.5 + i * 1.15, 4.7, 8.6); t.rotation.x = Math.PI; g.add(t);
const t2 = t.clone(); t2.position.y = 2.6; t2.rotation.x = 0; g.add(t2);
}
const cheekL = new THREE.Mesh(new THREE.IcosahedronGeometry(3.5, 1), mat);
cheekL.position.set(-6, 7, 6); g.add(cheekL);
const cheekR = cheekL.clone(); cheekR.position.x = 6; g.add(cheekR);
g.scale.setScalar(scale);
return g;
}
[
{ x: -70, z: -60, s: 1.8 }, { x: 85, z: -40, s: 2.1 },
{ x: -90, z: 55, s: 1.6 }, { x: 70, z: 80, s: 1.9 },
{ x: 30, z: -95, s: 1.4 },
].forEach(p => {
const m = buildGiantFaceMountain(p.s);
const y = terrain.userData.heightAt(p.x, p.z);
m.position.set(p.x, y, p.z);
m.rotation.y = Math.atan2(-p.x, -p.z);
scene.add(m);
});
// =============== PLAYER ===============
function buildPlayer() {
const root = new THREE.Group();
const armorMat = new THREE.MeshStandardMaterial({
color: 0x25252a, roughness: 0.55, metalness: 0.8,
emissive: 0x100000, emissiveIntensity: 0.5,
});
const darkMat = new THREE.MeshStandardMaterial({
color: 0x15151a, roughness: 0.85, metalness: 0.5,
});
const accentMat = new THREE.MeshStandardMaterial({
color: 0x3a0000, roughness: 0.5, metalness: 0.8,
emissive: 0x440000, emissiveIntensity: 0.7,
});
const torso = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1.9, 0.9), armorMat);
torso.position.y = 2.3; torso.castShadow = true; root.add(torso);
const chest = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.0, 0.35), darkMat);
chest.position.set(0, 2.6, 0.5); root.add(chest);
const pauldronGeo = new THREE.SphereGeometry(0.65, 12, 10, 0, Math.PI*2, 0, Math.PI/2);
const pL = new THREE.Mesh(pauldronGeo, armorMat);
pL.position.set(-1.05, 3.15, 0); pL.rotation.z = 0.2; pL.castShadow = true; root.add(pL);
const pR = new THREE.Mesh(pauldronGeo, armorMat);
pR.position.set(1.05, 3.15, 0); pR.rotation.z = -0.2; pR.castShadow = true; root.add(pR);
const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.28, 0.4, 8), darkMat);
neck.position.y = 3.35; root.add(neck);
const helmet = new THREE.Mesh(new THREE.SphereGeometry(0.55, 14, 12), armorMat);
helmet.position.y = 3.85; helmet.castShadow = true; root.add(helmet);
const visorMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });
const visor = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.09, 0.35), visorMat);
visor.position.set(0, 3.85, 0.45); root.add(visor);
const jaw = new THREE.Mesh(new THREE.BoxGeometry(0.65, 0.3, 0.55), darkMat);
jaw.position.set(0, 3.55, 0.15); root.add(jaw);
const horn = new THREE.Mesh(new THREE.ConeGeometry(0.1, 0.7, 6), accentMat);
horn.position.set(0, 4.35, 0); root.add(horn);
const eyeLightL = new THREE.PointLight(0xff2200, 0, 12, 2);
eyeLightL.position.set(-0.25, 3.85, 0.6); root.add(eyeLightL);
const eyeLightR = new THREE.PointLight(0xff2200, 0, 12, 2);
eyeLightR.position.set(0.25, 3.85, 0.6); root.add(eyeLightR);
const rageAura = new THREE.PointLight(0xff4400, 0, 25, 2);
rageAura.position.set(0, 2, 0); root.add(rageAura);
const auraCanvas = document.createElement('canvas');
auraCanvas.width = auraCanvas.height = 256;
const actx = auraCanvas.getContext('2d');
const agr = actx.createRadialGradient(128, 128, 10, 128, 128, 128);
agr.addColorStop(0, 'rgba(255,80,0,0.9)');
agr.addColorStop(0.4, 'rgba(255,20,0,0.4)');
agr.addColorStop(1, 'rgba(120,0,0,0)');
actx.fillStyle = agr;
actx.fillRect(0, 0, 256, 256);
const auraTex = new THREE.CanvasTexture(auraCanvas);
const auraSprite = new THREE.Sprite(new THREE.SpriteMaterial({
map: auraTex, transparent: true, opacity: 0,
blending: THREE.AdditiveBlending, depthWrite: false, fog: false,
}));
auraSprite.scale.set(8, 8, 1);
auraSprite.position.set(0, 2.5, 0);
root.add(auraSprite);
// ===== ARMS =====
// ساختار: armRoot (شانه) → upperArm → forearmRoot (آرنج) → foreArm → hand
const armL = new THREE.Group();
armL.position.set(-1.15, 3.0, 0);
root.add(armL);
const upperArmL = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.24, 0.9, 8), armorMat);
upperArmL.position.y = -0.45; upperArmL.castShadow = true;
armL.add(upperArmL);
const forearmL = new THREE.Group();
forearmL.position.y = -0.9;
armL.add(forearmL);
const foreL = new THREE.Mesh(new THREE.CylinderGeometry(0.24, 0.2, 0.8, 8), armorMat);
foreL.position.y = -0.4; foreL.castShadow = true;
forearmL.add(foreL);
const handL = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.35, 0.35), darkMat);
handL.position.y = -0.9;
forearmL.add(handL);
const armR = new THREE.Group();
armR.position.set(1.15, 3.0, 0);
root.add(armR);
const upperArmR = upperArmL.clone();
armR.add(upperArmR);
const forearmR = new THREE.Group();
forearmR.position.y = -0.9;
armR.add(forearmR);
const foreR = foreL.clone();
forearmR.add(foreR);
const handR = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.4, 0.4), darkMat);
handR.position.y = -0.9;
forearmR.add(handR);
// Legs
const legL = new THREE.Group();
legL.position.set(-0.45, 1.35, 0); root.add(legL);
const thighL = new THREE.Mesh(new THREE.CylinderGeometry(0.33, 0.3, 1.0, 8), armorMat);
thighL.position.y = -0.5; thighL.castShadow = true; legL.add(thighL);
const shinL = new THREE.Group();
shinL.position.y = -1.0; legL.add(shinL);
const shinMeshL = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.22, 1.0, 8), armorMat);
shinMeshL.position.y = -0.5; shinMeshL.castShadow = true; shinL.add(shinMeshL);
const footL = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.28, 0.7), darkMat);
footL.position.set(0, -1.1, 0.15); footL.castShadow = true; shinL.add(footL);
const legR = new THREE.Group();
legR.position.set(0.45, 1.35, 0); root.add(legR);
const thighR = thighL.clone(); legR.add(thighR);
const shinR = new THREE.Group();
shinR.position.y = -1.0; legR.add(shinR);
const shinMeshR = shinMeshL.clone(); shinR.add(shinMeshR);
const footR = footL.clone(); shinR.add(footR);
// ===== SWORD (ضخیم) =====
const swordPivot = new THREE.Group();
handR.add(swordPivot);
swordPivot.position.set(0, -0.2, 0);
const bladeMat = new THREE.MeshStandardMaterial({
color: 0x2a2a30, roughness: 0.5, metalness: 0.95,
emissive: 0x181818, emissiveIntensity: 0.5,
});
const blade = new THREE.Mesh(new THREE.BoxGeometry(0.8, 6.5, 0.3), bladeMat);
blade.position.y = -3.4;
blade.castShadow = true;
swordPivot.add(blade);
const edge = new THREE.Mesh(
new THREE.BoxGeometry(0.85, 6.5, 0.06),
new THREE.MeshStandardMaterial({
color: 0x99bbff, roughness: 0.25, metalness: 1.0,
emissive: 0x445588, emissiveIntensity: 0.7,
})
);
edge.position.set(0, -3.4, 0.16);
swordPivot.add(edge);
const guard = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.4, 0.65), darkMat);
guard.position.y = -0.05; swordPivot.add(guard);
const hilt = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.13, 0.95, 8), darkMat);
hilt.position.y = 0.45; swordPivot.add(hilt);
const pommel = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), accentMat);
pommel.position.y = 1.0; swordPivot.add(pommel);
root.userData = {
torso, helmet, armL, armR, forearmL: forearmL, forearmR: forearmR,
legL, legR, shinL, shinR, swordPivot,
visor, visorMat, auraSprite,
eyeLightL, eyeLightR, rageAura,
};
return root;
}
const player = buildPlayer();
scene.add(player);
// =============== GIANT ===============
function buildGiant() {
const root = new THREE.Group();
const fleshMat = new THREE.MeshStandardMaterial({
color: 0x6a1010, roughness: 0.85, metalness: 0.15,
emissive: 0x3a0000, emissiveIntensity: 0.6, flatShading: true,
});
const darkFleshMat = new THREE.MeshStandardMaterial({
color: 0x350404, roughness: 0.9, metalness: 0.1,
});
const torso = new THREE.Mesh(new THREE.BoxGeometry(3.5, 4.5, 2.0), fleshMat);
torso.position.y = 5.5; torso.castShadow = true; root.add(torso);
const chest = new THREE.Mesh(new THREE.IcosahedronGeometry(2.2, 1), fleshMat);
chest.position.set(0, 6.5, 0.3); chest.scale.set(1.1, 0.9, 0.9); root.add(chest);
const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.9, 0.9, 8), darkFleshMat);
neck.position.y = 8.2; root.add(neck);
const head = new THREE.Mesh(new THREE.IcosahedronGeometry(1.3, 1), fleshMat);
head.position.y = 9.4; head.castShadow = true; root.add(head);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff2200 });
const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.18, 8, 8), eyeMat);
eyeL.position.set(-0.5, 9.6, 1.1); root.add(eyeL);
const eyeR = eyeL.clone(); eyeR.position.x = 0.5; root.add(eyeR);
const mouth = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.25, 0.4),
new THREE.MeshBasicMaterial({ color: 0x000000 }));
mouth.position.set(0, 8.9, 1.15); root.add(mouth);
const armL = new THREE.Group();
armL.position.set(-2.2, 7.0, 0); root.add(armL);
const upperL = new THREE.Mesh(new THREE.CylinderGeometry(0.55, 0.5, 2.0, 8), fleshMat);
upperL.position.y = -1.0; upperL.castShadow = true; armL.add(upperL);
const foreL = new THREE.Group();
foreL.position.y = -2.0; armL.add(foreL);
const foreML = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.4, 1.8, 8), fleshMat);
foreML.position.y = -0.9; foreML.castShadow = true; foreL.add(foreML);
const handL = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.9, 0.7), darkFleshMat);
handL.position.y = -2.0; foreL.add(handL);
const armR = new THREE.Group();
armR.position.set(2.2, 7.0, 0); root.add(armR);
const upperR = upperL.clone(); armR.add(upperR);
const foreR = new THREE.Group();
foreR.position.y = -2.0; armR.add(foreR);
const foreMR = foreML.clone(); foreR.add(foreMR);
const handR = handL.clone(); foreR.add(handR);
const legL = new THREE.Group();
legL.position.set(-0.9, 3.5, 0); root.add(legL);
const thighL = new THREE.Mesh(new THREE.CylinderGeometry(0.75, 0.65, 2.2, 8), fleshMat);
thighL.position.y = -1.1; thighL.castShadow = true; legL.add(thighL);
const shinL = new THREE.Group();
shinL.position.y = -2.2; legL.add(shinL);
const shinML = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.5, 2.0, 8), fleshMat);
shinML.position.y = -1.0; shinML.castShadow = true; shinL.add(shinML);
const footL = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.6, 1.6), darkFleshMat);
footL.position.set(0, -2.2, 0.3); shinL.add(footL);
const legR = new THREE.Group();
legR.position.set(0.9, 3.5, 0); root.add(legR);
const thighR = thighL.clone(); legR.add(thighR);
const shinR = new THREE.Group();
shinR.position.y = -2.2; legR.add(shinR);
const shinMR = shinML.clone(); shinR.add(shinMR);
const footR = footL.clone(); shinR.add(footR);
root.userData = {
torso, head, armL, armR, forearmL: foreL, forearmR: foreR,
legL, legR, shinL, shinR,
hp: 100, dead: false,
walkPhase: Math.random() * Math.PI * 2,
speed: 2.2, lastSound: 0,
attackCooldown: Math.random() * 2,
attackAnim: 0, attackAnimDur: 0.7,
hitDone: false,
};
return root;
}
const MAX_ENEMIES = 6;
const enemies = [];
function spawnEnemy() {
if (enemies.length >= MAX_ENEMIES) return;
const angle = Math.random() * Math.PI * 2;
const dist = 35 + Math.random() * 40;
const x = player.position.x + Math.cos(angle) * dist;
const z = player.position.z + Math.sin(angle) * dist;
if (Math.abs(x) > 120 || Math.abs(z) > 120) return;
const g = buildGiant();
g.position.set(x, terrain.userData.heightAt(x, z), z);
scene.add(g);
enemies.push(g);
}
for (let i = 0; i < 4; i++) spawnEnemy();
// =============== CONTROLS ===============
const controls = {
moveVec: new THREE.Vector2(0, 0),
cameraYaw: 0, cameraPitch: 0.35, cameraDist: 9,
};
const playerState = {
speed: 0, facing: 0, walkPhase: 0,
attackTime: -1,
attackPhase: 0, // 0 = idle, 1 = swinging, 2 = cooldown
attackWaitTime: 0,
attackCooldownTime: 0, // 1 sec pause
attackDuration: 0.6,
currentAttackIndex: 0, // 0 = left, 1 = right, 2 = overhead
footstepTimer: 0,
attackHitDone: false,
};
// Joystick
const joystickEl = document.getElementById('joystick');
const stickEl = document.getElementById('stick');
let joyActive = false, joyId = null;
const JOY_RADIUS = 45;
function setJoyFromEvent(clientX, clientY) {
const rect = joystickEl.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
let dx = clientX - cx, dy = clientY - cy;
const len = Math.hypot(dx, dy);
if (len > JOY_RADIUS) { dx = dx / len * JOY_RADIUS; dy = dy / len * JOY_RADIUS; }
stickEl.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
controls.moveVec.x = dx / JOY_RADIUS;
controls.moveVec.y = dy / JOY_RADIUS;
}
joystickEl.addEventListener('pointerdown', (e) => {
e.preventDefault();
joyActive = true; joyId = e.pointerId;
joystickEl.setPointerCapture(e.pointerId);
setJoyFromEvent(e.clientX, e.clientY);
});
joystickEl.addEventListener('pointermove', (e) => {
if (!joyActive || e.pointerId !== joyId) return;
e.preventDefault();
setJoyFromEvent(e.clientX, e.clientY);
});
function releaseJoy(e) {
if (e.pointerId !== joyId) return;
joyActive = false; joyId = null;
stickEl.style.transform = 'translate(-50%, -50%)';
controls.moveVec.set(0, 0);
}
joystickEl.addEventListener('pointerup', releaseJoy);
joystickEl.addEventListener('pointercancel', releaseJoy);
// Camera drag
let camActive = false, camId = null;
let camLast = { x: 0, y: 0 };
renderer.domElement.addEventListener('pointerdown', (e) => {
if (!state.started) return;
if (camActive) return;
camActive = true; camId = e.pointerId;
camLast.x = e.clientX; camLast.y = e.clientY;
renderer.domElement.setPointerCapture(e.pointerId);
});
renderer.domElement.addEventListener('pointermove', (e) => {
if (!camActive || e.pointerId !== camId) return;
const dx = e.clientX - camLast.x;
const dy = e.clientY - camLast.y;
camLast.x = e.clientX; camLast.y = e.clientY;
controls.cameraYaw -= dx * 0.006;
controls.cameraPitch += dy * 0.005;
controls.cameraPitch = Math.max(-0.15, Math.min(0.9, controls.cameraPitch));
});
function releaseCam(e) {
if (e.pointerId !== camId) return;
camActive = false; camId = null;
}
renderer.domElement.addEventListener('pointerup', releaseCam);
renderer.domElement.addEventListener('pointercancel', releaseCam);
// Attack
const attackBtn = document.getElementById('attackBtn');
function triggerAttack(e) {
if (e) e.preventDefault();
if (!state.started) return;
if (playerState.attackPhase !== 0) return; // not idle
playerState.attackPhase = 1;
playerState.attackTime = 0;
playerState.attackHitDone = false;
playerState.currentAttackIndex = (playerState.currentAttackIndex + 1) % 3;
playSwordSwing();
attackBtn.classList.add('disabled');
}
attackBtn.addEventListener('pointerdown', triggerAttack);
attackBtn.addEventListener('touchstart', triggerAttack, { passive: false });
// Rage
const rageBtn = document.getElementById('rageBtn');
const rageOverlay = document.getElementById('rageOverlay');
const rageInfo = document.getElementById('rageInfo');
function triggerRage(e) {
if (e) e.preventDefault();
if (!state.started) return;
if (rageState.active || rageState.cooldown > 0) return;
rageState.active = true;
rageState.timer = rageState.duration;
rageState.cameraZoom = 1;
rageOverlay.classList.add('on');
rageBtn.classList.add('active');
playRageScream();
}
rageBtn.addEventListener('pointerdown', triggerRage);
rageBtn.addEventListener('touchstart', triggerRage, { passive: false });
// HP UI
const hpBarFill = document.getElementById('hpBarFill');
const hurtFlash = document.getElementById('hurtFlash');
function updateHpBar() {
const pct = Math.max(0, state.hp) / state.hpMax;
hpBarFill.style.width = (pct * 100) + '%';
const hue = 120 * pct;
const light = 30 + pct * 25;
hpBarFill.style.background = `linear-gradient(180deg, hsl(${hue}, 100%, ${light + 15}%), hsl(${hue}, 100%, ${light - 5}%))`;
hpBarFill.style.boxShadow = `0 0 12px hsla(${hue}, 100%, 50%, 0.8) inset`;
}
function damagePlayer(amount) {
state.hp = Math.max(0, state.hp - amount);
updateHpBar();
hurtFlash.style.opacity = '1';
setTimeout(() => { hurtFlash.style.opacity = '0'; }, 150);
playPlayerHitSound();
if (state.hp <= 0) {
state.hp = state.hpMax;
updateHpBar();
player.position.set(0, terrain.userData.heightAt(0, 0), 0);
}
}
// =============== AUDIO ===============
let audioCtx = null;
function initAudio() {
if (audioCtx) return;
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {}
}
function playFootstep(strength = 1) {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const bufferSize = audioCtx.sampleRate * 0.15;
const buf = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
const env = Math.pow(1 - i / bufferSize, 2.5);
data[i] = (Math.random() * 2 - 1) * env * 0.5;
}
const src = audioCtx.createBufferSource();
src.buffer = buf;
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 500 + Math.random() * 300;
const gain = audioCtx.createGain();
gain.gain.value = 0.35 * strength;
src.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
src.start(t);
}
function playSwordSwing() {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(220, t);
osc.frequency.exponentialRampToValueAtTime(60, t + 0.4);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.001, t);
gain.gain.exponentialRampToValueAtTime(0.4, t + 0.04);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.5);
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 900;
osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
osc.start(t); osc.stop(t + 0.55);
}
function playGiantSound(volume = 1) {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(45 + Math.random() * 15, t);
osc.frequency.linearRampToValueAtTime(30 + Math.random() * 15, t + 1.5);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.001, t);
gain.gain.exponentialRampToValueAtTime(0.5 * volume, t + 0.25);
gain.gain.exponentialRampToValueAtTime(0.001, t + 1.6);
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 320;
osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
osc.start(t); osc.stop(t + 1.7);
}
function playDeathSound() {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(120, t);
osc.frequency.exponentialRampToValueAtTime(20, t + 1.2);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.001, t);
gain.gain.exponentialRampToValueAtTime(0.5, t + 0.1);
gain.gain.exponentialRampToValueAtTime(0.001, t + 1.3);
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 500;
osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
osc.start(t); osc.stop(t + 1.4);
}
function playPlayerHitSound() {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
osc.type = 'square';
osc.frequency.setValueAtTime(120, t);
osc.frequency.exponentialRampToValueAtTime(40, t + 0.25);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.001, t);
gain.gain.exponentialRampToValueAtTime(0.35, t + 0.02);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 800;
osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
osc.start(t); osc.stop(t + 0.35);
}
function playRageScream() {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const osc1 = audioCtx.createOscillator();
osc1.type = 'sawtooth';
osc1.frequency.setValueAtTime(70, t);
osc1.frequency.exponentialRampToValueAtTime(45, t + 1.6);
const lfo1 = audioCtx.createOscillator();
lfo1.frequency.value = 22;
const lfo1Gain = audioCtx.createGain();
lfo1Gain.gain.value = 12;
lfo1.connect(lfo1Gain); lfo1Gain.connect(osc1.frequency);
lfo1.start(t); lfo1.stop(t + 1.7);
const gain1 = audioCtx.createGain();
gain1.gain.setValueAtTime(0.001, t);
gain1.gain.exponentialRampToValueAtTime(0.35, t + 0.08);
gain1.gain.exponentialRampToValueAtTime(0.001, t + 1.7);
const filter1 = audioCtx.createBiquadFilter();
filter1.type = 'lowpass'; filter1.frequency.value = 700;
osc1.connect(filter1); filter1.connect(gain1); gain1.connect(audioCtx.destination);
osc1.start(t); osc1.stop(t + 1.8);
const osc2 = audioCtx.createOscillator();
osc2.type = 'square';
osc2.frequency.setValueAtTime(180, t);
osc2.frequency.exponentialRampToValueAtTime(90, t + 1.4);
const gain2 = audioCtx.createGain();
gain2.gain.setValueAtTime(0.001, t);
gain2.gain.exponentialRampToValueAtTime(0.12, t + 0.1);
gain2.gain.exponentialRampToValueAtTime(0.001, t + 1.5);
const filter2 = audioCtx.createBiquadFilter();
filter2.type = 'bandpass'; filter2.frequency.value = 500; filter2.Q.value = 2;
osc2.connect(filter2); filter2.connect(gain2); gain2.connect(audioCtx.destination);
osc2.start(t); osc2.stop(t + 1.6);
const osc3 = audioCtx.createOscillator();
osc3.type = 'sawtooth';
osc3.frequency.setValueAtTime(420, t);
osc3.frequency.exponentialRampToValueAtTime(220, t + 1.2);
const gain3 = audioCtx.createGain();
gain3.gain.setValueAtTime(0.001, t);
gain3.gain.exponentialRampToValueAtTime(0.06, t + 0.15);
gain3.gain.exponentialRampToValueAtTime(0.001, t + 1.3);
const filter3 = audioCtx.createBiquadFilter();
filter3.type = 'highpass'; filter3.frequency.value = 300;
osc3.connect(filter3); filter3.connect(gain3); gain3.connect(audioCtx.destination);
osc3.start(t); osc3.stop(t + 1.4);
const bufferSize = audioCtx.sampleRate * 1.5;
const noiseBuf = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const nd = noiseBuf.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
const env = Math.pow(1 - i / bufferSize, 2);
nd[i] = (Math.random() * 2 - 1) * env * 0.5;
}
const noiseSrc = audioCtx.createBufferSource();
noiseSrc.buffer = noiseBuf;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'bandpass'; noiseFilter.frequency.value = 350; noiseFilter.Q.value = 1.2;
const noiseGain = audioCtx.createGain();
noiseGain.gain.value = 0.18;
noiseSrc.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(audioCtx.destination);
noiseSrc.start(t);
}
// Hit detection
const ATTACK_RANGE = 8.5;
function checkAttackHits() {
const px = player.position.x, pz = player.position.z;
for (const e of enemies) {
if (e.userData.dead) continue;
const dx = e.position.x - px, dz = e.position.z - pz;
const d = Math.hypot(dx, dz);
if (d <= ATTACK_RANGE + 1.5) {
e.userData.hp -= 200;
if (e.userData.hp <= 0) {
e.userData.dead = true;
playDeathSound();
state.kills++;
document.getElementById('kills').textContent = state.kills;
e.userData.deathTime = 0;
} else {
e.userData.stagger = 0.4;
}
}
}
}
// =============== PLAYER UPDATE ===============
function updatePlayer(dt) {
const inputX = controls.moveVec.x;
const inputY = controls.moveVec.y;
const inputMag = Math.min(1, Math.hypot(inputX, inputY));
const forwardX = Math.sin(controls.cameraYaw);
const forwardZ = Math.cos(controls.cameraYaw);
const rightX = -Math.cos(controls.cameraYaw);
const rightZ = Math.sin(controls.cameraYaw);
let moveX = 0, moveZ = 0;
if (inputMag > 0.01) {
moveX = rightX * inputX + forwardX * (-inputY);
moveZ = rightZ * inputX + forwardZ * (-inputY);
const len = Math.hypot(moveX, moveZ);
moveX /= len; moveZ /= len;
}
const running = inputMag > 0.7;
state.running = running;
// ===== SPEED =====
// سرعت عادی = سرعت RAGE قدیم (۱.۹ برابر قبلی)
// RAGE = دو برابر عادی (۳.۸x)
const rageMult = rageState.active ? 2.0 : 1.0;
const baseSpeed = (running ? 16.0 : 8.0) * rageMult;
const speed = baseSpeed * Math.min(1, inputMag);
const finalSpeed = speed;
player.position.x = Math.max(-120, Math.min(120, player.position.x + moveX * finalSpeed * dt));
player.position.z = Math.max(-120, Math.min(120, player.position.z + moveZ * finalSpeed * dt));
player.position.y = terrain.userData.heightAt(player.position.x, player.position.z);
playerState.speed = finalSpeed;
playerState.isRunning = running;
if (inputMag > 0.05 && playerState.attackPhase === 0) {
const targetFacing = Math.atan2(moveX, moveZ);
let diff = targetFacing - playerState.facing;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
playerState.facing += diff * Math.min(1, dt * 10);
}
player.rotation.y = playerState.facing;
const ud = player.userData;
// ===== WALK / RUN ANIMATION =====
if (finalSpeed > 0.1) {
const freq = (running ? 12 : 8) * (rageState.active ? 1.5 : 1);
playerState.walkPhase += dt * freq;
const s = Math.sin(playerState.walkPhase);
const c = Math.cos(playerState.walkPhase);
const amp = running ? 0.95 : 0.6;
ud.legL.rotation.x = s * amp;
ud.legR.rotation.x = -s * amp;
ud.shinL.rotation.x = Math.max(0, -s) * amp * 0.9;
ud.shinR.rotation.x = Math.max(0, s) * amp * 0.9;
// Right arm holds sword up
ud.armR.rotation.x = lerp(ud.armR.rotation.x, -0.5 + s * 0.1, 0.15);
ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.25, 0.15);
ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 1.7, 0.15);
// ✅ آرنج چپ: به جلو خم (نه عقب)
ud.forearmL.rotation.x = lerp(ud.forearmL.rotation.x, 0.6 + Math.max(0, s) * 0.4, 0.15);
ud.armL.rotation.x = -s * amp * 0.5;
ud.torso.position.y = 2.3 + Math.abs(c) * 0.06 * (running ? 1.6 : 1);
ud.torso.rotation.y = s * 0.08;
ud.torso.rotation.z = s * 0.05;
// Sword on shoulder when not attacking
if (playerState.attackPhase === 0) {
ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55, 0.12);
ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.12);
ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.12);
}
playerState.footstepTimer -= dt;
const interval = (running ? 0.25 : 0.45) / (rageState.active ? 1.5 : 1);
if (playerState.footstepTimer <= 0) {
playerState.footstepTimer = interval;
const footSide = s > 0 ? 1 : -1;
const footOffsetX = Math.cos(playerState.facing) * 0.45 * footSide;
const footOffsetZ = -Math.sin(playerState.facing) * 0.45 * footSide;
const fx = player.position.x + footOffsetX;
const fz = player.position.z + footOffsetZ;
spawnRipple(fx, terrain.userData.heightAt(fx, fz), fz, running ? 1.4 : 0.9);
playFootstep(running ? 1.2 : 0.8);
}
} else if (playerState.attackPhase === 0) {
// ===== IDLE =====
const t = performance.now() * 0.001;
const breathe = Math.sin(t * 1.4) * 0.025;
ud.torso.position.y = 2.3 + breathe;
ud.torso.rotation.y = lerp(ud.torso.rotation.y, Math.sin(t * 0.6) * 0.04, 0.08);
ud.torso.rotation.z = lerp(ud.torso.rotation.z, Math.sin(t * 0.5) * 0.015, 0.08);
ud.helmet.rotation.y = Math.sin(t * 0.7) * 0.08;
ud.helmet.rotation.x = lerp(ud.helmet.rotation.x, 0.05 + Math.sin(t * 1.3) * 0.02, 0.1);
// ✅ آرنج چپ به جلو خم
ud.armL.rotation.x = lerp(ud.armL.rotation.x, 0.05 + Math.sin(t * 1.2) * 0.06, 0.1);
ud.armL.rotation.z = lerp(ud.armL.rotation.z, 0.05, 0.1);
ud.forearmL.rotation.x = lerp(ud.forearmL.rotation.x, 0.55 + Math.sin(t*1.2+0.5) * 0.06, 0.1);
// Right arm holds sword on shoulder
ud.armR.rotation.x = lerp(ud.armR.rotation.x, -0.45, 0.1);
ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.15, 0.1);
ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 1.85, 0.1);
// Sword on shoulder
ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55, 0.1);
ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.1);
ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.1);
ud.legL.rotation.x = lerp(ud.legL.rotation.x, 0, 0.15);
ud.legR.rotation.x = lerp(ud.legR.rotation.x, 0, 0.15);
ud.shinL.rotation.x = lerp(ud.shinL.rotation.x, 0, 0.15);
ud.shinR.rotation.x = lerp(ud.shinR.rotation.x, 0, 0.15);
}
// ===== RAGE POSE =====
if (rageState.active) {
const t = performance.now() * 0.001;
const shake = Math.sin(t * 30) * 0.015;
ud.torso.rotation.x = 0.35 + shake;
ud.torso.rotation.z += Math.sin(t * 18) * 0.04;
ud.helmet.rotation.x = 0.25;
ud.armL.rotation.z = -0.7 + Math.sin(t * 12) * 0.05;
if (ud.auraSprite) {
ud.auraSprite.material.opacity = 0.5 + Math.sin(t * 8) * 0.2;
const s = 8 + Math.sin(t * 6) * 1.2;
ud.auraSprite.scale.set(s, s, 1);
}
} else {
ud.torso.rotation.x = lerp(ud.torso.rotation.x, 0, 0.12);
ud.helmet.rotation.x = lerp(ud.helmet.rotation.x, 0, 0.12);
if (ud.auraSprite) ud.auraSprite.material.opacity = lerp(ud.auraSprite.material.opacity, 0, 0.12);
}
// ===== ATTACK SYSTEM (3 animations + 1s pause) =====
if (playerState.attackPhase === 1) {
playerState.attackTime += dt;
const k = playerState.attackTime / playerState.attackDuration;
if (k >= 1) {
// Attack animation done
playerState.attackPhase = 2; // start cooldown
playerState.attackCooldownTime = 1.0;
} else {
const ease = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
if (playerState.currentAttackIndex === 0) {
// ===== ضربه چپ =====
// شمشیر از راست بالا به چپ پایین
ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55 + ease * 3.2, 0.4);
ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15 + ease * 1.6, 0.4);
ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2 - ease * 1.5, 0.4);
ud.torso.rotation.y = lerp(ud.torso.rotation.y, -ease * 1.2, 0.35);
ud.armR.rotation.x = lerp(ud.armR.rotation.x, -1.4 - ease * 0.6, 0.35);
ud.armR.rotation.z = lerp(ud.armR.rotation.z, 0.3 + ease * 0.5, 0.35);
ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 0.3, 0.35);
} else if (playerState.currentAttackIndex === 1) {
// ===== ضربه راست =====
// شمشیر از چپ بالا به راست پایین
ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55 + ease * 3.2, 0.4);
ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15 - ease * 1.6, 0.4);
ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2 + ease * 1.5, 0.4);
ud.torso.rotation.y = lerp(ud.torso.rotation.y, ease * 1.2, 0.35);
ud.armR.rotation.x = lerp(ud.armR.rotation.x, -1.4 - ease * 0.6, 0.35);
ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.3 - ease * 0.5, 0.35);
ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 0.3, 0.35);
} else {
// ===== ضربه از بالا رو زمین =====
// شمشیر از بالا میآد پایین (چکشی)
ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55 + ease * 4.5, 0.5);
ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.35);
ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.35);
ud.torso.rotation.x = lerp(ud.torso.rotation.x, 0.6 + ease * 0.35, 0.4);
ud.armR.rotation.x = lerp(ud.armR.rotation.x, -2.2 + ease * 2.5, 0.4);
ud.armR.rotation.z = lerp(ud.armR.rotation.z, 0, 0.35);
ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 0.2, 0.4);
}
// Hit detect at peak
if (!playerState.attackHitDone && k > 0.4 && k < 0.7) {
playerState.attackHitDone = true;
checkAttackHits();
}
}
} else if (playerState.attackPhase === 2) {
// ===== ۱ ثانیه مکث =====
playerState.attackCooldownTime -= dt;
if (playerState.attackCooldownTime <= 0) {
playerState.attackPhase = 0;
playerState.attackTime = -1;
attackBtn.classList.remove('disabled');
}
// Smooth return to idle pose
ud.swordPivot.rotation.x = lerp(ud.swordPivot.rotation.x, -2.55, 0.15);
ud.swordPivot.rotation.z = lerp(ud.swordPivot.rotation.z, -0.15, 0.15);
ud.swordPivot.rotation.y = lerp(ud.swordPivot.rotation.y, 0.2, 0.15);
ud.torso.rotation.y = lerp(ud.torso.rotation.y, 0, 0.15);
ud.torso.rotation.x = lerp(ud.torso.rotation.x, 0, 0.15);
ud.armR.rotation.x = lerp(ud.armR.rotation.x, -0.45, 0.15);
ud.armR.rotation.z = lerp(ud.armR.rotation.z, -0.15, 0.15);
ud.forearmR.rotation.x = lerp(ud.forearmR.rotation.x, 1.85, 0.15);
}
}
// Rage update
function updateRage(dt) {
if (rageState.cooldown > 0) {
rageState.cooldown -= dt;
if (rageState.cooldown <= 0) {
rageState.cooldown = 0;
rageBtn.classList.remove('on-cooldown');
}
}
if (rageState.active) {
rageState.timer -= dt;
rageState.eyeIntensity = Math.min(1, rageState.eyeIntensity + dt * 4);
const ud = player.userData;
const i = rageState.eyeIntensity;
ud.eyeLightL.intensity = 5 * i;
ud.eyeLightR.intensity = 5 * i;
ud.rageAura.intensity = 4 * i;
if (ud.visorMat) ud.visorMat.color.setRGB(1, 0.13 * (1 - i) + 0.5 * i, 0.5 * i);
rageState.cameraZoom = Math.min(1, rageState.cameraZoom + dt * 3);
rageInfo.textContent = '🔥 RAGE: ' + Math.ceil(rageState.timer) + 's';
if (rageState.timer <= 0) {
rageState.active = false;
rageState.cooldown = rageState.cooldownMax;
rageOverlay.classList.remove('on');
rageBtn.classList.remove('active');
rageBtn.classList.add('on-cooldown');
rageInfo.textContent = '';
}
} else {
rageState.eyeIntensity = Math.max(0, rageState.eyeIntensity - dt * 3);
const ud = player.userData;
const i = rageState.eyeIntensity;
ud.eyeLightL.intensity = 5 * i;
ud.eyeLightR.intensity = 5 * i;
ud.rageAura.intensity = 4 * i;
if (ud.visorMat) ud.visorMat.color.setRGB(1, 0.13, 0.13);
rageState.cameraZoom = Math.max(0, rageState.cameraZoom - dt * 2);
if (rageState.cooldown > 0) rageInfo.textContent = '⏳ RAGE: ' + Math.ceil(rageState.cooldown) + 's';
}
}
// =============== ENEMY UPDATE ===============
function updateEnemies(dt) {
const px = player.position.x, pz = player.position.z;
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
const ud = e.userData;
if (ud.dead) {
ud.deathTime = (ud.deathTime || 0) + dt;
const k = ud.deathTime / 1.2;
if (k >= 1) {
scene.remove(e);
e.traverse(o => {
if (o.geometry) o.geometry.dispose();
if (o.material) {
if (Array.isArray(o.material)) o.material.forEach(m => m.dispose());
else o.material.dispose();
}
});
enemies.splice(i, 1);
continue;
}
e.rotation.x = -k * Math.PI * 0.45;
e.position.y = terrain.userData.heightAt(e.position.x, e.position.z) - k * 0.5;
e.scale.setScalar(1 - k * 0.15);
continue;
}
if (ud.stagger > 0) ud.stagger -= dt;
if (ud.attackCooldown > 0) ud.attackCooldown -= dt;
const dx = px - e.position.x, dz = pz - e.position.z;
const dist = Math.hypot(dx, dz);
const now = performance.now() * 0.001;
if (dist < 60 && now - ud.lastSound > 3 + Math.random() * 5) {
ud.lastSound = now;
const vol = Math.max(0, 1 - dist / 60);
playGiantSound(vol);
}
if (ud.attackAnim > 0) {
ud.attackAnim -= dt;
const k = 1 - ud.attackAnim / ud.attackAnimDur;
const lunge = Math.sin(k * Math.PI);
e.position.x += Math.sin(e.rotation.y) * lunge * 2 * dt * 5;
e.position.z += Math.cos(e.rotation.y) * lunge * 2 * dt * 5;
ud.armR.rotation.x = -Math.PI * 0.6 * lunge;
ud.armL.rotation.x = -Math.PI * 0.6 * lunge;
ud.torso.rotation.x = 0.3 * lunge;
if (k > 0.4 && k < 0.6 && !ud.hitDone) {
ud.hitDone = true;
if (dist < 8) {
const dmg = 5 + Math.floor(Math.random() * 6);
damagePlayer(dmg);
}
}
if (k >= 1) { ud.attackAnim = 0; ud.hitDone = false; ud.torso.rotation.x = 0; }
continue;
}
const giantSpeedMult = rageState.active ? 0.7 : 1;
if (dist > 3.5 && dist < 70 && ud.stagger <= 0) {
const nx = dx / dist, nz = dz / dist;
const speed = ud.speed * giantSpeedMult;
e.position.x += nx * speed * dt;
e.position.z += nz * speed * dt;
const targetYaw = Math.atan2(dx, dz);
let diff = targetYaw - e.rotation.y;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
e.rotation.y += diff * Math.min(1, dt * 3);
ud.walkPhase += dt * 3.5;
const s = Math.sin(ud.walkPhase);
ud.legL.rotation.x = s * 0.6;
ud.legR.rotation.x = -s * 0.6;
ud.shinL.rotation.x = Math.max(0, -s) * 0.5;
ud.shinR.rotation.x = Math.max(0, s) * 0.5;
ud.armL.rotation.x = -s * 0.4;
ud.armR.rotation.x = s * 0.4;
ud.torso.rotation.z = s * 0.04;
ud.torso.position.y = 5.5 + Math.abs(Math.cos(ud.walkPhase)) * 0.15;
ud.head.rotation.z = s * 0.03;
if (dist < 6 && ud.attackCooldown <= 0) {
ud.attackCooldown = 1.5 + Math.random() * 1.0;
ud.attackAnim = ud.attackAnimDur;
ud.hitDone = false;
}
} else {
const t = performance.now() * 0.001 + ud.walkPhase;
ud.legL.rotation.x = lerp(ud.legL.rotation.x, 0, 0.1);
ud.legR.rotation.x = lerp(ud.legR.rotation.x, 0, 0.1);
ud.armL.rotation.x = lerp(ud.armL.rotation.x, Math.sin(t * 0.8) * 0.1, 0.1);
ud.armR.rotation.x = lerp(ud.armR.rotation.x, Math.sin(t * 0.8 + 1) * 0.1, 0.1);
ud.torso.position.y = 5.5 + Math.sin(t * 1.2) * 0.05;
if (dist < 6 && ud.attackCooldown <= 0) {
ud.attackCooldown = 1.5 + Math.random() * 1.0;
ud.attackAnim = ud.attackAnimDur;
ud.hitDone = false;
}
}
e.position.y = terrain.userData.heightAt(e.position.x, e.position.z);
}
if (Math.random() < dt * 0.15 && enemies.length < MAX_ENEMIES) spawnEnemy();
}
// Camera
const camTarget = new THREE.Vector3();
function updateCamera(dt) {
const yaw = controls.cameraYaw;
const pitch = controls.cameraPitch;
const dist = controls.cameraDist + rageState.cameraZoom * 4.5;
const offsetX = Math.sin(yaw) * Math.cos(pitch) * dist;
const offsetY = Math.sin(pitch) * dist + 3.5 + rageState.cameraZoom * 1.5;
const offsetZ = Math.cos(yaw) * Math.cos(pitch) * dist;
const desiredX = player.position.x - offsetX;
const desiredY = player.position.y + offsetY;
const desiredZ = player.position.z - offsetZ;
const l = Math.min(1, dt * 6);
camera.position.x += (desiredX - camera.position.x) * l;
camera.position.y += (desiredY - camera.position.y) * l;
camera.position.z += (desiredZ - camera.position.z) * l;
const groundY = terrain.userData.heightAt(camera.position.x, camera.position.z) + 1.5;
if (camera.position.y < groundY) camera.position.y = groundY;
camTarget.set(player.position.x, player.position.y + 3.2, player.position.z);
camera.lookAt(camTarget);
bloodGlow.position.set(player.position.x, player.position.y + 0.5, player.position.z);
bloodGlow.intensity = rageState.active ? 2.5 + Math.sin(performance.now() * 0.01) * 0.8 : 1.6;
bloodGlow.color.setHex(rageState.active ? 0xff2200 : 0x880000);
}
function updateSkyFaces() {
const t = performance.now() * 0.0001;
skyFaces.children.forEach((f, i) => {
f.position.y += Math.sin(t + i) * 0.02;
f.lookAt(camera.position.x * 0.05, f.position.y, camera.position.z * 0.05);
});
}
// Main loop
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
let dt = (now - lastTime) / 1000;
lastTime = now;
if (dt > 0.1) dt = 0.1;
if (state.started) {
updatePlayer(dt);
updateEnemies(dt);
updateCamera(dt);
updateRipples(dt);
updateSkyFaces();
updateRage(dt);
}
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
function startGame(e) {
if (e) e.preventDefault();
initAudio();
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
state.started = true;
const loading = document.getElementById('loading');
loading.style.opacity = '0';
setTimeout(() => { loading.style.display = 'none'; }, 1000);
document.getElementById('hud').style.display = 'block';
player.position.set(0, terrain.userData.heightAt(0, 0), 0);
playerState.facing = 0;
player.rotation.y = 0;
controls.cameraYaw = 0;
controls.cameraPitch = 0.35;
updateHpBar();
updateCamera(1);
}
const startBtn = document.getElementById('startBtn');
startBtn.addEventListener('pointerdown', startGame);
startBtn.addEventListener('touchstart', startGame, { passive: false });
startBtn.addEventListener('click', startGame);
</script>
</body>
</html>Game Source: Blood Eclipse
Creator: FrozenGecko13
Libraries: three
Complexity: complex (1555 lines, 59.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: blood-eclipse-frozengecko13" to link back to the original. Then publish at arcadelab.ai/publish.