your personal website on atproto - mirror blento.app

Merge pull request #6 from unbedenklich/feature/dino-game-card

added dino game card

authored by Florian and committed by GitHub 6a64ae29 d6e65ce6

+629 -1
+543
src/lib/cards/DinoGameCard/DinoGameCard.svelte
···
··· 1 + <script lang="ts"> 2 + import type { ContentComponentProps } from '../types'; 3 + import { onMount, onDestroy } from 'svelte'; 4 + 5 + let { item }: ContentComponentProps = $props(); 6 + 7 + let canvas: HTMLCanvasElement; 8 + let ctx: CanvasRenderingContext2D | null = null; 9 + let animationId: number; 10 + 11 + // Game state 12 + let gameState = $state<'idle' | 'playing' | 'gameover'>('idle'); 13 + let score = $state(0); 14 + let highScore = $state(0); 15 + 16 + // Sprite images (processed with transparent backgrounds) 17 + let spritesLoaded = $state(false); 18 + const sprites: Record<string, HTMLCanvasElement> = {}; 19 + let tilemap: HTMLImageElement | null = null; 20 + 21 + // Tile size (original is 16x16) 22 + const TILE_SIZE = 16; 23 + const TILEMAP_COLS = 20; 24 + 25 + // Dynamic scaling - will be calculated based on canvas size 26 + let scale = 2.5; 27 + let scaledTile = TILE_SIZE * scale; 28 + 29 + // Game constants (will be recalculated on resize) 30 + const GRAVITY_BASE = 0.6; 31 + const JUMP_FORCE_BASE = -12; 32 + let gravity = GRAVITY_BASE; 33 + let jumpForce = JUMP_FORCE_BASE; 34 + let groundHeight = scaledTile + 10; 35 + 36 + // Game objects 37 + let player = { 38 + x: 50, 39 + y: 0, 40 + width: scaledTile, 41 + height: scaledTile, 42 + velocityY: 0, 43 + isJumping: false, 44 + isDucking: false, 45 + frame: 0 46 + }; 47 + 48 + let obstacles: Array<{ 49 + x: number; 50 + y: number; 51 + width: number; 52 + height: number; 53 + type: 'ground' | 'air'; 54 + sprite: string; 55 + frame?: number; 56 + }> = []; 57 + 58 + let groundTiles: Array<{ x: number }> = []; 59 + 60 + let gameSpeed = 5; 61 + let frameCount = 0; 62 + 63 + // Sprite positions in tilemap (row, column - 1-indexed based on cells.txt) 64 + const SPRITE_POSITIONS: Record<string, { row: number; col: number }> = { 65 + // Player - row 14: walk (col 2-4), jump (col 5), fall (col 6) 66 + playerWalk1: { row: 14, col: 2 }, 67 + playerWalk2: { row: 14, col: 3 }, 68 + playerWalk3: { row: 14, col: 4 }, 69 + playerJump: { row: 14, col: 5 }, 70 + playerFall: { row: 14, col: 6 }, 71 + playerDuck: { row: 14, col: 6 }, // Use fall sprite for duck 72 + // Floor - row 5, column 6 73 + floor: { row: 5, col: 6 }, 74 + // Mushroom obstacle - row 3, column 15 75 + mushroom: { row: 3, col: 15 }, 76 + // Spikes obstacle - row 10, column 4 77 + spikes: { row: 10, col: 4 }, 78 + // Plants obstacles - columns 17-19, rows 1-2 79 + plant1: { row: 1, col: 17 }, 80 + plant2: { row: 1, col: 18 }, 81 + plant3: { row: 1, col: 19 }, 82 + plant4: { row: 2, col: 17 }, 83 + plant5: { row: 2, col: 18 }, 84 + plant6: { row: 2, col: 19 }, 85 + // Flying obstacles - row 20, columns 1-2 86 + bat1: { row: 20, col: 1 }, 87 + bat2: { row: 20, col: 2 } 88 + }; 89 + 90 + // Extract a tile from the tilemap and process it (white to black) 91 + function extractTile( 92 + img: HTMLImageElement, 93 + row: number, 94 + col: number 95 + ): HTMLCanvasElement { 96 + const offscreen = document.createElement('canvas'); 97 + offscreen.width = TILE_SIZE; 98 + offscreen.height = TILE_SIZE; 99 + const offCtx = offscreen.getContext('2d')!; 100 + 101 + // Calculate position (1-indexed to 0-indexed, with 1px spacing between tiles) 102 + const TILE_SPACING = 1; 103 + const sx = (col - 1) * (TILE_SIZE + TILE_SPACING); 104 + const sy = (row - 1) * (TILE_SIZE + TILE_SPACING); 105 + 106 + offCtx.drawImage(img, sx, sy, TILE_SIZE, TILE_SIZE, 0, 0, TILE_SIZE, TILE_SIZE); 107 + 108 + // Process: turn white to black for light mode 109 + const imageData = offCtx.getImageData(0, 0, TILE_SIZE, TILE_SIZE); 110 + const data = imageData.data; 111 + 112 + for (let i = 0; i < data.length; i += 4) { 113 + const r = data[i]; 114 + const g = data[i + 1]; 115 + const b = data[i + 2]; 116 + 117 + // Turn white/near-white pixels to black 118 + if (r > 220 && g > 220 && b > 220) { 119 + data[i] = 0; 120 + data[i + 1] = 0; 121 + data[i + 2] = 0; 122 + } 123 + } 124 + 125 + offCtx.putImageData(imageData, 0, 0); 126 + return offscreen; 127 + } 128 + 129 + async function loadSprites() { 130 + return new Promise<void>((resolve) => { 131 + const img = new Image(); 132 + img.onload = () => { 133 + tilemap = img; 134 + // Extract all sprites from the tilemap 135 + for (const [key, pos] of Object.entries(SPRITE_POSITIONS)) { 136 + sprites[key] = extractTile(img, pos.row, pos.col); 137 + } 138 + spritesLoaded = true; 139 + resolve(); 140 + }; 141 + img.onerror = () => resolve(); 142 + img.src = '/dino/Tilemap/monochrome_tilemap_transparent.png'; 143 + }); 144 + } 145 + 146 + function calculateScale() { 147 + if (!canvas) return; 148 + 149 + // Scale based on canvas height - aim for ~4 tiles vertically for gameplay area 150 + const targetTilesVertical = 5; 151 + scale = Math.max(1.5, Math.min(4, canvas.height / (TILE_SIZE * targetTilesVertical))); 152 + scaledTile = TILE_SIZE * scale; 153 + 154 + // Recalculate physics based on scale 155 + const scaleRatio = scale / 2.5; 156 + gravity = GRAVITY_BASE * scaleRatio; 157 + jumpForce = JUMP_FORCE_BASE * scaleRatio; 158 + groundHeight = scaledTile + 10 * scaleRatio; 159 + 160 + // Update player dimensions 161 + player.width = scaledTile; 162 + player.height = scaledTile; 163 + player.x = Math.max(30, scaledTile); 164 + } 165 + 166 + function resetGame() { 167 + calculateScale(); 168 + player = { 169 + x: Math.max(30, scaledTile), 170 + y: 0, 171 + width: scaledTile, 172 + height: scaledTile, 173 + velocityY: 0, 174 + isJumping: false, 175 + isDucking: false, 176 + frame: 0 177 + }; 178 + obstacles = []; 179 + gameSpeed = 3.5 * (scale / 2.5); 180 + score = 0; 181 + frameCount = 0; 182 + initGroundTiles(); 183 + } 184 + 185 + function initGroundTiles() { 186 + if (!canvas) return; 187 + groundTiles = []; 188 + // Add extra tiles to ensure no gaps 189 + const numTiles = Math.ceil(canvas.width / scaledTile) + 4; 190 + for (let i = 0; i < numTiles; i++) { 191 + groundTiles.push({ x: i * scaledTile }); 192 + } 193 + } 194 + 195 + function startGame() { 196 + resetGame(); 197 + gameState = 'playing'; 198 + } 199 + 200 + function jump() { 201 + if (gameState === 'idle') { 202 + startGame(); 203 + return; 204 + } 205 + if (gameState === 'gameover') { 206 + startGame(); 207 + return; 208 + } 209 + if (!player.isJumping && !player.isDucking) { 210 + player.velocityY = jumpForce; 211 + player.isJumping = true; 212 + } 213 + } 214 + 215 + function duck(ducking: boolean) { 216 + if (gameState !== 'playing') return; 217 + if (ducking && !player.isJumping) { 218 + player.isDucking = true; 219 + } else if (!ducking) { 220 + player.isDucking = false; 221 + } 222 + } 223 + 224 + function handleKeyDown(e: KeyboardEvent) { 225 + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { 226 + e.preventDefault(); 227 + jump(); 228 + } 229 + if (e.code === 'ArrowDown' || e.code === 'KeyS') { 230 + e.preventDefault(); 231 + duck(true); 232 + } 233 + } 234 + 235 + function handleKeyUp(e: KeyboardEvent) { 236 + if (e.code === 'ArrowDown' || e.code === 'KeyS') { 237 + duck(false); 238 + } 239 + } 240 + 241 + function handleTouch(e: TouchEvent) { 242 + e.preventDefault(); 243 + // Simple tap to jump, could add swipe down for duck 244 + jump(); 245 + } 246 + 247 + function spawnObstacle(canvasWidth: number, groundY: number) { 248 + const rand = Math.random(); 249 + 250 + // After score 100, start spawning flying obstacles 251 + const canSpawnFlying = score > 100; 252 + 253 + if (canSpawnFlying && rand < 0.2) { 254 + // Flying bat - requires ducking 255 + const batFrame = Math.random() > 0.5 ? 1 : 2; 256 + obstacles.push({ 257 + x: canvasWidth, 258 + y: groundY - scaledTile * 1.8, // Float above ground at player head height 259 + width: scaledTile, 260 + height: scaledTile, 261 + type: 'air', 262 + sprite: `bat${batFrame}`, 263 + frame: batFrame 264 + }); 265 + } else if (rand < 0.4) { 266 + // Spikes 267 + obstacles.push({ 268 + x: canvasWidth, 269 + y: groundY - scaledTile, 270 + width: scaledTile, 271 + height: scaledTile, 272 + type: 'ground', 273 + sprite: 'spikes' 274 + }); 275 + } else if (rand < 0.55) { 276 + // Mushroom 277 + obstacles.push({ 278 + x: canvasWidth, 279 + y: groundY - scaledTile, 280 + width: scaledTile, 281 + height: scaledTile, 282 + type: 'ground', 283 + sprite: 'mushroom' 284 + }); 285 + } else { 286 + // Plants 287 + const plantSprites = ['plant1', 'plant2', 'plant3', 'plant4', 'plant5', 'plant6']; 288 + const sprite = plantSprites[Math.floor(Math.random() * plantSprites.length)]; 289 + obstacles.push({ 290 + x: canvasWidth, 291 + y: groundY - scaledTile, 292 + width: scaledTile, 293 + height: scaledTile, 294 + type: 'ground', 295 + sprite 296 + }); 297 + } 298 + } 299 + 300 + function checkCollision( 301 + rect1: { x: number; y: number; width: number; height: number }, 302 + rect2: { x: number; y: number; width: number; height: number } 303 + ) { 304 + const padding = scaledTile * 0.3; 305 + return ( 306 + rect1.x + padding < rect2.x + rect2.width - padding && 307 + rect1.x + rect1.width - padding > rect2.x + padding && 308 + rect1.y + padding < rect2.y + rect2.height - padding && 309 + rect1.y + rect1.height - padding > rect2.y + padding 310 + ); 311 + } 312 + 313 + function drawSprite(spriteKey: string, x: number, y: number, width: number, height: number) { 314 + if (!ctx || !sprites[spriteKey]) return; 315 + ctx.imageSmoothingEnabled = false; 316 + ctx.drawImage(sprites[spriteKey], x, y, width, height); 317 + } 318 + 319 + function gameLoop() { 320 + if (!ctx || !canvas || !spritesLoaded) { 321 + animationId = requestAnimationFrame(gameLoop); 322 + return; 323 + } 324 + 325 + const canvasWidth = canvas.width; 326 + const canvasHeight = canvas.height; 327 + const groundY = canvasHeight - groundHeight; 328 + 329 + // Clear canvas (transparent to show card background) 330 + ctx.clearRect(0, 0, canvasWidth, canvasHeight); 331 + 332 + // Draw ground tiles - continuous floor (slight overlap to prevent gaps) 333 + for (const tile of groundTiles) { 334 + drawSprite('floor', Math.floor(tile.x), groundY, Math.ceil(scaledTile) + 1, scaledTile); 335 + } 336 + 337 + if (gameState === 'playing') { 338 + frameCount++; 339 + 340 + // Update ground tiles - seamless scrolling 341 + for (const tile of groundTiles) { 342 + tile.x -= gameSpeed; 343 + } 344 + 345 + // Find the rightmost tile and reposition tiles that went off-screen 346 + const rightmostX = Math.max(...groundTiles.map((t) => t.x)); 347 + for (const tile of groundTiles) { 348 + if (tile.x < -scaledTile) { 349 + tile.x = rightmostX + scaledTile; 350 + } 351 + } 352 + 353 + // Update player physics 354 + if (player.isJumping) { 355 + player.velocityY += gravity; 356 + player.y += player.velocityY; 357 + 358 + if (player.y >= groundY - player.height) { 359 + player.y = groundY - player.height; 360 + player.isJumping = false; 361 + player.velocityY = 0; 362 + } 363 + } else { 364 + player.y = groundY - player.height; 365 + } 366 + 367 + // Animate player (3 walk frames) 368 + if (frameCount % 8 === 0) { 369 + player.frame = (player.frame + 1) % 3; 370 + } 371 + 372 + // Animate flying obstacles 373 + for (const obs of obstacles) { 374 + if (obs.type === 'air' && frameCount % 12 === 0) { 375 + obs.frame = obs.frame === 1 ? 2 : 1; 376 + obs.sprite = `bat${obs.frame}`; 377 + } 378 + } 379 + 380 + // Spawn obstacles 381 + const baseSpawnRate = 120; 382 + const spawnRate = Math.max(60, baseSpawnRate - Math.floor(score / 100) * 5); 383 + if (frameCount % spawnRate === 0 || (obstacles.length === 0 && frameCount > 60)) { 384 + spawnObstacle(canvasWidth, groundY); 385 + } 386 + 387 + // Update obstacles 388 + obstacles = obstacles.filter((obs) => { 389 + obs.x -= gameSpeed; 390 + return obs.x > -obs.width; 391 + }); 392 + 393 + // Check collisions 394 + for (const obstacle of obstacles) { 395 + let playerHitbox; 396 + 397 + if (player.isDucking) { 398 + // Ducking hitbox - lower and shorter 399 + playerHitbox = { 400 + x: player.x, 401 + y: groundY - player.height * 0.5, 402 + width: player.width, 403 + height: player.height * 0.5 404 + }; 405 + } else if (player.isJumping) { 406 + playerHitbox = { 407 + x: player.x, 408 + y: player.y, 409 + width: player.width, 410 + height: player.height 411 + }; 412 + } else { 413 + playerHitbox = { 414 + x: player.x, 415 + y: groundY - player.height, 416 + width: player.width, 417 + height: player.height 418 + }; 419 + } 420 + 421 + if (checkCollision(playerHitbox, obstacle)) { 422 + gameState = 'gameover'; 423 + if (score > highScore) { 424 + highScore = score; 425 + } 426 + break; 427 + } 428 + } 429 + 430 + // Update score 431 + score = Math.floor(frameCount / 5); 432 + 433 + // Increase speed over time (slower progression) 434 + if (frameCount % 700 === 0) { 435 + gameSpeed = Math.min(gameSpeed + 0.2 * (scale / 2.5), 8 * (scale / 2.5)); 436 + } 437 + } 438 + 439 + // Draw obstacles 440 + for (const obstacle of obstacles) { 441 + drawSprite(obstacle.sprite, obstacle.x, obstacle.y, obstacle.width, obstacle.height); 442 + } 443 + 444 + // Draw player 445 + const playerY = player.isJumping 446 + ? player.y 447 + : player.isDucking 448 + ? groundY - player.height * 0.6 449 + : groundY - player.height; 450 + 451 + let playerSprite: string; 452 + if (player.isDucking) { 453 + playerSprite = 'playerDuck'; 454 + } else if (player.isJumping) { 455 + if (player.velocityY < 0) { 456 + playerSprite = 'playerJump'; 457 + } else { 458 + playerSprite = 'playerFall'; 459 + } 460 + } else { 461 + // 3-frame walk cycle 462 + const walkFrames = ['playerWalk1', 'playerWalk2', 'playerWalk3']; 463 + playerSprite = walkFrames[player.frame]; 464 + } 465 + 466 + // When ducking, draw shorter 467 + const drawHeight = player.isDucking ? player.height * 0.6 : player.height; 468 + drawSprite(playerSprite, player.x, playerY, player.width, drawHeight); 469 + 470 + // Draw score 471 + ctx.fillStyle = '#000000'; 472 + ctx.font = `bold ${Math.max(12, Math.floor(14 * (scale / 2.5)))}px monospace`; 473 + ctx.textAlign = 'right'; 474 + ctx.fillText(String(score).padStart(5, '0'), canvasWidth - 10, 25); 475 + 476 + if (highScore > 0) { 477 + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; 478 + ctx.fillText( 479 + 'HI ' + String(highScore).padStart(5, '0'), 480 + canvasWidth - 70 * (scale / 2.5), 481 + 25 482 + ); 483 + } 484 + 485 + // Draw game over text (no overlay background) 486 + if (gameState === 'gameover') { 487 + ctx.fillStyle = '#000000'; 488 + ctx.font = `bold ${Math.max(14, Math.floor(16 * (scale / 2.5)))}px monospace`; 489 + ctx.textAlign = 'center'; 490 + ctx.fillText('GAME OVER', canvasWidth / 2, canvasHeight / 2 - 30); 491 + } 492 + 493 + animationId = requestAnimationFrame(gameLoop); 494 + } 495 + 496 + function resizeCanvas() { 497 + if (!canvas) return; 498 + const container = canvas.parentElement; 499 + if (!container) return; 500 + canvas.width = container.clientWidth; 501 + canvas.height = container.clientHeight; 502 + calculateScale(); 503 + initGroundTiles(); 504 + } 505 + 506 + onMount(async () => { 507 + ctx = canvas.getContext('2d'); 508 + await loadSprites(); 509 + resizeCanvas(); 510 + 511 + const resizeObserver = new ResizeObserver(() => { 512 + resizeCanvas(); 513 + }); 514 + resizeObserver.observe(canvas.parentElement!); 515 + 516 + gameLoop(); 517 + 518 + return () => { 519 + resizeObserver.disconnect(); 520 + }; 521 + }); 522 + 523 + onDestroy(() => { 524 + if (animationId) { 525 + cancelAnimationFrame(animationId); 526 + } 527 + }); 528 + </script> 529 + 530 + <svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} /> 531 + 532 + <div class="relative h-full w-full overflow-hidden"> 533 + <canvas bind:this={canvas} class="h-full w-full dark:invert" ontouchstart={handleTouch}></canvas> 534 + 535 + {#if gameState === 'idle' || gameState === 'gameover'} 536 + <button 537 + onclick={startGame} 538 + class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform cursor-pointer rounded-lg border-2 border-black bg-white/30 px-6 py-3 font-mono text-sm font-bold text-black transition-colors hover:bg-black hover:text-white" 539 + > 540 + {gameState === 'gameover' ? 'PLAY AGAIN' : 'START'} 541 + </button> 542 + {/if} 543 + </div>
+21
src/lib/cards/DinoGameCard/SidebarItemDinoGameCard.svelte
···
··· 1 + <script lang="ts"> 2 + import { Button } from '@foxui/core'; 3 + 4 + let { onclick }: { onclick: () => void } = $props(); 5 + </script> 6 + 7 + <Button {onclick} variant="ghost" class="w-full justify-start"> 8 + <svg 9 + xmlns="http://www.w3.org/2000/svg" 10 + viewBox="0 0 24 24" 11 + fill="currentColor" 12 + class="text-accent-600 dark:text-accent-400" 13 + > 14 + <!-- Dino silhouette --> 15 + <path 16 + d="M18 4h-2v2h-2v2h-2V6h-2V4H8v2H6v2H4v2H2v8h2v2h2v-2h2v-2h2v2h2v-2h2v2h2v-2h2V8h-2V6h-2V4zm-8 8H8v-2h2v2zm4 0h-2v-2h2v2z" 17 + /> 18 + </svg> 19 + 20 + Dino Game</Button 21 + >
+17
src/lib/cards/DinoGameCard/index.ts
···
··· 1 + import type { CardDefinition } from '../types'; 2 + import DinoGameCard from './DinoGameCard.svelte'; 3 + import SidebarItemDinoGameCard from './SidebarItemDinoGameCard.svelte'; 4 + 5 + export const DinoGameCardDefinition = { 6 + type: 'dino-game', 7 + contentComponent: DinoGameCard, 8 + sidebarComponent: SidebarItemDinoGameCard, 9 + allowSetColor: true, 10 + createNew: (card) => { 11 + card.w = 4; 12 + card.h = 4; 13 + card.mobileW = 8; 14 + card.mobileH = 6; 15 + card.cardData = {}; 16 + } 17 + } as CardDefinition & { type: 'dino-game' };
+3 -1
src/lib/cards/index.ts
··· 3 import { BigSocialCardDefinition } from './BigSocialCard'; 4 import { BlueskyMediaCardDefinition } from './BlueskyMediaCard'; 5 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 6 import { EmbedCardDefinition } from './EmbedCard'; 7 import { ImageCardDefinition } from './ImageCard'; 8 import { LinkCardDefinition } from './LinkCard'; ··· 28 MapCardDefinition, 29 ATProtoCollectionsCardDefinition, 30 SectionCardDefinition, 31 - BlueskyMediaCardDefinition 32 ] as const; 33 34 export const CardDefinitionsByType = AllCardDefinitions.reduce(
··· 3 import { BigSocialCardDefinition } from './BigSocialCard'; 4 import { BlueskyMediaCardDefinition } from './BlueskyMediaCard'; 5 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 6 + import { DinoGameCardDefinition } from './DinoGameCard'; 7 import { EmbedCardDefinition } from './EmbedCard'; 8 import { ImageCardDefinition } from './ImageCard'; 9 import { LinkCardDefinition } from './LinkCard'; ··· 29 MapCardDefinition, 30 ATProtoCollectionsCardDefinition, 31 SectionCardDefinition, 32 + BlueskyMediaCardDefinition, 33 + DinoGameCardDefinition 34 ] as const; 35 36 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+6
static/dino/Instructions.url
···
··· 1 + [InternetShortcut] 2 + URL=https://kenney.nl/documentation/game-assets/tilemaps 3 + IDList= 4 + HotKey=0 5 + [{000214A0-0000-0000-C000-000000000046}] 6 + Prop3=19,11
+23
static/dino/License.txt
···
··· 1 + 2 + 3 + 1-Bit Platformer Pack (1.1) 4 + 5 + Created/distributed by Kenney (www.kenney.nl) 6 + Creation date: 02-11-2021 7 + 8 + ------------------------------ 9 + 10 + License: (Creative Commons Zero, CC0) 11 + http://creativecommons.org/publicdomain/zero/1.0/ 12 + 13 + This content is free to use in personal, educational and commercial projects. 14 + 15 + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) 16 + 17 + ------------------------------ 18 + 19 + Donate: http://support.kenney.nl 20 + Patreon: http://patreon.com/kenney/ 21 + 22 + Follow on Twitter for updates: 23 + http://twitter.com/KenneyNL
static/dino/Preview.png

This is a binary file and will not be displayed.

static/dino/Sample.png

This is a binary file and will not be displayed.

static/dino/Tilemap/monochrome_tilemap.png

This is a binary file and will not be displayed.

static/dino/Tilemap/monochrome_tilemap_packed.png

This is a binary file and will not be displayed.

static/dino/Tilemap/monochrome_tilemap_transparent.png

This is a binary file and will not be displayed.

static/dino/Tilemap/monochrome_tilemap_transparent_packed.png

This is a binary file and will not be displayed.

+9
static/dino/Tilesheet.txt
···
··· 1 + Tilesheet information: 2 + 3 + Tile size • 16px × 16px 4 + Space between tiles • 1px × 1px 5 + --- 6 + Total tiles (horizontal) • 20 tiles 7 + Total tiles (vertical) • 20 tiles 8 + --- 9 + Total tiles in sheet • 400 tiles
+7
static/dino/cells.txt
···
··· 1 + (all columns and rows are counted starting with 1, 1 at the top left) 2 + 3 + - floor at row 5, column 6 4 + - player at row 14, column 1-2 for walk cycle, column 4-5 for jump and column 6 for fall 5 + - mushroom (obstacle) at row 3, column 15 6 + - spikes (obstacle) at row 10, column 4 7 + - plants (obstacles) at column 17-19 and row 1-2