Faulty Coding: Nega 64
by FlashGalaxy171631 lines75.3 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Faulty Coding: Nega 64</title>
<!-- Import Retro Arcade Font -->
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
body { margin: 0; overflow: hidden; background: #000; font-family: 'Press Start 2P', monospace; user-select: none; }
/* Retro GameCube/Pixel Filter Overlay */
#gamecube-filter {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 300;
background:
radial-gradient(circle, rgba(0,0,0,0) 45%, rgba(0,0,0,0.55) 85%),
linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.22) 50%);
background-size: 100% 4px;
}
/* Global UI Screens */
.screen {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: #080205; z-index: 100;
display: flex; flex-direction: column; align-items: center; justify-content: center;
border: 8px solid #330011; box-sizing: border-box;
}
.hidden { display: none !important; }
/* Main Title Screen */
#title-screen h1 {
color: #ff3355; font-size: 3rem; text-align: center;
text-shadow: 0 0 20px #ff0033, 0 0 40px #550000;
animation: glitchText 0.3s infinite;
margin-bottom: 10px;
}
#title-screen h2 { color: #00ff41; font-size: 1rem; margin-bottom: 40px; text-shadow: 0 0 10px #00ff41; }
.menu-options { display: flex; flex-direction: column; gap: 15px; align-items: center; }
.menu-btn {
background: #111; color: #fff; border: 3px solid #444; padding: 12px 25px;
font-family: 'Press Start 2P', monospace; font-size: 0.9rem; cursor: pointer;
transition: all 0.1s; text-shadow: 0 0 5px #fff;
}
.menu-btn.selected, .menu-btn:hover {
background: #00ff41; color: #000; border-color: #fff;
box-shadow: 0 0 15px #00ff41; transform: scale(1.05);
}
/* Rules / Settings Screen */
.rules-container { display: flex; gap: 30px; margin: 30px 0; flex-wrap: wrap; justify-content: center; }
.rule-box {
background: #110508; border: 3px solid #ff3355; padding: 20px; text-align: center; width: 220px;
}
.rule-box h3 { color: #ffcc00; font-size: 0.8rem; margin-top: 0; }
.rule-val { color: #00ff41; font-size: 1.2rem; margin: 15px 0; text-shadow: 0 0 8px #00ff41; }
/* Metrics System Screen */
.stats-box {
background: #001104; border: 2px solid #00ff41; padding: 25px; width: 450px; font-size: 0.7rem; color: #fff; line-height: 1.8;
}
/* Character Select Screen */
.select-grid { display: flex; gap: 15px; margin-top: 25px; flex-wrap: wrap; justify-content: center; max-width: 800px; }
.char-card {
width: 120px; height: 160px; border: 4px solid #444;
background: #111; display: flex; flex-direction: column; align-items: center;
justify-content: space-between; padding: 10px 5px; box-sizing: border-box;
}
.char-preview {
width: 70px; height: 70px; background: #000; border: 2px solid #222;
display: flex; align-items: center; justify-content: center; font-size: 1.5rem;
}
.selectors-status { display: flex; gap: 30px; margin-top: 25px; width: 750px; justify-content: space-around; }
.player-select-box {
border: 3px solid #00ff41; padding: 15px; text-align: center; width: 330px;
background: #001104; box-shadow: 0 0 15px rgba(0,255,65,0.3); font-size: 0.65rem;
}
.player-select-box.p2-box { border-color: #ffcc00; color: #ffcc00; background: #141100; box-shadow: 0 0 15px rgba(255,204,0,0.3); }
/* Finisher Cinematic Overlay */
#finisher-flash {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255, 0, 0, 0); z-index: 0; pointer-events: none; opacity: 0;
transition: opacity 0.1s ease-in-out;
box-shadow: inset 0 0 150px #ff0000;
}
#finisher-flash.active { opacity: 1; background: rgba(255, 0, 0, 0.75); animation: flashPulse 0.1s infinite alternate; }
@keyframes flashPulse {
0% { background: rgba(255, 0, 0, 0.65); }
100% { background: rgba(255, 50, 50, 0.95); }
}
/* Countdown 3-2-1-GO Overlay */
#countdown-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
z-index: 150; pointer-events: none;
}
.countdown-text {
font-size: 7rem; color: #ffcc00; text-shadow: 0 0 30px #ff0000, 0 0 60px #ff3355;
animation: countPulse 0.8s ease-out infinite alternate;
}
@keyframes countPulse {
0% { transform: scale(0.8); opacity: 0.5; }
100% { transform: scale(1.1); opacity: 1; }
}
/* Game Over / Victory Overlay Layers */
#game-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(0, 0, 0, 0.9); z-index: 200; pointer-events: none; opacity: 0; transition: opacity 0.4s;
}
#game-overlay.active { opacity: 1; pointer-events: auto; }
.game-callout {
font-size: 6rem; color: #ffcc00; text-shadow: 0 0 30px #ff0000, 0 0 60px #ff3355;
animation: zoomPulse 0.3s infinite alternate;
}
#victory-container {
display: none; flex-direction: column; align-items: center; justify-content: center;
}
#victory-container.show { display: flex; }
.winner-callout {
font-size: 2.2rem; color: #00ff41; text-shadow: 0 0 20px #00ff41;
margin-bottom: 30px; text-align: center;
}
.final-stats-summary {
color: #fff; font-size: 0.8rem; margin-bottom: 30px; text-align: center; line-height: 1.8;
border: 2px dashed #ff3355; padding: 15px; background: #110005;
}
@keyframes zoomPulse {
0% { transform: scale(1); }
100% { transform: scale(1.06); }
}
/* In-Game HUD */
#hud {
position: absolute; bottom: 20px; width: 100%; display: flex;
justify-content: space-around; pointer-events: none; z-index: 10;
font-size: 1.1rem; text-shadow: 0 0 10px #000;
}
.p1-hud { color: #00ff41; }
.p2-hud { color: #ffcc00; }
#match-info {
position: absolute; top: 15px; width: 100%; display: flex; justify-content: center; gap: 50px;
pointer-events: none; z-index: 10; color: #fff; font-size: 1.1rem;
text-shadow: 0 0 8px #ff0033;
}
.dmg-popup {
position: absolute; color: #ffcc00; font-weight: bold; font-size: 1.2rem;
pointer-events: none; z-index: 20; text-shadow: 0 0 6px #ff0000;
animation: floatUp 0.6s ease-out forwards;
}
.score-popup {
position: absolute; color: #00ff41; font-weight: bold; font-size: 1.8rem;
pointer-events: none; z-index: 25; text-shadow: 0 0 10px #00ff41;
animation: scorePopupAnim 0.8s ease-out forwards;
}
@keyframes floatUp {
0% { transform: translateY(0) scale(1.2); opacity: 1; }
100% { transform: translateY(-40px) scale(0.8); opacity: 0; }
}
@keyframes scorePopupAnim {
0% { transform: translateY(0) scale(1.4); opacity: 1; }
70% { transform: translateY(-45px) scale(1.0); opacity: 1; }
100% { transform: translateY(-70px) scale(0.8); opacity: 0; }
}
#bg-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; }
/* Pixel-crunch style rendering for Three.js canvas */
canvas:not(#bg-canvas) {
position: relative;
z-index: 2;
image-rendering: pixelated;
}
/* Stage Select / Five Code-Themed Stages */
#stage-select-screen { background: #05070a; }
.stage-grid {
display: grid;
grid-template-columns: repeat(5, minmax(135px, 1fr));
gap: 12px;
width: min(920px, 94vw);
margin-top: 18px;
}
.stage-card {
min-height: 185px;
background: #090d12;
border: 3px solid #333;
padding: 10px;
box-sizing: border-box;
cursor: pointer;
transition: transform .1s, box-shadow .1s, border-color .1s;
text-align: center;
}
.stage-card:hover, .stage-card.selected {
transform: translateY(-4px) scale(1.03);
border-color: #00ff41;
box-shadow: 0 0 18px rgba(0,255,65,.45);
}
.stage-preview {
height: 105px;
border: 2px solid #222;
margin-bottom: 10px;
position: relative;
overflow: hidden;
background: #020404;
}
.stage-preview canvas {
width: 100%;
height: 100%;
display: block;
image-rendering: pixelated;
}
.stage-preview-label {
position: absolute;
left: 5px;
bottom: 4px;
padding: 2px 4px;
background: rgba(0,0,0,.72);
color: #fff;
font: 7px monospace;
pointer-events: none;
}
.stage-card h3 { margin: 4px 0; font-size: .62rem; color: #00ff41; }
.stage-card p { margin: 5px 0 0; color: #777; font-size: .42rem; line-height: 1.45; }
.stage-status { color: #ffcc00; font-size: .55rem; margin-top: 12px; }
@keyframes glitchText {
0% { transform: translate(0, 0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(2px, -2px); }
60% { transform: translate(-1px, -1px); }
80% { transform: translate(1px, 1px); }
100% { transform: translate(0, 0); }
}
</style>
</head>
<body>
<!-- Retro Filter Overlay -->
<div id="gamecube-filter"></div>
<!-- Finisher Cinematic Flash Overlay -->
<div id="finisher-flash"></div>
<!-- Countdown 3-2-1-GO Overlay -->
<div id="countdown-overlay" class="hidden">
<div id="countdown-display" class="countdown-text">3</div>
</div>
<!-- 1. MAIN TITLE SCREEN -->
<div id="title-screen" class="screen">
<h1>NEGA 64</h1>
<h2>ABSTRACT DYNAMIC TILT UPDATE</h2>
<div class="menu-options">
<button class="menu-btn selected" onclick="goToRules()">1. VS BATTLE</button>
<button class="menu-btn" onclick="openStats()">2. SYSTEM STATS</button>
</div>
<p style="color: #666; font-size: 0.6rem; margin-top: 50px;">PRESS [ENTER] OR CLICK TO START</p>
</div>
<!-- 2. RULES SCREEN -->
<div id="rules-screen" class="screen hidden">
<h1 style="color: #ff3355; font-size: 1.8rem; text-shadow: 0 0 10px #ff0033;">MATCH RULES</h1>
<div class="rules-container">
<div class="rule-box">
<h3>GAME MODE</h3>
<div class="rule-val" id="rule-mode-display" style="color: #ffcc00;">STOCK</div>
<p style="font-size: 0.5rem; color: #aaa;">[W/S] Toggle Mode</p>
</div>
<div class="rule-box" id="stock-rule-container">
<h3>STOCKS</h3>
<div class="rule-val" id="rule-stock-display">3</div>
<p style="font-size: 0.5rem; color: #aaa;">[Up/Down] Change</p>
</div>
<div class="rule-box">
<h3>TIME LIMIT</h3>
<div class="rule-val" id="rule-time-display">INF</div>
<p style="font-size: 0.5rem; color: #aaa;">[A/D] Change</p>
</div>
</div>
<button class="menu-btn selected" onclick="goToCharSelect()" style="margin-top: 10px;">CONTINUE TO SELECT ></button>
</div>
<!-- 3. SYSTEM STATS SCREEN -->
<div id="stats-screen" class="screen hidden">
<h1 style="color: #00ff41; font-size: 1.5rem; text-shadow: 0 0 10px #00ff41;">SYSTEM METRICS</h1>
<div class="stats-box">
<p>TOTAL MATCHES: <span id="stat-matches" style="color:#ffcc00">0</span></p>
<p>P1 K.O.s: <span id="stat-p1k0" style="color:#00ff41">0</span></p>
<p>P2/CPU K.O.s: <span id="stat-p2k0" style="color:#ffcc00">0</span></p>
<p>ENGINE STATUS: <span style="color:#00ff41">STABLE (60 FPS)</span></p>
</div>
<button class="menu-btn" onclick="closeStats()" style="margin-top: 30px;">< BACK TO MENU</button>
</div>
<!-- 4. CHARACTER SELECT SCREEN -->
<div id="char-select-screen" class="screen hidden">
<h1 style="text-shadow: 0 0 15px #ff0033; color: #ff3355; font-size: 1.6rem; margin-bottom: 5px;">CHARACTER SELECT</h1>
<p style="color: #888; font-size: 0.6rem;">CHOOSE YOUR FIGHTER & P2 CONTROLLER</p>
<div class="select-grid">
<div class="char-card">
<div style="color: #00ff41; font-weight: bold; font-size: 0.7rem;">0</div>
<div class="char-preview" style="color: #00ff41;">0</div>
<span style="font-size: 0.55rem; color: #aaa;">ZERO</span>
</div>
<div class="char-card">
<div style="color: #00ff41; font-weight: bold; font-size: 0.7rem;">1</div>
<div class="char-preview" style="color: #00ff41;">1</div>
<span style="font-size: 0.55rem; color: #aaa;">ONE</span>
</div>
<div class="char-card">
<div style="color: #00ff41; font-weight: bold; font-size: 0.7rem;">2</div>
<div class="char-preview" style="font-size: 1.1rem;">{ }</div>
<span style="font-size: 0.55rem; color: #aaa;">SYNTAX</span>
</div>
<div class="char-card">
<div style="color: #00ff41; font-weight: bold; font-size: 0.7rem;">3</div>
<div class="char-preview" style="color: #ff3355; font-size: 1.1rem;">#BUG</div>
<span style="font-size: 0.55rem; color: #aaa;">BUG</span>
</div>
<div class="char-card">
<div style="color: #00ff41; font-weight: bold; font-size: 0.7rem;">4</div>
<div class="char-preview" style="color: #00ffcc; font-size: 1.1rem;">[::]</div>
<span style="font-size: 0.55rem; color: #aaa;">STACK</span>
</div>
</div>
<div class="selectors-status">
<div class="player-select-box">
<h3 style="margin: 0 0 5px 0;">PLAYER 1</h3>
<p style="margin: 4px 0;">Choice: <span id="p1-choice-text" style="font-size: 0.9rem;">0</span></p>
<p id="p1-status" style="margin: 4px 0; color: #ff3355;">[UNLOCKED]</p>
<p style="font-size: 0.45rem; color: #888; margin-top: 3px;">[1-5] Switch | [Space] Lock</p>
</div>
<div class="player-select-box p2-box">
<h3 style="margin: 0 0 5px 0;">PLAYER 2 / CPU</h3>
<p style="margin: 4px 0;">Choice: <span id="p2-choice-text" style="font-size: 0.9rem;">1</span></p>
<p style="margin: 4px 0;">Mode: <span id="p2-mode-text" style="font-size: 0.8rem; color:#00ff41;">CPU (LV 5)</span></p>
<p id="p2-status" style="margin: 4px 0; color: #ff3355;">[UNLOCKED]</p>
<p style="font-size: 0.45rem; color: #ccaa00; margin-top: 3px;">[Arrows] Switch | [C] Toggle CPU/Human | [+/-] CPU Lv | [Enter] Lock</p>
</div>
</div>
</div>
<!-- 5. STAGE SELECT SCREEN -->
<div id="stage-select-screen" class="screen hidden">
<h1 style="color:#00ff41; font-size:1.7rem; text-shadow:0 0 15px #00ff41;">STAGE SELECT</h1>
<p style="color:#888; font-size:.58rem;">CHOOSE A CODE-THEMED BATTLEFIELD • SMASH-STYLE STAGE SELECT</p>
<div class="stage-grid">
<div class="stage-card selected" data-stage="0" onclick="selectStage(0)">
<div class="stage-preview"><canvas id="stage-preview-0"></canvas><div class="stage-preview-label">LIVE PREVIEW</div></div>
<h3>01 // TERMINAL</h3><p>Balanced compiler deck with two command platforms.</p>
</div>
<div class="stage-card" data-stage="1" onclick="selectStage(1)">
<div class="stage-preview"><canvas id="stage-preview-1"></canvas><div class="stage-preview-label">LIVE PREVIEW</div></div>
<h3>02 // ERROR ROOM</h3><p>Broken-code layout with offset platforms and a center gap.</p>
</div>
<div class="stage-card" data-stage="2" onclick="selectStage(2)">
<div class="stage-preview"><canvas id="stage-preview-2"></canvas><div class="stage-preview-label">LIVE PREVIEW</div></div>
<h3>03 // STACK OVERFLOW</h3><p>Vertical stack layout built around staggered ledges.</p>
</div>
<div class="stage-card" data-stage="3" onclick="selectStage(3)">
<div class="stage-preview"><canvas id="stage-preview-3"></canvas><div class="stage-preview-label">LIVE PREVIEW</div></div>
<h3>04 // INFINITE LOOP</h3><p>Long central route with looping upper platforms.</p>
</div>
<div class="stage-card" data-stage="4" onclick="selectStage(4)">
<div class="stage-preview"><canvas id="stage-preview-4"></canvas><div class="stage-preview-label">LIVE PREVIEW</div></div>
<h3>05 // SOURCE VAULT</h3><p>Wide vault arena with a high split-platform layer.</p>
</div>
</div>
<div class="stage-status">SELECTED: <span id="stage-choice-text">01 // TERMINAL</span> | [1-5] SWITCH | [ENTER] START</div>
</div>
<!-- Game Over / Victory Overlay Sequence -->
<div id="game-overlay">
<div id="game-callout-text" class="game-callout">GAME!</div>
<div id="victory-container">
<div class="winner-callout" id="winner-text">PLAYER 1 WINS</div>
<div class="final-stats-summary" id="final-stats-text">P1 SCORE: 3 | P2 SCORE: 1</div>
<button class="menu-btn selected" onclick="location.reload()" style="font-size: 0.8rem;">MAIN MENU</button>
</div>
</div>
<!-- Match Overlay HUD -->
<div id="match-info" class="hidden">
<div>STAGE: <span id="hud-stage-name">01 // TERMINAL</span> | TIME: <span id="hud-time">INF</span></div>
</div>
<div id="hud" class="hidden">
<div class="p1-hud">P1 (<span id="p1-char-hud">0</span>) | <span id="p1-hud-label">STOCKS: <span id="p1-stocks">3</span></span> | <span id="p1-pct">0</span>%</div>
<div class="p2-hud"><span id="p2-label-name">P2</span> (<span id="p2-char-hud">1</span>) | <span id="p2-hud-label">STOCKS: <span id="p2-stocks">3</span></span> | <span id="p2-pct">0</span>%</div>
</div>
<canvas id="bg-canvas"></canvas>
<!-- Three.js Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// --- Audio & BGM Setup using Raw GitHub Links ---
const audioBaseUrl = "https://raw.githubusercontent.com/noxxerw2007-crypto/quick-minecraft-hit-audio/main/";
const sfx = {
swing: new Audio(audioBaseUrl + "Punch%20swing.mp3"),
hit: new Audio(audioBaseUrl + "Hitsound%20from%20Super%20Smash%20Bros%20Melee.mp3"),
crit: new Audio(audioBaseUrl + "Smash%20Bros%20Death%20Sound%20Effect.mp3"),
death: new Audio(audioBaseUrl + "Super%20Smash%20Bros%20(N64)%20-%20Blast%20KO%20Sound.mp3"),
game: new Audio(audioBaseUrl + "Super%20Smash%20Bros.%20Melee%20GAME!%20sound%20effect.mp3"),
select: new Audio(audioBaseUrl + "Melee%20Menu%20Select%20Sound.mp3"),
announcer321: new Audio(audioBaseUrl + "Making%20The%20(SSBM)%20Announcer%20Saying_%203...%202...%201...%20GO!%20Instead%20of_%20Ready...%20GO!.mp3")
};
const bgm = {
menu: new Audio(audioBaseUrl + "Super%20Smash%20Bros.%20Melee%20-%20Battlefield%20-%20%5BAwedecai%20Remix%5D.mp3"),
ingame: new Audio(audioBaseUrl + "Flat%20Zone%20-%20Super%20Smash%20Bros.%20Melee%20Remix.mp3"),
victory: new Audio(audioBaseUrl + "Character%20Select%20-%20Super%20Smash%20Bros..mp3")
};
bgm.menu.loop = true;
bgm.ingame.loop = true;
bgm.victory.loop = false;
bgm.menu.volume = 0.4;
bgm.ingame.volume = 0.4;
bgm.victory.volume = 0.5;
let musicStarted = false;
function tryStartMenuMusic() {
if (!musicStarted) {
musicStarted = true;
bgm.menu.play().catch(e => {});
}
}
window.addEventListener('click', tryStartMenuMusic);
window.addEventListener('keydown', tryStartMenuMusic);
function playSfx(soundObj) {
try {
const clone = soundObj.cloneNode();
clone.volume = 0.6;
clone.play().catch(e => {});
} catch (err) {
soundObj.currentTime = 0;
soundObj.play().catch(e => {});
}
}
let currentGameState = 'TITLE';
let matchRules = { mode: 'STOCK', stocks: 3, time: 'INF' };
let statsData = { matches: 0, p1KOs: 0, p2KOs: 0 };
let matchEnded = false;
let matchTimerSeconds = 0;
let timerInterval = null;
let matchIsLocked = true;
const choices = { p1: 0, p2: 1 };
const lockedIn = { p1: false, p2: false };
let p2IsCPU = true;
let cpuLevel = 5;
let finisherTimer = 0;
// Five selectable code-themed stages.
let selectedStage = 0;
let activeStageName = '01 // TERMINAL';
let stageDecorGroup = null;
const stageDefinitions = [
{
name: '01 // TERMINAL',
bg: { base: '#050b07', accent: '#00ff41', trail: 'rgba(0,18,8,0.28)', glyphs: ['0','1','{','}',';','>'] },
platforms: [
[24, -2, 4, 0, -2], [6, .6, 3, -6, 1.2], [6, .6, 3, 6, 1.2]
]
},
{
name: '02 // ERROR ROOM',
bg: { base: '#100407', accent: '#ff3355', trail: 'rgba(24,0,4,0.28)', glyphs: ['#','!','404','ERR','?','/'] },
platforms: [
[18, 1, 4, 0, -2], [6, .6, 3, -8, 2.4], [6, .6, 3, 8, 2.4], [5, .55, 3, -2.5, 4.5], [5, .55, 3, 4.5, 5.7]
]
},
{
name: '03 // STACK OVERFLOW',
bg: { base: '#031017', accent: '#00ffcc', trail: 'rgba(0,15,24,0.28)', glyphs: ['[ ]','::','PUSH','POP','{ }','=>'] },
platforms: [
[16, 1, 4, 0, -2], [5, .55, 3, -6.5, 1], [5, .55, 3, 6.5, 1],
[4.5, .55, 3, -3.5, 4], [4.5, .55, 3, 3.5, 6.5], [3.8, .5, 3, -1, 9]
]
},
{
name: '04 // INFINITE LOOP',
bg: { base: '#100d03', accent: '#ffcc00', trail: 'rgba(20,12,0,0.28)', glyphs: ['while','1','for','++','--','=>'] },
platforms: [
[26, 1, 4, 0, -2], [5.5, .55, 3, -7, 2], [5.5, .55, 3, 7, 2],
[4.5, .5, 3, -9, 5.5], [4.5, .5, 3, 9, 5.5], [7, .55, 3, 0, 6.8]
]
},
{
name: '05 // SOURCE VAULT',
bg: { base: '#080416', accent: '#b58cff', trail: 'rgba(10,2,24,0.28)', glyphs: ['<div>','</>','CSS','JS','HTML','{}'] },
platforms: [
[28, 1, 4, 0, -2], [7, .6, 3, -7, 2.2], [7, .6, 3, 7, 2.2],
[5, .55, 3, -3.5, 6], [5, .55, 3, 3.5, 6], [3.5, .5, 3, 0, 9]
]
}
];
// --- Smash-style live stage thumbnails ---
function renderStageThumbnail(index) {
const canvas = document.getElementById('stage-preview-' + index);
if (!canvas) return;
const def = stageDefinitions[index];
const rect = canvas.getBoundingClientRect();
const w = Math.max(240, Math.floor(rect.width * 2));
const h = Math.max(160, Math.floor(rect.height * 2));
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.fillStyle = def.bg.base;
ctx.fillRect(0, 0, w, h);
// Code-grid backdrop.
ctx.globalAlpha = 0.22;
ctx.fillStyle = def.bg.accent;
ctx.font = '12px monospace';
for (let x = 0; x < w; x += 42) {
for (let y = 14; y < h; y += 22) {
ctx.fillText(def.bg.glyphs[(x / 42 + y / 22 + index) % def.bg.glyphs.length], x, y);
}
}
ctx.globalAlpha = 1;
// Convert the actual world layout into a miniature Smash-like view.
const worldLeft = -16, worldRight = 16, worldBottom = -4, worldTop = 11;
const sx = w / (worldRight - worldLeft);
const sy = h / (worldTop - worldBottom);
function px(x) { return (x - worldLeft) * sx; }
function py(y) { return h - (y - worldBottom) * sy; }
def.platforms.forEach((spec, i) => {
const [pw, ph, pd, x, y] = spec;
const left = px(x - pw / 2);
const top = py(y + ph / 2);
const width = pw * sx;
const height = Math.max(4, ph * sy);
ctx.fillStyle = 'rgba(0,0,0,.65)';
ctx.fillRect(left + 2, top + 3, width, height);
ctx.fillStyle = def.bg.accent;
ctx.fillRect(left, top, width, height);
ctx.fillStyle = 'rgba(255,255,255,.28)';
ctx.fillRect(left, top, width, Math.max(2, height * .18));
ctx.strokeStyle = '#fff';
ctx.globalAlpha = .35;
ctx.strokeRect(left, top, width, height);
ctx.globalAlpha = 1;
});
// Tiny fighter silhouettes showing the scale of the arena.
const ground = def.platforms[0];
const fighterY = ground[4] + ground[1] / 2 + 0.9;
[ground[3] - 3, ground[3] + 3].forEach((fx, i) => {
ctx.fillStyle = i === 0 ? '#00ff41' : '#ffcc00';
ctx.fillRect(px(fx) - 3, py(fighterY) - 8, 6, 8);
ctx.fillRect(px(fx) - 5, py(fighterY) - 3, 10, 3);
});
}
function renderAllStageThumbnails() {
for (let i = 0; i < stageDefinitions.length; i++) renderStageThumbnail(i);
}
window.addEventListener('resize', () => {
if (currentGameState === 'STAGE_SELECT') renderAllStageThumbnails();
});
function selectStage(index) {
selectedStage = ((index % stageDefinitions.length) + stageDefinitions.length) % stageDefinitions.length;
activeStageName = stageDefinitions[selectedStage].name;
document.querySelectorAll('.stage-card').forEach((card, i) => card.classList.toggle('selected', i === selectedStage));
document.getElementById('stage-choice-text').innerText = activeStageName;
playSfx(sfx.select);
applyStage(selectedStage);
requestAnimationFrame(renderAllStageThumbnails);
}
function goToStageSelect() {
playSfx(sfx.select);
document.getElementById('char-select-screen').classList.add('hidden');
document.getElementById('stage-select-screen').classList.remove('hidden');
currentGameState = 'STAGE_SELECT';
selectStage(selectedStage);
requestAnimationFrame(renderAllStageThumbnails);
}
function startSelectedStage() {
currentGameState = 'PLAYING';
statsData.matches++;
bgm.menu.pause();
bgm.menu.currentTime = 0;
bgm.ingame.play().catch(e => {});
document.getElementById('stage-select-screen').classList.add('hidden');
document.getElementById('stage-select-screen').style.display = '';
document.getElementById('match-info').classList.remove('hidden');
document.getElementById('hud').classList.remove('hidden');
runCountdownSequence();
}
function goToRules() {
playSfx(sfx.select);
document.getElementById('title-screen').classList.add('hidden');
document.getElementById('rules-screen').classList.remove('hidden');
currentGameState = 'RULES';
}
function openStats() {
playSfx(sfx.select);
document.getElementById('title-screen').classList.add('hidden');
document.getElementById('stats-screen').classList.remove('hidden');
currentGameState = 'STATS';
document.getElementById('stat-matches').innerText = statsData.matches;
document.getElementById('stat-p1k0').innerText = statsData.p1KOs;
document.getElementById('stat-p2k0').innerText = statsData.p2KOs;
}
function closeStats() {
playSfx(sfx.select);
document.getElementById('stats-screen').classList.add('hidden');
document.getElementById('title-screen').classList.remove('hidden');
currentGameState = 'TITLE';
}
function goToCharSelect() {
playSfx(sfx.select);
document.getElementById('rules-screen').classList.add('hidden');
document.getElementById('char-select-screen').classList.remove('hidden');
currentGameState = 'CHAR_SELECT';
}
window.addEventListener('keydown', (e) => {
if (currentGameState === 'RULES') {
if (e.code === 'KeyW' || e.code === 'KeyS' || e.code === 'ArrowUp' || e.code === 'ArrowDown') {
playSfx(sfx.select);
if (matchRules.mode === 'STOCK') {
matchRules.mode = 'STATS';
matchRules.time = '3:00';
document.getElementById('rule-mode-display').innerText = 'STATS';
document.getElementById('rule-mode-display').style.color = '#00ff41';
document.getElementById('stock-rule-container').style.opacity = '0.3';
document.getElementById('rule-time-display').innerText = matchRules.time;
} else {
matchRules.mode = 'STOCK';
matchRules.time = 'INF';
document.getElementById('rule-mode-display').innerText = 'STOCK';
document.getElementById('rule-mode-display').style.color = '#ffcc00';
document.getElementById('stock-rule-container').style.opacity = '1';
document.getElementById('rule-time-display').innerText = matchRules.time;
}
}
if (matchRules.mode === 'STOCK') {
if (e.code === 'ArrowUp') {
playSfx(sfx.select);
matchRules.stocks = matchRules.stocks >= 9 ? 1 : matchRules.stocks + 1;
document.getElementById('rule-stock-display').innerText = matchRules.stocks;
}
if (e.code === 'ArrowDown') {
playSfx(sfx.select);
matchRules.stocks = matchRules.stocks <= 1 ? 9 : matchRules.stocks - 1;
document.getElementById('rule-stock-display').innerText = matchRules.stocks;
}
}
if (e.code === 'KeyD' || e.code === 'ArrowRight' || e.code === 'KeyA' || e.code === 'ArrowLeft') {
playSfx(sfx.select);
if (matchRules.mode === 'STATS') {
matchRules.time = matchRules.time === '3:00' ? '2:00' : '3:00';
} else {
matchRules.time = matchRules.time === 'INF' ? '3:00' : (matchRules.time === '3:00' ? '2:00' : 'INF');
}
document.getElementById('rule-time-display').innerText = matchRules.time;
}
if (e.code === 'Enter') goToCharSelect();
} else if (currentGameState === 'STAGE_SELECT') {
if (e.code === 'Digit1') selectStage(0);
if (e.code === 'Digit2') selectStage(1);
if (e.code === 'Digit3') selectStage(2);
if (e.code === 'Digit4') selectStage(3);
if (e.code === 'Digit5') selectStage(4);
if (e.code === 'ArrowLeft') selectStage(selectedStage - 1);
if (e.code === 'ArrowRight') selectStage(selectedStage + 1);
if (e.code === 'Enter') startSelectedStage();
} else if (currentGameState === 'CHAR_SELECT') {
if (['Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5'].includes(e.code)) {
playSfx(sfx.select);
choices.p1 = parseInt(e.code.replace('Digit', '')) - 1;
document.getElementById('p1-choice-text').innerText = choices.p1;
}
if (e.code === 'Space') {
playSfx(sfx.select);
lockedIn.p1 = true;
p1.charType = choices.p1;
document.getElementById('p1-char-hud').innerText = p1.charType;
setupHudForMode();
rebuildPlayerMesh(p1);
document.getElementById('p1-status').innerText = '[LOCKED]';
document.getElementById('p1-status').style.color = '#00ff41';
}
if (e.code === 'KeyC') {
playSfx(sfx.select);
p2IsCPU = !p2IsCPU;
document.getElementById('p2-mode-text').innerText = p2IsCPU ? `CPU (LV ${cpuLevel})` : 'HUMAN';
document.getElementById('p2-label-name').innerText = p2IsCPU ? `CPU` : `P2`;
}
if (p2IsCPU && (e.code === 'Equal' || e.code === 'NumpadAdd')) {
playSfx(sfx.select);
cpuLevel = Math.min(10, cpuLevel + 1);
document.getElementById('p2-mode-text').innerText = `CPU (LV ${cpuLevel})`;
}
if (p2IsCPU && (e.code === 'Minus' || e.code === 'NumpadSubtract')) {
playSfx(sfx.select);
cpuLevel = Math.max(1, cpuLevel - 1);
document.getElementById('p2-mode-text').innerText = `CPU (LV ${cpuLevel})`;
}
if (e.code === 'ArrowUp') { playSfx(sfx.select); choices.p2 = (choices.p2 + 1) % 5; document.getElementById('p2-choice-text').innerText = choices.p2; }
if (e.code === 'ArrowDown') { playSfx(sfx.select); choices.p2 = (choices.p2 - 1 + 5) % 5; document.getElementById('p2-choice-text').innerText = choices.p2; }
if (e.code === 'Enter') {
playSfx(sfx.select);
lockedIn.p2 = true;
p2.charType = choices.p2;
document.getElementById('p2-char-hud').innerText = p2.charType;
setupHudForMode();
rebuildPlayerMesh(p2);
document.getElementById('p2-status').innerText = '[LOCKED]';
document.getElementById('p2-status').style.color = '#ffcc00';
}
if (lockedIn.p1 && lockedIn.p2) {
currentGameState = 'PLAYING';
statsData.matches++;
bgm.menu.pause();
bgm.menu.currentTime = 0;
bgm.ingame.play().catch(e => {});
document.getElementById('char-select-screen').style.display = 'none';
document.getElementById('match-info').classList.remove('hidden');
document.getElementById('hud').classList.remove('hidden');
runCountdownSequence();
}
}
});
function runCountdownSequence() {
matchIsLocked = true;
const overlay = document.getElementById('countdown-overlay');
const display = document.getElementById('countdown-display');
overlay.classList.remove('hidden');
playSfx(sfx.announcer321);
let steps = ['3', '2', '1', 'GO!'];
let index = 0;
function nextStep() {
if (index < steps.length) {
display.innerText = steps[index];
index++;
let delay = (index === steps.length) ? 800 : 1000;
setTimeout(nextStep, delay);
} else {
overlay.classList.add('hidden');
matchIsLocked = false;
startMatchTimer();
}
}
setTimeout(nextStep, 1000);
}
function setupHudForMode() {
if (matchRules.mode === 'STOCK') {
p1.stocks = matchRules.stocks;
p2.stocks = matchRules.stocks;
document.getElementById('p1-hud-label').innerHTML = 'STOCKS: <span id="p1-stocks">' + p1.stocks + '</span>';
document.getElementById('p2-hud-label').innerHTML = 'STOCKS: <span id="p2-stocks">' + p2.stocks + '</span>';
} else {
p1.score = 0; p2.score = 0;
document.getElementById('p1-hud-label').innerHTML = 'PTS: <span id="p1-pts">0</span>';
document.getElementById('p2-hud-label').innerHTML = 'PTS: <span id="p2-pts">0</span>';
}
document.getElementById('hud-time').innerText = matchRules.time;
const stageHud = document.getElementById('hud-stage-name');
if (stageHud) stageHud.innerText = activeStageName;
}
function startMatchTimer() {
if (matchRules.time === 'INF') return;
const parts = matchRules.time.split(':');
matchTimerSeconds = parseInt(parts[0]) * 60 + parseInt(parts[1]);
timerInterval = setInterval(() => {
if (matchEnded) { clearInterval(timerInterval); return; }
matchTimerSeconds--;
let m = Math.floor(matchTimerSeconds / 60);
let s = matchTimerSeconds % 60;
document.getElementById('hud-time').innerText = m + ':' + (s < 10 ? '0' : '') + s;
if (matchTimerSeconds <= 0) {
clearInterval(timerInterval);
triggerGameOver();
}
}, 1000);
}
// --- Background Setup ---
const bgCanvas = document.getElementById('bg-canvas');
const bgCtx = bgCanvas.getContext('2d');
function resizeBg() { bgCanvas.width = window.innerWidth; bgCanvas.height = window.innerHeight; }
window.addEventListener('resize', resizeBg);
resizeBg();
const fontSize = 18;
let columns = Math.floor(bgCanvas.width / fontSize);
let drops = Array(columns).fill(1);
function drawAnimatedBg() {
if (currentGameState === 'PLAYING' || currentGameState === 'STAGE_SELECT') {
const def = stageDefinitions[selectedStage];
bgCtx.fillStyle = def.bg.base;
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
drawStageBackdrop();
// Preserve the original binary rain as an additional code layer.
bgCtx.fillStyle = def.bg.accent;
bgCtx.globalAlpha = .35;
bgCtx.font = 'bold ' + fontSize + 'px monospace';
for (let i = 0; i < drops.length; i++) {
const text = Math.random() < 0.5 ? '0' : '1';
bgCtx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > bgCanvas.height && Math.random() > 0.975) drops[i] = 0;
drops[i]++;
}
bgCtx.globalAlpha = 1;
} else {
bgCtx.fillStyle = 'rgba(10, 0, 2, 0.25)';
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
bgCtx.fillStyle = '#ff0033';
bgCtx.font = 'bold ' + fontSize + 'px monospace';
for (let i = 0; i < drops.length; i++) {
const text = Math.random() < 0.5 ? '0' : '1';
bgCtx.fillText(text, i * fontSize, drops[i] * fontSize);
if (drops[i] * fontSize > bgCanvas.height && Math.random() > 0.975) drops[i] = 0;
drops[i]++;
}
}
}
// --- Three.js Setup with Pixel Scale Factor matching prototype ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// Tuned sweet-spot pixel scale factor integrated from prototype
const pixelScaleFactor = 2.2;
renderer.setPixelRatio(Math.max(window.devicePixelRatio / pixelScaleFactor, 0.5));
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0x002205, 1.0));
const dirLight = new THREE.DirectionalLight(0x00ff41, 1.5);
dirLight.position.set(5, 10, 10);
scene.add(dirLight);
// Stage & Platforms
const platformWidth = 24;
const platformTextureCanvas = document.createElement('canvas');
platformTextureCanvas.width = 512; platformTextureCanvas.height = 128;
const pCtx = platformTextureCanvas.getContext('2d');
pCtx.fillStyle = '#0a140a'; pCtx.fillRect(0, 0, 512, 128);
pCtx.fillStyle = '#ffcc00'; pCtx.font = 'bold 16px monospace';
for (let x = 0; x < 512; x += 24) {
for (let y = 16; y < 128; y += 24) {
pCtx.fillText(Math.random() < 0.5 ? '010' : '101', x, y);
}
}
const platformTex = new THREE.CanvasTexture(platformTextureCanvas);
platformTex.magFilter = THREE.NearestFilter; platformTex.minFilter = THREE.NearestFilter;
const mainPlatformMat = new THREE.MeshStandardMaterial({ map: platformTex, roughness: 0.6, metalness: 0.4, flatShading: true });
const platform = new THREE.Mesh(new THREE.BoxGeometry(platformWidth, 1, 4), mainPlatformMat);
platform.position.set(0, -2, 0);
scene.add(platform);
const platLeft = new THREE.Mesh(new THREE.BoxGeometry(6, 0.6, 3), mainPlatformMat);
platLeft.position.set(-6, 1.2, 0);
scene.add(platLeft);
const platRight = new THREE.Mesh(new THREE.BoxGeometry(6, 0.6, 3), mainPlatformMat);
platRight.position.set(6, 1.2, 0);
scene.add(platRight);
const platforms = [platform, platLeft, platRight];
// --- Added Five-Stage System ---
// The original prototype platforms remain intact; they are hidden while a selected
// stage is active, so no original gameplay systems are removed.
const originalPlatforms = platforms.slice();
originalPlatforms.forEach(p => p.visible = false);
function makeStageMaterial(def) {
const c = document.createElement('canvas');
c.width = 512; c.height = 128;
const ctx = c.getContext('2d');
ctx.fillStyle = def.bg.base; ctx.fillRect(0, 0, c.width, c.height);
ctx.fillStyle = def.bg.accent;
ctx.font = 'bold 16px monospace';
for (let x = 0; x < c.width; x += 64) {
for (let y = 18; y < c.height; y += 30) {
ctx.fillText(def.bg.glyphs[(x / 64 + y / 30) % def.bg.glyphs.length], x, y);
}
}
const tex = new THREE.CanvasTexture(c);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return new THREE.MeshStandardMaterial({ map: tex, roughness: .7, metalness: .25, flatShading: true });
}
function buildStageGeometry(def) {
if (stageDecorGroup) scene.remove(stageDecorGroup);
stageDecorGroup = new THREE.Group();
const mat = makeStageMaterial(def);
def.platforms.forEach(spec => {
const [w, h, d, x, y] = spec;
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat.clone());
mesh.position.set(x, y, 0);
stageDecorGroup.add(mesh);
});
// Decorative floating code panels make every stage visually distinct.
const panelMat = new THREE.MeshBasicMaterial({ color: def.bg.accent, transparent: true, opacity: .16 });
const panelCount = selectedStage + 3;
for (let i = 0; i < panelCount; i++) {
const panel = new THREE.Mesh(new THREE.BoxGeometry(2.5 + (i % 2), .08, .25), panelMat);
panel.position.set(-10 + i * (20 / Math.max(1, panelCount - 1)), 8 + (i % 3) * 2, -1);
panel.rotation.z = (i % 2 ? -.08 : .08);
stageDecorGroup.add(panel);
}
scene.add(stageDecorGroup);
// Rebuild the collision list from the actual stage platform specs.
// Decorative code panels are intentionally NOT collision platforms.
platforms.length = 0;
def.platforms.forEach(spec => {
const [w, h, d, x, y] = spec;
const collisionMesh = new THREE.Mesh(
new THREE.BoxGeometry(w, h, d),
new THREE.MeshBasicMaterial({ visible: false })
);
collisionMesh.position.set(x, y, 0);
collisionMesh.updateMatrixWorld(true);
scene.add(collisionMesh);
platforms.push(collisionMesh);
});
// Spawn points adapt to each layout.
p1.spawnX = -4; p2.spawnX = 4;
p1.spawnY = def.platforms[0][4] + 1.05;
p2.spawnY = def.platforms[0][4] + 1.05;
p1.x = p1.spawnX; p2.x = p2.spawnX;
p1.y = p1.spawnY; p2.y = p2.spawnY;
}
function applyStage(index) {
const def = stageDefinitions[index];
activeStageName = def.name;
buildStageGeometry(def);
blastzone.left = -30;
blastzone.right = 30;
blastzone.top = 18;
blastzone.bottom = -17;
document.getElementById('hud-time').innerText = matchRules.time;
}
function drawStageBackdrop() {
const def = stageDefinitions[selectedStage];
bgCtx.fillStyle = def.bg.trail;
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
bgCtx.fillStyle = def.bg.accent;
bgCtx.font = 'bold 16px monospace';
const spacing = 70;
const cols = Math.ceil(bgCanvas.width / spacing);
const rows = Math.ceil(bgCanvas.height / 38);
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
if ((i * 7 + j * 3 + selectedStage) % 5 === 0) {
const glyph = def.bg.glyphs[(i + j) % def.bg.glyphs.length];
bgCtx.globalAlpha = .13 + ((i + j) % 4) * .03;
bgCtx.fillText(glyph, i * spacing, j * 38 + 25);
}
}
}
bgCtx.globalAlpha = 1;
}
const BASE_GREEN = 0x005511;
const DARK_GREEN = 0x001504;
const WHITE_FLASH = 0xffffff;
const blastzone = { left: -26, right: 26, top: 18, bottom: -16 };
const gravity = -0.016;
const maxWalkSpeed = 0.11;
const groundAccel = 0.015;
const friction = 0.85;
const airMobility = 0.013;
const jumpForce = 0.38;
const shortHopMult = 0.55;
const charHalfHeight = 0.85;
let hitstopFrames = 0;
const p1 = { x: -4, y: -0.65, vx: 0, vy: 0, mesh: null, spawnX: -4, spawnY: -0.65, isJumping: false, facing: 1, charType: 0, attackType: null, attackCooldown: 0, attackAnimTimer: 0, stunTimer: 0, hitRandomRotX: 0, hitRandomRotY: 0, hitRandomRotZ: 0, percent: 0, stocks: 3, score: 0, respawnTimer: 0, isDead: false };
const p2 = { x: 4, y: -0.65, vx: 0, vy: 0, mesh: null, spawnX: 4, spawnY: -0.65, isJumping: false, facing: -1, charType: 1, attackType: null, attackCooldown: 0, attackAnimTimer: 0, stunTimer: 0, hitRandomRotX: 0, hitRandomRotY: 0, hitRandomRotZ: 0, percent: 0, stocks: 3, score: 0, respawnTimer: 0, isDead: false };
const projectiles = [];
const activeHitboxes = [];
const hitSparks = [];
const sparkGeo = new THREE.BoxGeometry(0.12, 0.12, 0.12);
function spawnHitSparks(x, y, isSmash = false) {
for (let i = 0; i < (isSmash ? 16 : 8); i++) {
const spark = new THREE.Mesh(sparkGeo, new THREE.MeshBasicMaterial({ color: isSmash ? 0xff0033 : 0xffcc00 }));
spark.position.set(x, y + 0.2, 0.3);
const angle = Math.random() * Math.PI * 2;
const speed = (isSmash ? 0.09 : 0.05) + Math.random() * 0.08;
scene.add(spark);
hitSparks.push({ mesh: spark, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 12 });
}
}
function spawnDeathEffect(x, y) {
playSfx(sfx.death);
for (let i = 0; i < 35; i++) {
const spark = new THREE.Mesh(sparkGeo, new THREE.MeshBasicMaterial({ color: Math.random() < 0.5 ? 0xff3355 : 0xffcc00 }));
spark.position.set(x, y, 0);
const angle = Math.random() * Math.PI * 2;
const speed = 0.08 + Math.random() * 0.22;
scene.add(spark);
hitSparks.push({ mesh: spark, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 25 });
}
}
// Pure Abstract N64 Geometry Meshes (No Limbs/Hands)
function createN64Mesh(type) {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color: BASE_GREEN, emissive: 0x002205, roughness: 0.4, flatShading: true });
if (type === 0) {
const ring = new THREE.Mesh(new THREE.TorusGeometry(0.52, 0.14, 6, 8), mat);
ring.scale.set(0.95, 1.6, 1.0);
group.add(ring);
} else if (type === 1) {
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.35, 1.7, 5, 1), mat);
body.position.y = 0.15;
const flag = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.25, 0.35), mat);
flag.position.set(-0.25, 0.85, 0); flag.rotation.z = 0.4;
const base = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.2, 0.45), mat);
base.position.y = -0.7;
group.add(body); group.add(flag); group.add(base);
} else if (type === 2) {
const shapeL = new THREE.Shape();
shapeL.moveTo(0, 0.8); shapeL.lineTo(-0.25, 0.8); shapeL.lineTo(-0.25, 0.35); shapeL.lineTo(-0.45, 0.15);
shapeL.lineTo(-0.25, -0.05); shapeL.lineTo(-0.25, -0.8); shapeL.lineTo(0, -0.8); shapeL.lineTo(0, -0.62);
shapeL.lineTo(-0.15, -0.62); shapeL.lineTo(-0.15, -0.15); shapeL.lineTo(-0.32, 0.05); shapeL.lineTo(-0.15, 0.25);
shapeL.lineTo(-0.15, 0.62); shapeL.lineTo(0, 0.62); shapeL.closePath();
const extrudeSettings = { depth: 0.18, bevelEnabled: true, bevelSegments: 1, steps: 1, bevelSize: 0.03, bevelThickness: 0.03 };
const braceGeomL = new THREE.ExtrudeGeometry(shapeL, extrudeSettings);
braceGeomL.center();
const leftBrace = new THREE.Mesh(braceGeomL, mat);
leftBrace.position.set(-0.35, 0, 0);
const rightBrace = leftBrace.clone();
rightBrace.scale.set(-1, 1, 1);
rightBrace.position.set(0.35, 0, 0);
const centerNode = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.25, 0.25), mat);
centerNode.position.set(0, 0, 0);
group.add(leftBrace); group.add(rightBrace); group.add(centerNode);
} else if (type === 3) {
const body = new THREE.Mesh(new THREE.OctahedronGeometry(0.7, 0), mat);
const spike1 = new THREE.Mesh(new THREE.ConeGeometry(0.2, 0.9, 4), mat);
spike1.position.set(0.6, 0.5, 0.2); spike1.rotation.z = -0.9;
const spike2 = spike1.clone();
spike2.position.set(-0.6, -0.5, -0.2); spike2.rotation.z = 2.2;
group.add(body); group.add(spike1); group.add(spike2);
} else {
const block1 = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.25, 0.6), mat);
block1.position.y = -0.5;
const block2 = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.25, 0.6), mat);
block2.position.y = -0.15;
const block3 = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.25, 0.6), mat);
block3.position.y = 0.2;
const block4 = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.25, 0.6), mat);
block4.position.y = 0.55;
group.add(block1); group.add(block2); group.add(block3); group.add(block4);
}
scene.add(group);
return group;
}
function rebuildPlayerMesh(p) {
if (p.mesh) scene.remove(p.mesh);
p.mesh = createN64Mesh(p.charType);
}
p1.mesh = createN64Mesh(p1.charType);
p2.mesh = createN64Mesh(p2.charType);
applyStage(0);
const keys = {};
window.addEventListener('keydown', (e) => keys[e.code] = true);
window.addEventListener('keyup', (e) => keys[e.code] = false);
const camTargetPos = new THREE.Vector3(0, 0, 16);
function showScorePopup(x, y, scoringPlayer) {
const vector = new THREE.Vector3(x, y + 0.8, 0).project(camera);
const popup = document.createElement('div');
popup.className = 'score-popup';
popup.innerText = '+1 PTS!';
popup.style.color = scoringPlayer === p1 ? '#00ff41' : '#ffcc00';
popup.style.left = ((vector.x * 0.5 + 0.5) * window.innerWidth) + 'px';
popup.style.top = ((-(vector.y * 0.5 - 0.5) * window.innerHeight)) + 'px';
document.body.appendChild(popup);
setTimeout(() => popup.remove(), 800);
}
function triggerGameOver(forcedWinner = null) {
if (matchEnded) return;
matchEnded = true;
playSfx(sfx.game);
bgm.ingame.pause();
bgm.ingame.currentTime = 0;
bgm.victory.play().catch(e => {});
let winner = forcedWinner;
if (!winner) {
if (matchRules.mode === 'STATS') {
if (p1.score > p2.score) winner = p1;
else if (p2.score > p1.score) winner = p2;
else winner = Math.random() < 0.5 ? p1 : p2;
} else {
winner = p1.stocks > p2.stocks ? p1 : p2;
}
}
const overlay = document.getElementById('game-overlay');
const calloutText = document.getElementById('game-callout-text');
const victoryContainer = document.getElementById('victory-container');
const winnerText = document.getElementById('winner-text');
const finalStatsText = document.getElementById('final-stats-text');
overlay.classList.add('active');
calloutText.style.display = 'block';
victoryContainer.classList.remove('show');
setTimeout(() => {
calloutText.style.display = 'none';
victoryContainer.classList.add('show');
const winnerName = winner === p1 ? "PLAYER 1 WINS!" : (p2IsCPU ? "CPU WINS!" : "PLAYER 2 WINS!");
winnerText.innerText = winnerName;
if (matchRules.mode === 'STATS') {
finalStatsText.innerHTML = `FINAL STATS (POINTS MODE)<br>P1 SCORE: ${p1.score} | P2 SCORE: ${p2.score}`;
} else {
finalStatsText.innerHTML = `FINAL STATS (STOCK MODE)<br>P1 K.O.s: ${statsData.p1KOs} | P2 K.O.s: ${statsData.p2KOs}`;
}
}, 3000);
}
function handleElimination(p, opponent) {
if (p.isDead) return;
p.isDead = true;
spawnDeathEffect(p.x, p.y);
if (p.mesh) p.mesh.visible = false;
opponent.score++;
if (matchRules.mode === 'STOCK') {
p.stocks--;
if (p === p1) {
document.getElementById('p1-stocks').innerText = Math.max(0, p.stocks);
statsData.p2KOs++;
} else {
document.getElementById('p2-stocks').innerText = Math.max(0, p.stocks);
statsData.p1KOs++;
}
} else {
if (p === p1) {
document.getElementById('p1-pts').innerText = p1.score;
statsData.p2KOs++;
} else {
document.getElementById('p2-pts').innerText = p2.score;
statsData.p1KOs++;
}
}
showScorePopup(p.x, p.y, opponent);
if (matchRules.mode === 'STOCK' && p.stocks <= 0) {
triggerGameOver(opponent);
return;
}
p.respawnTimer = 60;
}
function respawnPlayer(p) {
p.x = p.spawnX; p.y = p.spawnY; p.vx = 0; p.vy = 0;
p.isJumping = false; p.attackType = null; p.attackCooldown = 0;
p.attackAnimTimer = 0; p.stunTimer = 0; p.percent = 0;
p.isDead = false;
if (p.mesh) p.mesh.visible = true;
updateHud();
}
function updateHud() {
document.getElementById('p1-pct').innerText = Math.floor(p1.percent);
document.getElementById('p2-pct').innerText = Math.floor(p2.percent);
}
function showDamagePopup(x, y, damageVal) {
const vector = new THREE.Vector3(x, y + 0.5, 0).project(camera);
const popup = document.createElement('div');
popup.className = 'dmg-popup';
popup.innerText = '+' + damageVal + '%';
popup.style.left = ((vector.x * 0.5 + 0.5) * window.innerWidth) + 'px';
popup.style.top = ((-(vector.y * 0.5 - 0.5) * window.innerHeight)) + 'px';
document.body.appendChild(popup);
setTimeout(() => popup.remove(), 600);
}
function handleSpecialsAndAttacks(attacker, opponent, upKey, downKey, jumpKey, attackKey, smashKey, specialKey, isCPU = false) {
if (matchEnded || attacker.isDead || matchIsLocked) return;
if (attacker.attackCooldown > 0) {
attacker.attackCooldown--;
if (attacker.attackAnimTimer > 0) attacker.attackAnimTimer--;
return;
}
let doSpecial = isCPU ? (Math.random() < (cpuLevel * 0.005)) : keys[specialKey];
let inMeleeRange = Math.abs(attacker.x - opponent.x) <= 2.2 && Math.abs(attacker.y - opponent.y) <= 1.8;
let doAttack = isCPU ? (inMeleeRange && Math.random() < (cpuLevel * 0.035)) : (keys[smashKey] || keys[attackKey]);
if (doSpecial) {
playSfx(sfx.swing);
attacker.attackCooldown = 45; attacker.attackAnimTimer = 20; attacker.attackType = 'special';
if (attacker.charType === 3) {
attacker.x += attacker.facing * 3.5;
activeHitboxes.push({ x: attacker.x, y: attacker.y, width: 1.8, height: 1.4, owner: attacker, target: opponent, damage: 11, isSmash: false, lifespan: 8 });
} else if (attacker.charType === 4) {
const pMesh = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.5, 0.5), new THREE.MeshBasicMaterial({ color: 0x00ffcc }));
pMesh.position.set(attacker.x + attacker.facing * 0.8, attacker.y, 0);
scene.add(pMesh);
projectiles.push({ mesh: pMesh, vx: attacker.facing * 0.32, owner: attacker, target: opponent, damage: 12, life: 50 });
} else if (attacker.charType === 2) {
activeHitboxes.push({ x: attacker.x + attacker.facing * 1.2, y: attacker.y, width: 1.6, height: 1.6, owner: attacker, target: opponent, damage: 10, isSmash: true, lifespan: 7 });
} else {
const pMesh = new THREE.Mesh(new THREE.TorusGeometry(0.35, 0.08, 6, 8), new THREE.MeshBasicMaterial({ color: 0x00ff41 }));
pMesh.position.set(attacker.x + attacker.facing * 0.8, attacker.y, 0);
scene.add(pMesh);
projectiles.push({ mesh: pMesh, vx: attacker.facing * 0.28, owner: attacker, target: opponent, damage: 8, life: 45 });
}
return;
}
if (doAttack) {
playSfx(sfx.swing);
const isSmash = isCPU ? (cpuLevel >= 5 && Math.random() < 0.25) : keys[smashKey];
let dmgValue = isSmash ? 13 : 8;
if (attacker.charType === 3) dmgValue = isSmash ? 14 : 9;
if (attacker.charType === 4) dmgValue = isSmash ? 15 : 8;
attacker.attackCooldown = isSmash ? 28 : 18;
attacker.attackAnimTimer = isSmash ? 14 : 9;
attacker.attackType = isSmash ? 'smash' : 'attack';
activeHitboxes.push({
x: attacker.x + attacker.facing * 0.9,
y: attacker.y,
width: isSmash ? 1.5 : 1.0,
height: isSmash ? 1.3 : 0.9,
owner: attacker,
target: opponent,
damage: dmgValue,
isSmash: isSmash,
lifespan: isSmash ? 8 : 5
});
} else if (attacker.attackAnimTimer === 0) {
attacker.attackType = null;
}
}
function checkGrounded(p) {
const feetY = p.y - charHalfHeight;
let bestGround = false;
for (let plat of platforms) {
if (!plat || !plat.geometry) continue;
// Explicit world-space platform bounds.
const halfW = plat.geometry.parameters.width * 0.5;
const halfH = plat.geometry.parameters.height * 0.5;
const left = plat.position.x - halfW;
const right = plat.position.x + halfW;
const top = plat.position.y + halfH;
// Only land when crossing the top surface while falling.
if (p.x >= left - 0.05 && p.x <= right + 0.05 &&
p.vy <= 0 &&
feetY <= top + 0.18 &&
feetY >= top - 0.55) {
if (bestGround === false || top > bestGround) bestGround = top;
}
}
return bestGround === false ? false : bestGround + charHalfHeight;
}
function updateCPUAI(cpu, target) {
if (matchEnded || cpu.stunTimer > 0 || cpu.isDead || matchIsLocked) return;
const diffX = target.x - cpu.x;
cpu.facing = diffX > 0 ? 1 : -1;
if (Math.abs(diffX) > 1.2) {
cpu.vx = Math.sign(diffX) * maxWalkSpeed * (0.4 + (cpuLevel / 10) * 0.6);
} else {
cpu.vx *= friction;
}
const groundY = checkGrounded(cpu);
if (groundY !== false && target.y > cpu.y + 1.5 && Math.random() < (cpuLevel * 0.04)) {
cpu.vy = jumpForce;
cpu.isJumping = true;
cpu.y += 0.05;
}
}
function updatePlayerPhysics(p, opponent, leftKey, rightKey, upKey, downKey, jumpKey, attackKey, smashKey, specialKey, isCPU = false) {
if (matchEnded) return;
if (p.isDead) {
if (p.respawnTimer > 0) {
p.respawnTimer--;
if (p.respawnTimer === 0) {
respawnPlayer(p);
}
}
return;
}
if (matchIsLocked) {
p.vx = 0;
p.vy = 0;
return;
}
if (isCPU) {
updateCPUAI(p, opponent);
}
handleSpecialsAndAttacks(p, opponent, upKey, downKey, jumpKey, attackKey, smashKey, specialKey, isCPU);
if (p.stunTimer > 0) p.stunTimer--;
const groundY = checkGrounded(p);
if (groundY !== false) {
p.y = groundY; p.vy = 0; p.isJumping = false;
if (!isCPU) {
if (keys[leftKey]) { p.vx = Math.max(p.vx - groundAccel, -maxWalkSpeed); p.facing = -1; }
else if (keys[rightKey]) { p.vx = Math.min(p.vx + groundAccel, maxWalkSpeed); p.facing = 1; }
else { p.vx *= friction; if (Math.abs(p.vx) < 0.001) p.vx = 0; }
} else {
p.vx *= friction;
}
if ((!isCPU && keys[jumpKey])) { p.vy = jumpForce; p.isJumping = true; p.y += 0.05; }
} else {
if (!isCPU) {
if (keys[leftKey]) { p.vx = Math.max(p.vx - airMobility, -maxWalkSpeed * 1.15); p.facing = -1; }
else if (keys[rightKey]) { p.vx = Math.min(p.vx + airMobility, maxWalkSpeed * 1.15); p.facing = 1; }
}
if (p.isJumping && !isCPU && !keys[jumpKey] && p.vy > 0) { p.vy *= shortHopMult; p.isJumping = false; }
p.vy += gravity;
}
p.x += p.vx; p.y += p.vy;
if (p.x < blastzone.left || p.x > blastzone.right || p.y > blastzone.top || p.y < blastzone.bottom) {
handleElimination(p, opponent);
}
}
// --- Main Animation Loop ---
function animate() {
requestAnimationFrame(animate);
drawAnimatedBg();
if (currentGameState !== 'PLAYING') {
renderer.render(scene, camera);
return;
}
if (finisherTimer > 0) {
finisherTimer--;
if (finisherTimer === 0) {
document.getElementById('finisher-flash').classList.remove('active');
}
[p1, p2].forEach(p => {
if (p.mesh && !p.isDead) {
p.mesh.position.set(p.x + (Math.random() - 0.5) * 0.45, p.y + (Math.random() - 0.5) * 0.45, (Math.random() - 0.5) * 0.35);
}
});
renderer.render(scene, camera);
return;
}
if (hitstopFrames > 0 && !matchEnded) {
hitstopFrames--;
renderer.render(scene, camera);
return;
}
updatePlayerPhysics(p1, p2, 'KeyA', 'KeyD', 'KeyW', 'KeyS', 'Space', 'KeyF', 'KeyR', 'KeyT', false);
updatePlayerPhysics(p2, p1, 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'ShiftRight', 'KeyL', 'Slash', 'KeyP', p2IsCPU);
for (let i = activeHitboxes.length - 1; i >= 0; i--) {
const hb = activeHitboxes[i];
hb.lifespan--;
if (hb.target.isDead) {
activeHitboxes.splice(i, 1);
continue;
}
const targetBox = { minX: hb.target.x - 0.6, maxX: hb.target.x + 0.6, minY: hb.target.y - 0.8, maxY: hb.target.y + 0.8 };
const hitboxBounds = { minX: hb.x - hb.width / 2, maxX: hb.x + hb.width / 2, minY: hb.y - hb.height / 2, maxY: hb.y + hb.height / 2 };
if (hitboxBounds.minX < targetBox.maxX && hitboxBounds.maxX > targetBox.minX &&
hitboxBounds.minY < targetBox.maxY && hitboxBounds.maxY > targetBox.minY) {
const isCritFinisher = hb.isSmash && hb.target.percent >= 120 && hb.target.stocks === 1;
if (isCritFinisher) {
playSfx(sfx.crit);
} else {
playSfx(sfx.hit);
}
hb.target.percent += hb.damage;
updateHud();
showDamagePopup(hb.target.x, hb.target.y, hb.damage);
spawnHitSparks(hb.target.x, hb.target.y, hb.isSmash);
if (isCritFinisher) {
finisherTimer = 48; // 0.8 seconds at 60 FPS
document.getElementById('finisher-flash').classList.add('active');
}
const percentMultiplier = hb.target.percent * 0.0038;
const baseKb = hb.isSmash ? (hb.target.percent >= 120 ? 0.38 : 0.16) : 0.07;
const finalKb = baseKb * (1.0 + percentMultiplier * 2.2);
hb.target.vx = hb.owner.facing * finalKb;
hb.target.vy = finalKb * (hb.isSmash && hb.target.percent >= 120 ? 1.4 : 0.9);
hb.target.stunTimer = hb.isSmash ? Math.min(35, 15 + Math.floor(hb.target.percent * 0.18)) : 10;
// Random static rotation on hit with no keyframes/recovery
hb.target.hitRandomRotX = (Math.random() - 0.5) * 3.5;
hb.target.hitRandomRotY = (Math.random() - 0.5) * 3.5;
hb.target.hitRandomRotZ = (Math.random() - 0.5) * 4.0;
hitstopFrames = hb.isSmash ? 9 : 3;
activeHitboxes.splice(i, 1);
continue;
}
if (hb.lifespan <= 0) {
activeHitboxes.splice(i, 1);
}
}
for (let i = projectiles.length - 1; i >= 0; i--) {
const pr = projectiles[i];
pr.mesh.position.x += pr.vx; pr.life--;
if (!pr.target.isDead && Math.abs(pr.mesh.position.x - pr.target.x) < 1.2 && Math.abs(pr.mesh.position.y - pr.target.y) < 1.2) {
playSfx(sfx.hit);
pr.target.percent += pr.damage; pr.target.vx = Math.sign(pr.vx) * 0.12; pr.target.vy = 0.08; pr.target.stunTimer = 12;
pr.target.hitRandomRotX = (Math.random() - 0.5) * 3.0;
pr.target.hitRandomRotY = (Math.random() - 0.5) * 3.0;
pr.target.hitRandomRotZ = (Math.random() - 0.5) * 3.5;
updateHud(); showDamagePopup(pr.target.x, pr.target.y, pr.damage); spawnHitSparks(pr.target.x, pr.target.y);
scene.remove(pr.mesh); projectiles.splice(i, 1); continue;
}
if (pr.life <= 0) { scene.remove(pr.mesh); projectiles.splice(i, 1); }
}
for (let i = hitSparks.length - 1; i >= 0; i--) {
const s = hitSparks[i];
s.mesh.position.x += s.vx; s.mesh.position.y += s.vy; s.life--;
if (s.life <= 0) { scene.remove(s.mesh); hitSparks.splice(i, 1); }
}
// Abstract Dynamic Tilt & Squash Animation Variants
[p1, p2].forEach(p => {
if (p.mesh && !p.isDead) {
const isStunned = p.stunTimer > 0;
const activeColor = isStunned ? WHITE_FLASH : (Math.random() < 0.1 ? DARK_GREEN : BASE_GREEN);
p.mesh.children.forEach(child => {
if (child.material) child.material.color.setHex(activeColor);
});
p.mesh.position.set(p.x, p.y, 0);
if (isStunned) {
// Apply static random hit rotation instantly with no keyframed interpolation
p.mesh.rotation.set(p.hitRandomRotX, p.hitRandomRotY, p.hitRandomRotZ);
p.mesh.scale.set(1.0, 1.0, 1.0);
} else if (p.attackAnimTimer > 0) {
// Dynamic unique attack distortion variants depending on attack type
const attackProgress = p.attackAnimTimer;
if (p.attackType === 'smash') {
p.mesh.rotation.set(0, p.facing === 1 ? 0 : Math.PI, p.facing * -0.9);
p.mesh.scale.set(1.4, 0.7, 1.2);
} else if (p.attackType === 'special') {
p.mesh.rotation.set(0, p.facing === 1 ? 0 : Math.PI, (attackProgress * 0.5) * p.facing);
p.mesh.scale.set(1.1, 1.3, 0.9);
} else {
p.mesh.rotation.set(0, p.facing === 1 ? 0 : Math.PI, p.facing * -0.45);
p.mesh.scale.set(1.25, 0.85, 1.0);
}
} else {
// Normal movement / idle squash and tilt based on velocity direction
let moveTilt = p.vx * 1.5 * p.facing;
let stretchFactor = 1.0 + Math.abs(p.vx) * 0.4;
let squashFactor = 1.0 - Math.abs(p.vx) * 0.2;
p.mesh.rotation.set(0, p.facing === 1 ? 0 : Math.PI, moveTilt);
p.mesh.scale.set(stretchFactor, squashFactor, 1.0);
}
}
});
// Camera tracking
if (p1.mesh && p2.mesh) {
const midX = (p1.x + p2.x) / 2;
const midY = Math.max((p1.y + p2.y) / 2 + 0.5, 0);
const distance = Math.abs(p1.x - p2.x);
let zoomFactor = finisherTimer > 0 ? 0.025 : 0.85;
let targetZ = finisherTimer > 0 ? 2.8 : (6 + Math.max(distance, 6) * zoomFactor);
let shakeX = 0;
let shakeY = 0;
if (finisherTimer > 0) {
shakeX = (Math.random() - 0.5) * 0.25;
shakeY = (Math.random() - 0.5) * 0.25;
}
camTargetPos.set(midX + shakeX, midY + shakeY, targetZ);
camera.position.lerp(camTargetPos, finisherTimer > 0 ? 0.4 : 0.1);
camera.lookAt(midX + shakeX, midY + shakeY, 0);
}
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.max(window.devicePixelRatio / pixelScaleFactor, 0.5));
columns = Math.floor(window.innerWidth / fontSize);
drops = Array(columns).fill(1);
});
</script>
</body>
</html>Game Source: Faulty Coding: Nega 64
Creator: FlashGalaxy17
Libraries: three
Complexity: complex (1631 lines, 75.3 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: faulty-coding-nega-64-flashgalaxy17" to link back to the original. Then publish at arcadelab.ai/publish.