<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PARRY & BLOCK TRAINER</title>
<style>
:root {
--bg-dark: #070b12;
--panel-bg: #0c131d;
--border-cyan: #22d3ee;
--text-cyan: #38bdf8;
--text-main: #e2e8f0;
--text-muted: #94a3b8;
--danger-red: #f43f5e;
--success-green: #10b981;
--warn-yellow: #facc15;
}
* {
box-sizing: border-box;
user-select: none;
margin: 0;
padding: 0;
font-family: 'Segoe UI', 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif;
}
body {
background-color: #030508;
color: var(--text-main);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 16px;
}
.app-frame {
width: 100%;
max-width: 1040px;
background-color: var(--bg-dark);
border: 2px solid var(--border-cyan);
border-radius: 16px;
box-shadow: 0 0 24px rgba(34, 211, 238, 0.25);
padding: 18px 22px;
display: flex;
flex-direction: column;
gap: 14px;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: 1.5px;
color: #67e8f9;
text-shadow: 0 0 12px rgba(34, 211, 238, 0.7);
}
.menu-btn {
background: rgba(15, 23, 42, 0.9);
color: var(--text-main);
border: 1.5px solid var(--border-cyan);
border-radius: 6px;
padding: 7px 14px;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
}
.menu-btn:hover {
background: rgba(34, 211, 238, 0.15);
box-shadow: 0 0 10px rgba(34, 211, 238, 0.4);
}
/* Canvas Container */
.canvas-wrapper {
position: relative;
width: 100%;
height: 440px;
background-color: #06090f;
border: 1px solid #1e293b;
border-radius: 8px;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
/* Judgment Overlay */
.judgment-banner {
position: absolute;
top: 34px;
left: 50%;
transform: translateX(-50%);
font-size: 2.35rem;
font-weight: 900;
letter-spacing: 1px;
pointer-events: none;
white-space: nowrap;
opacity: 0;
transition: opacity 0.12s ease-out, transform 0.12s ease-out;
text-shadow: 0 0 18px currentColor;
}
.judgment-banner.visible {
opacity: 1;
}
/* Pause / Config Modal */
.config-modal {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 470px;
max-width: 94%;
height: 100%;
background: rgba(11, 17, 27, 0.96);
border-left: 1.5px solid var(--border-cyan);
border-right: 1.5px solid var(--border-cyan);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.85);
padding: 20px 26px;
overflow-y: auto;
display: none;
flex-direction: column;
gap: 14px;
z-index: 20;
}
.config-modal.open {
display: flex;
}
.config-modal h2 {
text-align: center;
color: #67e8f9;
font-size: 1.5rem;
letter-spacing: 1.5px;
margin-bottom: 4px;
text-shadow: 0 0 10px rgba(34, 211, 238, 0.5);
}
.config-check {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
}
.config-check input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #3b82f6;
cursor: pointer;
}
.slider-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.slider-label {
font-size: 0.88rem;
font-weight: 600;
color: #cbd5e1;
}
.slider-label span {
font-weight: 800;
}
.val-red { color: #f43f5e; }
.val-yellow { color: #facc15; }
.val-cyan { color: #38bdf8; }
input[type="range"] {
width: 100%;
accent-color: #3b82f6;
cursor: pointer;
}
.keybind-section-title {
font-size: 0.92rem;
font-weight: 700;
margin-top: 4px;
color: #f8fafc;
}
.keybind-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.keybind-item {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 0.85rem;
color: #cbd5e1;
}
.keybind-btn {
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
padding: 8px;
color: #38bdf8;
font-weight: 700;
font-size: 0.9rem;
text-align: center;
cursor: pointer;
transition: all 0.15s;
}
.keybind-btn:hover, .keybind-btn.listening {
border-color: var(--border-cyan);
background: #162238;
color: #facc15;
}
.resume-btn {
margin-top: 8px;
background: #112432;
border: 1px solid var(--border-cyan);
color: #e2e8f0;
padding: 10px;
border-radius: 6px;
font-weight: 700;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.15s;
}
.resume-btn:hover {
background: #19374d;
}
/* Middle Cooldown & Action Row */
.mid-controls {
display: flex;
justify-content: center;
gap: 16px;
}
.cd-box {
width: 210px;
background: var(--panel-bg);
border: 1px solid #1e293b;
border-radius: 8px;
padding: 10px 14px;
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
}
.cd-title {
font-size: 0.78rem;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.cd-bar-track {
width: 100%;
height: 6px;
background: #1e293b;
border-radius: 999px;
overflow: hidden;
}
.cd-bar-fill {
height: 100%;
width: 100%;
background: var(--border-cyan);
box-shadow: 0 0 8px var(--border-cyan);
transition: width 0.05s linear;
}
.action-btn {
width: 100%;
background: #1e293b;
border: 1px solid #475569;
border-radius: 5px;
color: #f8fafc;
font-size: 0.8rem;
font-weight: 700;
padding: 5px 0;
cursor: pointer;
}
.action-btn.ready {
border-color: var(--border-cyan);
color: #67e8f9;
}
/* Bottom Status Panels */
.bottom-panels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.stat-panel {
background: var(--panel-bg);
border: 1px solid #1e293b;
border-radius: 8px;
padding: 14px 18px;
display: flex;
flex-direction: column;
gap: 10px;
}
.panel-heading {
font-size: 0.82rem;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.92rem;
font-weight: 700;
}
.stat-val-cyan { color: #22d3ee; }
.stat-val-purple { color: #a855f7; }
.stat-val-yellow { color: #facc15; }
.stat-val-red { color: #f43f5e; }
.stat-val-orange { color: #fb923c; }
.gauge-track {
width: 100%;
height: 8px;
background: #1e293b;
border-radius: 999px;
overflow: hidden;
}
.gauge-stamina {
height: 100%;
width: 100%;
background: #10b981;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
transition: width 0.08s linear, background-color 0.15s;
}
.gauge-stamina.exhausted {
background: #f43f5e;
box-shadow: 0 0 10px rgba(244, 63, 94, 0.6);
}
.gauge-enemy {
height: 100%;
width: 100%;
background: #f43f5e;
box-shadow: 0 0 10px rgba(244, 63, 94, 0.6);
transition: width 0.1s linear;
}
.footer-note {
text-align: center;
font-size: 0.78rem;
color: var(--text-muted);
margin-top: 2px;
}
.footer-note strong {
color: #cbd5e1;
}
</style>
</head>
<body>
<div class="app-frame">
<!-- Header -->
<div class="header">
<div class="title">PARRY & BLOCK TRAINER</div>
<button class="menu-btn" id="toggleMenuBtn">設定メニュー [ESC]</button>
</div>
<!-- Main Game Field -->
<div class="canvas-wrapper">
<canvas id="gameCanvas" width="992" height="440"></canvas>
<div class="judgment-banner" id="judgmentBanner">HIT! (被弾)</div>
<!-- Pause / Config Overlay -->
<div class="config-modal" id="configModal">
<h2>PAUSE / CONFIG</h2>
<label class="config-check">
<input type="checkbox" id="cfgEndless" checked />
エンドレスモード (即時連戦・完全非停止)
</label>
<div class="slider-group">
<div class="slider-label">Hitboxの長さ (リーチ): <span class="val-red" id="lblReach">55px</span></div>
<input type="range" id="cfgReach" min="35" max="110" step="1" value="55" />
</div>
<div class="slider-group">
<div class="slider-label">攻撃持続時間: <span class="val-red" id="lblActive">200ms</span></div>
<input type="range" id="cfgActive" min="80" max="450" step="10" value="200" />
</div>
<div class="slider-group">
<div class="slider-label">予備動作基準時間: <span class="val-yellow" id="lblWindup">0.20s</span></div>
<input type="range" id="cfgWindup" min="0.10" max="0.80" step="0.02" value="0.20" />
</div>
<div class="slider-group">
<div class="slider-label">敵の追尾速度: <span class="val-cyan" id="lblSpeed">4.2</span></div>
<input type="range" id="cfgSpeed" min="1.5" max="7.5" step="0.1" value="4.2" />
</div>
<div class="slider-group">
<div class="slider-label">ダッシュ速度: <span class="val-cyan" id="lblDashSpeed">360</span></div>
<input type="range" id="cfgDashSpeed" min="250" max="650" step="10" value="360" />
</div>
<label class="config-check">
<input type="checkbox" id="cfgFeint" checked />
フェイント・BLOCK BAIT有効化 (OFFで完全単発直線攻撃)
</label>
<label class="config-check">
<input type="checkbox" id="cfgCombo" />
連続攻撃モード (2〜3連撃)
</label>
<label class="config-check">
<input type="checkbox" id="cfgTelegraph" checked />
予兆 (黄色発光)
</label>
<div class="keybind-section-title">キー割り当て設定</div>
<div class="keybind-grid">
<div class="keybind-item">
<span>ガード:</span>
<button class="keybind-btn" data-bind="block" id="bindBlockBtn">Space</button>
</div>
<div class="keybind-item">
<span>パンチ/パリィ:</span>
<button class="keybind-btn" data-bind="punch" id="bindPunchBtn">KeyR</button>
</div>
<div class="keybind-item">
<span>ダッシュ:</span>
<button class="keybind-btn" data-bind="dash" id="bindDashBtn">ShiftLeft</button>
</div>
</div>
<button class="resume-btn" id="resumeBtn">ゲーム再開</button>
</div>
</div>
<!-- Middle Cooldown & Action Bar -->
<div class="mid-controls">
<div class="cd-box">
<div class="cd-title">BLOCK CD</div>
<div class="cd-bar-track"><div class="cd-bar-fill" id="blockCdBar"></div></div>
</div>
<div class="cd-box">
<div class="cd-title">PUNCH CD (15s)</div>
<div class="cd-bar-track"><div class="cd-bar-fill" id="punchCdBar"></div></div>
</div>
<div class="cd-box">
<div class="cd-title">ACTION</div>
<button class="action-btn ready" id="actionPunchBtn">PUNCH [R]</button>
</div>
</div>
<!-- Bottom Status Panels -->
<div class="bottom-panels">
<div class="stat-panel">
<div class="panel-heading">自キャラ STAMINA & REACTION</div>
<div class="stat-row">
<span>スタミナ:</span>
<span class="stat-val-cyan" id="playerStaminaText">100 / 100</span>
</div>
<div class="gauge-track">
<div class="gauge-stamina" id="playerStaminaBar"></div>
</div>
<div class="stat-row">
<span>直近の反応時間:</span>
<span class="stat-val-cyan" id="reactionTimeText">--- ms</span>
</div>
<div class="stat-row">
<span>成功率:</span>
<span class="stat-val-cyan" id="successRateText">0%</span>
</div>
</div>
<div class="stat-panel">
<div class="panel-heading">敵 AI BEHAVIOR & STAMINA</div>
<div class="stat-row">
<span>AI状態:</span>
<span class="stat-val-cyan" id="aiStateText">CHASING</span>
</div>
<div class="stat-row">
<span>敵スタミナ:</span>
<span class="stat-val-cyan" id="enemyStaminaText">100 / 100</span>
</div>
<div class="gauge-track">
<div class="gauge-enemy" id="enemyStaminaBar"></div>
</div>
</div>
</div>
<div class="footer-note">
・💥 <strong>超極太波動砲アップデート</strong>: 壁めり込み3秒以上で発射される波動砲をさらに強化!直撃すると超ノックバックののち<strong>光の粒子となって蒸発し、初期位置へ戻されます</strong>。
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// UI Elements
const judgmentBanner = document.getElementById('judgmentBanner');
const configModal = document.getElementById('configModal');
const toggleMenuBtn = document.getElementById('toggleMenuBtn');
const resumeBtn = document.getElementById('resumeBtn');
const blockCdBar = document.getElementById('blockCdBar');
const punchCdBar = document.getElementById('punchCdBar');
const actionPunchBtn = document.getElementById('actionPunchBtn');
const playerStaminaText = document.getElementById('playerStaminaText');
const playerStaminaBar = document.getElementById('playerStaminaBar');
const reactionTimeText = document.getElementById('reactionTimeText');
const successRateText = document.getElementById('successRateText');
const aiStateText = document.getElementById('aiStateText');
const enemyStaminaText = document.getElementById('enemyStaminaText');
const enemyStaminaBar = document.getElementById('enemyStaminaBar');
// Config Inputs
const cfgEndless = document.getElementById('cfgEndless');
const cfgReach = document.getElementById('cfgReach');
const cfgActive = document.getElementById('cfgActive');
const cfgWindup = document.getElementById('cfgWindup');
const cfgSpeed = document.getElementById('cfgSpeed');
const cfgDashSpeed = document.getElementById('cfgDashSpeed');
const cfgFeint = document.getElementById('cfgFeint');
const cfgCombo = document.getElementById('cfgCombo');
const cfgTelegraph = document.getElementById('cfgTelegraph');
const lblReach = document.getElementById('lblReach');
const lblActive = document.getElementById('lblActive');
const lblWindup = document.getElementById('lblWindup');
const lblSpeed = document.getElementById('lblSpeed');
const lblDashSpeed = document.getElementById('lblDashSpeed');
const bindButtons = {
block: document.getElementById('bindBlockBtn'),
punch: document.getElementById('bindPunchBtn'),
dash: document.getElementById('bindDashBtn')
};
// Game Settings & Keybinds
const settings = {
endless: true,
reach: 55,
activeMs: 200,
windupSec: 0.20,
enemySpeed: 4.2,
dashSpeed: 360,
feint: true,
combo: false,
telegraph: true
};
const keybinds = {
block: 'Space',
punch: 'KeyR',
dash: 'ShiftLeft'
};
let listeningForBind = null;
let isPaused = false;
// World Bounds & Obstacles (2D Top-down Camera World)
const WORLD = {
minX: -650,
maxX: 650,
minY: -500,
maxY: 500
};
const obstacles = [
{ x: 260, y: -60, w: 95, h: 120 },
{ x: -240, y: -260, w: 120, h: 140 },
{ x: 70, y: 120, w: 75, h: 170 },
{ x: -340, y: 160, w: 130, h: 90 }
];
// Visual Effects System
const particles = [];
const shockwaves = [];
const slashFlares = [];
let screenShakeTimer = 0;
let screenShakeIntensity = 0;
let hitstopTimer = 0;
let screenFlashAlpha = 0;
let screenFlashColor = '#ffffff';
function triggerScreenShake(intensity, duration) {
screenShakeIntensity = Math.max(screenShakeIntensity, intensity);
screenShakeTimer = Math.max(screenShakeTimer, duration);
}
function triggerScreenFlash(color, alpha) {
screenFlashColor = color;
screenFlashAlpha = alpha;
}
function spawnParticles(x, y, count, color, speedMin, speedMax, lifeMin, lifeMax, sizeMin = 2, sizeMax = 5, upwardDrift = 0) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = speedMin + Math.random() * (speedMax - speedMin);
const life = lifeMin + Math.random() * (lifeMax - lifeMin);
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed + upwardDrift,
life,
maxLife: life,
color,
size: sizeMin + Math.random() * (sizeMax - sizeMin)
});
}
}
function spawnDirectionalSparks(x, y, baseAngle, spread, count, color, speedMin, speedMax) {
for (let i = 0; i < count; i++) {
const angle = baseAngle + (Math.random() - 0.5) * spread;
const speed = speedMin + Math.random() * (speedMax - speedMin);
const life = 0.22 + Math.random() * 0.28;
particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life,
maxLife: life,
color,
size: 2.5 + Math.random() * 4.0,
isSpark: true
});
}
}
function spawnShockwave(x, y, startR, endR, color, duration, lineWidth = 4) {
shockwaves.push({
x,
y,
startR,
endR,
color,
life: duration,
maxLife: duration,
lineWidth
});
}
function spawnParryFlare(x, y, angle) {
slashFlares.push({
x,
y,
angle,
life: 0.35,
maxLife: 0.35
});
}
// Hitbox Afterimages
const hitboxTrails = [];
let trailSpawnTimer = 0;
function spawnHitboxTrail(x, y, angle, length, width, type) {
hitboxTrails.push({
x,
y,
angle,
length,
width,
type,
life: 0.45,
maxLife: 0.45
});
}
// Entities
const PLAYER_SPAWN = { x: 0, y: 0 };
const player = {
x: PLAYER_SPAWN.x,
y: PLAYER_SPAWN.y,
radius: 17,
angle: -Math.PI / 2,
speed: 220,
stamina: 100,
maxStamina: 100,
staminaExhausted: false,
staminaRegenDelay: 0,
isDashing: false,
lungeVx: 0,
lungeVy: 0,
// Block & Parry State & Animation
isBlocking: false,
blockHoldTimer: 0,
blockMaxHold: 1.5,
blockCd: 0,
blockCdMax: 2.2,
blockStartTime: 0,
blockDeployAnim: 0,
blockImpactFlash: 0,
parryWindowTimer: 0,
// Punch & Parry Animation State
punchCd: 0,
punchCdMax: 15.0,
punchActiveTimer: 0,
punchVisualTimer: 0,
punchVisualMax: 0.22,
punchAngle: 0,
punchIsParryAttempt: false,
punchHitResolved: false,
punchTrailTimer: 0,
bodySquash: 0,
// 隠し仕様:壁めり込み状態
wallClipTimer: 0,
isInsideObstacle: false,
insideObstacleTimer: 0,
// 波動砲直撃時の蒸発・リスポーン演出状態
isVaporizing: false,
vaporizeTimer: 0,
vaporizeDuration: 0.85,
vaporizeTrailTimer: 0,
respawnBeamTimer: 0
};
const enemy = {
x: 160,
y: 0,
radius: 17,
angle: Math.PI,
stamina: 100,
maxStamina: 100,
state: 'CHASING', // CHASING, BAITING, WINDUP, ATTACKING, STUNNED, DISARMED, CONFUSED, HADOU_CHARGE, HADOU_FIRE
stateTimer: 0,
currentWindupDuration: 0.20,
attackCooldown: 0.6,
attackTriggerDelay: -1,
preferredAttackDistMult: 0.85,
strafeSign: 1,
strafeTimer: 1.5,
attackAngle: 0,
attackTactic: 'NORMAL',
moveVx: 0,
moveVy: 0,
windupStartTime: 0,
activeStartTime: 0,
hasHitThisSwing: false,
hasBeenBlockedThisSwing: false,
feintPlanned: false,
misdirectOffset: 0,
comboRemaining: 0,
knockbackVx: 0,
knockbackVy: 0,
avoidDirSign: 1,
baitStrafeSign: 1,
baitCooldown: 0,
hitFlashTimer: 0,
// 波動砲・頭上アイコン用
hadouAngle: 0,
hadouHitThisBeam: false,
confusedLookTimer: 0
};
// Stats & Banner
const stats = {
attempts: 0,
successes: 0,
lastReactionMs: null
};
let bannerTimeout = null;
function showJudgment(text, color) {
judgmentBanner.textContent = text;
judgmentBanner.style.color = color;
judgmentBanner.classList.add('visible');
if (bannerTimeout) clearTimeout(bannerTimeout);
bannerTimeout = setTimeout(() => {
judgmentBanner.classList.remove('visible');
}, 1500);
}
// Input Tracking
const keysDown = new Set();
window.addEventListener('keydown', (e) => {
if (listeningForBind) {
e.preventDefault();
keybinds[listeningForBind] = e.code;
bindButtons[listeningForBind].textContent = e.code;
bindButtons[listeningForBind].classList.remove('listening');
if (listeningForBind === 'punch') {
const shortKey = e.code.replace('Key', '');
actionPunchBtn.textContent = `PUNCH [${shortKey}]`;
}
listeningForBind = null;
return;
}
if (e.code === 'Escape') {
e.preventDefault();
togglePause();
return;
}
if (isPaused || player.isVaporizing) return;
if (!keysDown.has(e.code)) {
keysDown.add(e.code);
if (e.code === keybinds.block) {
e.preventDefault();
triggerBlockStart();
} else if (e.code === keybinds.punch) {
e.preventDefault();
triggerPunch();
} else if (e.code === keybinds.dash) {
e.preventDefault();
}
}
});
window.addEventListener('keyup', (e) => {
keysDown.delete(e.code);
});
actionPunchBtn.addEventListener('click', () => {
if (!isPaused && !player.isVaporizing) triggerPunch();
});
// Config UI Listeners
function togglePause() {
isPaused = !isPaused;
configModal.classList.toggle('open', isPaused);
}
toggleMenuBtn.addEventListener('click', togglePause);
resumeBtn.addEventListener('click', togglePause);
cfgEndless.addEventListener('change', () => settings.endless = cfgEndless.checked);
cfgReach.addEventListener('input', () => {
settings.reach = parseInt(cfgReach.value, 10);
lblReach.textContent = `${settings.reach}px`;
});
cfgActive.addEventListener('input', () => {
settings.activeMs = parseInt(cfgActive.value, 10);
lblActive.textContent = `${settings.activeMs}ms`;
});
cfgWindup.addEventListener('input', () => {
settings.windupSec = parseFloat(cfgWindup.value);
lblWindup.textContent = `${settings.windupSec.toFixed(2)}s`;
});
cfgSpeed.addEventListener('input', () => {
settings.enemySpeed = parseFloat(cfgSpeed.value);
lblSpeed.textContent = settings.enemySpeed.toFixed(1);
});
cfgDashSpeed.addEventListener('input', () => {
settings.dashSpeed = parseInt(cfgDashSpeed.value, 10);
lblDashSpeed.textContent = `${settings.dashSpeed}`;
});
cfgFeint.addEventListener('change', () => settings.feint = cfgFeint.checked);
cfgCombo.addEventListener('change', () => settings.combo = cfgCombo.checked);
cfgTelegraph.addEventListener('change', () => settings.telegraph = cfgTelegraph.checked);
Object.entries(bindButtons).forEach(([action, btn]) => {
btn.addEventListener('click', () => {
Object.values(bindButtons).forEach(b => b.classList.remove('listening'));
listeningForBind = action;
btn.classList.add('listening');
btn.textContent = 'キーを押してください...';
});
});
// Actions
function triggerBlockStart() {
if (player.blockCd > 0 || player.isBlocking || player.isVaporizing) return;
player.isBlocking = true;
player.blockHoldTimer = player.blockMaxHold;
player.blockCd = player.blockCdMax;
player.blockStartTime = performance.now();
player.parryWindowTimer = 1.0;
player.blockDeployAnim = 0.01;
player.bodySquash = -0.22;
spawnShockwave(player.x, player.y, player.radius * 0.6, player.radius + 20, 'rgba(16, 185, 129, 0.75)', 0.22, 3);
spawnDirectionalSparks(
player.x + Math.cos(player.angle) * 18,
player.y + Math.sin(player.angle) * 18,
player.angle,
1.4,
8,
'#34d399',
60,
160
);
}
function triggerPunch() {
if (player.punchCd > 0 || player.isVaporizing) return;
const isParry = player.parryWindowTimer > 0;
const isDashingNow = player.isDashing || keysDown.has(keybinds.dash);
player.punchCd = player.punchCdMax;
player.punchActiveTimer = 0.16;
player.punchVisualMax = isParry ? 0.26 : 0.20;
player.punchVisualTimer = player.punchVisualMax;
player.punchAngle = player.angle;
player.punchIsParryAttempt = isParry;
player.punchHitResolved = false;
player.punchTrailTimer = 0.025;
player.bodySquash = isParry ? 0.35 : 0.25;
// 【隠しバグ仕様】ダッシュしながらパリィパンチすると一定時間障害物のすり抜けが発生!
if (isParry && isDashingNow) {
player.wallClipTimer = 0.24;
}
const stepSpeed = isParry ? 540 : 310;
player.lungeVx = Math.cos(player.angle) * stepSpeed;
player.lungeVy = Math.sin(player.angle) * stepSpeed;
const muzzleX = player.x + Math.cos(player.angle) * 20;
const muzzleY = player.y + Math.sin(player.angle) * 20;
if (isParry) {
spawnShockwave(muzzleX, muzzleY, 8, 52, 'rgba(250, 204, 21, 0.85)', 0.24, 3.5);
spawnDirectionalSparks(muzzleX, muzzleY, player.angle, 0.9, 14, '#facc15', 140, 340);
} else {
spawnShockwave(muzzleX, muzzleY, 6, 38, 'rgba(56, 189, 248, 0.7)', 0.18, 2.5);
spawnDirectionalSparks(muzzleX, muzzleY, player.angle, 0.7, 8, '#38bdf8', 110, 260);
}
spawnHitboxTrail(player.x, player.y, player.angle, isParry ? 62 : 58, isParry ? 44 : 42, 'player');
evaluatePlayerPunchHit();
}
function evaluatePlayerPunchHit() {
if (player.punchHitResolved) return;
const punchReach = player.punchIsParryAttempt ? 62 : 58;
const punchWidth = player.punchIsParryAttempt ? 44 : 42;
const hitEnemy = checkRectCircleHit(
player.x,
player.y,
player.punchAngle,
punchReach,
punchWidth,
enemy.x,
enemy.y,
enemy.radius
);
if (hitEnemy) {
player.punchHitResolved = true;
const contactX = (player.x + enemy.x) / 2;
const contactY = (player.y + enemy.y) / 2;
if (player.punchIsParryAttempt || player.parryWindowTimer > 0) {
executeParry(contactX, contactY);
} else {
enemy.state = 'DISARMED';
enemy.stateTimer = 3.0;
enemy.comboRemaining = 0;
enemy.attackTriggerDelay = -1;
enemy.moveVx = 0;
enemy.moveVy = 0;
enemy.knockbackVx = Math.cos(player.punchAngle) * 440;
enemy.knockbackVy = Math.sin(player.punchAngle) * 440;
enemy.stamina = Math.max(0, enemy.stamina - 15);
enemy.hitFlashTimer = 0.22;
hitstopTimer = 0.045;
triggerScreenShake(7, 0.18);
spawnShockwave(contactX, contactY, 10, 72, '#38bdf8', 0.28, 4);
spawnParticles(contactX, contactY, 18, '#38bdf8', 120, 360, 0.2, 0.45, 2.5, 5);
spawnDirectionalSparks(contactX, contactY, player.punchAngle, 1.3, 14, '#bae6fd', 180, 420);
showJudgment('COUNTER PUNCH! (3s封じ)', '#38bdf8');
}
}
}
function executeParry(contactX, contactY) {
player.parryWindowTimer = 0;
player.isBlocking = false;
player.blockHoldTimer = 0;
enemy.state = 'STUNNED';
enemy.stateTimer = 2.0;
enemy.comboRemaining = 0;
enemy.attackTriggerDelay = -1;
enemy.moveVx = 0;
enemy.moveVy = 0;
enemy.knockbackVx = Math.cos(player.punchAngle) * 820;
enemy.knockbackVy = Math.sin(player.punchAngle) * 820;
enemy.stamina = Math.max(0, enemy.stamina - 25);
enemy.hitFlashTimer = 0.35;
hitstopTimer = 0.075;
triggerScreenShake(13, 0.28);
triggerScreenFlash('#fef08a', 0.22);
spawnParryFlare(contactX, contactY, player.punchAngle);
spawnShockwave(contactX, contactY, 12, 115, '#facc15', 0.36, 5.5);
spawnShockwave(contactX, contactY, 6, 70, '#ffffff', 0.24, 3.0);
spawnParticles(contactX, contactY, 32, '#facc15', 160, 520, 0.25, 0.55, 3, 6);
spawnParticles(contactX, contactY, 16, '#ffffff', 120, 420, 0.18, 0.40, 2, 4.5);
spawnDirectionalSparks(contactX, contactY, player.punchAngle, 1.8, 22, '#fef08a', 220, 560);
let label = 'PREDICTION PARRY (0ms)';
let color = '#facc15';
let reactionMs = 0;
if (enemy.activeStartTime > 0 && player.blockStartTime >= enemy.activeStartTime) {
reactionMs = Math.round(player.blockStartTime - enemy.activeStartTime);
label = `HITBOX DRAG (+${reactionMs}ms)`;
color = '#10b981';
} else if (enemy.windupStartTime > 0 && player.blockStartTime >= enemy.windupStartTime) {
reactionMs = Math.round(player.blockStartTime - enemy.windupStartTime);
label = `REACT PARRY (${reactionMs}ms)`;
color = '#22d3ee';
} else {
reactionMs = enemy.windupStartTime > 0
? Math.round(player.blockStartTime - enemy.windupStartTime)
: 0;
label = `PREDICTION PARRY (0ms)`;
color = '#facc15';
}
stats.attempts++;
stats.successes++;
stats.lastReactionMs = reactionMs;
updateStatsUI();
showJudgment(label, color);
if (settings.endless && enemy.stamina <= 0) {
enemy.stamina = enemy.maxStamina;
}
}
// 波動砲直撃時の蒸発&初期位置リスポーン開始
function triggerPlayerVaporize(beamAngle) {
player.isBlocking = false;
player.blockHoldTimer = 0;
player.parryWindowTimer = 0;
player.isInsideObstacle = false;
player.insideObstacleTimer = 0;
player.wallClipTimer = 0;
player.isVaporizing = true;
player.vaporizeTimer = player.vaporizeDuration;
player.vaporizeTrailTimer = 0;
// 超特大ノックバックで吹っ飛ばしながら消滅!
player.lungeVx = Math.cos(beamAngle) * 1450;
player.lungeVy = Math.sin(beamAngle) * 1450;
hitstopTimer = 0.085;
triggerScreenShake(26, 0.65);
triggerScreenFlash('#ffffff', 0.55);
// 大爆発&蒸発パーティクル
spawnShockwave(player.x, player.y, 10, 180, '#ffffff', 0.45, 7);
spawnShockwave(player.x, player.y, 15, 240, '#f43f5e', 0.55, 6);
spawnShockwave(player.x, player.y, 8, 140, '#38bdf8', 0.50, 5);
spawnParticles(player.x, player.y, 55, '#ffffff', 120, 620, 0.35, 0.85, 2.5, 6.5, -40);
spawnParticles(player.x, player.y, 45, '#38bdf8', 100, 520, 0.40, 0.90, 2.5, 6.0, -60);
spawnParticles(player.x, player.y, 35, '#f43f5e', 150, 680, 0.30, 0.75, 3.0, 7.0);
spawnDirectionalSparks(player.x, player.y, beamAngle, 1.1, 40, '#fef08a', 320, 880);
showJudgment('VAPORIZED!! (波動砲で蒸発)', '#f43f5e');
}
function checkRectCircleHit(ox, oy, angle, length, width, cx, cy, cr) {
const dx = cx - ox;
const dy = cy - oy;
const cos = Math.cos(-angle);
const sin = Math.sin(-angle);
const localX = dx * cos - dy * sin;
const localY = dx * sin + dy * cos;
const minX = 0;
const maxX = length;
const minY = -width / 2;
const maxY = width / 2;
const closestX = Math.max(minX, Math.min(localX, maxX));
const closestY = Math.max(minY, Math.min(localY, maxY));
const distX = localX - closestX;
const distY = localY - closestY;
return (distX * distX + distY * distY) <= (cr * cr);
}
function checkPlayerInsideAnyObstacle() {
for (const obs of obstacles) {
if (
player.x >= obs.x &&
player.x <= obs.x + obs.w &&
player.y >= obs.y &&
player.y <= obs.y + obs.h
) {
return true;
}
}
return false;
}
function resolveCollisions(entity, isPlayer = false) {
entity.x = Math.max(WORLD.minX + entity.radius, Math.min(WORLD.maxX - entity.radius, entity.x));
entity.y = Math.max(WORLD.minY + entity.radius, Math.min(WORLD.maxY - entity.radius, entity.y));
if (isPlayer) {
const insideNow = checkPlayerInsideAnyObstacle();
if (player.wallClipTimer > 0 || insideNow || player.isVaporizing) {
return;
}
}
for (const obs of obstacles) {
const closestX = Math.max(obs.x, Math.min(entity.x, obs.x + obs.w));
const closestY = Math.max(obs.y, Math.min(entity.y, obs.y + obs.h));
const dx = entity.x - closestX;
const dy = entity.y - closestY;
const distSq = dx * dx + dy * dy;
if (distSq < entity.radius * entity.radius) {
const dist = Math.sqrt(distSq) || 0.001;
const overlap = entity.radius - dist;
entity.x += (dx / dist) * overlap;
entity.y += (dy / dist) * overlap;
}
}
}
function isPointBlocked(px, py, pad) {
if (px < WORLD.minX + pad || px > WORLD.maxX - pad || py < WORLD.minY + pad || py > WORLD.maxY - pad) {
return true;
}
for (const obs of obstacles) {
if (
px > obs.x - pad &&
px < obs.x + obs.w + pad &&
py > obs.y - pad &&
py < obs.y + obs.h + pad
) {
return true;
}
}
return false;
}
function getClearanceScore(x, y, angle, maxDist, pad) {
const steps = 6;
for (let i = 1; i <= steps; i++) {
const d = (maxDist / steps) * i;
if (isPointBlocked(x + Math.cos(angle) * d, y + Math.sin(angle) * d, pad)) {
return (i - 1) / steps;
}
}
return 1.0;
}
function getObstacleAvoidedAngle(fromX, fromY, desiredAngle) {
const pad = enemy.radius + 8;
const lookAhead = 76;
if (getClearanceScore(fromX, fromY, desiredAngle, lookAhead, pad) >= 1.0) {
return desiredAngle;
}
let bestAngle = desiredAngle;
let bestScore = -999;
const candidateOffsets = [
0.35, -0.35,
0.70, -0.70,
1.05, -1.05,
1.40, -1.40,
1.85, -1.85,
2.35, -2.35
];
for (const offset of candidateOffsets) {
const cand = desiredAngle + offset;
const clearance = getClearanceScore(fromX, fromY, cand, lookAhead, pad);
const alignment = Math.cos(offset);
const sideBias = (Math.sign(offset) === enemy.avoidDirSign) ? 0.08 : 0;
const score = clearance * 2.4 + alignment * 0.65 + sideBias;
if (score > bestScore) {
bestScore = score;
bestAngle = cand;
}
}
const diff = Math.atan2(Math.sin(bestAngle - desiredAngle), Math.cos(bestAngle - desiredAngle));
if (Math.abs(diff) > 0.1) {
enemy.avoidDirSign = Math.sign(diff);
}
return bestAngle;
}
function resetAttackPacing() {
enemy.attackCooldown = 0.55 + Math.random() * 0.75;
enemy.attackTriggerDelay = -1;
enemy.preferredAttackDistMult = 0.72 + Math.random() * 0.28;
}
function updateStatsUI() {
if (stats.lastReactionMs !== null) {
reactionTimeText.textContent = `${stats.lastReactionMs} ms`;
}
const rate = stats.attempts > 0 ? Math.round((stats.successes / stats.attempts) * 100) : 0;
successRateText.textContent = `${rate}%`;
}
function updateEffects(dt) {
if (screenShakeTimer > 0) {
screenShakeTimer = Math.max(0, screenShakeTimer - dt);
if (screenShakeTimer <= 0) screenShakeIntensity = 0;
}
if (screenFlashAlpha > 0) {
screenFlashAlpha = Math.max(0, screenFlashAlpha - dt * 1.4);
}
if (player.blockImpactFlash > 0) {
player.blockImpactFlash = Math.max(0, player.blockImpactFlash - dt * 3.5);
}
if (player.respawnBeamTimer > 0) {
player.respawnBeamTimer = Math.max(0, player.respawnBeamTimer - dt);
}
if (enemy.hitFlashTimer > 0) {
enemy.hitFlashTimer = Math.max(0, enemy.hitFlashTimer - dt);
}
if (player.blockDeployAnim > 0 && player.blockDeployAnim < 1) {
player.blockDeployAnim = Math.min(1, player.blockDeployAnim + dt * 9.5);
}
player.bodySquash *= Math.pow(0.0005, dt);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= Math.pow(0.02, dt);
p.vy *= Math.pow(0.02, dt);
}
for (let i = shockwaves.length - 1; i >= 0; i--) {
const s = shockwaves[i];
s.life -= dt;
if (s.life <= 0) {
shockwaves.splice(i, 1);
}
}
for (let i = slashFlares.length - 1; i >= 0; i--) {
const f = slashFlares[i];
f.life -= dt;
if (f.life <= 0) {
slashFlares.splice(i, 1);
}
}
}
// Main Update Loop
let lastTimestamp = performance.now();
function update(dt) {
if (isPaused) return;
updateEffects(dt);
if (hitstopTimer > 0) {
hitstopTimer = Math.max(0, hitstopTimer - dt);
return;
}
for (let i = hitboxTrails.length - 1; i >= 0; i--) {
const t = hitboxTrails[i];
t.life -= dt;
if (t.life <= 0) {
hitboxTrails.splice(i, 1);
}
}
if (player.wallClipTimer > 0) player.wallClipTimer = Math.max(0, player.wallClipTimer - dt);
if (player.blockCd > 0) player.blockCd = Math.max(0, player.blockCd - dt);
if (player.punchCd > 0) player.punchCd = Math.max(0, player.punchCd - dt);
if (player.parryWindowTimer > 0) player.parryWindowTimer = Math.max(0, player.parryWindowTimer - dt);
if (player.punchVisualTimer > 0) player.punchVisualTimer = Math.max(0, player.punchVisualTimer - dt);
if (player.staminaRegenDelay > 0) player.staminaRegenDelay = Math.max(0, player.staminaRegenDelay - dt);
if (enemy.baitCooldown > 0) enemy.baitCooldown = Math.max(0, enemy.baitCooldown - dt);
if (enemy.attackCooldown > 0) enemy.attackCooldown = Math.max(0, enemy.attackCooldown - dt);
enemy.strafeTimer -= dt;
if (enemy.strafeTimer <= 0) {
enemy.strafeSign *= -1;
enemy.strafeTimer = 1.2 + Math.random() * 1.6;
}
if (player.isBlocking) {
player.blockHoldTimer -= dt;
if (player.blockHoldTimer <= 0) {
player.isBlocking = false;
player.blockHoldTimer = 0;
player.blockDeployAnim = 0;
}
} else {
player.blockDeployAnim = 0;
}
// 蒸発中の処理(吹っ飛びながら粒子に分解 → 初期位置にリスポーン)
if (player.isVaporizing) {
player.vaporizeTimer -= dt;
player.x += player.lungeVx * dt;
player.y += player.lungeVy * dt;
player.lungeVx *= Math.pow(0.015, dt);
player.lungeVy *= Math.pow(0.015, dt);
// 外周壁だけは超えないように制限
player.x = Math.max(WORLD.minX + player.radius, Math.min(WORLD.maxX - player.radius, player.x));
player.y = Math.max(WORLD.minY + player.radius, Math.min(WORLD.maxY - player.radius, player.y));
// 吹っ飛び軌跡から蒸発する光の粒を撒き散らす
player.vaporizeTrailTimer -= dt;
if (player.vaporizeTrailTimer <= 0) {
spawnParticles(player.x, player.y, 6, '#67e8f9', 30, 140, 0.25, 0.55, 2, 5, -55);
spawnParticles(player.x, player.y, 4, '#ffffff', 20, 100, 0.20, 0.45, 2, 4, -70);
player.vaporizeTrailTimer = 0.025;
}
if (player.vaporizeTimer <= 0) {
// 初期位置 (0, 0) に復活!
player.isVaporizing = false;
player.x = PLAYER_SPAWN.x;
player.y = PLAYER_SPAWN.y;
player.lungeVx = 0;
player.lungeVy = 0;
player.stamina = player.maxStamina;
player.staminaExhausted = false;
player.respawnBeamTimer = 0.45;
spawnShockwave(player.x, player.y, 65, 10, '#22d3ee', 0.35, 5);
spawnShockwave(player.x, player.y, 8, 85, '#67e8f9', 0.40, 4);
spawnParticles(player.x, player.y, 28, '#67e8f9', 80, 260, 0.25, 0.55, 2, 5, -45);
showJudgment('RESPAWNED (初期位置へ帰還)', '#38bdf8');
}
} else {
// 通常のプレイヤー移動処理
let moveX = 0;
let moveY = 0;
if (keysDown.has('KeyW')) moveY -= 1;
if (keysDown.has('KeyS')) moveY += 1;
if (keysDown.has('KeyA')) moveX -= 1;
if (keysDown.has('KeyD')) moveX += 1;
const hasMoveInput = (moveX !== 0 || moveY !== 0);
if (hasMoveInput) {
const len = Math.hypot(moveX, moveY);
moveX /= len;
moveY /= len;
player.angle = Math.atan2(moveY, moveX);
}
if (player.stamina <= 0) {
player.stamina = 0;
player.staminaExhausted = true;
} else if (player.staminaExhausted && player.stamina >= 25) {
player.staminaExhausted = false;
}
const dashKeyHeld = keysDown.has(keybinds.dash);
const canDash = dashKeyHeld && !player.staminaExhausted && player.stamina > 0;
if (canDash) {
player.isDashing = true;
player.stamina = Math.max(0, player.stamina - 36 * dt);
player.staminaRegenDelay = 0.45;
if (player.stamina <= 0) {
player.staminaExhausted = true;
}
const dirX = hasMoveInput ? moveX : Math.cos(player.angle);
const dirY = hasMoveInput ? moveY : Math.sin(player.angle);
const blockSlow = player.isBlocking ? 0.7 : 1.0;
player.x += dirX * settings.dashSpeed * blockSlow * dt;
player.y += dirY * settings.dashSpeed * blockSlow * dt;
} else {
player.isDashing = false;
if (player.staminaRegenDelay <= 0 && player.stamina < player.maxStamina) {
player.stamina = Math.min(player.maxStamina, player.stamina + 24 * dt);
}
const moveMult = player.isBlocking ? 0.6 : 1.0;
player.x += moveX * player.speed * moveMult * dt;
player.y += moveY * player.speed * moveMult * dt;
}
if (Math.hypot(player.lungeVx, player.lungeVy) > 5) {
player.x += player.lungeVx * dt;
player.y += player.lungeVy * dt;
player.lungeVx *= Math.pow(0.0001, dt);
player.lungeVy *= Math.pow(0.0001, dt);
}
resolveCollisions(player, true);
// 障害物へのめり込み状態&滞在時間の更新
const wasInside = player.isInsideObstacle;
player.isInsideObstacle = checkPlayerInsideAnyObstacle();
if (player.isInsideObstacle) {
if (!wasInside) {
spawnParticles(player.x, player.y, 14, '#22d3ee', 80, 220, 0.2, 0.4, 2, 4);
showJudgment('WALL CLIP! (壁めり込み発動)', '#67e8f9');
}
player.insideObstacleTimer += dt;
} else {
player.insideObstacleTimer = 0;
}
if (player.punchActiveTimer > 0) {
player.punchActiveTimer = Math.max(0, player.punchActiveTimer - dt);
player.punchTrailTimer -= dt;
if (player.punchTrailTimer <= 0) {
spawnHitboxTrail(
player.x,
player.y,
player.punchAngle,
player.punchIsParryAttempt ? 62 : 58,
player.punchIsParryAttempt ? 44 : 42,
'player'
);
player.punchTrailTimer = 0.025;
}
evaluatePlayerPunchHit();
}
}
if (Math.hypot(enemy.knockbackVx, enemy.knockbackVy) > 5) {
enemy.x += enemy.knockbackVx * dt;
enemy.y += enemy.knockbackVy * dt;
enemy.knockbackVx *= Math.pow(0.02, dt);
enemy.knockbackVy *= Math.pow(0.02, dt);
}
// Enemy AI State Machine
const toPlayerX = player.x - enemy.x;
const toPlayerY = player.y - enemy.y;
const distToPlayer = Math.hypot(toPlayerX, toPlayerY);
const baseAngleToPlayer = Math.atan2(toPlayerY, toPlayerX);
// プレイヤーが障害物の中にめり込んでいる場合、敵が困惑「?」状態へ移行
if (
!player.isVaporizing &&
player.isInsideObstacle &&
enemy.state !== 'CONFUSED' &&
enemy.state !== 'HADOU_CHARGE' &&
enemy.state !== 'HADOU_FIRE' &&
enemy.state !== 'STUNNED'
) {
enemy.state = 'CONFUSED';
enemy.confusedLookTimer = 0.4;
enemy.moveVx = 0;
enemy.moveVy = 0;
}
const runSpeed = settings.enemySpeed * 60;
const hasLineOfSight = getClearanceScore(enemy.x, enemy.y, baseAngleToPlayer, distToPlayer, enemy.radius * 0.65) >= 0.95;
const maxAttackStartRange = settings.reach + runSpeed * (settings.windupSec + (settings.activeMs / 1000) * 0.65);
const currentTriggerRange = maxAttackStartRange * enemy.preferredAttackDistMult;
if (enemy.state === 'CONFUSED') {
enemy.confusedLookTimer -= dt;
if (enemy.confusedLookTimer <= 0) {
enemy.confusedLookTimer = 0.45 + Math.random() * 0.35;
enemy.angle = baseAngleToPlayer + (Math.random() - 0.5) * 1.4;
}
if (!player.isInsideObstacle) {
resetAttackPacing();
enemy.state = 'CHASING';
} else if (player.insideObstacleTimer >= 3.0) {
// 3秒以上とどまると「!?」を出して超極太波動砲チャージ開始!
enemy.state = 'HADOU_CHARGE';
enemy.stateTimer = 1.15;
enemy.hadouAngle = baseAngleToPlayer;
enemy.angle = enemy.hadouAngle;
triggerScreenShake(7, 1.15);
spawnShockwave(enemy.x, enemy.y, 85, 12, '#f43f5e', 0.45, 4);
showJudgment('!? 敵が超高出力波動砲を溜めている!', '#f43f5e');
}
} else if (enemy.state === 'HADOU_CHARGE') {
enemy.stateTimer -= dt;
enemy.hadouAngle = baseAngleToPlayer;
enemy.angle = enemy.hadouAngle;
// 激しい収束エネルギー粒子&火花
for (let k = 0; k < 2; k++) {
const spawnAng = Math.random() * Math.PI * 2;
const dist = 55 + Math.random() * 65;
const px = enemy.x + Math.cos(spawnAng) * dist;
const py = enemy.y + Math.sin(spawnAng) * dist;
particles.push({
x: px,
y: py,
vx: (enemy.x - px) * 4.6,
vy: (enemy.y - py) * 4.6,
life: 0.24,
maxLife: 0.24,
color: Math.random() < 0.4 ? '#ffffff' : (Math.random() < 0.5 ? '#f43f5e' : '#facc15'),
size: 3 + Math.random() * 3.5
});
}
if (enemy.stateTimer <= 0) {
// 超極太波動砲発射!!
enemy.state = 'HADOU_FIRE';
enemy.stateTimer = 0.85;
enemy.hadouHitThisBeam = false;
// 発射反動で敵自身も少し後ろへノックバック
enemy.knockbackVx = -Math.cos(enemy.hadouAngle) * 260;
enemy.knockbackVy = -Math.sin(enemy.hadouAngle) * 260;
triggerScreenShake(22, 0.80);
triggerScreenFlash('#f43f5e', 0.42);
spawnShockwave(enemy.x, enemy.y, 15, 210, '#f43f5e', 0.50, 8);
spawnShockwave(enemy.x, enemy.y, 10, 140, '#facc15', 0.38, 5);
spawnDirectionalSparks(enemy.x, enemy.y, enemy.hadouAngle, 0.9, 55, '#facc15', 280, 850);
spawnDirectionalSparks(enemy.x, enemy.y, enemy.hadouAngle, 0.5, 35, '#ffffff', 350, 950);
showJudgment('超極太波動砲!! (GIGA HADOUHOU!)', '#f43f5e');
}
} else if (enemy.state === 'HADOU_FIRE') {
enemy.stateTimer -= dt;
enemy.angle = enemy.hadouAngle;
// 発射中もビーム周囲から火花が散り続ける
if (Math.random() < 0.85) {
const distAlong = 30 + Math.random() * 550;
const bx = enemy.x + Math.cos(enemy.hadouAngle) * distAlong;
const by = enemy.y + Math.sin(enemy.hadouAngle) * distAlong;
spawnDirectionalSparks(bx, by, enemy.hadouAngle + (Math.random() < 0.5 ? 1.2 : -1.2), 0.7, 2, '#facc15', 140, 380);
}
if (!enemy.hadouHitThisBeam && !player.isVaporizing) {
const hitBeam = checkRectCircleHit(
enemy.x,
enemy.y,
enemy.hadouAngle,
1500,
110, // ビームの極太当たり判定(幅110px)
player.x,
player.y,
player.radius
);
if (hitBeam) {
enemy.hadouHitThisBeam = true;
if (player.parryWindowTimer > 0 && player.punchActiveTimer > 0) {
executeParry(player.x, player.y);
} else {
stats.attempts++;
updateStatsUI();
triggerPlayerVaporize(enemy.hadouAngle);
}
}
}
if (enemy.stateTimer <= 0) {
resetAttackPacing();
enemy.state = 'CHASING';
}
} else if (enemy.state === 'STUNNED') {
enemy.stateTimer -= dt;
if (enemy.stateTimer <= 0) {
resetAttackPacing();
enemy.state = 'CHASING';
}
} else if (enemy.state === 'DISARMED') {
enemy.stateTimer -= dt;
enemy.angle = baseAngleToPlayer;
if (!player.isVaporizing && distToPlayer > settings.reach * 0.85) {
const moveAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baseAngleToPlayer);
enemy.x += Math.cos(moveAngle) * (runSpeed * 0.72) * dt;
enemy.y += Math.sin(moveAngle) * (runSpeed * 0.72) * dt;
}
if (enemy.stateTimer <= 0) {
resetAttackPacing();
enemy.state = 'CHASING';
}
} else if (enemy.state === 'CHASING') {
if (player.isVaporizing) {
// プレイヤーが蒸発している間は中央付近を警戒して待機
enemy.angle = baseAngleToPlayer;
} else {
if (
settings.feint &&
enemy.attackCooldown <= 0 &&
enemy.baitCooldown <= 0 &&
hasLineOfSight &&
distToPlayer <= maxAttackStartRange * 1.05 &&
distToPlayer > settings.reach * 0.95
) {
if (Math.random() < 0.06 * dt) {
enemy.state = 'BAITING';
enemy.stateTimer = 0.20;
enemy.baitCooldown = 10.0;
enemy.baitStrafeSign = Math.random() < 0.5 ? 1 : -1;
enemy.angle = baseAngleToPlayer;
return;
}
}
if (enemy.attackCooldown <= 0 && hasLineOfSight && distToPlayer <= currentTriggerRange) {
if (enemy.attackTriggerDelay < 0) {
enemy.attackTriggerDelay = Math.random() < 0.55 ? 0 : (0.08 + Math.random() * 0.35);
}
enemy.attackTriggerDelay -= dt;
if (enemy.attackTriggerDelay <= 0) {
enemy.attackTriggerDelay = -1;
chooseAndStartAttack(baseAngleToPlayer);
return;
}
} else if (distToPlayer > maxAttackStartRange * 1.15) {
enemy.attackTriggerDelay = -1;
}
let desiredMoveAngle = baseAngleToPlayer;
const closeOrbitDist = enemy.radius + player.radius + 8;
if (distToPlayer <= closeOrbitDist) {
desiredMoveAngle = baseAngleToPlayer + (Math.PI * 0.45) * enemy.strafeSign;
}
const steerAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, desiredMoveAngle);
enemy.angle = baseAngleToPlayer;
if (distToPlayer > enemy.radius + player.radius + 3) {
enemy.x += Math.cos(steerAngle) * runSpeed * dt;
enemy.y += Math.sin(steerAngle) * runSpeed * dt;
}
}
} else if (enemy.state === 'BAITING') {
enemy.stateTimer -= dt;
enemy.angle = baseAngleToPlayer;
const baitAngle = baseAngleToPlayer + Math.PI * 0.55 * enemy.baitStrafeSign;
const safeAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baitAngle);
enemy.x += Math.cos(safeAngle) * (runSpeed * 0.85) * dt;
enemy.y += Math.sin(safeAngle) * (runSpeed * 0.85) * dt;
if (enemy.stateTimer <= 0) {
if (!player.isVaporizing && Math.random() < 0.80 && distToPlayer <= maxAttackStartRange + 25) {
chooseAndStartAttack(baseAngleToPlayer);
} else {
enemy.attackCooldown = 0.35 + Math.random() * 0.4;
enemy.state = 'CHASING';
}
}
} else if (enemy.state === 'WINDUP') {
enemy.stateTimer -= dt;
const steerAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baseAngleToPlayer);
if (enemy.attackTactic === 'BACKSTEP') {
const backAngle = getObstacleAvoidedAngle(enemy.x, enemy.y, baseAngleToPlayer + Math.PI);
enemy.x += Math.cos(backAngle) * (runSpeed * 0.55) * dt;
enemy.y += Math.sin(backAngle) * (runSpeed * 0.55) * dt;
} else if (distToPlayer > enemy.radius + player.radius + 4) {
enemy.x += Math.cos(steerAngle) * runSpeed * dt;
enemy.y += Math.sin(steerAngle) * runSpeed * dt;
}
if (settings.feint) {
if (enemy.attackTactic === 'MISDIRECT' || enemy.attackTactic === 'BAIT_WHIFF') {
enemy.attackAngle = baseAngleToPlayer + enemy.misdirectOffset;
} else {
enemy.attackAngle = baseAngleToPlayer;
}
if (enemy.feintPlanned && enemy.stateTimer <= enemy.currentWindupDuration * 0.42) {
enemy.feintPlanned = false;
enemy.attackCooldown = 0.3 + Math.random() * 0.35;
enemy.state = 'CHASING';
return;
}
} else {
enemy.attackAngle = baseAngleToPlayer;
}
enemy.angle = enemy.attackAngle;
if (enemy.stateTimer <= 0) {
enemy.state = 'ATTACKING';
enemy.stateTimer = settings.activeMs / 1000;
enemy.activeStartTime = performance.now();
enemy.hasHitThisSwing = false;
enemy.hasBeenBlockedThisSwing = false;
trailSpawnTimer = 0.024;
if (enemy.attackTactic === 'BACKSTEP') {
enemy.moveVx = -Math.cos(enemy.attackAngle) * (runSpeed * 0.45);
enemy.moveVy = -Math.sin(enemy.attackAngle) * (runSpeed * 0.45);
} else if (enemy.attackTactic === 'BAIT_WHIFF') {
enemy.moveVx = -Math.cos(enemy.attackAngle) * (runSpeed * 0.25);
enemy.moveVy = -Math.sin(enemy.attackAngle) * (runSpeed * 0.25);
} else {
enemy.moveVx = Math.cos(enemy.attackAngle) * runSpeed;
enemy.moveVy = Math.sin(enemy.attackAngle) * runSpeed;
}
spawnHitboxTrail(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, 'enemy');
}
} else if (enemy.state === 'ATTACKING') {
enemy.stateTimer -= dt;
if (enemy.attackTactic === 'BACKSTEP' || enemy.attackTactic === 'BAIT_WHIFF' || distToPlayer > enemy.radius + player.radius + 4) {
enemy.x += enemy.moveVx * dt;
enemy.y += enemy.moveVy * dt;
}
trailSpawnTimer -= dt;
if (trailSpawnTimer <= 0) {
spawnHitboxTrail(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, 'enemy');
trailSpawnTimer = 0.024;
}
if (!enemy.hasHitThisSwing && !enemy.hasBeenBlockedThisSwing && !player.isVaporizing) {
const hit = checkRectCircleHit(
enemy.x,
enemy.y,
enemy.attackAngle,
settings.reach,
34,
player.x,
player.y,
player.radius
);
if (hit) {
if (player.isBlocking) {
enemy.hasBeenBlockedThisSwing = true;
player.punchCd = 0;
player.parryWindowTimer = 1.0;
player.blockImpactFlash = 1.0;
player.bodySquash = -0.32;
const shieldX = player.x + Math.cos(player.angle) * (player.radius + 10);
const shieldY = player.y + Math.sin(player.angle) * (player.radius + 10);
spawnShockwave(shieldX, shieldY, 10, 68, '#10b981', 0.28, 4.5);
spawnParticles(shieldX, shieldY, 20, '#34d399', 100, 320, 0.2, 0.45, 2.5, 5);
spawnDirectionalSparks(shieldX, shieldY, player.angle, 2.2, 16, '#a7f3d0', 140, 360);
triggerScreenShake(6, 0.16);
hitstopTimer = 0.035;
enemy.knockbackVx = -Math.cos(enemy.attackAngle) * 180;
enemy.knockbackVy = -Math.sin(enemy.attackAngle) * 180;
const react = Math.max(0, Math.round(player.blockStartTime - enemy.windupStartTime));
stats.lastReactionMs = react;
updateStatsUI();
showJudgment('BLOCKED! (1秒以内にパンチでパリィ)', '#10b981');
} else {
enemy.hasHitThisSwing = true;
stats.attempts++;
const react = Math.round(performance.now() - enemy.windupStartTime);
stats.lastReactionMs = react;
updateStatsUI();
triggerScreenShake(9, 0.22);
triggerScreenFlash('#f43f5e', 0.18);
spawnParticles(player.x, player.y, 18, '#f43f5e', 120, 340, 0.2, 0.4, 3, 5);
showJudgment('HIT! (被弾)', '#f43f5e');
}
}
}
if (enemy.stateTimer <= 0) {
enemy.moveVx = 0;
enemy.moveVy = 0;
if (enemy.comboRemaining > 0 && !player.isVaporizing) {
enemy.comboRemaining--;
const nextAngle = Math.atan2(player.y - enemy.y, player.x - enemy.x);
chooseAndStartAttack(nextAngle, true);
} else {
resetAttackPacing();
enemy.state = 'CHASING';
}
}
}
resolveCollisions(enemy, false);
updateHUD();
}
function chooseAndStartAttack(targetAngle, isFollowUpCombo = false) {
enemy.state = 'WINDUP';
const windupJitter = 0.75 + Math.random() * 0.50;
enemy.currentWindupDuration = Math.max(0.10, settings.windupSec * windupJitter);
enemy.stateTimer = enemy.currentWindupDuration;
enemy.windupStartTime = performance.now();
enemy.activeStartTime = 0;
enemy.attackAngle = targetAngle;
enemy.feintPlanned = false;
enemy.misdirectOffset = 0;
if (!isFollowUpCombo && settings.combo) {
enemy.comboRemaining = Math.floor(Math.random() * 2) + 1;
}
if (!settings.feint) {
enemy.attackTactic = 'NORMAL';
return;
}
const roll = Math.random();
if (roll < 0.86) {
enemy.attackTactic = 'NORMAL';
} else if (roll < 0.90) {
enemy.attackTactic = 'BACKSTEP';
} else if (roll < 0.95) {
enemy.attackTactic = 'MISDIRECT';
enemy.misdirectOffset = (Math.random() < 0.5 ? 1 : -1) * 0.42;
} else if (roll < 0.98) {
enemy.attackTactic = 'BAIT_WHIFF';
enemy.misdirectOffset = (Math.random() < 0.5 ? 1 : -1) * 0.22;
} else {
enemy.attackTactic = 'NORMAL';
enemy.feintPlanned = true;
}
}
function updateHUD() {
const blockRatio = 1 - (player.blockCd / player.blockCdMax);
blockCdBar.style.width = `${Math.max(0, Math.min(100, blockRatio * 100))}%`;
const punchRatio = 1 - (player.punchCd / player.punchCdMax);
punchCdBar.style.width = `${Math.max(0, Math.min(100, punchRatio * 100))}%`;
actionPunchBtn.classList.toggle('ready', player.punchCd <= 0 && !player.isVaporizing);
const stamInt = Math.round(player.stamina);
playerStaminaText.textContent = player.isVaporizing ? 'VAPORIZED...' : `${stamInt} / ${player.maxStamina}`;
playerStaminaBar.style.width = player.isVaporizing ? '0%' : `${(player.stamina / player.maxStamina) * 100}%`;
playerStaminaBar.classList.toggle('exhausted', player.staminaExhausted || player.isVaporizing);
if (enemy.state === 'CONFUSED') {
aiStateText.textContent = `CONFUSED (?) [${player.insideObstacleTimer.toFixed(1)}s]`;
aiStateText.className = 'stat-val-yellow';
} else if (enemy.state === 'HADOU_CHARGE') {
aiStateText.textContent = 'CHARGING GIGA HADOU (!?)';
aiStateText.className = 'stat-val-red';
} else if (enemy.state === 'HADOU_FIRE') {
aiStateText.textContent = 'GIGA HADOUHOU!!';
aiStateText.className = 'stat-val-red';
} else if (enemy.state === 'STUNNED') {
aiStateText.textContent = 'STUNNED!';
aiStateText.className = 'stat-val-purple';
} else if (enemy.state === 'DISARMED') {
aiStateText.textContent = 'NO ATTACK (3s)';
aiStateText.className = 'stat-val-yellow';
} else if (enemy.state === 'BAITING') {
aiStateText.textContent = 'BLOCK BAIT!';
aiStateText.className = 'stat-val-orange';
} else if (enemy.state === 'WINDUP') {
if (enemy.attackTactic === 'BACKSTEP') {
aiStateText.textContent = 'BACKSTEP WINDUP!';
} else if (enemy.attackTactic === 'BAIT_WHIFF' || enemy.attackTactic === 'MISDIRECT') {
aiStateText.textContent = 'BAIT / FEINT!';
} else {
aiStateText.textContent = 'WINDUP!';
}
aiStateText.className = 'stat-val-yellow';
} else if (enemy.state === 'ATTACKING') {
if (enemy.attackTactic === 'BACKSTEP') {
aiStateText.textContent = 'BACKSTEP ATK!';
} else {
aiStateText.textContent = 'ATTACKING!';
}
aiStateText.className = 'stat-val-red';
} else {
aiStateText.textContent = 'CHASING';
aiStateText.className = 'stat-val-cyan';
}
const enemyStamInt = Math.round(enemy.stamina);
enemyStaminaText.textContent = `${enemyStamInt} / ${enemy.maxStamina}`;
enemyStaminaBar.style.width = `${(enemy.stamina / enemy.maxStamina) * 100}%`;
}
// Rendering (2D Camera centered on Player)
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let shakeX = 0;
let shakeY = 0;
if (screenShakeTimer > 0) {
shakeX = (Math.random() - 0.5) * 2 * screenShakeIntensity;
shakeY = (Math.random() - 0.5) * 2 * screenShakeIntensity;
}
const camX = canvas.width / 2 - player.x + shakeX;
const camY = canvas.height / 2 - player.y + shakeY;
ctx.save();
ctx.translate(camX, camY);
// 1. Background Grid
const gridSize = 64;
const startX = Math.floor((player.x - canvas.width / 2) / gridSize) * gridSize;
const endX = player.x + canvas.width / 2 + gridSize;
const startY = Math.floor((player.y - canvas.height / 2) / gridSize) * gridSize;
const endY = player.y + canvas.height / 2 + gridSize;
ctx.strokeStyle = 'rgba(30, 41, 59, 0.45)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let gx = startX; gx <= endX; gx += gridSize) {
if (gx >= WORLD.minX && gx <= WORLD.maxX) {
ctx.moveTo(gx, Math.max(WORLD.minY, startY));
ctx.lineTo(gx, Math.min(WORLD.maxY, endY));
}
}
for (let gy = startY; gy <= endY; gy += gridSize) {
if (gy >= WORLD.minY && gy <= WORLD.maxY) {
ctx.moveTo(Math.max(WORLD.minX, startX), gy);
ctx.lineTo(Math.min(WORLD.maxX, endX), gy);
}
}
ctx.stroke();
// 2. Red Out-of-Bounds Boundary Lines
ctx.save();
ctx.strokeStyle = '#f43f5e';
ctx.lineWidth = 6;
ctx.shadowColor = '#f43f5e';
ctx.shadowBlur = 18;
ctx.strokeRect(
WORLD.minX,
WORLD.minY,
WORLD.maxX - WORLD.minX,
WORLD.maxY - WORLD.minY
);
ctx.restore();
// 3. Cyan Obstacles
for (const obs of obstacles) {
ctx.save();
ctx.fillStyle = 'rgba(34, 211, 238, 0.08)';
ctx.strokeStyle = '#22d3ee';
ctx.lineWidth = 2;
ctx.shadowColor = '#22d3ee';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.roundRect(obs.x, obs.y, obs.w, obs.h, 8);
ctx.fill();
ctx.stroke();
ctx.restore();
}
// 4. Hitbox Afterimages
for (const t of hitboxTrails) {
const ratio = Math.max(0, t.life / t.maxLife);
const strokeAlpha = 0.42 * ratio;
const fillAlpha = 0.08 * ratio;
ctx.save();
ctx.translate(t.x, t.y);
ctx.rotate(t.angle);
ctx.lineWidth = 1.0;
ctx.setLineDash([2, 2]);
if (t.type === 'enemy') {
ctx.strokeStyle = `rgba(244, 63, 94, ${strokeAlpha})`;
ctx.fillStyle = `rgba(244, 63, 94, ${fillAlpha})`;
} else {
ctx.strokeStyle = `rgba(56, 189, 248, ${strokeAlpha})`;
ctx.fillStyle = `rgba(56, 189, 248, ${fillAlpha})`;
}
ctx.fillRect(0, -t.width / 2, t.length, t.width);
ctx.strokeRect(0, -t.width / 2, t.length, t.width);
ctx.restore();
}
// 5. Enemy Current Attack Hitbox & Hadouhou Laser
if (enemy.state === 'ATTACKING') {
drawAttackBox(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, true);
} else if (enemy.state === 'WINDUP') {
drawAttackBox(enemy.x, enemy.y, enemy.attackAngle, settings.reach, 34, false);
} else if (enemy.state === 'HADOU_CHARGE') {
drawHadouCharge();
} else if (enemy.state === 'HADOU_FIRE') {
drawHadouBeam();
}
// 6. Player Punch & Parry Animated Strike
if (player.punchVisualTimer > 0 && !player.isVaporizing) {
drawPlayerPunchAnimation();
}
// 7. Enemy Entity & Overhead Icon (? / !?)
drawEnemyEntity();
// 8. Player Entity & Block Shield Animation (蒸発完了後は消滅)
drawPlayerEntity();
// 9. Shockwaves, Parry Flares & Particles
drawVisualEffects();
ctx.restore();
// Screen Flash Overlay
if (screenFlashAlpha > 0) {
ctx.save();
ctx.fillStyle = screenFlashColor;
ctx.globalAlpha = screenFlashAlpha;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
}
function drawEnemyEntity() {
ctx.save();
let enemyColor = '#94a3b8';
let enemyGlow = 'transparent';
if (enemy.hitFlashTimer > 0) {
enemyColor = '#ffffff';
enemyGlow = '#facc15';
} else if (enemy.state === 'HADOU_CHARGE' || enemy.state === 'HADOU_FIRE') {
enemyColor = '#f43f5e';
enemyGlow = '#f43f5e';
} else if (enemy.state === 'CONFUSED') {
enemyColor = '#facc15';
enemyGlow = '#facc15';
} else if (enemy.state === 'STUNNED') {
enemyColor = '#9333ea';
enemyGlow = '#a855f7';
} else if (enemy.state === 'BAITING') {
enemyColor = '#fb923c';
enemyGlow = '#fb923c';
} else if (enemy.state === 'WINDUP' && settings.telegraph) {
enemyColor = '#facc15';
enemyGlow = '#facc15';
} else if (enemy.state === 'ATTACKING') {
enemyColor = '#f43f5e';
enemyGlow = '#f43f5e';
} else if (enemy.state === 'DISARMED') {
enemyColor = '#64748b';
}
ctx.fillStyle = enemyColor;
ctx.shadowColor = enemyGlow;
ctx.shadowBlur = enemyGlow !== 'transparent' ? 22 : 0;
ctx.beginPath();
ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI * 2);
ctx.fill();
// 頭上の「?」および「!?」アイコン描画
if (enemy.state === 'CONFUSED') {
const bob = Math.sin(performance.now() * 0.012) * 3;
ctx.font = '900 26px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#facc15';
ctx.shadowColor = '#facc15';
ctx.shadowBlur = 14;
ctx.fillText('?', enemy.x, enemy.y - enemy.radius - 8 + bob);
} else if (enemy.state === 'HADOU_CHARGE' || enemy.state === 'HADOU_FIRE') {
const shakeIconX = (Math.random() - 0.5) * 6;
const shakeIconY = (Math.random() - 0.5) * 6;
ctx.font = '900 34px "Segoe UI", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#f43f5e';
ctx.shadowColor = '#f43f5e';
ctx.shadowBlur = 22;
ctx.fillText('!?', enemy.x + shakeIconX, enemy.y - enemy.radius - 10 + shakeIconY);
}
ctx.restore();
}
function drawHadouCharge() {
const progress = 1 - (enemy.stateTimer / 1.15);
const now = performance.now();
// 1. 敵の足元に広がる回転エネルギー魔法陣リング
ctx.save();
ctx.translate(enemy.x, enemy.y);
ctx.rotate(now * 0.008);
ctx.strokeStyle = `rgba(244, 63, 94, ${0.4 + progress * 0.6})`;
ctx.lineWidth = 3;
ctx.shadowColor = '#f43f5e';
ctx.shadowBlur = 24;
ctx.setLineDash([14, 8]);
ctx.beginPath();
ctx.arc(0, 0, 26 + progress * 34, 0, Math.PI * 2);
ctx.stroke();
ctx.rotate(-now * 0.016);
ctx.strokeStyle = `rgba(250, 204, 21, ${0.4 + progress * 0.6})`;
ctx.lineWidth = 2;
ctx.setLineDash([8, 10]);
ctx.beginPath();
ctx.arc(0, 0, 18 + progress * 22, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
// 2. 射線予測ライン(極太レーザーの予告帯)
ctx.save();
ctx.translate(enemy.x, enemy.y);
ctx.rotate(enemy.hadouAngle);
ctx.fillStyle = `rgba(244, 63, 94, ${0.06 + progress * 0.14})`;
ctx.fillRect(20, -55, 1500, 110);
ctx.strokeStyle = `rgba(244, 63, 94, ${0.35 + progress * 0.6})`;
ctx.lineWidth = 2 + progress * 5;
ctx.setLineDash([16, 10]);
ctx.beginPath();
ctx.moveTo(20, -55);
ctx.lineTo(1500, -55);
ctx.moveTo(20, 55);
ctx.lineTo(1500, 55);
ctx.stroke();
ctx.setLineDash([]);
// 3. 手元に溜まる超巨大な臨界エネルギー球
const pulse = 1 + Math.sin(now * 0.06) * 0.15;
const orbR = (10 + progress * 28) * pulse;
ctx.fillStyle = '#f43f5e';
ctx.shadowColor = '#f43f5e';
ctx.shadowBlur = 35;
ctx.beginPath();
ctx.arc(24, 0, orbR, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#facc15';
ctx.beginPath();
ctx.arc(24, 0, orbR * 0.72, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(24, 0, orbR * 0.45, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawHadouBeam() {
const ratio = Math.max(0, enemy.stateTimer / 0.85);
const pulse = 1 + Math.sin(performance.now() * 0.09) * 0.08;
const beamWidth = 110 * (0.35 + 0.65 * ratio) * pulse;
ctx.save();
ctx.translate(enemy.x, enemy.y);
ctx.rotate(enemy.hadouAngle);
// 1. 最外周の広域真紅オーラ
ctx.fillStyle = `rgba(244, 63, 94, ${0.55 * ratio})`;
ctx.shadowColor = '#f43f5e';
ctx.shadowBlur = 45;
ctx.fillRect(16, -(beamWidth * 1.35) / 2, 1500, beamWidth * 1.35);
// 2. メインの極太真紅ビーム層
ctx.fillStyle = `rgba(244, 63, 94, ${0.92 * ratio})`;
ctx.fillRect(16, -beamWidth / 2, 1500, beamWidth);
// 3. 中間の黄金・オレンジエネルギー層
ctx.fillStyle = `rgba(250, 204, 21, ${0.95 * ratio})`;
ctx.fillRect(16, -(beamWidth * 0.62) / 2, 1500, beamWidth * 0.62);
// 4. 中心の白熱プラズマコア
ctx.fillStyle = `rgba(255, 255, 255, ${ratio})`;
ctx.fillRect(16, -(beamWidth * 0.32) / 2, 1500, beamWidth * 0.32);
// 5. ビームの周囲をうねる螺旋エネルギー波(2本のサイン波)
ctx.strokeStyle = `rgba(255, 255, 255, ${0.85 * ratio})`;
ctx.lineWidth = 4 * ratio;
ctx.beginPath();
const phase = performance.now() * 0.03;
for (let x = 20; x <= 1400; x += 25) {
const y = Math.sin(x * 0.025 - phase) * (beamWidth * 0.52);
if (x === 20) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.strokeStyle = `rgba(250, 204, 21, ${0.85 * ratio})`;
ctx.beginPath();
for (let x = 20; x <= 1400; x += 25) {
const y = -Math.sin(x * 0.025 - phase) * (beamWidth * 0.52);
if (x === 20) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// 6. 発射口の超巨大閃光球&衝撃リング
ctx.fillStyle = '#ffffff';
ctx.shadowColor = '#facc15';
ctx.shadowBlur = 40;
ctx.beginPath();
ctx.arc(24, 0, beamWidth * 0.78, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = `rgba(255, 255, 255, ${ratio})`;
ctx.lineWidth = 5;
ctx.beginPath();
ctx.ellipse(45, 0, 18, beamWidth * 0.85, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
function drawPlayerPunchAnimation() {
const progress = 1 - (player.punchVisualTimer / player.punchVisualMax);
const extend = progress < 0.35
? (progress / 0.35)
: Math.max(0, 1 - ((progress - 0.35) / 0.65));
const alpha = Math.max(0, 1 - Math.pow(progress, 1.8));
const isParry = player.punchIsParryAttempt;
const w = isParry ? 44 : 42;
const l = isParry ? 62 : 58;
const fistDist = 14 + extend * (l - 12);
ctx.save();
ctx.translate(player.x, player.y);
ctx.rotate(player.punchAngle);
ctx.setLineDash([2, 2]);
ctx.fillStyle = isParry
? `rgba(250, 204, 21, ${0.18 * alpha})`
: `rgba(56, 189, 248, ${0.14 * alpha})`;
ctx.strokeStyle = isParry
? `rgba(250, 204, 21, ${0.75 * alpha})`
: `rgba(103, 232, 249, ${0.7 * alpha})`;
ctx.lineWidth = 1.2;
ctx.fillRect(0, -w / 2, l, w);
ctx.strokeRect(0, -w / 2, l, w);
ctx.setLineDash([]);
const grad = ctx.createLinearGradient(8, 0, fistDist, 0);
if (isParry) {
grad.addColorStop(0, 'rgba(250, 204, 21, 0)');
grad.addColorStop(0.5, `rgba(250, 204, 21, ${0.45 * alpha})`);
grad.addColorStop(1, `rgba(255, 255, 255, ${0.9 * alpha})`);
} else {
grad.addColorStop(0, 'rgba(56, 189, 248, 0)');
grad.addColorStop(0.5, `rgba(56, 189, 248, ${0.4 * alpha})`);
grad.addColorStop(1, `rgba(186, 230, 253, ${0.85 * alpha})`);
}
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(10, -5);
ctx.lineTo(fistDist, -13);
ctx.lineTo(fistDist + 6, 0);
ctx.lineTo(fistDist, 13);
ctx.lineTo(10, 5);
ctx.closePath();
ctx.fill();
ctx.shadowColor = isParry ? '#facc15' : '#38bdf8';
ctx.shadowBlur = isParry ? 20 : 14;
ctx.fillStyle = isParry ? `rgba(254, 240, 138, ${alpha})` : `rgba(224, 242, 254, ${alpha})`;
ctx.beginPath();
ctx.arc(fistDist, 0, isParry ? 11 : 9, 0, Math.PI * 2);
ctx.fill();
if (isParry) {
ctx.strokeStyle = `rgba(250, 204, 21, ${alpha})`;
ctx.lineWidth = 3.5;
ctx.beginPath();
ctx.arc(fistDist - 8, 0, 24, -Math.PI * 0.42, Math.PI * 0.42);
ctx.stroke();
}
ctx.restore();
}
function drawPlayerEntity() {
// 蒸発中は開始直後だけ白く燃え尽きる残像を描画し、その後完全に消える
if (player.isVaporizing) {
const remainRatio = Math.max(0, player.vaporizeTimer / player.vaporizeDuration);
if (remainRatio > 0.65) {
const fade = (remainRatio - 0.65) / 0.35;
ctx.save();
ctx.translate(player.x, player.y);
ctx.globalAlpha = fade;
ctx.fillStyle = '#ffffff';
ctx.shadowColor = '#38bdf8';
ctx.shadowBlur = 30;
ctx.beginPath();
ctx.arc(0, 0, player.radius * (1.4 - fade * 0.4), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
return;
}
ctx.save();
ctx.translate(player.x, player.y);
// リスポーン直後の光の柱リング演出
if (player.respawnBeamTimer > 0) {
const rRatio = player.respawnBeamTimer / 0.45;
ctx.save();
ctx.strokeStyle = `rgba(103, 232, 249, ${rRatio})`;
ctx.lineWidth = 4 * rRatio;
ctx.shadowColor = '#22d3ee';
ctx.shadowBlur = 20;
ctx.beginPath();
ctx.arc(0, 0, player.radius + 12 * rRatio, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
if (player.isBlocking) {
const remainRatio = Math.max(0, player.blockHoldTimer / player.blockMaxHold);
const deployScale = 0.55 + 0.45 * player.blockDeployAnim;
const impactBoost = player.blockImpactFlash;
ctx.strokeStyle = impactBoost > 0 ? '#ffffff' : '#10b981';
ctx.lineWidth = 3.5 + impactBoost * 2.5;
ctx.shadowColor = '#10b981';
ctx.shadowBlur = 16 + impactBoost * 18;
ctx.beginPath();
ctx.arc(0, 0, (player.radius + 7) * deployScale, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * remainRatio);
ctx.stroke();
ctx.save();
ctx.rotate(player.angle);
const shieldRadius = (player.radius + 14) * deployScale + impactBoost * 5;
const arcSpan = Math.PI * 0.68;
ctx.strokeStyle = impactBoost > 0
? `rgba(255, 255, 255, ${0.95})`
: `rgba(52, 211, 153, ${0.85 * player.blockDeployAnim})`;
ctx.lineWidth = 5 + impactBoost * 4;
ctx.shadowColor = impactBoost > 0 ? '#ffffff' : '#10b981';
ctx.shadowBlur = 22;
ctx.beginPath();
ctx.arc(0, 0, shieldRadius, -arcSpan / 2, arcSpan / 2);
ctx.stroke();
ctx.strokeStyle = `rgba(167, 243, 208, ${0.55 * player.blockDeployAnim})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, 0, shieldRadius - 5, -arcSpan * 0.42, arcSpan * 0.42);
ctx.stroke();
ctx.restore();
} else if (player.parryWindowTimer > 0) {
const spin = performance.now() * 0.005;
const pulse = 1 + Math.sin(performance.now() * 0.025) * 0.08;
ctx.save();
ctx.rotate(spin);
ctx.strokeStyle = 'rgba(250, 204, 21, 0.85)';
ctx.lineWidth = 2.5;
ctx.shadowColor = '#facc15';
ctx.shadowBlur = 12;
ctx.setLineDash([6, 5]);
ctx.beginPath();
ctx.arc(0, 0, (player.radius + 7) * pulse, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
if (player.isInsideObstacle) {
ctx.globalAlpha = 0.72;
}
ctx.save();
ctx.rotate(player.angle);
const stretchX = 1 + player.bodySquash;
const stretchY = 1 - player.bodySquash * 0.55;
ctx.scale(stretchX, stretchY);
ctx.fillStyle = player.blockImpactFlash > 0
? '#a7f3d0'
: (player.isBlocking ? '#10b981' : '#06b6d4');
ctx.shadowColor = player.isBlocking ? '#10b981' : '#22d3ee';
ctx.shadowBlur = player.isBlocking ? 20 : 16;
ctx.beginPath();
ctx.arc(0, 0, player.radius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#facc15';
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.moveTo(player.radius + 9, 0);
ctx.lineTo(player.radius + 1, -5);
ctx.lineTo(player.radius + 1, 5);
ctx.closePath();
ctx.fill();
ctx.restore();
ctx.restore();
}
function drawVisualEffects() {
for (const s of shockwaves) {
const progress = 1 - (s.life / s.maxLife);
const currentR = s.startR + (s.endR - s.startR) * (1 - Math.pow(1 - progress, 2.5));
const alpha = Math.max(0, 1 - progress);
ctx.save();
ctx.strokeStyle = s.color;
ctx.globalAlpha = alpha;
ctx.lineWidth = s.lineWidth * (1 - progress * 0.6);
ctx.shadowColor = s.color;
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(s.x, s.y, currentR, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
for (const f of slashFlares) {
const progress = 1 - (f.life / f.maxLife);
const alpha = Math.max(0, 1 - Math.pow(progress, 1.5));
const len = 85 * (0.4 + 0.6 * Math.sin(Math.min(1, progress * 2) * Math.PI * 0.5));
const thick = 9 * (1 - progress);
ctx.save();
ctx.translate(f.x, f.y);
ctx.rotate(f.angle + Math.PI / 4);
ctx.globalAlpha = alpha;
ctx.shadowColor = '#facc15';
ctx.shadowBlur = 24;
for (let k = 0; k < 2; k++) {
ctx.rotate(Math.PI / 2);
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.moveTo(-len, 0);
ctx.quadraticCurveTo(0, -thick, len, 0);
ctx.quadraticCurveTo(0, thick, -len, 0);
ctx.fill();
ctx.strokeStyle = '#facc15';
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.restore();
}
for (const p of particles) {
const alpha = Math.max(0, p.life / p.maxLife);
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.shadowColor = p.color;
ctx.shadowBlur = 8;
if (p.isSpark) {
const speed = Math.hypot(p.vx, p.vy);
const ang = Math.atan2(p.vy, p.vx);
ctx.translate(p.x, p.y);
ctx.rotate(ang);
const streak = Math.min(22, speed * 0.045);
ctx.fillRect(-streak / 2, -p.size * 0.4, streak, p.size * 0.8);
} else {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
function drawAttackBox(x, y, angle, length, width, isActive) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
ctx.setLineDash([2, 2]);
if (isActive) {
ctx.lineWidth = 1.2;
ctx.strokeStyle = 'rgba(244, 63, 94, 0.65)';
ctx.fillStyle = 'rgba(244, 63, 94, 0.14)';
} else {
ctx.lineWidth = 1.0;
ctx.strokeStyle = 'rgba(244, 63, 94, 0.30)';
ctx.fillStyle = 'rgba(244, 63, 94, 0.04)';
}
ctx.fillRect(0, -width / 2, length, width);
ctx.strokeRect(0, -width / 2, length, width);
ctx.restore();
}
function loop(now) {
const dt = Math.min(0.05, (now - lastTimestamp) / 1000);
lastTimestamp = now;
update(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>
To embed this project on your website, copy the following code and paste it into your website's HTML: