vod jam and earl vod.atverkackt.de
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

Less eye wateringly painful world map

+157 -74
+2 -1
src/lib/api.ts
··· 115 115 month: 'short', 116 116 day: 'numeric', 117 117 hour: '2-digit', 118 - minute: '2-digit' 118 + minute: '2-digit', 119 + timeZone: 'America/Los_Angeles' 119 120 }); 120 121 } 121 122
+90 -70
src/lib/game/GameWorld.svelte
··· 38 38 focusVideoId?: string; 39 39 } = $props(); 40 40 41 - // --- Seeded PRNG --- 42 - function mulberry32(seed: number): number { 43 - let t = seed + 0x6d2b79f5; 44 - t = Math.imul(t ^ (t >>> 15), t | 1); 45 - t ^= t + Math.imul(t ^ (t >>> 7), t | 61); 46 - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; 47 - } 48 - 49 - // --- World dimensions --- 41 + // --- Dynamic world dimensions based on video count --- 50 42 const spacing = 250; 51 - 52 - let worldWidth = $derived.by(() => { 53 - const cols = Math.ceil(Math.sqrt(videos.length)) || 1; 54 - return Math.max(cols * spacing + GAME_CONSTANTS.WORLD_PADDING * 2, 4000); 55 - }); 56 - 57 - let worldHeight = $derived.by(() => { 58 - const cols = Math.ceil(Math.sqrt(videos.length)) || 1; 59 - const rows = Math.ceil(videos.length / cols) || 1; 60 - return Math.max(rows * spacing + GAME_CONSTANTS.WORLD_PADDING * 2, 3000); 61 - }); 43 + let worldWidth = $derived(Math.max(4000, Math.ceil(Math.sqrt(videos.length)) * spacing + GAME_CONSTANTS.WORLD_PADDING * 2)); 44 + let worldHeight = $derived(Math.max(3000, Math.ceil(videos.length / Math.ceil(Math.sqrt(videos.length))) * spacing + GAME_CONSTANTS.WORLD_PADDING * 2)); 62 45 63 46 let worldConfig: WorldConfig = $derived({ 64 47 width: worldWidth, ··· 69 52 spawnPoint: { x: worldWidth / 2, y: worldHeight / 2 } 70 53 }); 71 54 72 - // --- Present distribution --- 55 + // --- Present distribution (constrained to walkable area) --- 73 56 function distributePresents(vids: VideoRecord[], worldW: number, worldH: number): WorldPresent[] { 74 57 if (vids.length === 0) return []; 75 58 ··· 88 71 const jitterX = (((seed * 7 + 13) % 100) - 50) / 50 * cellW * 0.3; 89 72 const jitterY = (((seed * 13 + 7) % 100) - 50) / 50 * cellH * 0.3; 90 73 74 + const x = padding + col * cellW + cellW / 2 + jitterX; 75 + const y = padding + row * cellH + cellH / 2 + jitterY; 76 + 91 77 return { 92 78 id: rkey, 93 79 uri: v.uri, 94 - position: { 95 - x: padding + col * cellW + cellW / 2 + jitterX, 96 - y: padding + row * cellH + cellH / 2 + jitterY 97 - }, 80 + position: { x, y }, 98 81 title: v.value.title, 99 82 creator: v.value.creator, 100 83 duration: v.value.duration, ··· 107 90 }); 108 91 } 109 92 110 - // --- Scenery generation --- 111 - function generateScenery(worldW: number, worldH: number, presentPositions: Vec2[]): SceneryItem[] { 112 - const items: SceneryItem[] = []; 113 - const count = Math.floor((worldW * worldH) / 40000); 114 - for (let i = 0; i < count; i++) { 115 - const x = mulberry32(i * 37) * (worldW - 200) + 100; 116 - const y = mulberry32(i * 73 + 11) * (worldH - 200) + 100; 117 - const tooClose = presentPositions.some((p) => Math.hypot(p.x - x, p.y - y) < 80); 118 - if (tooClose) continue; 119 - const types = ['tree', 'bush'] as const; 120 - const type = types[Math.floor(mulberry32(i * 17) * types.length)]; 121 - items.push({ 122 - type, 123 - position: { x, y }, 124 - sprite: type === 'tree' ? '/sprites/scenery/tree-1.png' : '/sprites/scenery/bush-1.png', 125 - scale: 1.5 + mulberry32(i * 53) * 1.0, 126 - zOffset: 0 127 - }); 128 - } 129 - return items; 130 - } 131 - 132 93 // --- Reactive state --- 133 94 let presents: WorldPresent[] = $state([]); 134 - let sceneryItems: SceneryItem[] = $state([]); 135 95 136 96 let playerPosition: Vec2 = $state({ x: 0, y: 0 }); 137 97 let playerDirection: Vec2 = $state({ x: 0, y: 1 }); ··· 146 106 const inputManager = new InputManager(); 147 107 const gameLoop = new GameLoop(); 148 108 149 - // Rebuild presents and scenery when videos or world size change 109 + // --- Scenery generation (trees) --- 110 + // Simple hash for deterministic pseudo-random numbers 111 + function mulberry32(seed: number): () => number { 112 + return () => { 113 + seed |= 0; seed = (seed + 0x6d2b79f5) | 0; 114 + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); 115 + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; 116 + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; 117 + }; 118 + } 119 + 120 + function generateScenery(worldW: number, worldH: number): SceneryItem[] { 121 + const items: SceneryItem[] = []; 122 + const padding = GAME_CONSTANTS.WORLD_PADDING / 2; 123 + const area = (worldW - padding * 2) * (worldH - padding * 2); 124 + const count = Math.floor(area / 40000); // ~1 tree per 40k px² 125 + const rand = mulberry32(12345); 126 + 127 + for (let i = 0; i < count; i++) { 128 + const x = padding + rand() * (worldW - padding * 2); 129 + const y = padding + rand() * (worldH - padding * 2); 130 + const scale = 0.8 + rand() * 0.4; // 0.8–1.2 131 + items.push({ 132 + type: 'tree', 133 + position: { x, y }, 134 + sprite: '/sprites/terrain/tree.webp', 135 + scale, 136 + zOffset: 0 137 + }); 138 + } 139 + return items; 140 + } 141 + 142 + let sceneryItems: SceneryItem[] = $state([]); 143 + let sceneryCanvas: HTMLCanvasElement | undefined = $state(undefined); 144 + 145 + // Pre-render all trees onto a single canvas 146 + function renderSceneryCanvas(items: SceneryItem[], worldW: number, worldH: number) { 147 + if (items.length === 0 || !sceneryCanvas) return; 148 + 149 + sceneryCanvas.width = worldW; 150 + sceneryCanvas.height = worldH; 151 + const ctx = sceneryCanvas.getContext('2d')!; 152 + ctx.imageSmoothingEnabled = false; 153 + 154 + // Sort by Y for depth ordering 155 + const sorted = [...items].sort((a, b) => a.position.y - b.position.y); 156 + 157 + const treeImg = new Image(); 158 + treeImg.src = '/sprites/terrain/tree.webp'; 159 + treeImg.onload = () => { 160 + for (const item of sorted) { 161 + const w = treeImg.naturalWidth * item.scale; 162 + const h = treeImg.naturalHeight * item.scale; 163 + ctx.drawImage(treeImg, item.position.x - w / 2, item.position.y - h, w, h); 164 + } 165 + }; 166 + } 167 + 168 + // Rebuild presents when videos change 150 169 $effect(() => { 151 170 const distributed = distributePresents(videos, worldWidth, worldHeight); 152 171 presents = distributed; 153 - sceneryItems = generateScenery( 154 - worldWidth, 155 - worldHeight, 156 - distributed.map((p) => p.position) 157 - ); 158 172 159 173 // Enrich presents with schedule data 160 174 fetchScheduleEvents() ··· 177 191 .catch(() => { 178 192 // Silently ignore schedule fetch failures — presents still work without enrichment 179 193 }); 194 + 195 + sceneryItems = generateScenery(worldWidth, worldHeight); 196 + }); 197 + 198 + // Re-render scenery canvas when items or canvas element change 199 + $effect(() => { 200 + if (sceneryItems.length > 0 && sceneryCanvas) { 201 + renderSceneryCanvas(sceneryItems, worldWidth, worldHeight); 202 + } 180 203 }); 181 204 182 205 // Set initial player position at world center (or focused present, or restored position) ··· 312 335 313 336 <div class="game-world" bind:this={containerEl}> 314 337 <WorldMap {worldConfig} {playerPosition}> 315 - {#each sceneryItems as item} 316 - <div 317 - class="scenery" 318 - style="position:absolute; left:{item.position.x}px; top:{item.position.y}px; z-index:{Math.floor(item.position.y)}; transform:scale({item.scale})" 319 - > 320 - <img src={item.sprite} alt="" style="image-rendering:pixelated" /> 321 - </div> 322 - {/each} 338 + <canvas 339 + bind:this={sceneryCanvas} 340 + class="scenery-layer" 341 + style=" 342 + position: absolute; 343 + top: 0; 344 + left: 0; 345 + width: {worldConfig.width}px; 346 + height: {worldConfig.height}px; 347 + pointer-events: none; 348 + image-rendering: pixelated; 349 + " 350 + /> 323 351 {#each presents as present (present.id)} 324 352 <Present {present} onselect={handlePresentSelect} /> 325 353 {/each} ··· 338 366 user-select: none; 339 367 } 340 368 341 - .scenery { 342 - pointer-events: none; 343 - image-rendering: pixelated; 344 - } 345 - 346 - .scenery img { 347 - display: block; 348 - } 349 369 </style>
+65 -3
src/lib/game/WorldMap.svelte
··· 3 3 import type { Vec2, WorldConfig } from './types'; 4 4 import { GAME_CONSTANTS } from './types'; 5 5 6 + const GRASS_TILES = [ 7 + '/sprites/terrain/grass-tile.webp', 8 + '/sprites/terrain/grass-tile-2.webp', 9 + '/sprites/terrain/grass-tile-3.webp', 10 + '/sprites/terrain/grass-tile-4.webp', 11 + '/sprites/terrain/grass-tile-5.webp' 12 + ]; 13 + 14 + let grassBgUrl = $state(''); 15 + 16 + // Build a composite grass pattern on a canvas for tile variety 17 + async function buildGrassPattern(tileSize: number, cols: number, rows: number): Promise<string> { 18 + const images = await Promise.all( 19 + GRASS_TILES.map((src) => { 20 + const img = new Image(); 21 + img.src = src; 22 + return new Promise<HTMLImageElement>((resolve) => { 23 + img.onload = () => resolve(img); 24 + }); 25 + }) 26 + ); 27 + 28 + const canvas = document.createElement('canvas'); 29 + canvas.width = cols * tileSize; 30 + canvas.height = rows * tileSize; 31 + const ctx = canvas.getContext('2d')!; 32 + 33 + // Deterministic pseudo-random tile selection 34 + let seed = 42; 35 + const rand = () => { 36 + seed = (seed * 1664525 + 1013904223) & 0x7fffffff; 37 + return seed / 0x7fffffff; 38 + }; 39 + 40 + for (let r = 0; r < rows; r++) { 41 + for (let c = 0; c < cols; c++) { 42 + const img = images[Math.floor(rand() * images.length)]; 43 + ctx.drawImage(img, c * tileSize, r * tileSize, tileSize, tileSize); 44 + } 45 + } 46 + 47 + return new Promise<string>((resolve) => { 48 + canvas.toBlob((blob) => { 49 + resolve(URL.createObjectURL(blob!)); 50 + }, 'image/png'); 51 + }); 52 + } 53 + 6 54 let { 7 55 worldConfig, 8 56 playerPosition, ··· 26 74 27 75 let viewportEl: HTMLDivElement; 28 76 77 + // Build a repeating grass pattern from multiple tiles (4×4 block) 78 + $effect(() => { 79 + let currentUrl: string | undefined; 80 + buildGrassPattern(worldConfig.tileSize, 4, 4).then((url) => { 81 + currentUrl = url; 82 + grassBgUrl = url; 83 + }); 84 + return () => { 85 + if (currentUrl) URL.revokeObjectURL(currentUrl); 86 + }; 87 + }); 88 + 29 89 // Track viewport dimensions via ResizeObserver 30 90 $effect(() => { 31 91 if (!viewportEl) return; ··· 74 134 style=" 75 135 width: {worldConfig.width}px; 76 136 height: {worldConfig.height}px; 77 - transform: translate({-cameraLeft}px, {-cameraTop}px); 78 - background-size: {worldConfig.tileSize}px {worldConfig.tileSize}px; 137 + transform: translate3d({-cameraLeft}px, {-cameraTop}px, 0); 138 + {grassBgUrl ? `background-image: url(${grassBgUrl});` : ''} 139 + background-size: {worldConfig.tileSize * 4}px {worldConfig.tileSize * 4}px; 79 140 " 80 141 > 81 142 <div class="depth-container"> ··· 98 159 top: 0; 99 160 left: 0; 100 161 will-change: transform; 101 - background-image: url('/sprites/terrain/grass-tile.png'); 162 + background-image: url('/sprites/terrain/grass-tile.webp'); 102 163 background-repeat: repeat; 164 + background-color: #2d7d2d; 103 165 } 104 166 105 167 .depth-container {
static/sprites/terrain/grass-tile-2.webp

This is a binary file and will not be displayed.

static/sprites/terrain/grass-tile-3.webp

This is a binary file and will not be displayed.

static/sprites/terrain/grass-tile-4.webp

This is a binary file and will not be displayed.

static/sprites/terrain/grass-tile-5.webp

This is a binary file and will not be displayed.

static/sprites/terrain/grass-tile.png

This is a binary file and will not be displayed.

static/sprites/terrain/grass-tile.webp

This is a binary file and will not be displayed.

static/sprites/terrain/tree.webp

This is a binary file and will not be displayed.

static/world-map-limit.png

This is a binary file and will not be displayed.

static/world-map.png

This is a binary file and will not be displayed.