repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Robin-hood Game
stars
latest
clone command
git clone gitlawb://did:key:z6MkwMR9...adEc/robin-hood-gamegit clone gitlawb://did:key:z6MkwMR9.../robin-hood-gamec8777c8csync from playground7h ago| #1 | import { |
| #2 | Environment, |
| #3 | GAME_SIZE, |
| #4 | GROUND_Y, |
| #5 | GROUND_HEIGHT, |
| #6 | } from './constants'; |
| #7 | import type { Robin, Obstacle, Enemy, CoinBag, Arrow, Particle, BackgroundLayer } from './types'; |
| #8 | |
| #9 | // ── Color palettes per environment ── |
| #10 | const PALETTES: Record<Environment, { |
| #11 | sky: string; skyBottom: string; |
| #12 | ground: string; groundDark: string; groundLine: string; |
| #13 | bgFar: string; bgMid: string; |
| #14 | trees: string[]; accents: string[]; |
| #15 | }> = { |
| #16 | forest: { |
| #17 | sky: '#4a7c59', skyBottom: '#87CEEB', |
| #18 | ground: '#5a3a1a', groundDark: '#3d2810', groundLine: '#6b4c2a', |
| #19 | bgFar: '#2d5a27', bgMid: '#3a7a30', |
| #20 | trees: ['#2d5a27', '#1e4d2b', '#3a7a30'], |
| #21 | accents: ['#8B4513', '#654321', '#228B22'], |
| #22 | }, |
| #23 | castle: { |
| #24 | sky: '#2c2c54', skyBottom: '#474787', |
| #25 | ground: '#6b6b6b', groundDark: '#4a4a4a', groundLine: '#888888', |
| #26 | bgFar: '#3d3d6b', bgMid: '#555580', |
| #27 | trees: ['#555577', '#444466', '#666688'], |
| #28 | accents: ['#8B7355', '#A0522D', '#C0C0C0'], |
| #29 | }, |
| #30 | village: { |
| #31 | sky: '#87CEEB', skyBottom: '#b0e0e6', |
| #32 | ground: '#6b8e23', groundDark: '#556b2f', groundLine: '#7ccd7c', |
| #33 | bgFar: '#6b9b37', bgMid: '#8fbc8f', |
| #34 | trees: ['#6b9b37', '#5a8a2a', '#8fbc8f'], |
| #35 | accents: ['#CD853F', '#DEB887', '#B22222'], |
| #36 | }, |
| #37 | cave: { |
| #38 | sky: '#1a1a2e', skyBottom: '#16213e', |
| #39 | ground: '#3d3d3d', groundDark: '#2a2a2a', groundLine: '#555555', |
| #40 | bgFar: '#0f0f23', bgMid: '#1a1a33', |
| #41 | trees: ['#333344', '#2a2a3a', '#444455'], |
| #42 | accents: ['#9370DB', '#7B68EE', '#6A5ACD'], |
| #43 | }, |
| #44 | snow: { |
| #45 | sky: '#b0c4de', skyBottom: '#e0e8f0', |
| #46 | ground: '#f0f0f0', groundDark: '#d0d0d0', groundLine: '#ffffff', |
| #47 | bgFar: '#8ba4c4', bgMid: '#a0b8d0', |
| #48 | trees: ['#2d5a27', '#1e4d2b', '#3a6a30'], |
| #49 | accents: ['#FFFFFF', '#E8E8E8', '#B0C4DE'], |
| #50 | }, |
| #51 | }; |
| #52 | |
| #53 | // ── Draw helpers ── |
| #54 | function drawPixelRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, color: string) { |
| #55 | ctx.fillStyle = color; |
| #56 | ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h)); |
| #57 | } |
| #58 | |
| #59 | function drawPixelCircle(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, color: string) { |
| #60 | ctx.fillStyle = color; |
| #61 | // Simple pixel circle |
| #62 | for (let dy = -r; dy <= r; dy++) { |
| #63 | for (let dx = -r; dx <= r; dx++) { |
| #64 | if (dx * dx + dy * dy <= r * r + r * 0.5) { |
| #65 | ctx.fillRect(Math.round(cx + dx), Math.round(cy + dy), 1, 1); |
| #66 | } |
| #67 | } |
| #68 | } |
| #69 | } |
| #70 | |
| #71 | // ── Background ── |
| #72 | export function drawBackground(ctx: CanvasRenderingContext2D, env: Environment, frameCount: number, layers: BackgroundLayer[]) { |
| #73 | const pal = PALETTES[env]; |
| #74 | const grd = ctx.createLinearGradient(0, 0, 0, GROUND_Y); |
| #75 | grd.addColorStop(0, pal.sky); |
| #76 | grd.addColorStop(1, pal.skyBottom); |
| #77 | ctx.fillStyle = grd; |
| #78 | ctx.fillRect(0, 0, GAME_SIZE, GROUND_Y); |
| #79 | |
| #80 | // Far background mountains/hills (speeds x4 for 15fps, originally designed for 60fps) |
| #81 | ctx.fillStyle = pal.bgFar; |
| #82 | for (let i = 0; i < 5; i++) { |
| #83 | const baseX = ((i * 120 - (frameCount * 1.2) % 600) % 600 + 600) % 600 - 100; |
| #84 | const h = 60 + (i % 3) * 20; |
| #85 | drawHill(ctx, baseX, GROUND_Y - 10, 140, h, pal.bgFar); |
| #86 | } |
| #87 | |
| #88 | // Mid background trees/structures |
| #89 | ctx.fillStyle = pal.bgMid; |
| #90 | for (let i = 0; i < 8; i++) { |
| #91 | const baseX = ((i * 80 - (frameCount * 2.8) % 640) % 640 + 640) % 640 - 80; |
| #92 | drawTreeSilhouette(ctx, baseX, GROUND_Y - 5, 20 + (i % 3) * 8, pal.bgMid, env); |
| #93 | } |
| #94 | |
| #95 | // Ground |
| #96 | drawPixelRect(ctx, 0, GROUND_Y, GAME_SIZE, GROUND_HEIGHT, pal.ground); |
| #97 | drawPixelRect(ctx, 0, GROUND_Y, GAME_SIZE, 3, pal.groundLine); |
| #98 | |
| #99 | // Ground texture dots |
| #100 | ctx.fillStyle = pal.groundDark; |
| #101 | for (let i = 0; i < 30; i++) { |
| #102 | const gx = ((i * 47 - (frameCount * layers[0]?.speed * 4) % GAME_SIZE) % GAME_SIZE + GAME_SIZE) % GAME_SIZE; |
| #103 | const gy = GROUND_Y + 8 + (i * 13) % 40; |
| #104 | ctx.fillRect(Math.round(gx), Math.round(gy), 2, 2); |
| #105 | } |
| #106 | |
| #107 | // Environment-specific details |
| #108 | if (env === 'snow') { |
| #109 | // Snowflakes |
| #110 | ctx.fillStyle = '#ffffff'; |
| #111 | for (let i = 0; i < 20; i++) { |
| #112 | const sx = ((i * 73 + frameCount * 2) % GAME_SIZE); |
| #113 | const sy = ((i * 97 + frameCount * 1.2 + i * 17) % GROUND_Y); |
| #114 | ctx.fillRect(Math.round(sx), Math.round(sy), 2, 2); |
| #115 | } |
| #116 | } |
| #117 | |
| #118 | if (env === 'cave') { |
| #119 | // Stalactites |
| #120 | ctx.fillStyle = '#333344'; |
| #121 | for (let i = 0; i < 6; i++) { |
| #122 | const sx = ((i * 80 - (frameCount * 0.8) % 480) % 480 + 480) % 480 - 40; |
| #123 | drawStalactite(ctx, sx, 0, 15 + (i % 3) * 5); |
| #124 | } |
| #125 | } |
| #126 | } |
| #127 | |
| #128 | function drawHill(ctx: CanvasRenderingContext2D, x: number, baseY: number, width: number, height: number, color: string) { |
| #129 | ctx.fillStyle = color; |
| #130 | for (let i = 0; i < width; i++) { |
| #131 | const h = height * Math.sin((i / width) * Math.PI); |
| #132 | ctx.fillRect(Math.round(x + i), Math.round(baseY - h), 1, Math.round(h)); |
| #133 | } |
| #134 | } |
| #135 | |
| #136 | function drawTreeSilhouette(ctx: CanvasRenderingContext2D, x: number, baseY: number, size: number, color: string, env: Environment) { |
| #137 | if (env === 'castle') { |
| #138 | // Castle tower silhouette |
| #139 | ctx.fillStyle = color; |
| #140 | ctx.fillRect(Math.round(x), Math.round(baseY - size * 2), size * 0.6, size * 2); |
| #141 | // Battlements |
| #142 | for (let i = 0; i < 3; i++) { |
| #143 | ctx.fillRect(Math.round(x + i * size * 0.2), Math.round(baseY - size * 2.3), size * 0.12, size * 0.3); |
| #144 | } |
| #145 | } else if (env === 'village') { |
| #146 | // House silhouette |
| #147 | ctx.fillStyle = color; |
| #148 | ctx.fillRect(Math.round(x), Math.round(baseY - size), size * 1.2, size); |
| #149 | // Roof |
| #150 | for (let i = 0; i < size * 0.6; i++) { |
| #151 | const h = (size * 0.5) * (1 - i / (size * 0.6)); |
| #152 | ctx.fillRect(Math.round(x - 2 + i), Math.round(baseY - size - h), 1, Math.round(h) + 1); |
| #153 | } |
| #154 | } else { |
| #155 | // Tree silhouette |
| #156 | ctx.fillStyle = color; |
| #157 | ctx.fillRect(Math.round(x + size * 0.35), Math.round(baseY - size * 0.6), size * 0.3, size * 0.6); |
| #158 | // Foliage |
| #159 | for (let layer = 0; layer < 3; layer++) { |
| #160 | const ly = baseY - size * 0.6 - layer * size * 0.25; |
| #161 | const lw = size * (1 - layer * 0.25); |
| #162 | drawPixelRect(ctx, x + (size - lw) / 2, ly - size * 0.3, lw, size * 0.3, color); |
| #163 | } |
| #164 | } |
| #165 | } |
| #166 | |
| #167 | function drawStalactite(ctx: CanvasRenderingContext2D, x: number, y: number, h: number) { |
| #168 | for (let i = 0; i < h; i++) { |
| #169 | const w = Math.max(1, (h - i) / 3); |
| #170 | ctx.fillRect(Math.round(x + (h / 3 - w / 2)), Math.round(y + i), Math.round(w), 1); |
| #171 | } |
| #172 | } |
| #173 | |
| #174 | // ── Robin Hood ── |
| #175 | export function drawRobin(ctx: CanvasRenderingContext2D, robin: Robin) { |
| #176 | const { x, y, width, height, onGround, animFrame, invulnerable, shooting } = robin; |
| #177 | |
| #178 | // Blink when invulnerable |
| #179 | if (invulnerable > 0 && Math.floor(invulnerable / 4) % 2 === 0) return; |
| #180 | |
| #181 | const bob = onGround ? Math.sin(animFrame * 0.3) * 1.5 : 0; |
| #182 | |
| #183 | // Shadow |
| #184 | ctx.fillStyle = 'rgba(0,0,0,0.3)'; |
| #185 | ctx.fillRect(Math.round(x + 2), Math.round(GROUND_Y - 2), Math.round(width - 4), 4); |
| #186 | |
| #187 | // Legs |
| #188 | const legOffset = onGround ? Math.sin(animFrame * 0.4) * 3 : -2; |
| #189 | drawPixelRect(ctx, x + 4, y + height - 12 + bob, 6, 12, '#4a3520'); |
| #190 | drawPixelRect(ctx, x + width - 10, y + height - 12 + bob + legOffset, 6, 12, '#4a3520'); |
| #191 | |
| #192 | // Boots |
| #193 | drawPixelRect(ctx, x + 3, y + height - 3 + bob, 8, 3, '#2a1a0a'); |
| #194 | drawPixelRect(ctx, x + width - 11, y + height - 3 + bob + legOffset, 8, 3, '#2a1a0a'); |
| #195 | |
| #196 | // Body/tunic (green) |
| #197 | drawPixelRect(ctx, x + 2, y + 10 + bob, width - 4, height - 22, '#2d8a2d'); |
| #198 | drawPixelRect(ctx, x + 3, y + 12 + bob, width - 6, height - 26, '#35a035'); |
| #199 | |
| #200 | // Belt |
| #201 | drawPixelRect(ctx, x + 2, y + height - 14 + bob, width - 4, 3, '#8B4513'); |
| #202 | drawPixelRect(ctx, x + width / 2 - 2, y + height - 15 + bob, 4, 5, '#DAA520'); |
| #203 | |
| #204 | // Cape/hood |
| #205 | drawPixelRect(ctx, x - 1, y + 8 + bob, 4, 20, '#1a5c1a'); |
| #206 | |
| #207 | // Arms |
| #208 | if (shooting) { |
| #209 | // Shooting pose - extended arm |
| #210 | drawPixelRect(ctx, x + width, y + 14 + bob, 10, 4, '#DEB887'); |
| #211 | // Other arm back |
| #212 | drawPixelRect(ctx, x - 4, y + 16 + bob, 6, 4, '#DEB887'); |
| #213 | } else { |
| #214 | // Running arms |
| #215 | const armSwing = onGround ? Math.sin(animFrame * 0.4) * 4 : 0; |
| #216 | drawPixelRect(ctx, x + width - 2, y + 14 + bob + armSwing, 4, 8, '#DEB887'); |
| #217 | drawPixelRect(ctx, x - 2, y + 14 + bob - armSwing, 4, 8, '#DEB887'); |
| #218 | } |
| #219 | |
| #220 | // Head |
| #221 | drawPixelRect(ctx, x + 5, y + bob, width - 10, 12, '#DEB887'); |
| #222 | |
| #223 | // Eyes |
| #224 | drawPixelRect(ctx, x + width - 8, y + 4 + bob, 3, 3, '#000'); |
| #225 | drawPixelRect(ctx, x + width - 7, y + 4 + bob, 1, 1, '#fff'); |
| #226 | |
| #227 | // Robin Hood hat (feathered cap) |
| #228 | drawPixelRect(ctx, x + 3, y - 4 + bob, width - 6, 6, '#2d8a2d'); |
| #229 | drawPixelRect(ctx, x + 1, y - 2 + bob, width - 2, 4, '#35a035'); |
| #230 | // Feather |
| #231 | drawPixelRect(ctx, x + width - 2, y - 8 + bob, 2, 8, '#ff4444'); |
| #232 | drawPixelRect(ctx, x + width - 1, y - 10 + bob, 2, 3, '#ff6666'); |
| #233 | } |
| #234 | |
| #235 | // ── Obstacles ── |
| #236 | export function drawObstacle(ctx: CanvasRenderingContext2D, obs: Obstacle) { |
| #237 | const { x, y, width, height, type } = obs; |
| #238 | |
| #239 | switch (type) { |
| #240 | case 'barrel': |
| #241 | drawPixelRect(ctx, x, y, width, height, '#8B6914'); |
| #242 | drawPixelRect(ctx, x + 1, y + 1, width - 2, height - 2, '#A07828'); |
| #243 | // Bands |
| #244 | drawPixelRect(ctx, x - 1, y + 4, width + 2, 2, '#555'); |
| #245 | drawPixelRect(ctx, x - 1, y + height - 6, width + 2, 2, '#555'); |
| #246 | // Top |
| #247 | drawPixelRect(ctx, x + 2, y, width - 4, 2, '#6B4C12'); |
| #248 | break; |
| #249 | |
| #250 | case 'branch': |
| #251 | // Log on ground |
| #252 | drawPixelRect(ctx, x, y + height - 8, width, 8, '#654321'); |
| #253 | drawPixelRect(ctx, x + 1, y + height - 7, width - 2, 6, '#8B6914'); |
| #254 | // Bark texture |
| #255 | drawPixelRect(ctx, x + 4, y + height - 6, 3, 2, '#553311'); |
| #256 | drawPixelRect(ctx, x + width - 8, y + height - 5, 4, 2, '#553311'); |
| #257 | // Branch stubs |
| #258 | drawPixelRect(ctx, x + 6, y + height - 12, 3, 5, '#654321'); |
| #259 | drawPixelRect(ctx, x + width - 10, y + height - 14, 3, 7, '#654321'); |
| #260 | break; |
| #261 | |
| #262 | case 'rock': |
| #263 | drawPixelRect(ctx, x, y + 2, width, height - 2, '#777'); |
| #264 | drawPixelRect(ctx, x + 2, y, width - 4, 4, '#888'); |
| #265 | drawPixelRect(ctx, x + 1, y + 1, width - 2, height - 3, '#999'); |
| #266 | // Highlights |
| #267 | drawPixelRect(ctx, x + 3, y + 3, 4, 2, '#aaa'); |
| #268 | break; |
| #269 | |
| #270 | case 'fence': |
| #271 | // Posts |
| #272 | drawPixelRect(ctx, x, y, 4, height, '#8B6914'); |
| #273 | drawPixelRect(ctx, x + width - 4, y, 4, height, '#8B6914'); |
| #274 | // Rails |
| #275 | drawPixelRect(ctx, x, y + 6, width, 3, '#A07828'); |
| #276 | drawPixelRect(ctx, x, y + height - 10, width, 3, '#A07828'); |
| #277 | // Pointed tops |
| #278 | drawPixelRect(ctx, x, y - 3, 4, 3, '#6B4C12'); |
| #279 | drawPixelRect(ctx, x + width - 4, y - 3, 4, 3, '#6B4C12'); |
| #280 | break; |
| #281 | |
| #282 | case 'crate': |
| #283 | drawPixelRect(ctx, x, y, width, height, '#A07828'); |
| #284 | drawPixelRect(ctx, x + 1, y + 1, width - 2, height - 2, '#C49538'); |
| #285 | // Cross planks |
| #286 | drawPixelRect(ctx, x, y + height / 2 - 1, width, 2, '#8B6914'); |
| #287 | drawPixelRect(ctx, x + width / 2 - 1, y, 2, height, '#8B6914'); |
| #288 | // Nails |
| #289 | drawPixelRect(ctx, x + 3, y + 3, 2, 2, '#555'); |
| #290 | drawPixelRect(ctx, x + width - 5, y + 3, 2, 2, '#555'); |
| #291 | break; |
| #292 | } |
| #293 | } |
| #294 | |
| #295 | // ── Enemies ── |
| #296 | export function drawEnemy(ctx: CanvasRenderingContext2D, enemy: Enemy) { |
| #297 | const { x, y, width, height, type, animFrame, health } = enemy; |
| #298 | const bob = Math.sin(animFrame * 0.2) * 1; |
| #299 | |
| #300 | switch (type) { |
| #301 | case 'guard': |
| #302 | // Body armor |
| #303 | drawPixelRect(ctx, x + 2, y + 10 + bob, width - 4, height - 16, '#888'); |
| #304 | drawPixelRect(ctx, x + 3, y + 12 + bob, width - 6, height - 20, '#aaa'); |
| #305 | // Legs |
| #306 | drawPixelRect(ctx, x + 4, y + height - 8 + bob, 5, 8, '#555'); |
| #307 | drawPixelRect(ctx, x + width - 9, y + height - 8 + bob, 5, 8, '#555'); |
| #308 | // Head with helmet |
| #309 | drawPixelRect(ctx, x + 5, y + bob, width - 10, 12, '#DEB887'); |
| #310 | drawPixelRect(ctx, x + 3, y - 3 + bob, width - 6, 6, '#666'); |
| #311 | // Visor |
| #312 | drawPixelRect(ctx, x + width - 9, y + 4 + bob, 5, 3, '#333'); |
| #313 | // Shield |
| #314 | drawPixelRect(ctx, x - 3, y + 10 + bob, 6, 14, '#8B0000'); |
| #315 | drawPixelRect(ctx, x - 2, y + 12 + bob, 4, 10, '#CD5C5C'); |
| #316 | // Sword |
| #317 | drawPixelRect(ctx, x + width, y + 8 + bob, 2, 16, '#C0C0C0'); |
| #318 | drawPixelRect(ctx, x + width - 1, y + 8 + bob, 4, 3, '#8B6914'); |
| #319 | break; |
| #320 | |
| #321 | case 'knight': |
| #322 | // Full plate armor |
| #323 | drawPixelRect(ctx, x + 2, y + 10 + bob, width - 4, height - 16, '#C0C0C0'); |
| #324 | drawPixelRect(ctx, x + 3, y + 12 + bob, width - 6, height - 20, '#D8D8D8'); |
| #325 | // Legs |
| #326 | drawPixelRect(ctx, x + 4, y + height - 8 + bob, 5, 8, '#A0A0A0'); |
| #327 | drawPixelRect(ctx, x + width - 9, y + height - 8 + bob, 5, 8, '#A0A0A0'); |
| #328 | // Helmet |
| #329 | drawPixelRect(ctx, x + 4, y - 2 + bob, width - 8, 14, '#A0A0A0'); |
| #330 | drawPixelRect(ctx, x + width - 8, y + 4 + bob, 4, 4, '#222'); |
| #331 | // Plume |
| #332 | drawPixelRect(ctx, x + width / 2 - 1, y - 6 + bob, 3, 5, '#8B0000'); |
| #333 | // Sword |
| #334 | drawPixelRect(ctx, x + width, y + 6 + bob, 2, 18, '#E8E8E8'); |
| #335 | drawPixelRect(ctx, x + width - 1, y + 6 + bob, 4, 3, '#DAA520'); |
| #336 | break; |
| #337 | |
| #338 | case 'bandit': |
| #339 | // Leather vest |
| #340 | drawPixelRect(ctx, x + 2, y + 10 + bob, width - 4, height - 16, '#4a3520'); |
| #341 | drawPixelRect(ctx, x + 3, y + 12 + bob, width - 6, height - 20, '#5a4530'); |
| #342 | // Pants |
| #343 | drawPixelRect(ctx, x + 4, y + height - 8 + bob, 5, 8, '#3a2a15'); |
| #344 | drawPixelRect(ctx, x + width - 9, y + height - 8 + bob, 5, 8, '#3a2a15'); |
| #345 | // Head with mask |
| #346 | drawPixelRect(ctx, x + 5, y + bob, width - 10, 12, '#DEB887'); |
| #347 | drawPixelRect(ctx, x + 4, y + 2 + bob, width - 8, 4, '#222'); |
| #348 | // Bandana |
| #349 | drawPixelRect(ctx, x + 3, y - 1 + bob, width - 6, 4, '#8B0000'); |
| #350 | // Club |
| #351 | drawPixelRect(ctx, x + width, y + 10 + bob, 4, 12, '#654321'); |
| #352 | drawPixelRect(ctx, x + width, y + 10 + bob, 6, 4, '#8B6914'); |
| #353 | break; |
| #354 | |
| #355 | case 'wolf': |
| #356 | // Body |
| #357 | drawPixelRect(ctx, x, y + height - 14, width, 14, '#666'); |
| #358 | drawPixelRect(ctx, x + 1, y + height - 13, width - 2, 12, '#888'); |
| #359 | // Head |
| #360 | drawPixelRect(ctx, x + width - 10, y + height - 22, 12, 10, '#777'); |
| #361 | drawPixelRect(ctx, x + width - 8, y + height - 20, 8, 6, '#888'); |
| #362 | // Snout |
| #363 | drawPixelRect(ctx, x + width - 2, y + height - 18, 5, 4, '#999'); |
| #364 | // Eye |
| #365 | drawPixelRect(ctx, x + width - 6, y + height - 20, 2, 2, '#ff0'); |
| #366 | // Ears |
| #367 | drawPixelRect(ctx, x + width - 10, y + height - 26, 3, 5, '#666'); |
| #368 | drawPixelRect(ctx, x + width - 4, y + height - 26, 3, 5, '#666'); |
| #369 | // Legs |
| #370 | drawPixelRect(ctx, x + 2, y + height - 6, 3, 6, '#555'); |
| #371 | drawPixelRect(ctx, x + 8, y + height - 6, 3, 6, '#555'); |
| #372 | drawPixelRect(ctx, x + width - 10, y + height - 6, 3, 6, '#555'); |
| #373 | drawPixelRect(ctx, x + width - 4, y + height - 6, 3, 6, '#555'); |
| #374 | // Tail |
| #375 | drawPixelRect(ctx, x - 4, y + height - 16, 5, 3, '#777'); |
| #376 | break; |
| #377 | |
| #378 | case 'archer': |
| #379 | // Body |
| #380 | drawPixelRect(ctx, x + 2, y + 10 + bob, width - 4, height - 16, '#556B2F'); |
| #381 | drawPixelRect(ctx, x + 3, y + 12 + bob, width - 6, height - 20, '#6B8E23'); |
| #382 | // Legs |
| #383 | drawPixelRect(ctx, x + 4, y + height - 8 + bob, 5, 8, '#4a3520'); |
| #384 | drawPixelRect(ctx, x + width - 9, y + height - 8 + bob, 5, 8, '#4a3520'); |
| #385 | // Head |
| #386 | drawPixelRect(ctx, x + 5, y + bob, width - 10, 12, '#DEB887'); |
| #387 | // Hood |
| #388 | drawPixelRect(ctx, x + 3, y - 2 + bob, width - 6, 6, '#2F4F2F'); |
| #389 | // Bow |
| #390 | drawPixelRect(ctx, x + width, y + 4 + bob, 2, 18, '#8B6914'); |
| #391 | drawPixelRect(ctx, x + width + 1, y + 4 + bob, 1, 18, '#654321'); |
| #392 | break; |
| #393 | } |
| #394 | |
| #395 | // Health indicator |
| #396 | if (health > 1) { |
| #397 | drawPixelRect(ctx, x, y - 6, width, 3, '#333'); |
| #398 | drawPixelRect(ctx, x, y - 6, width * (health / 2), 3, '#ff3333'); |
| #399 | } |
| #400 | } |
| #401 | |
| #402 | // ── Coin Bags ── |
| #403 | export function drawCoinBag(ctx: CanvasRenderingContext2D, bag: CoinBag) { |
| #404 | const { x, y, width, height, animFrame, sparkle } = bag; |
| #405 | const bob = Math.sin(animFrame * 0.15) * 2; |
| #406 | |
| #407 | // Bag |
| #408 | drawPixelRect(ctx, x + 2, y + 6 + bob, width - 4, height - 6, '#8B6914'); |
| #409 | drawPixelRect(ctx, x + 3, y + 8 + bob, width - 6, height - 10, '#A07828'); |
| #410 | |
| #411 | // Tie at top |
| #412 | drawPixelRect(ctx, x + width / 2 - 2, y + 4 + bob, 4, 4, '#C49538'); |
| #413 | drawPixelRect(ctx, x + width / 2 - 3, y + 3 + bob, 6, 2, '#8B6914'); |
| #414 | |
| #415 | // Coin symbols |
| #416 | drawPixelRect(ctx, x + width / 2 - 3, y + 14 + bob, 6, 6, '#DAA520'); |
| #417 | drawPixelRect(ctx, x + width / 2 - 2, y + 15 + bob, 4, 4, '#FFD700'); |
| #418 | |
| #419 | // Sparkle effect |
| #420 | if (sparkle > 0) { |
| #421 | ctx.fillStyle = '#FFD700'; |
| #422 | const sparkleSize = sparkle * 0.5; |
| #423 | ctx.fillRect(Math.round(x + width / 2 - sparkleSize), Math.round(y + bob - 2), Math.round(sparkleSize * 2), 1); |
| #424 | ctx.fillRect(Math.round(x + width / 2), Math.round(y + bob - 2 - sparkleSize), 1, Math.round(sparkleSize * 2)); |
| #425 | } |
| #426 | } |
| #427 | |
| #428 | // ── Arrows ── |
| #429 | export function drawArrow(ctx: CanvasRenderingContext2D, arrow: Arrow) { |
| #430 | const { x, y } = arrow; |
| #431 | // Shaft |
| #432 | drawPixelRect(ctx, x, y, 14, 2, '#8B6914'); |
| #433 | // Arrowhead |
| #434 | drawPixelRect(ctx, x + 14, y - 1, 4, 4, '#C0C0C0'); |
| #435 | drawPixelRect(ctx, x + 16, y, 2, 2, '#888'); |
| #436 | // Fletching |
| #437 | drawPixelRect(ctx, x - 2, y - 1, 3, 1, '#ff3333'); |
| #438 | drawPixelRect(ctx, x - 2, y + 2, 3, 1, '#ff3333'); |
| #439 | } |
| #440 | |
| #441 | // ── Particles ── |
| #442 | export function drawParticle(ctx: CanvasRenderingContext2D, p: Particle) { |
| #443 | const alpha = p.life / p.maxLife; |
| #444 | ctx.globalAlpha = alpha; |
| #445 | ctx.fillStyle = p.color; |
| #446 | ctx.fillRect(Math.round(p.x), Math.round(p.y), Math.round(p.size), Math.round(p.size)); |
| #447 | ctx.globalAlpha = 1; |
| #448 | } |
| #449 | |
| #450 | // ── HUD ── |
| #451 | export function drawHUD(ctx: CanvasRenderingContext2D, lives: number, coins: number, score: number, highScore: number) { |
| #452 | // Hearts |
| #453 | for (let i = 0; i < lives; i++) { |
| #454 | const hx = GAME_SIZE - 22 - i * 24; |
| #455 | drawHeart(ctx, hx, 10, 18, '#ff3333'); |
| #456 | } |
| #457 | |
| #458 | // Coin count |
| #459 | drawPixelRect(ctx, 10, 8, 14, 14, '#DAA520'); |
| #460 | drawPixelRect(ctx, 11, 9, 12, 12, '#FFD700'); |
| #461 | drawPixelRect(ctx, 14, 12, 6, 6, '#DAA520'); |
| #462 | ctx.fillStyle = '#FFD700'; |
| #463 | ctx.font = 'bold 14px monospace'; |
| #464 | ctx.textAlign = 'left'; |
| #465 | ctx.fillText(`${coins}`, 28, 20); |
| #466 | |
| #467 | // Score |
| #468 | ctx.fillStyle = '#fff'; |
| #469 | ctx.font = 'bold 12px monospace'; |
| #470 | ctx.textAlign = 'left'; |
| #471 | ctx.fillText(`Score: ${score}`, 10, 38); |
| #472 | |
| #473 | // High score |
| #474 | if (highScore > 0) { |
| #475 | ctx.fillStyle = '#aaa'; |
| #476 | ctx.font = '10px monospace'; |
| #477 | ctx.fillText(`Best: ${highScore}`, 10, 52); |
| #478 | } |
| #479 | } |
| #480 | |
| #481 | function drawHeart(ctx: CanvasRenderingContext2D, x: number, y: number, size: number, color: string) { |
| #482 | ctx.fillStyle = color; |
| #483 | const s = size / 16; |
| #484 | // Pixel art heart pattern |
| #485 | const pattern = [ |
| #486 | [0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0], |
| #487 | [0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0], |
| #488 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| #489 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| #490 | [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], |
| #491 | [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], |
| #492 | [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], |
| #493 | [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], |
| #494 | [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0], |
| #495 | [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], |
| #496 | ]; |
| #497 | for (let row = 0; row < pattern.length; row++) { |
| #498 | for (let col = 0; col < pattern[row].length; col++) { |
| #499 | if (pattern[row][col]) { |
| #500 | ctx.fillRect(Math.round(x + col * s), Math.round(y + row * s), Math.ceil(s), Math.ceil(s)); |
| #501 | } |
| #502 | } |
| #503 | } |
| #504 | // Highlight |
| #505 | ctx.fillStyle = '#ff6666'; |
| #506 | ctx.fillRect(Math.round(x + 2 * s), Math.round(y + 1 * s), Math.ceil(s), Math.ceil(s)); |
| #507 | } |
| #508 | |
| #509 | // ── Game Over overlay ── |
| #510 | export function drawGameOver(ctx: CanvasRenderingContext2D, score: number, coins: number, highScore: number) { |
| #511 | ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; |
| #512 | ctx.fillRect(0, 0, GAME_SIZE, GAME_SIZE); |
| #513 | |
| #514 | ctx.fillStyle = '#ff3333'; |
| #515 | ctx.font = 'bold 36px monospace'; |
| #516 | ctx.textAlign = 'center'; |
| #517 | ctx.fillText('GAME OVER', GAME_SIZE / 2, 140); |
| #518 | |
| #519 | ctx.fillStyle = '#FFD700'; |
| #520 | ctx.font = 'bold 18px monospace'; |
| #521 | ctx.fillText(`Score: ${score}`, GAME_SIZE / 2, 190); |
| #522 | |
| #523 | ctx.fillStyle = '#DAA520'; |
| #524 | ctx.font = 'bold 16px monospace'; |
| #525 | ctx.fillText(`Coins: ${coins}`, GAME_SIZE / 2, 220); |
| #526 | |
| #527 | if (score >= highScore && highScore > 0) { |
| #528 | ctx.fillStyle = '#ff6'; |
| #529 | ctx.font = 'bold 14px monospace'; |
| #530 | ctx.fillText('NEW HIGH SCORE!', GAME_SIZE / 2, 250); |
| #531 | } else if (highScore > 0) { |
| #532 | ctx.fillStyle = '#aaa'; |
| #533 | ctx.font = '14px monospace'; |
| #534 | ctx.fillText(`Best: ${highScore}`, GAME_SIZE / 2, 250); |
| #535 | } |
| #536 | |
| #537 | ctx.fillStyle = '#fff'; |
| #538 | ctx.font = 'bold 16px monospace'; |
| #539 | ctx.fillText('...', GAME_SIZE / 2, 300); |
| #540 | } |
| #541 | |
| #542 | // ── Start screen ── |
| #543 | export function drawStartScreen(ctx: CanvasRenderingContext2D, frameCount: number) { |
| #544 | // Dark bg with gradient |
| #545 | ctx.fillStyle = '#1a2a1a'; |
| #546 | ctx.fillRect(0, 0, GAME_SIZE, GAME_SIZE); |
| #547 | |
| #548 | // Title |
| #549 | ctx.fillStyle = '#35a035'; |
| #550 | ctx.font = 'bold 28px monospace'; |
| #551 | ctx.textAlign = 'center'; |
| #552 | ctx.fillText('ROBIN HOOD', GAME_SIZE / 2, 120); |
| #553 | |
| #554 | ctx.fillStyle = '#8B6914'; |
| #555 | ctx.font = 'bold 22px monospace'; |
| #556 | ctx.fillText('Endless Runner', GAME_SIZE / 2, 155); |
| #557 | |
| #558 | // Animated Robin |
| #559 | const bob = Math.sin(frameCount * 0.05) * 3; |
| #560 | drawPixelRect(ctx, GAME_SIZE / 2 - 14, 190 + bob, 28, 40, '#2d8a2d'); |
| #561 | drawPixelRect(ctx, GAME_SIZE / 2 - 12, 192 + bob, 24, 36, '#35a035'); |
| #562 | drawPixelRect(ctx, GAME_SIZE / 2 - 14, 230 + bob, 28, 4, '#4a3520'); |
| #563 | // Head |
| #564 | drawPixelRect(ctx, GAME_SIZE / 2 - 9, 180 + bob, 18, 12, '#DEB887'); |
| #565 | // Hat |
| #566 | drawPixelRect(ctx, GAME_SIZE / 2 - 11, 174 + bob, 22, 8, '#2d8a2d'); |
| #567 | // Feather |
| #568 | drawPixelRect(ctx, GAME_SIZE / 2 + 8, 168 + bob, 3, 10, '#ff4444'); |
| #569 | |
| #570 | // Arrow |
| #571 | const arrowBob = Math.sin(frameCount * 0.08) * 2; |
| #572 | drawPixelRect(ctx, GAME_SIZE / 2 + 14, 200 + bob + arrowBob, 20, 2, '#8B6914'); |
| #573 | drawPixelRect(ctx, GAME_SIZE / 2 + 34, 199 + bob + arrowBob, 4, 4, '#C0C0C0'); |
| #574 | |
| #575 | // Instructions |
| #576 | ctx.fillStyle = '#fff'; |
| #577 | ctx.font = 'bold 14px monospace'; |
| #578 | ctx.fillText('TAP to Start', GAME_SIZE / 2, 280); |
| #579 | |
| #580 | ctx.fillStyle = '#aaa'; |
| #581 | ctx.font = '12px monospace'; |
| #582 | ctx.fillText('Tap screen = Jump', GAME_SIZE / 2, 310); |
| #583 | ctx.fillText('Bow button = Shoot', GAME_SIZE / 2, 330); |
| #584 | ctx.fillText('Avoid obstacles, shoot enemies!', GAME_SIZE / 2, 350); |
| #585 | } |
| #586 |