Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Axeman: Timber Battle</title> | |
| <!-- Importing a nice font for the minimal look --> | |
| <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700;900&display=swap" rel="stylesheet"> | |
| <!-- FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-color: #1A1A2E; | |
| --tree-color: #8B7355; | |
| --cut-line: #FF2E63; | |
| --player-color: #4ECDC4; | |
| --axe-color: #C7C7C7; | |
| --text-color: #FFFFFF; | |
| --score-color: #FFD166; | |
| --ui-overlay: rgba(26, 26, 46, 0.9); | |
| --accent: #E94560; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| user-select: none; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| font-family: 'Montserrat', sans-serif; | |
| overflow: hidden; | |
| width: 100vw; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| /* Header */ | |
| header { | |
| width: 100%; | |
| padding: 10px 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(0,0,0,0.2); | |
| position: absolute; | |
| top: 0; | |
| z-index: 10; | |
| } | |
| h1 { | |
| font-size: 1.2rem; | |
| margin: 0; | |
| font-weight: 900; | |
| letter-spacing: 1px; | |
| color: var(--player-color); | |
| } | |
| .anycoder-link { | |
| color: var(--text-color); | |
| text-decoration: none; | |
| font-size: 0.8rem; | |
| opacity: 0.7; | |
| transition: opacity 0.3s; | |
| } | |
| .anycoder-link:hover { opacity: 1; text-decoration: underline; } | |
| /* Game Container */ | |
| #game-container { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| canvas { | |
| display: block; | |
| box-shadow: 0 0 50px rgba(0,0,0,0.5); | |
| } | |
| /* UI Overlays */ | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: var(--ui-overlay); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 20; | |
| transition: opacity 0.3s ease; | |
| } | |
| .hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .menu-title { | |
| font-size: 3rem; | |
| font-weight: 900; | |
| color: var(--cut-line); | |
| text-transform: uppercase; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 0px var(--player-color); | |
| text-align: center; | |
| } | |
| .menu-subtitle { | |
| font-size: 1rem; | |
| color: var(--score-color); | |
| margin-bottom: 30px; | |
| } | |
| .btn { | |
| background: var(--accent); | |
| color: white; | |
| border: none; | |
| padding: 15px 40px; | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| border-radius: 50px; | |
| cursor: pointer; | |
| margin: 10px; | |
| transition: transform 0.1s, background 0.2s; | |
| box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4); | |
| font-family: inherit; | |
| min-width: 200px; | |
| } | |
| .btn:hover { | |
| transform: scale(1.05); | |
| background: #ff5e78; | |
| } | |
| .btn:active { | |
| transform: scale(0.95); | |
| } | |
| .mode-select { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| .mode-btn { | |
| background: transparent; | |
| border: 2px solid var(--player-color); | |
| color: var(--player-color); | |
| padding: 10px 20px; | |
| } | |
| .mode-btn.active { | |
| background: var(--player-color); | |
| color: var(--bg-color); | |
| } | |
| /* HUD */ | |
| #hud { | |
| position: absolute; | |
| top: 60px; | |
| left: 0; | |
| width: 100%; | |
| padding: 0 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| pointer-events: none; | |
| z-index: 5; | |
| } | |
| .hud-item { | |
| text-align: center; | |
| } | |
| .score-label { | |
| font-size: 0.8rem; | |
| opacity: 0.8; | |
| } | |
| .score-value { | |
| font-size: 2rem; | |
| font-weight: 900; | |
| color: var(--score-color); | |
| } | |
| #combo-display { | |
| position: absolute; | |
| top: 20%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| font-size: 2.5rem; | |
| font-weight: 900; | |
| color: var(--cut-line); | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| text-shadow: 0 0 10px var(--cut-line); | |
| pointer-events: none; | |
| } | |
| .controls-hint { | |
| margin-top: 20px; | |
| display: flex; | |
| gap: 20px; | |
| font-size: 0.9rem; | |
| opacity: 0.7; | |
| } | |
| .key { | |
| border: 1px solid rgba(255,255,255,0.3); | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| } | |
| /* Touch Controls for Mobile */ | |
| #touch-zones { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| z-index: 2; | |
| } | |
| .touch-zone { | |
| flex: 1; | |
| /* background: rgba(255,0,0,0.1); Debugging */ | |
| } | |
| /* Responsive Adjustments */ | |
| @media (max-width: 600px) { | |
| .menu-title { font-size: 2rem; } | |
| .btn { padding: 12px 30px; min-width: 160px; font-size: 1rem; } | |
| .controls-hint { display: none; } /* Hide keyboard hints on mobile */ | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1><i class="fas fa-tree"></i> AXEMAN</h1> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a> | |
| </header> | |
| <div id="game-container"> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- HUD --> | |
| <div id="hud" class="hidden"> | |
| <div class="hud-item"> | |
| <div class="score-label">SCORE</div> | |
| <div id="score-val" class="score-value">0</div> | |
| </div> | |
| <div class="hud-item"> | |
| <div class="score-label">TIME</div> | |
| <div id="time-val" class="score-value">--</div> | |
| </div> | |
| <div class="hud-item"> | |
| <div class="score-label">BEST</div> | |
| <div id="best-val" class="score-value">0</div> | |
| </div> | |
| </div> | |
| <div id="combo-display">COMBO x2</div> | |
| <!-- Touch Zones (Invisible) --> | |
| <div id="touch-zones" class="hidden"> | |
| <div class="touch-zone" id="touch-left"></div> | |
| <div class="touch-zone" id="touch-right"></div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="start-screen" class="overlay"> | |
| <div class="menu-title">AXEMAN</div> | |
| <div class="menu-subtitle">CUT FAST. DON'T MISTAKE.</div> | |
| <div class="mode-select"> | |
| <button class="btn mode-btn active" data-mode="classic">CLASSIC</button> | |
| <button class="btn mode-btn" data-mode="timeattack">TIME ATTACK (90s)</button> | |
| <button class="btn mode-btn" data-mode="survival">SURVIVAL</button> | |
| </div> | |
| <button id="start-btn" class="btn">PLAY GAME</button> | |
| <div class="controls-hint"> | |
| <span><span class="key">A</span> / <span class="key">←</span> Left</span> | |
| <span><span class="key">D</span> / <span class="key">→</span> Right</span> | |
| </div> | |
| </div> | |
| <!-- Game Over Screen --> | |
| <div id="game-over-screen" class="overlay hidden"> | |
| <h1 style="color: var(--cut-line); font-size: 3rem;">GAME OVER</h1> | |
| <div style="font-size: 1.5rem; margin: 10px;">Score: <span id="final-score" style="color:var(--score-color)">0</span></div> | |
| <div style="font-size: 1rem; margin-bottom: 30px;">Trees Cut: <span id="final-trees">0</span></div> | |
| <button id="restart-btn" class="btn">TRY AGAIN</button> | |
| <button id="menu-btn" class="btn" style="background:transparent; border:1px solid white; margin-top:10px;">MAIN MENU</button> | |
| </div> | |
| </div> | |
| <script> | |
| /** | |
| * AXEMAN GAME ENGINE | |
| * Built with Vanilla JS, HTML5 Canvas, and Web Audio API | |
| */ | |
| // --- Configuration --- | |
| const CONFIG = { | |
| colors: { | |
| bg: '#1A1A2E', | |
| tree: '#8B7355', | |
| treeLight: '#A68B6B', | |
| cutLine: '#FF2E63', | |
| player: '#4ECDC4', | |
| axe: '#C7C7C7', | |
| gold: '#FFD166', | |
| bird: '#FFFFFF', | |
| ui: '#FFFFFF' | |
| }, | |
| gravity: 0.5, | |
| chopForce: 15, | |
| treeBaseHeight: 400, // Starting height | |
| segmentHeight: 40, | |
| maxComboTime: 2000 // ms to keep combo | |
| }; | |
| // --- Audio System (Web Audio API) --- | |
| const AudioSys = { | |
| ctx: null, | |
| init: function() { | |
| window.AudioContext = window.AudioContext || window.webkitAudioContext; | |
| this.ctx = new AudioContext(); | |
| }, | |
| playTone: function(freq, type, duration, vol = 0.1) { | |
| if (!this.ctx) return; | |
| const osc = this.ctx.createOscillator(); | |
| const gain = this.ctx.createGain(); | |
| osc.type = type; | |
| osc.frequency.setValueAtTime(freq, this.ctx.currentTime); | |
| gain.gain.setValueAtTime(vol, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration); | |
| osc.connect(gain); | |
| gain.connect(this.ctx.destination); | |
| osc.start(); | |
| osc.stop(this.ctx.currentTime + duration); | |
| }, | |
| playNoise: function(duration, vol = 0.2) { | |
| if (!this.ctx) return; | |
| const bufferSize = this.ctx.sampleRate * duration; | |
| const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); | |
| const data = buffer.getChannelData(0); | |
| for (let i = 0; i < bufferSize; i++) { | |
| data[i] = Math.random() * 2 - 1; | |
| } | |
| const noise = this.ctx.createBufferSource(); | |
| noise.buffer = buffer; | |
| const gain = this.ctx.createGain(); | |
| // Lowpass filter for "thud" sound | |
| const filter = this.ctx.createBiquadFilter(); | |
| filter.type = 'lowpass'; | |
| filter.frequency.value = 1000; | |
| gain.gain.setValueAtTime(vol, this.ctx.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration); | |
| noise.connect(filter); | |
| filter.connect(gain); | |
| gain.connect(this.ctx.destination); | |
| noise.start(); | |
| }, | |
| sfx: { | |
| chop: () => { | |
| AudioSys.playNoise(0.1, 0.3); | |
| AudioSys.playTone(150, 'square', 0.1, 0.1); | |
| }, | |
| hitBranch: () => { | |
| AudioSys.playTone(100, 'sawtooth', 0.3, 0.3); | |
| }, | |
| score: () => { | |
| AudioSys.playTone(600, 'sine', 0.1, 0.1); | |
| setTimeout(() => AudioSys.playTone(900, 'sine', 0.2, 0.1), 100); | |
| }, | |
| combo: () => { | |
| AudioSys.playTone(400, 'triangle', 0.1, 0.1); | |
| setTimeout(() => AudioSys.playTone(600, 'triangle', 0.1, 0.1), 50); | |
| setTimeout(() => AudioSys.playTone(800, 'triangle', 0.2, 0.1), 100); | |
| }, | |
| powerup: () => { | |
| AudioSys.playTone(300, 'sine', 0.1, 0.2); | |
| AudioSys.playTone(600, 'sine', 0.3, 0.2); | |
| }, | |
| gameOver: () => { | |
| AudioSys.playTone(200, 'sawtooth', 1.0, 0.5); | |
| setTimeout(() => AudioSys.playTone(100, 'sawtooth', 1.0, 0.5), 200); | |
| } | |
| } | |
| }; | |
| // --- Game State Management --- | |
| const Game = { | |
| canvas: document.getElementById('gameCanvas'), | |
| ctx: document.getElementById('gameCanvas').getContext('2d'), | |
| width: 0, | |
| height: 0, | |
| state: 'MENU', // MENU, PLAYING, GAMEOVER | |
| mode: 'classic', // classic, timeattack, survival | |
| lastTime: 0, | |
| score: 0, | |
| bestScore: parseInt(localStorage.getItem('axeman_best')) || 0, | |
| treesCut: 0, | |
| // Mechanics | |
| playerSide: 1, // 1 = Right, -1 = Left | |
| lastChopSide: 0, // 0 = none, 1 = right, -1 = left | |
| treeHP: 10, | |
| treeMaxHP: 10, | |
| treeWidth: 120, | |
| timeRemaining: 0, | |
| combo: 0, | |
| comboTimer: 0, | |
| // Entities | |
| particles: [], | |
| powerUps: [], | |
| activePowerUp: null, // {type: 'GOLDEN_AXE', endTime: timestamp} | |
| birds: [], // {x, y, side} | |
| // Visual Shake | |
| shake: 0, | |
| init: function() { | |
| this.resize(); | |
| window.addEventListener('resize', () => this.resize()); | |
| this.setupInputs(); | |
| this.loop(0); | |
| // UI Updates | |
| document.getElementById('best-val').innerText = this.bestScore; | |
| }, | |
| resize: function() { | |
| this.width = window.innerWidth; | |
| this.height = window.innerHeight; | |
| this.canvas.width = this.width; | |
| this.canvas.height = this.height; | |
| }, | |
| start: function(mode) { | |
| AudioSys.init(); | |
| this.mode = mode; | |
| this.state = 'PLAYING'; | |
| this.score = 0; | |
| this.treesCut = 0; | |
| this.combo = 0; | |
| this.comboTimer = 0; | |
| this.activePowerUp = null; | |
| this.birds = []; | |
| this.powerUps = []; | |
| // Tree Setup | |
| this.treeHP = 10; | |
| this.treeMaxHP = 10; | |
| this.lastChopSide = 0; // Reset so first hit is always safe | |
| // Mode Setup | |
| if (mode === 'timeattack') { | |
| this.timeRemaining = 90; | |
| } else if (mode === 'classic') { | |
| this.timeRemaining = 10; // Initial buffer | |
| } else { | |
| this.timeRemaining = 9999; | |
| } | |
| // UI | |
| document.getElementById('start-screen').classList.add('hidden'); | |
| document.getElementById('game-over-screen').classList.add('hidden'); | |
| document.getElementById('hud').classList.remove('hidden'); | |
| document.getElementById('touch-zones').classList.remove('hidden'); | |
| this.updateHUD(); | |
| }, | |
| endGame: function() { | |
| this.state = 'GAMEOVER'; | |
| AudioSys.sfx.gameOver(); | |
| if (this.score > this.bestScore) { | |
| this.bestScore = this.score; | |
| localStorage.setItem('axeman_best', this.bestScore); | |
| } | |
| document.getElementById('game-over-screen').classList.remove('hidden'); | |
| document.getElementById('hud').classList.add('hidden'); | |
| document.getElementById('touch-zones').classList.add('hidden'); | |
| document.getElementById('final-score').innerText = this.score; | |
| document.getElementById('final-trees').innerText = this.treesCut; | |
| }, | |
| setupInputs: function() { | |
| // Keyboard | |
| window.addEventListener('keydown', (e) => { | |
| if (this.state !== 'PLAYING') return; | |
| if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') this.chop(-1); | |
| if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') this.chop(1); | |
| }); | |
| // Touch | |
| const leftZone = document.getElementById('touch-left'); | |
| const rightZone = document.getElementById('touch-right'); | |
| const handleTouch = (side) => (e) => { | |
| e.preventDefault(); | |
| if (this.state === 'PLAYING') this.chop(side); | |
| }; | |
| leftZone.addEventListener('touchstart', handleTouch(-1)); | |
| rightZone.addEventListener('touchstart', handleTouch(1)); | |
| // Mode Selection | |
| document.querySelectorAll('.mode-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| this.mode = btn.dataset.mode; | |
| }); | |
| }); | |
| // Menu Buttons | |
| document.getElementById('start-btn').addEventListener('click', () => this.start(this.mode)); | |
| document.getElementById('restart-btn').addEventListener('click', () => this.start(this.mode)); | |
| document.getElementById('menu-btn').addEventListener('click', () => { | |
| document.getElementById('game-over-screen').classList.add('hidden'); | |
| document.getElementById('start-screen').classList.remove('hidden'); | |
| this.state = 'MENU'; | |
| }); | |
| }, | |
| chop: function(side) { | |
| // 1. Validate Rule: Don't hit same side twice | |
| if (this.lastChopSide === side) { | |
| this.triggerShake(20); | |
| AudioSys.sfx.hitBranch(); | |
| this.endGame(); | |
| return; | |
| } | |
| this.lastChopSide = side; | |
| this.playerSide = side; | |
| // 2. Audio & Visuals | |
| AudioSys.sfx.chop(); | |
| this.spawnParticles(side); | |
| this.triggerShake(5); | |
| // 3. Logic | |
| let damage = 1; | |
| if (this.activePowerUp && this.activePowerUp.type === 'GOLDEN_AXE') damage = 2; | |
| // Check for obstacles on this segment (Bird) | |
| // Simplified: Random chance bird spawns on current hit side | |
| // If bird exists, damage is 0 or negative? Let's say bird needs 2 hits. | |
| // For simplicity: Bird just adds HP to tree if present. | |
| const birdIndex = this.birds.findIndex(b => b.side === side && Math.abs(b.y - (this.height - 150)) < 50); | |
| if (birdIndex !== -1) { | |
| AudioSys.playTone(800, 'square', 0.1); // Tweet | |
| this.birds.splice(birdIndex, 1); | |
| this.treeHP += 1; // Penalty: extra hit needed | |
| this.shake = 10; | |
| } | |
| this.treeHP -= damage; | |
| // 4. Combo System | |
| this.combo++; | |
| this.comboTimer = CONFIG.maxComboTime; | |
| // 5. Score | |
| let points = 10 + (this.combo * 2); | |
| if (this.activePowerUp && this.activePowerUp.type === 'MAGNET') points *= 2; | |
| this.score += points; | |
| // 6. Tree Logic | |
| if (this.treeHP <= 0) { | |
| this.treeDown(); | |
| } | |
| // 7. Time Management (Classic Mode) | |
| if (this.mode === 'classic') { | |
| this.timeRemaining = Math.min(this.timeRemaining + 1, 10); | |
| } | |
| this.updateHUD(); | |
| }, | |
| treeDown: function() { | |
| AudioSys.sfx.score(); | |
| this.treesCut++; | |
| // Increase difficulty | |
| const speedBoost = Math.min(5, Math.floor(this.treesCut / 2)); | |
| this.treeMaxHP = Math.min(20, 10 + speedBoost); | |
| this.treeHP = this.treeMaxHP; | |
| this.lastChopSide = 0; // Reset side restriction for new tree | |
| // Visual: Tree falls | |
| this.triggerShake(10); | |
| // Chance for PowerUp | |
| if (Math.random() < 0.2) this.spawnPowerUp(); | |
| // Spawn new bird occasionally | |
| if (Math.random() < 0.3) { | |
| this.birds.push({ | |
| side: Math.random() > 0.5 ? 1 : -1, | |
| y: this.height - 200 - Math.random() * 200 | |
| }); | |
| } | |
| }, | |
| spawnPowerUp: function() { | |
| const types = ['GOLDEN_AXE', 'SLOW_TIME', 'COMBO_LOCK', 'MAGNET']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| this.powerUps.push({ | |
| x: this.width / 2 + (Math.random() * 100 - 50), | |
| y: -50, | |
| type: type, | |
| active: true | |
| }); | |
| }, | |
| activatePowerUp: function(type) { | |
| AudioSys.sfx.powerup(); | |
| const duration = 10000; // 10s | |
| if (type === 'SLOW_TIME') { | |
| // Logic handled in update | |
| } | |
| this.activePowerUp = { | |
| type: type, | |
| endTime: Date.now() + duration | |
| }; | |
| }, | |
| spawnParticles: function(side) { | |
| const count = 10; | |
| const originX = this.width / 2 + (side * (this.treeWidth / 2)); | |
| const originY = this.height - 180; | |
| for (let i = 0; i < count; i++) { | |
| this.particles.push({ | |
| x: originX, | |
| y: originY, | |
| vx: (side * Math.random() * 5) + (Math.random() - 0.5), | |
| vy: -Math.random() * 10, | |
| life: 1.0, | |
| color: CONFIG.colors.treeLight | |
| }); | |
| } | |
| }, | |
| triggerShake: function(intensity) { | |
| this.shake = intensity; | |
| }, | |
| updateHUD: function() { | |
| document.getElementById('score-val').innerText = this.score; | |
| let timeDisplay = ""; | |
| if (this.mode === 'timeattack') { | |
| timeDisplay = Math.ceil(this.timeRemaining) + "s"; | |
| } else if (this.mode |