TECH DETAILS

Zelda Adventure - Day 25

Advanced

Table of Contents

01 / Game Architecture

The game follows a classic action-adventure architecture with separate systems for rendering, physics, and game logic. The core loop runs at 60 FPS using requestAnimationFrame.

┌─────────────────────────────────────────────────────────┐ │ GAME LOOP │ ├─────────────────────────────────────────────────────────┤ │ ┌─────────┐ ┌──────────┐ ┌────────────┐ │ │ │ INPUT │ → │ UPDATE │ → │ RENDER │ │ │ └─────────┘ └──────────┘ └────────────┘ │ │ ↓ ↓ ↓ │ │ Keyboard Player Move Draw Map │ │ Touch Enemies AI Draw Entities │ │ D-Pad Collisions Draw Effects │ │ Combat Draw HUD │ └─────────────────────────────────────────────────────────┘
function gameLoop(timestamp) { const deltaTime = timestamp - lastTime; lastTime = timestamp; if (gameState === 'playing') { update(deltaTime); } render(); animationFrame++; requestAnimationFrame(gameLoop); }

State Management

Four game states: start, playing, gameover, victory

Entity System

Player, enemies, items, projectiles as separate arrays

Room System

Multiple rooms with their own maps and entities

Particle System

Visual feedback for hits, pickups, and effects

02 / Tile-Based World System

The world is built using a 2D array where each number represents a tile type. This approach is memory-efficient and allows for easy level design.

// Tile type definitions const TILES = { GRASS: 0, WALL: 1, WATER: 2, TREE: 3, DOOR: 4, FLOOR: 5, DUNGEON_WALL: 6, CHEST: 7, STAIRS: 8, TRIFORCE: 9 }; // Room definition with map and entities const rooms = { overworld: { map: [ [3,3,3,3,3,1,1,1,...], // Trees, walls [3,0,0,0,3,1,0,0,...], // Grass walkable ... ], enemies: [...], items: [...], doors: [...] } };
Rendering Optimization

Each tile is 16x16 pixels. The map renders top-to-bottom, left-to-right, allowing proper layering. Tile details like grass tufts use modulo operations for visual variety without extra data.

// Adding visual variety to grass tiles if ((x + y) % 3 === 0) { ctx.fillStyle = COLORS.grassDark; ctx.fillRect(px + 4, py + 4, 2, 2); ctx.fillRect(px + 10, py + 10, 2, 2); }

03 / Collision Detection

The game uses corner-based tile collision. Before moving, we check if all four corners of the entity's hitbox would be in walkable tiles.

┌────────────────────────────────────┐ │ COLLISION CHECK │ ├────────────────────────────────────┤ │ │ │ ●─────────────● ← Top corners │ │ │ ENTITY │ │ │ │ HITBOX │ │ │ ●─────────────● ← Bot corners │ │ │ │ Check each corner against tiles │ │ If ANY corner hits solid → BLOCK │ └────────────────────────────────────┘
function canMove(x, y, width, height) { const corners = [ { x: x + 1, y: y + 1 }, // Top-left { x: x + width - 1, y: y + 1 }, // Top-right { x: x + 1, y: y + height - 1 }, // Bottom-left { x: x + width - 1, y: y + height - 1 } ]; for (const corner of corners) { const tileX = Math.floor(corner.x / TILE_SIZE); const tileY = Math.floor(corner.y / TILE_SIZE); const tile = currentMap[tileY][tileX]; if (tile === TILES.WALL || tile === TILES.TREE || tile === TILES.WATER) { return false; } } return true; }
Edge Case: Doors

Doors are special tiles - they block movement unless the player has a key. When touched with a key, the door tile is replaced with a floor tile dynamically.

04 / Enemy AI Patterns

Each enemy type has unique behavior patterns, creating varied gameplay. The AI is state-based with timers for actions.

Octorok

Random patrol + projectile shooting every 90-150 frames

Moblin

Chases player, moves toward player position

Stalfos

Patrol, changes direction on wall collision

Darknut

Aggressive chase, higher HP, deals double damage

// Moblin AI - Chase player case 'moblin': const dx = player.x - enemy.x; const dy = player.y - enemy.y; // Choose dominant axis for movement if (Math.abs(dx) > Math.abs(dy)) { enemy.direction = dx > 0 ? 'right' : 'left'; } else { enemy.direction = dy > 0 ? 'down' : 'up'; } // Move every 3 frames (slower than player) if (enemy.moveTimer % 3 === 0) { moveEnemy(enemy, 1); } break;
// Octorok projectile shooting function shootProjectile(enemy) { const angle = Math.atan2( player.y - enemy.y, player.x - enemy.x ); projectiles.push({ x: enemy.x + enemy.width / 2, y: enemy.y + enemy.height / 2, vx: Math.cos(angle) * 3, vy: Math.sin(angle) * 3, type: 'rock' }); }

05 / Combat System

Combat uses a sword attack with directional hitbox. The system includes knockback, invincibility frames, and visual feedback through particles.

┌───────────────────────────────────────┐ │ SWORD ATTACK │ ├───────────────────────────────────────┤ │ │ │ ▲ UP │ │ │ │ │ │ 20px range │ │ ◄──────── [P] ────────► │ │ LEFT │ RIGHT │ │ │ │ │ ▼ DOWN │ │ │ │ Attack duration: 15 frames │ │ Stun on hit: 30 frames │ │ Player invincibility: 60 frames │ └───────────────────────────────────────┘
function checkSwordHit() { const swordRange = 20; let swordX = player.x + player.width / 2; let swordY = player.y + player.height / 2; // Extend sword position based on direction switch (player.direction) { case 'up': swordY -= swordRange; break; case 'down': swordY += swordRange; break; case 'left': swordX -= swordRange; break; case 'right': swordX += swordRange; break; } enemies.forEach(enemy => { const dist = Math.hypot( swordX - (enemy.x + enemy.width / 2), swordY - (enemy.y + enemy.height / 2) ); if (dist < 18) { enemy.health--; enemy.stunned = true; enemy.stunnedTimer = 30; // Apply knockback const angle = Math.atan2( enemy.y - player.y, enemy.x - player.x ); enemy.x += Math.cos(angle) * 10; enemy.y += Math.sin(angle) * 10; } }); }
Visual Feedback

Every hit spawns particles at the impact point. Enemies flash white when stunned using frame-based visibility toggle. The player flashes when invincible.

06 / Room Transitions

The game supports multiple rooms (overworld and dungeon). Each room has its own map, enemies, items, and door connections.

function loadRoom(roomName) { const room = rooms[roomName]; currentMap = room.map; // Create fresh enemy instances enemies = room.enemies.map(e => ({ x: e.x * TILE_SIZE, y: e.y * TILE_SIZE, type: e.type, health: e.type === 'darknut' ? 4 : 2, direction: 'down', moveTimer: 0, shootTimer: Math.random() * 120, stunned: false })); // Copy items to avoid mutation items = room.items.map(i => ({ x: i.x * TILE_SIZE + 4, y: i.y * TILE_SIZE + 4, type: i.type, collected: false })); // Reset projectiles projectiles = []; }
// Door/stairs transition logic const tile = currentMap[playerTileY][playerTileX]; if (tile === TILES.STAIRS) { const door = room.doors.find(d => d.x === playerTileX && d.y === playerTileY ); if (door) { currentRoom = door.targetRoom; loadRoom(currentRoom); player.x = door.targetX * TILE_SIZE; player.y = door.targetY * TILE_SIZE; } }
Chest State Persistence

Chests remember their opened state in the room definition. When re-entering a room, opened chests stay open while enemies respawn - classic Zelda behavior!