From 022d27c0d8c0fc2432c85d07632355d4db2843a0 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 18:57:17 +0000 Subject: [PATCH] Add OrbitHive to the Arcade with Orbit and Classic skins (0.79.0) Co-Authored-By: Claude Opus 5.5 (1M context) --- VERIFICATION-0.79.0.md | 11 + android/app/build.gradle.kts | 4 +- .../main/assets/orbithive/barlow-semibold.ttf | Bin 0 -> 108348 bytes android/app/src/main/assets/orbithive/game.js | 1134 +++++++++++++++++ .../app/src/main/assets/orbithive/index.html | 17 + .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../main/java/tv/orbit/classic/OrbitArcade.kt | 94 +- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- docs/refinement-notes.md | 4 + 9 files changed, 1241 insertions(+), 27 deletions(-) create mode 100644 VERIFICATION-0.79.0.md create mode 100644 android/app/src/main/assets/orbithive/barlow-semibold.ttf create mode 100644 android/app/src/main/assets/orbithive/game.js create mode 100644 android/app/src/main/assets/orbithive/index.html diff --git a/VERIFICATION-0.79.0.md b/VERIFICATION-0.79.0.md new file mode 100644 index 0000000..c28da9e --- /dev/null +++ b/VERIFICATION-0.79.0.md @@ -0,0 +1,11 @@ +# Orbit TV 0.79.0: OrbitHive in the Arcade + +Paul's Hive Harvest game, renamed OrbitHive, runs in the Arcade beside Snake. It's JavaScript he had built, all drawn in code, about 50 KB plus a font, shown in a WebView with network loads blocked. Progress stays on the box. + +- Two skins, under Help & Settings: Orbit, the default, with starry space and the bee in a helmet, and Classic, the game as it was. +- Names that echoed Beehive Bedlam are gone from the game's text: no Buzz and no Bedlam. +- Back goes through the game's own menus, then leaves to the Arcade. + +Checked: both skins and a level start in a headless browser with screenshots. Build, lint and 294 unit tests pass. + +Not checked until installed: the remote and Back in the WebView on the box, sound, and how smoothly it runs. Rules resemble Beehive Bedlam's: fine for Paul's own use, but they want a proper look before any public release. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index f8dfbc1..852c9e9 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,8 +9,8 @@ android { applicationId = "tv.orbit.classic" minSdk = 26 targetSdk = 36 - versionCode = 85 - versionName = "0.78.0-idents" + versionCode = 86 + versionName = "0.79.0-orbithive" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/assets/orbithive/barlow-semibold.ttf b/android/app/src/main/assets/orbithive/barlow-semibold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7c974a4aa67c689a4b2a844b48f624cb4eeaa6bd GIT binary patch literal 108348 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�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 new Date().toISOString().slice(0, 10); + + // ---------- canvas ---------- + const canvas = document.getElementById('game'); + const ctx = canvas.getContext('2d'); + let scale = 1, ox = 0, oy = 0, dpr = 1; + function resize() { + dpr = Math.min(window.devicePixelRatio || 1, 2); + canvas.width = Math.round(innerWidth * dpr); + canvas.height = Math.round(innerHeight * dpr); + scale = Math.min(canvas.width / W, canvas.height / H); + ox = (canvas.width - W * scale) / 2; + oy = (canvas.height - H * scale) / 2; + flowerSprites = makeFlowerSprites(); + } + + // Pre-drawn flowers, one per colour, so the grid draws quickly on TV boxes. + let flowerSprites = []; + function makeFlowerSprites() { + const size = Math.ceil(D * Math.max(scale, 0.5) * 1.1); + return COLOURS.map((c) => { + const cv = document.createElement('canvas'); + cv.width = cv.height = size; + const g = cv.getContext('2d'); + const k = size / (D * 1.1); + g.translate(size / 2, size / 2); + g.scale(k, k); + drawFlowerShape(g, 0, 0, R, c); + return cv; + }); + } + function drawFlowerShape(g, x, y, r, c) { + g.save(); + g.translate(x, y); + g.fillStyle = 'rgba(0,0,0,.25)'; + g.beginPath(); g.arc(2, 3, r * 0.98, 0, Math.PI * 2); g.fill(); + for (let i = 0; i < 6; i++) { + const a = (i / 6) * Math.PI * 2 + Math.PI / 6; + const px = Math.cos(a) * r * 0.5, py = Math.sin(a) * r * 0.5; + g.fillStyle = c.dark; + g.beginPath(); g.arc(px, py, r * 0.5, 0, Math.PI * 2); g.fill(); + g.fillStyle = c.petal; + g.beginPath(); g.arc(px * 0.96, py * 0.96, r * 0.44, 0, Math.PI * 2); g.fill(); + } + g.fillStyle = 'rgba(255,255,255,.35)'; + g.beginPath(); g.arc(-r * 0.25, -r * 0.35, r * 0.2, 0, Math.PI * 2); g.fill(); + const grad = g.createRadialGradient(-r * 0.1, -r * 0.1, 1, 0, 0, r * 0.38); + grad.addColorStop(0, '#fff3a0'); grad.addColorStop(1, '#f2b400'); + g.fillStyle = grad; + g.beginPath(); g.arc(0, 0, r * 0.36, 0, Math.PI * 2); g.fill(); + g.strokeStyle = 'rgba(120,70,0,.5)'; g.lineWidth = 1.5; g.stroke(); + g.restore(); + } + function drawFlower(x, y, colour, s = 1, alpha = 1, rot = 0) { + const sp = flowerSprites[colour]; + if (!sp) return; + const size = D * 1.1 * s; + ctx.save(); + ctx.globalAlpha = alpha; + ctx.translate(x, y); + if (rot) ctx.rotate(rot); + ctx.drawImage(sp, -size / 2, -size / 2, size, size); + ctx.restore(); + } + + // ---------- sound ---------- + let audio = null; + function beep(freq, dur, type = 'sine', vol = 0.08, slide = 0) { + if (!save.sound) return; + try { + audio = audio || new (window.AudioContext || window.webkitAudioContext)(); + const o = audio.createOscillator(), g = audio.createGain(); + o.type = type; o.frequency.value = freq; + if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(40, freq + slide), audio.currentTime + dur); + g.gain.value = vol; + g.gain.exponentialRampToValueAtTime(0.0001, audio.currentTime + dur); + o.connect(g); g.connect(audio.destination); + o.start(); o.stop(audio.currentTime + dur); + } catch (e) { /* audio not available */ } + } + const sfx = { + shoot: () => beep(520, 0.12, 'triangle', 0.06, 300), + stick: () => beep(220, 0.08, 'sine', 0.06), + pop: (n) => { for (let i = 0; i < Math.min(n, 5); i++) setTimeout(() => beep(600 + i * 120, 0.1, 'sine', 0.05), i * 45); }, + drop: () => beep(400, 0.35, 'triangle', 0.05, -300), + move: () => beep(880, 0.04, 'square', 0.02), + lid: () => beep(110, 0.4, 'sawtooth', 0.05, -40), + win: () => [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => beep(f, 0.18, 'triangle', 0.06), i * 120)), + lose: () => [392, 330, 262, 196].forEach((f, i) => setTimeout(() => beep(f, 0.22, 'sawtooth', 0.04), i * 150)), + boom: () => beep(90, 0.5, 'sawtooth', 0.09, -60), + }; + + // ---------- levels ---------- + function rng(seed) { + let s = (seed * 2654435761) >>> 0 || 1; + return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296); + } + const cellsIn = (r) => (r % 2 ? COLS - 1 : COLS); + const SHAPES = [ + (r) => true, + (r, c, n) => { const w = cellsIn(r); return c >= Math.floor(r / 2) && c < w - Math.floor(r / 2); }, + (r, c, n) => { const heart = ['01100110', '11111111', '11111111', '01111110', '00111100', '00011000']; const row = heart[Math.min(r, 5)]; const w = cellsIn(r); const x = Math.round(((c + (r % 2 ? 0.5 : 0)) / (w - (r % 2 ? 0 : 1))) * 7); return r < 6 && row[x] === '1'; }, + (r, c) => r % 3 !== 2, + (r, c) => c % 3 !== 1, + (r, c) => (((r >> 1) + (c >> 1)) % 2) === 0, + (r, c) => c < 3 || c > cellsIn(r) - 4 || r < 2, + (r, c, n) => { const w = cellsIn(r); return r < 2 || c === 0 || c === w - 1 || r === n - 1 || (r > 3 && Math.abs(c - (w - 1) / 2) < 1.6); }, + (r, c, n) => Math.abs(c - (cellsIn(r) - 1) / 2) <= Math.min(r, n - 1 - r) + 1, + (r, c) => (r + c) % 4 !== 3, + (r, c, n) => c < 2 || c > cellsIn(r) - 3 || (r >= Math.floor(n / 2) - 1 && r <= Math.floor(n / 2)), + (r, c) => Math.abs(((c + r) % 6) - 3) > 0, + ]; + function levelInfo(n) { + const colours = Math.min(6, 3 + Math.floor((n - 1) / 15)); + const rows = Math.min(10, 4 + Math.floor((n - 1) / 8) + (n % 3 === 0 ? 1 : 0)); + const shape = n <= 3 ? 0 : (n * 7) % SHAPES.length; + const panic = Math.max(7, 20 - ((n - 1) / 120) * 13); // seconds until the ceiling pushes down + return { colours, rows, shape, panic }; + } + function buildLevel(n) { + const info = levelInfo(n); + const rand = rng(n * 7919 + 13); + const grid = emptyGrid(); + const pick = SHAPES[info.shape]; + let count = 0; + for (let r = 0; r < info.rows; r++) { + for (let c = 0; c < cellsIn(r); c++) { + if (!pick(r, c, info.rows)) continue; + let col = Math.floor(rand() * info.colours); + // Clump colours a little so there are matches to find. + if (rand() < 0.45) { + const left = c > 0 ? grid[r][c - 1] : -1; + const up = r > 0 ? grid[r - 1][Math.min(c, cellsIn(r - 1) - 1)] : -1; + if (left >= 0 && rand() < 0.6) col = left; else if (up >= 0) col = up; + } + grid[r][c] = col; + count++; + } + } + if (count < 8) for (let r = 0; r < 3; r++) for (let c = 0; c < cellsIn(r); c++) grid[r][c] = Math.floor(rand() * info.colours); + // Keep only flowers that hang from the ceiling. + const keep = attachedSet(grid); + forEachCell(grid, (r, c) => { if (grid[r][c] >= 0 && !keep.has(key(r, c))) grid[r][c] = -1; }); + return { grid, info }; + } + + // ---------- grid helpers ---------- + function emptyGrid() { const g = []; for (let r = 0; r < MAXROWS; r++) g.push(new Array(cellsIn(r)).fill(-1)); return g; } + const key = (r, c) => r * 32 + c; + function forEachCell(g, fn) { for (let r = 0; r < MAXROWS; r++) for (let c = 0; c < cellsIn(r); c++) fn(r, c); } + function cellPos(r, c) { return { x: FX + R + c * D + (r % 2 ? R : 0), y: game.top + R + r * RH }; } + function neighbours(r, c) { + const odd = r % 2 === 1; + const list = odd + ? [[r - 1, c], [r - 1, c + 1], [r, c - 1], [r, c + 1], [r + 1, c], [r + 1, c + 1]] + : [[r - 1, c - 1], [r - 1, c], [r, c - 1], [r, c + 1], [r + 1, c - 1], [r + 1, c]]; + return list.filter(([a, b]) => a >= 0 && a < MAXROWS && b >= 0 && b < cellsIn(a)); + } + function attachedSet(g) { + const seen = new Set(), stack = []; + for (let c = 0; c < cellsIn(0); c++) if (g[0][c] >= 0) { seen.add(key(0, c)); stack.push([0, c]); } + while (stack.length) { + const [r, c] = stack.pop(); + for (const [a, b] of neighbours(r, c)) { + const k = key(a, b); + if (!seen.has(k) && g[a][b] >= 0) { seen.add(k); stack.push([a, b]); } + } + } + return seen; + } + function cluster(g, r, c) { + const col = g[r][c], seen = new Set([key(r, c)]), out = [[r, c]], stack = [[r, c]]; + while (stack.length) { + const [a, b] = stack.pop(); + for (const [x, y] of neighbours(a, b)) { + const k = key(x, y); + if (!seen.has(k) && g[x][y] === col) { seen.add(k); out.push([x, y]); stack.push([x, y]); } + } + } + return out; + } + function coloursLeft(g) { const s = new Set(); forEachCell(g, (r, c) => { if (g[r][c] >= 0) s.add(g[r][c]); }); return [...s]; } + function flowerCount(g) { let n = 0; forEachCell(g, (r, c) => { if (g[r][c] >= 0) n++; }); return n; } + + // ---------- game state ---------- + const game = { + level: 1, grid: null, info: null, top: TOP0, angle: 0, aimVel: 0, + current: 0, next: 0, shot: null, effects: [], score: 0, panic: 0, drops: 0, + swaps: 5, bombArmed: false, time: 0, bannerT: 0, banner: '', stars: 0, + }; + let screen = 'title'; + let focus = 0; + let wheel = { angle: 0, speed: 0, prize: null, spun: false }; + + function startLevel(n) { + const lv = buildLevel(n); + Object.assign(game, { + level: n, grid: lv.grid, info: lv.info, top: TOP0, angle: 0, aimVel: 0, shot: null, effects: [], + score: 0, panic: 0, drops: 0, swaps: 5, bombArmed: false, time: 0, banner: `Level ${n}`, bannerT: 1.6, + }); + game.current = randomColour(); + game.next = randomColour(); + setScreen('play'); + } + function randomColour() { + const left = coloursLeft(game.grid); + const pool = left.length ? left : [0, 1, 2]; + return pool[Math.floor(Math.random() * pool.length)]; + } + + // ---------- shooting ---------- + function fire() { + if (game.shot || screen !== 'play') return; + const a = game.angle; + game.shot = { x: LAUNCH.x, y: LAUNCH.y - 28, vx: Math.sin(a) * SHOT_SPEED, vy: -Math.cos(a) * SHOT_SPEED, colour: game.current, bomb: game.bombArmed }; + if (game.bombArmed) { game.bombArmed = false; save.bombs = Math.max(0, save.bombs - 1); persist(); } + game.current = game.next; + game.next = randomColour(); + sfx.shoot(); + } + function swap() { + if (screen !== 'play' || game.shot || game.swaps <= 0) return; + [game.current, game.next] = [game.next, game.current]; + game.swaps--; + sfx.move(); + } + function toggleBomb() { + if (screen !== 'play' || game.shot) return; + if (!game.bombArmed && save.bombs <= 0) { game.banner = 'No Honeybombs left'; game.bannerT = 1.2; return; } + game.bombArmed = !game.bombArmed; + sfx.move(); + } + + function updateShot(dt) { + const s = game.shot; + if (!s) return; + let dist = SHOT_SPEED * dt; + while (dist > 0 && game.shot) { + const step = Math.min(dist, 6); + dist -= step; + const k = step / SHOT_SPEED; + s.x += s.vx * k; s.y += s.vy * k; + if (s.x < FX + R) { s.x = FX + R + (FX + R - s.x); s.vx = Math.abs(s.vx); beep(300, 0.03, 'square', 0.02); } + if (s.x > FX2 - R) { s.x = FX2 - R - (s.x - (FX2 - R)); s.vx = -Math.abs(s.vx); beep(300, 0.03, 'square', 0.02); } + if (s.y - R <= game.top) { land(s); return; } + let hit = false; + forEachCell(game.grid, (r, c) => { + if (hit || game.grid[r][c] < 0) return; + const p = cellPos(r, c); + const dx = p.x - s.x, dy = p.y - s.y; + if (dx * dx + dy * dy < (D - 6) * (D - 6)) hit = true; + }); + if (hit) { land(s); return; } + } + } + + function land(s) { + game.shot = null; + // Choose the nearest free cell that touches the ceiling or another flower. + let best = null, bestD = Infinity; + const approxR = Math.round((s.y - game.top - R) / RH); + for (let r = Math.max(0, approxR - 1); r <= Math.min(MAXROWS - 1, approxR + 1); r++) { + for (let c = 0; c < cellsIn(r); c++) { + if (game.grid[r][c] >= 0) continue; + const supported = r === 0 || neighbours(r, c).some(([a, b]) => game.grid[a][b] >= 0); + if (!supported) continue; + const p = cellPos(r, c); + const d = (p.x - s.x) ** 2 + (p.y - s.y) ** 2; + if (d < bestD) { bestD = d; best = [r, c]; } + } + } + if (!best) { best = [0, Math.max(0, Math.min(COLS - 1, Math.round((s.x - FX - R) / D)))]; } + const [r, c] = best; + game.grid[r][c] = s.colour; + sfx.stick(); + + let popped = []; + if (s.bomb) { + const centre = cellPos(r, c); + forEachCell(game.grid, (a, b) => { + if (game.grid[a][b] < 0) return; + const p = cellPos(a, b); + if ((p.x - centre.x) ** 2 + (p.y - centre.y) ** 2 <= (D * 2.1) ** 2) popped.push([a, b]); + }); + sfx.boom(); + game.effects.push({ type: 'ring', x: centre.x, y: centre.y, t: 0 }); + } else { + const group = cluster(game.grid, r, c); + if (group.length >= 3) popped = group; + } + if (popped.length) { + for (const [a, b] of popped) { + const p = cellPos(a, b); + game.effects.push({ type: 'pop', x: p.x, y: p.y, colour: game.grid[a][b], t: 0 }); + game.grid[a][b] = -1; + } + game.score += popped.length * 10; + sfx.pop(popped.length); + // Anything no longer hanging from the ceiling falls away. + const keep = attachedSet(game.grid); + let fallen = 0; + forEachCell(game.grid, (a, b) => { + if (game.grid[a][b] >= 0 && !keep.has(key(a, b))) { + const p = cellPos(a, b); + game.effects.push({ type: 'fall', x: p.x, y: p.y, vx: (Math.random() - 0.5) * 160, vy: -120 - Math.random() * 120, rot: 0, vr: (Math.random() - 0.5) * 8, colour: game.grid[a][b], t: 0 }); + game.grid[a][b] = -1; + fallen++; + } + }); + if (fallen) { + const bonus = 20 * fallen * fallen; + game.score += bonus; + game.effects.push({ type: 'text', x: s.x, y: s.y, text: `+${bonus}`, t: 0 }); + sfx.drop(); + } + } + if (flowerCount(game.grid) === 0) return win(); + checkBreach(); + if (!coloursLeft(game.grid).includes(game.current)) game.current = randomColour(); + if (!coloursLeft(game.grid).includes(game.next)) game.next = randomColour(); + } + + function checkBreach() { + let breached = false; + forEachCell(game.grid, (r, c) => { if (game.grid[r][c] >= 0 && cellPos(r, c).y + R > DEADLINE) breached = true; }); + if (breached) lose(); + } + + function win() { + game.score += 1000 + Math.round((1 - game.panic) * 500); + game.stars = game.drops <= 1 ? 3 : game.drops <= 3 ? 2 : 1; + save.stars[game.level] = Math.max(save.stars[game.level] || 0, game.stars); + save.best[game.level] = Math.max(save.best[game.level] || 0, game.score); + save.unlocked = Math.max(save.unlocked, Math.min(LEVELS, game.level + 1)); + persist(); + sfx.win(); + setScreen('win'); + } + function lose() { sfx.lose(); setScreen('lose'); } + + // ---------- menus ---------- + function buttonsFor(name) { + const B = (label, x, y, w, h, act, extra) => Object.assign({ label, x, y, w, h, act }, extra || {}); + switch (name) { + case 'title': { + const list = [B('PLAY', 520, 330, 240, 64, () => startLevel(Math.min(save.unlocked, LEVELS))), B('LEVELS', 520, 410, 240, 64, () => setScreen('levels')), B('HELP & SETTINGS', 520, 490, 240, 64, () => setScreen('help'))]; + if (save.lastSpin !== today()) list.push(B('DAILY PRIZE', 520, 570, 240, 64, () => { wheel = { angle: wheel.angle, speed: 0, prize: null, spun: false }; setScreen('daily'); }, { glow: true })); + return list; + } + case 'levels': { + const list = []; + const page = save.page; + for (let i = 0; i < PER_PAGE; i++) { + const n = page * PER_PAGE + i + 1; + if (n > LEVELS) break; + const col = i % 5, row = Math.floor(i / 5); + list.push(B(String(n), 400 + col * 110 + (row % 2 ? 55 : 0) - 27, 170 + row * 100, 100, 92, () => { if (n <= save.unlocked) startLevel(n); else sfx.lose(); }, { hex: true, level: n })); + } + list.push(B('◀', 260, 330, 80, 80, () => { save.page = Math.max(0, save.page - 1); persist(); refreshButtons(); }, { round: true })); + list.push(B('▶', 940, 330, 80, 80, () => { save.page = Math.min(Math.ceil(LEVELS / PER_PAGE) - 1, save.page + 1); persist(); refreshButtons(); }, { round: true })); + list.push(B('BACK', 540, 600, 200, 56, () => setScreen('title'))); + return list; + } + case 'help': + return [ + B(`SKIN: ${save.skin === 'orbit' ? 'ORBIT' : 'CLASSIC'}`, 210, 560, 260, 56, () => { save.skin = save.skin === 'orbit' ? 'classic' : 'orbit'; skyCache = null; persist(); refreshButtons(); }), + B(`GUIDE LINE: ${save.guide ? 'ON' : 'OFF'}`, 510, 560, 260, 56, () => { save.guide = !save.guide; persist(); refreshButtons(); }), + B(`SOUND: ${save.sound ? 'ON' : 'OFF'}`, 810, 560, 260, 56, () => { save.sound = !save.sound; persist(); refreshButtons(); }), + B('BACK', 540, 632, 200, 56, () => setScreen('title')), + ]; + case 'pause': + return [B('RESUME', 520, 300, 240, 60, () => setScreen('play')), B('RESTART', 520, 375, 240, 60, () => startLevel(game.level)), B('LEVELS', 520, 450, 240, 60, () => setScreen('levels')), B('MENU', 520, 525, 240, 60, () => setScreen('title'))]; + case 'win': + return [ + B('NEXT LEVEL', 520, 430, 240, 60, () => startLevel(Math.min(LEVELS, game.level + 1))), + B('REPLAY', 520, 505, 240, 60, () => startLevel(game.level)), + B('LEVELS', 520, 580, 240, 60, () => setScreen('levels')), + ]; + case 'lose': + return [B('TRY AGAIN', 520, 430, 240, 60, () => startLevel(game.level)), B('LEVELS', 520, 505, 240, 60, () => setScreen('levels')), B('MENU', 520, 580, 240, 60, () => setScreen('title'))]; + case 'daily': + return wheel.spun + ? [B('COLLECT', 520, 610, 240, 60, () => setScreen('title'))] + : [B('SPIN', 520, 610, 240, 60, spinWheel)]; + default: return []; + } + } + let buttons = []; + function refreshButtons() { buttons = buttonsFor(screen); focus = Math.min(focus, Math.max(0, buttons.length - 1)); } + function setScreen(name) { + screen = name; + buttons = buttonsFor(name); + focus = 0; + if (name === 'levels') { + const want = Math.min(save.unlocked, LEVELS); + const idx = buttons.findIndex((b) => b.level === want); + if (idx >= 0) focus = idx; else { save.page = Math.floor((want - 1) / PER_PAGE); buttons = buttonsFor(name); focus = Math.max(0, buttons.findIndex((b) => b.level === want)); } + } + } + function moveFocus(dx, dy) { + if (!buttons.length) return; + const cur = buttons[focus]; + const cx = cur.x + cur.w / 2, cy = cur.y + cur.h / 2; + let best = -1, bestScore = Infinity; + buttons.forEach((b, i) => { + if (i === focus) return; + const bx = b.x + b.w / 2 - cx, by = b.y + b.h / 2 - cy; + const along = bx * dx + by * dy; + if (along <= 4) return; + const across = Math.abs(bx * dy - by * dx); + const score = along + across * 2.2; + if (score < bestScore) { bestScore = score; best = i; } + }); + if (best >= 0) { focus = best; sfx.move(); } + } + + // ---------- daily prize wheel ---------- + const PRIZES = [1, 2, 1, 3, 1, 2, 1, 5]; + function spinWheel() { + if (wheel.speed > 0 || wheel.spun) return; + wheel.speed = 14 + Math.random() * 6; + sfx.shoot(); + } + function updateWheel(dt) { + if (wheel.speed <= 0) return; + wheel.angle += wheel.speed * dt; + wheel.speed = Math.max(0, wheel.speed - 6 * dt); + if (Math.floor(wheel.angle * 8 / (Math.PI * 2)) !== Math.floor((wheel.angle - wheel.speed * dt) * 8 / (Math.PI * 2))) beep(1200, 0.02, 'square', 0.02); + if (wheel.speed === 0) { + const seg = PRIZES.length; + // The pointer sits at the top of the wheel; find which segment is under it. + const a = (((-Math.PI / 2 - wheel.angle) % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2); + const idx = Math.floor(a / ((Math.PI * 2) / seg)) % seg; + wheel.prize = PRIZES[idx]; + wheel.spun = true; + save.bombs += wheel.prize; + save.lastSpin = today(); + persist(); + sfx.win(); + refreshButtons(); + } + } + + // ---------- input ---------- + const held = { left: false, right: false }; + let heldTime = 0; + function onAction(action) { + if (screen === 'play') { + if (action === 'ok') fire(); + else if (action === 'down') swap(); + else if (action === 'up') toggleBomb(); + else if (action === 'back') setScreen('pause'); + return; + } + if (action === 'left') moveFocus(-1, 0); + else if (action === 'right') moveFocus(1, 0); + else if (action === 'up') moveFocus(0, -1); + else if (action === 'down') moveFocus(0, 1); + else if (action === 'ok') { const b = buttons[focus]; if (b) b.act(); } + else if (action === 'back') { + if (screen === 'pause') setScreen('play'); + else if (screen === 'levels' || screen === 'help' || screen === 'daily') setScreen('title'); + else if (screen === 'win' || screen === 'lose') setScreen('levels'); + else if (screen === 'title' && window.OrbitApp) window.OrbitApp.leave(); + } + } + function keyAction(e) { + const k = e.key, code = e.keyCode; + if (k === 'ArrowLeft' || code === 21) return 'left'; + if (k === 'ArrowRight' || code === 22) return 'right'; + if (k === 'ArrowUp' || code === 19) return 'up'; + if (k === 'ArrowDown' || code === 20) return 'down'; + if (k === 'Enter' || k === ' ' || k === 'Select' || code === 23 || code === 66 || code === 13) return 'ok'; + if (k === 'Escape' || k === 'Backspace' || k === 'GoBack' || k === 'BrowserBack' || code === 4 || code === 27 || code === 8) return 'back'; + if (k === 'MediaPlayPause' || k === 'ContextMenu' || k === 'p' || code === 179 || code === 82) return 'pause'; + if (k === 's') return 'down'; + if (k === 'b') return 'up'; + return null; + } + addEventListener('keydown', (e) => { + const a = keyAction(e); + if (!a) return; + e.preventDefault(); + if (a === 'pause') { if (screen === 'play') setScreen('pause'); else if (screen === 'pause') setScreen('play'); return; } + if (screen === 'play' && (a === 'left' || a === 'right')) { + if (!held[a]) { held[a] = true; heldTime = 0; game.angle += (a === 'left' ? -1 : 1) * 0.012; } + return; + } + if (e.repeat && a === 'ok') return; + onAction(a); + }); + addEventListener('keyup', (e) => { + const a = keyAction(e); + if (a === 'left' || a === 'right') held[a] = false; + }); + addEventListener('blur', () => { held.left = held.right = false; if (screen === 'play') setScreen('pause'); }); + + function toGame(ev) { + const rect = canvas.getBoundingClientRect(); + return { x: ((ev.clientX - rect.left) * dpr - ox) / scale, y: ((ev.clientY - rect.top) * dpr - oy) / scale }; + } + function hitButton(p) { return buttons.findIndex((b) => p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h); } + function aimAt(p) { + if (p.y > LAUNCH.y - 10) return; + game.angle = clampAngle(Math.atan2(p.x - LAUNCH.x, LAUNCH.y - p.y)); + } + let pointerDown = false; + canvas.addEventListener('pointermove', (ev) => { + const p = toGame(ev); + if (screen === 'play') { if (ev.pointerType === 'mouse' || pointerDown) aimAt(p); return; } + const i = hitButton(p); + if (i >= 0 && i !== focus) focus = i; + }); + canvas.addEventListener('pointerdown', (ev) => { + canvas.focus(); + pointerDown = true; + const p = toGame(ev); + if (screen === 'play') { + if (hudHit(p)) return; + aimAt(p); + if (ev.pointerType === 'mouse') fire(); + return; + } + const i = hitButton(p); + if (i >= 0) { focus = i; buttons[i].act(); } + }); + canvas.addEventListener('pointerup', (ev) => { + if (screen === 'play' && pointerDown && ev.pointerType !== 'mouse') { + const p = toGame(ev); + if (!hudHit(p) && p.y < LAUNCH.y - 10) fire(); + } + pointerDown = false; + }); + function hudHit(p) { + if (p.y < 668) { + if (p.x > 1150 && p.y < 80) { setScreen('pause'); return true; } + return false; + } + if (p.x > 860 && p.x < 990) { swap(); return true; } + if (p.x > 990 && p.x < 1130) { toggleBomb(); return true; } + if (p.x > 1150) { setScreen('pause'); return true; } + return false; + } + + // Game pads (Android TV controllers). + const padPrev = {}; + function pollPads() { + const pads = navigator.getGamepads ? navigator.getGamepads() : []; + for (const pad of pads) { + if (!pad) continue; + const btn = (i) => !!(pad.buttons[i] && pad.buttons[i].pressed); + const ax = pad.axes[0] || 0; + const state = { + left: btn(14) || ax < -0.5, right: btn(15) || ax > 0.5, up: btn(12) || btn(3), down: btn(13) || btn(2), + ok: btn(0) || btn(7), back: btn(1), pause: btn(9), + }; + for (const k in state) { + const was = padPrev[pad.index + k]; + if (state[k] && !was) { + if (screen === 'play' && (k === 'left' || k === 'right')) { held[k] = true; heldTime = 0; } + else if (k === 'pause') onAction(screen === 'play' ? 'back' : 'ok'); + else onAction(k); + } + if (!state[k] && was && (k === 'left' || k === 'right')) held[k] = false; + padPrev[pad.index + k] = state[k]; + } + } + } + + // ---------- update ---------- + const MAX_ANGLE = 1.38; + const clampAngle = (a) => Math.max(-MAX_ANGLE, Math.min(MAX_ANGLE, a)); + function update(dt) { + pollPads(); + game.time += dt; + if (screen === 'daily') updateWheel(dt); + if (screen !== 'play') { updateEffects(dt); return; } + // Aim with weight: slow at first, faster the longer the button is held. + const dir = (held.right ? 1 : 0) - (held.left ? 1 : 0); + if (dir) { + heldTime += dt; + const speed = 0.45 + Math.min(1, heldTime / 0.7) * 1.35; + game.angle = clampAngle(game.angle + dir * speed * dt); + } else heldTime = 0; + + updateShot(dt); + if (screen !== 'play') return; + + // Panic bar: when it fills, the ceiling pushes down one row. + game.panic += dt / game.info.panic; + if (game.panic >= 1) { + game.panic = 0; + game.top += RH; + game.drops++; + game.effects.push({ type: 'shake', t: 0 }); + sfx.lid(); + checkBreach(); + } + if (game.bannerT > 0) game.bannerT -= dt; + updateEffects(dt); + } + function updateEffects(dt) { + for (const f of game.effects) { + f.t += dt; + if (f.type === 'fall') { f.vy += 1400 * dt; f.x += f.vx * dt; f.y += f.vy * dt; f.rot += f.vr * dt; } + } + game.effects = game.effects.filter((f) => (f.type === 'fall' ? f.y < H + 60 : f.t < (f.type === 'text' ? 0.9 : f.type === 'ring' ? 0.5 : f.type === 'shake' ? 0.3 : 0.35))); + } + + // ---------- drawing ---------- + let skyCache = null; + // The Orbit skin: deep space, a ringed planet and a slow twinkle, kept dark so the flowers stand out. + const STARS = Array.from({ length: 150 }, (_, i) => ({ x: (i * 397) % W, y: (i * 211) % H, r: 0.6 + (i % 4) * 0.5, phase: i * 0.7 })); + function drawSpace() { + if (!skyCache) { + skyCache = document.createElement('canvas'); + skyCache.width = W; skyCache.height = H; + const g = skyCache.getContext('2d'); + const grad = g.createLinearGradient(0, 0, W, H); + grad.addColorStop(0, '#040a1e'); grad.addColorStop(0.6, '#0b1538'); grad.addColorStop(1, '#2a1760'); + g.fillStyle = grad; g.fillRect(0, 0, W, H); + const nebula = g.createRadialGradient(1080, 180, 20, 1080, 180, 460); + nebula.addColorStop(0, 'rgba(160,80,255,.22)'); nebula.addColorStop(1, 'rgba(160,80,255,0)'); + g.fillStyle = nebula; g.fillRect(0, 0, W, H); + const planet = g.createRadialGradient(1090, 590, 18, 1130, 620, 150); + planet.addColorStop(0, '#6c8cff'); planet.addColorStop(0.7, '#3a3fa8'); planet.addColorStop(1, '#1a1650'); + g.save(); g.translate(1130, 620); g.rotate(-0.24); + g.strokeStyle = 'rgba(255,216,74,.4)'; g.lineWidth = 7; + g.beginPath(); g.ellipse(0, 0, 215, 44, 0, Math.PI, Math.PI * 2); g.stroke(); + g.rotate(0.24); g.fillStyle = planet; g.beginPath(); g.arc(0, 0, 120, 0, Math.PI * 2); g.fill(); g.rotate(-0.24); + g.beginPath(); g.ellipse(0, 0, 215, 44, 0, 0, Math.PI); g.stroke(); + g.restore(); + g.fillStyle = '#c9d4e8'; g.beginPath(); g.arc(150, 140, 34, 0, Math.PI * 2); g.fill(); + g.fillStyle = '#aab6cc'; g.beginPath(); g.arc(140, 130, 7, 0, Math.PI * 2); g.arc(163, 150, 5, 0, Math.PI * 2); g.fill(); + } + ctx.drawImage(skyCache, 0, 0); + for (const s of STARS) { + ctx.fillStyle = `rgba(255,255,255,${0.35 + 0.35 * Math.sin(game.time * 1.4 + s.phase)})`; + ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill(); + } + } + + function drawSky() { + if (save.skin === 'orbit') { drawSpace(); return; } + if (!skyCache) { + skyCache = document.createElement('canvas'); + skyCache.width = W; skyCache.height = H; + const g = skyCache.getContext('2d'); + const grad = g.createLinearGradient(0, 0, 0, H); + grad.addColorStop(0, '#3d8fd6'); grad.addColorStop(0.7, '#8fd0f5'); grad.addColorStop(1, '#bfe6f7'); + g.fillStyle = grad; g.fillRect(0, 0, W, H); + g.fillStyle = '#6cbf43'; + g.beginPath(); g.moveTo(0, 560); g.quadraticCurveTo(250, 480, 520, 560); g.quadraticCurveTo(800, 640, 1280, 520); g.lineTo(1280, 720); g.lineTo(0, 720); g.fill(); + g.fillStyle = '#4f9e32'; + g.beginPath(); g.moveTo(0, 640); g.quadraticCurveTo(400, 590, 760, 650); g.quadraticCurveTo(1000, 690, 1280, 620); g.lineTo(1280, 720); g.lineTo(0, 720); g.fill(); + for (let i = 0; i < 26; i++) { + const x = (i * 97) % W, y = 600 + ((i * 53) % 100); + g.fillStyle = ['#ffd54a', '#ffffff', '#e57373'][i % 3]; + g.beginPath(); g.arc(x, y, 5, 0, Math.PI * 2); g.fill(); + } + } + ctx.drawImage(skyCache, 0, 0); + // Turning sun rays. + ctx.save(); + ctx.translate(170, 120); + ctx.rotate(game.time * 0.05); + ctx.fillStyle = 'rgba(255,246,201,.28)'; + for (let i = 0; i < 14; i++) { + ctx.rotate((Math.PI * 2) / 14); + ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(1400, -110); ctx.lineTo(1400, 110); ctx.fill(); + } + ctx.restore(); + ctx.fillStyle = '#ffd54a'; + ctx.beginPath(); ctx.arc(170, 120, 62, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = '#f2a900'; + ctx.beginPath(); ctx.arc(150, 112, 6, 0, Math.PI * 2); ctx.arc(190, 112, 6, 0, Math.PI * 2); ctx.fill(); + ctx.strokeStyle = '#f2a900'; ctx.lineWidth = 4; + ctx.beginPath(); ctx.arc(170, 128, 22, 0.2 * Math.PI, 0.8 * Math.PI); ctx.stroke(); + } + + function hexPath(x, y, r) { + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const a = Math.PI / 6 + (i * Math.PI) / 3; + const px = x + Math.cos(a) * r, py = y + Math.sin(a) * r; + if (i) ctx.lineTo(px, py); else ctx.moveTo(px, py); + } + ctx.closePath(); + } + + function drawHive() { + // Wooden frame. + ctx.fillStyle = '#7a4a22'; + roundRect(FX - 26, 8, FW + 52, 668, 18); ctx.fill(); + const wood = ctx.createLinearGradient(FX - 26, 0, FX2 + 26, 0); + wood.addColorStop(0, '#9b6531'); wood.addColorStop(0.5, '#c48848'); wood.addColorStop(1, '#9b6531'); + ctx.fillStyle = wood; + roundRect(FX - 20, 14, FW + 40, 656, 14); ctx.fill(); + // Honeycomb interior. + ctx.fillStyle = '#1d1209'; + ctx.fillRect(FX, TOP0 - 6, FW, 680 - TOP0 - 6 - 8); + ctx.save(); + ctx.beginPath(); ctx.rect(FX, TOP0 - 6, FW, 680 - TOP0 - 14); ctx.clip(); + ctx.strokeStyle = '#4a2f16'; ctx.lineWidth = 2; + ctx.fillStyle = '#2a1a0e'; + for (let r = -1; r < 16; r++) { + for (let c = 0; c < 12; c++) { + const x = FX + R + c * D + (Math.abs(r) % 2 ? R : 0), y = TOP0 + R + r * RH; + hexPath(x, y, R + 1); ctx.fill(); ctx.stroke(); + } + } + ctx.restore(); + // Lid that pushes down. + const lidH = game.top - TOP0; + const lid = ctx.createLinearGradient(0, 14, 0, game.top); + lid.addColorStop(0, '#b77b3e'); lid.addColorStop(1, '#8a5a2b'); + ctx.fillStyle = lid; + ctx.fillRect(FX - 6, 14, FW + 12, game.top - 14); + ctx.fillStyle = '#6d4220'; + ctx.fillRect(FX - 6, game.top - 6, FW + 12, 6); + ctx.strokeStyle = 'rgba(0,0,0,.18)'; ctx.lineWidth = 2; + for (let y = 30; y < game.top - 8; y += 18) { ctx.beginPath(); ctx.moveTo(FX, y); ctx.lineTo(FX2, y); ctx.stroke(); } + if (lidH > 0) { + ctx.fillStyle = 'rgba(255,255,255,.18)'; + ctx.font = `700 18px ${FONT}`; ctx.textAlign = 'center'; + ctx.fillText('▼'.repeat(Math.min(game.drops, 8)), W / 2, game.top - 14); + } + // Launcher line. + ctx.strokeStyle = 'rgba(255,213,74,.35)'; ctx.setLineDash([10, 8]); ctx.lineWidth = 2; + ctx.beginPath(); ctx.moveTo(FX, DEADLINE); ctx.lineTo(FX2, DEADLINE); ctx.stroke(); + ctx.setLineDash([]); + } + + function roundRect(x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); + } + + function drawGrid() { + forEachCell(game.grid, (r, c) => { + const col = game.grid[r][c]; + if (col < 0) return; + const p = cellPos(r, c); + drawFlower(p.x, p.y, col); + }); + } + + function guidePoints() { + const pts = [{ x: LAUNCH.x, y: LAUNCH.y - 28 }]; + let x = LAUNCH.x, y = LAUNCH.y - 28, vx = Math.sin(game.angle), vy = -Math.cos(game.angle); + for (let i = 0; i < 700; i++) { + x += vx * 4; y += vy * 4; + if (x < FX + R) { x = FX + R; vx = -vx; pts.push({ x, y }); } + if (x > FX2 - R) { x = FX2 - R; vx = -vx; pts.push({ x, y }); } + if (y - R <= game.top) break; + let hit = false; + forEachCell(game.grid, (r, c) => { + if (hit || game.grid[r][c] < 0) return; + const p = cellPos(r, c); + if ((p.x - x) ** 2 + (p.y - y) ** 2 < (D - 6) ** 2) hit = true; + }); + if (hit) break; + } + pts.push({ x, y }); + return pts; + } + + function drawLauncher() { + if (save.guide && !game.shot) { + const pts = guidePoints(); + ctx.save(); + ctx.strokeStyle = 'rgba(255,240,180,.75)'; ctx.lineWidth = 3; ctx.setLineDash([4, 12]); + ctx.lineDashOffset = -game.time * 40; + ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); + for (const p of pts.slice(1)) ctx.lineTo(p.x, p.y); + ctx.stroke(); + ctx.restore(); + } + // Pendulum launcher. + ctx.save(); + ctx.translate(LAUNCH.x, LAUNCH.y); + ctx.rotate(game.angle); + ctx.strokeStyle = '#e8a33a'; ctx.lineWidth = 5; ctx.lineJoin = 'round'; + ctx.beginPath(); ctx.moveTo(-16, 8); ctx.lineTo(0, -70); ctx.lineTo(16, 8); ctx.stroke(); + ctx.fillStyle = '#ffd54a'; + ctx.beginPath(); ctx.arc(0, -70, 5, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + if (!game.shot) { + if (game.bombArmed) drawBomb(LAUNCH.x, LAUNCH.y - 28, 1 + Math.sin(game.time * 8) * 0.05); + else drawFlower(LAUNCH.x, LAUNCH.y - 28, game.current); + } + drawBee(LAUNCH.x - 92, LAUNCH.y - 8 + Math.sin(game.time * 3) * 4, 1.15, game.angle * 0.3); + } + + function drawBomb(x, y, s = 1) { + ctx.save(); + ctx.translate(x, y); ctx.scale(s, s); + ctx.fillStyle = '#f2a900'; + hexPath(0, 0, R); ctx.fill(); + ctx.strokeStyle = '#7a4400'; ctx.lineWidth = 3; ctx.stroke(); + ctx.fillStyle = '#7a4400'; + ctx.font = `700 22px ${FONT}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText('✸', 0, 1); + ctx.restore(); + } + + function drawBee(x, y, s, tilt = 0) { + ctx.save(); + ctx.translate(x, y); ctx.scale(s, s); ctx.rotate(tilt); + const flap = Math.sin(game.time * 30) * 0.25; + ctx.fillStyle = 'rgba(227,242,253,.85)'; + ctx.save(); ctx.rotate(-0.5 + flap); ctx.beginPath(); ctx.ellipse(-12, -22, 16, 10, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); + ctx.save(); ctx.rotate(0.5 - flap); ctx.beginPath(); ctx.ellipse(12, -22, 16, 10, 0, 0, Math.PI * 2); ctx.fill(); ctx.restore(); + ctx.fillStyle = '#ffcc1a'; ctx.strokeStyle = '#3b2a12'; ctx.lineWidth = 3; + ctx.beginPath(); ctx.ellipse(0, 0, 22, 26, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + ctx.save(); ctx.beginPath(); ctx.ellipse(0, 0, 22, 26, 0, 0, Math.PI * 2); ctx.clip(); + ctx.fillStyle = '#3b2a12'; ctx.fillRect(-24, 4, 48, 6); ctx.fillRect(-24, 15, 48, 6); + ctx.restore(); + ctx.fillStyle = '#4fc3f7'; ctx.strokeStyle = '#1a237e'; ctx.lineWidth = 2.5; + ctx.beginPath(); ctx.arc(-8, -10, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + ctx.beginPath(); ctx.arc(8, -10, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + ctx.fillStyle = '#fff'; + ctx.beginPath(); ctx.arc(-10, -12, 2, 0, Math.PI * 2); ctx.arc(6, -12, 2, 0, Math.PI * 2); ctx.fill(); + ctx.strokeStyle = '#3b2a12'; ctx.lineWidth = 2; + ctx.beginPath(); ctx.moveTo(-5, -25); ctx.quadraticCurveTo(-10, -40, -18, -38); ctx.moveTo(5, -25); ctx.quadraticCurveTo(10, -40, 18, -38); ctx.stroke(); + ctx.beginPath(); ctx.arc(0, -1, 6, 0.15 * Math.PI, 0.85 * Math.PI); ctx.stroke(); + if (save.skin === 'orbit') { + // An astronaut's helmet over the head. + ctx.fillStyle = 'rgba(191,227,255,.2)'; ctx.strokeStyle = '#e8edf5'; ctx.lineWidth = 3.5; + ctx.beginPath(); ctx.arc(0, -10, 21, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + ctx.fillStyle = 'rgba(255,255,255,.55)'; ctx.beginPath(); ctx.ellipse(-8, -20, 6, 3.5, -0.6, 0, Math.PI * 2); ctx.fill(); + ctx.strokeStyle = '#b7cbe8'; ctx.lineWidth = 5; ctx.beginPath(); ctx.arc(0, -10, 21, 0.28 * Math.PI, 0.72 * Math.PI); ctx.stroke(); + } + ctx.restore(); + } + + function drawEffects() { + for (const f of game.effects) { + if (f.type === 'pop') { + const k = f.t / 0.35; + drawFlower(f.x, f.y, f.colour, 1 + k * 0.8, 1 - k); + ctx.fillStyle = COLOURS[f.colour].petal; + for (let i = 0; i < 6; i++) { + const a = (i / 6) * Math.PI * 2; + ctx.globalAlpha = 1 - k; + ctx.beginPath(); ctx.arc(f.x + Math.cos(a) * k * 50, f.y + Math.sin(a) * k * 50, 6 * (1 - k), 0, Math.PI * 2); ctx.fill(); + } + ctx.globalAlpha = 1; + } else if (f.type === 'fall') { + drawFlower(f.x, f.y, f.colour, 1, 1, f.rot); + } else if (f.type === 'text') { + ctx.globalAlpha = 1 - f.t / 0.9; + ctx.font = `700 30px ${FONT}`; ctx.textAlign = 'center'; + ctx.lineWidth = 5; ctx.strokeStyle = '#7a4400'; ctx.strokeText(f.text, f.x, f.y - f.t * 60); + ctx.fillStyle = '#ffd54a'; ctx.fillText(f.text, f.x, f.y - f.t * 60); + ctx.globalAlpha = 1; + } else if (f.type === 'ring') { + ctx.strokeStyle = `rgba(255,213,74,${1 - f.t / 0.5})`; ctx.lineWidth = 10; + ctx.beginPath(); ctx.arc(f.x, f.y, 20 + f.t * 300, 0, Math.PI * 2); ctx.stroke(); + } + } + } + + function text(str, x, y, size, colour = '#fff', align = 'center', weight = 700, stroke) { + ctx.font = `${weight} ${size}px ${FONT}`; + ctx.textAlign = align; ctx.textBaseline = 'alphabetic'; + if (stroke) { ctx.lineWidth = size / 6; ctx.strokeStyle = stroke; ctx.lineJoin = 'round'; ctx.strokeText(str, x, y); } + ctx.fillStyle = colour; ctx.fillText(str, x, y); + } + + function panel(x, y, w, h) { + ctx.fillStyle = 'rgba(70,40,15,.82)'; + roundRect(x, y, w, h, 16); ctx.fill(); + ctx.strokeStyle = '#f2a900'; ctx.lineWidth = 3; ctx.stroke(); + } + + function drawHud() { + // Bottom bar. + ctx.fillStyle = '#a86d34'; + roundRect(FX - 20, 668, FW + 40, 46, 10); ctx.fill(); + ctx.fillStyle = '#2a1a0e'; + roundRect(W / 2 - 90, 676, 180, 32, 6); ctx.fill(); + text(String(game.score).padStart(6, '0'), W / 2, 701, 24, '#fff', 'center', 700); + // Next flower. + text('NEXT', FX + 14, 699, 15, '#fff3c4', 'left', 700); + drawFlower(FX + 86, 691, game.next, 0.62); + // Swap and Honeybomb counters. + ctx.fillStyle = 'rgba(0,0,0,.25)'; + roundRect(FX2 - 196, 674, 92, 34, 8); ctx.fill(); + roundRect(FX2 - 98, 674, 92, 34, 8); ctx.fill(); + text(`⇅ ${game.swaps}`, FX2 - 150, 699, 18, '#fff', 'center', 700); + ctx.save(); ctx.translate(FX2 - 78, 691); ctx.scale(0.5, 0.5); drawBomb(0, 0, game.bombArmed ? 1.2 : 1); ctx.restore(); + text(`×${save.bombs}`, FX2 - 38, 699, 18, game.bombArmed ? '#ffd54a' : '#fff', 'center', 700); + + // Left panel: level, stars challenge, panic bar. + panel(80, 200, 250, 330); + text(`LEVEL ${game.level}`, 205, 248, 30, '#ffd54a', 'center', 700, '#7a4400'); + const target = game.drops <= 1 ? 3 : game.drops <= 3 ? 2 : 1; + text('★'.repeat(target) + '☆'.repeat(3 - target), 205, 292, 30, '#ffd54a'); + text('Star challenge: clear before', 205, 322, 14, '#fff3c4', 'center', 500); + text('the ceiling drops twice', 205, 340, 14, '#fff3c4', 'center', 500); + text('CEILING', 205, 380, 16, '#fff', 'center', 700); + ctx.fillStyle = '#2a1a0e'; + roundRect(110, 394, 190, 30, 15); ctx.fill(); + const pc = game.panic; + const barCol = pc > 0.8 ? (Math.floor(game.time * 8) % 2 ? '#e53935' : '#ff7043') : pc > 0.55 ? '#f5a623' : '#7cc242'; + ctx.fillStyle = barCol; + roundRect(114, 398, Math.max(22, 182 * pc), 22, 11); ctx.fill(); + text(`Drops: ${game.drops}`, 205, 460, 18, '#fff', 'center', 500); + text(`Best: ${save.best[game.level] || 0}`, 205, 494, 18, '#fff', 'center', 500); + + // Right panel: remote help. + panel(950, 200, 250, 330); + text('REMOTE', 1075, 244, 22, '#ffd54a', 'center', 700, '#7a4400'); + const rows = [['◀ ▶', 'Aim'], ['OK', 'Fire'], ['▼', 'Swap flower'], ['▲', 'Honeybomb'], ['BACK', 'Pause']]; + rows.forEach(([k, v], i) => { + ctx.fillStyle = 'rgba(255,255,255,.14)'; + roundRect(972, 268 + i * 50, 76, 34, 8); ctx.fill(); + text(k, 1010, 292 + i * 50, 16, '#fff', 'center', 700); + text(v, 1062, 292 + i * 50, 17, '#fff3c4', 'left', 500); + }); + // Pause button (touch). + ctx.fillStyle = 'rgba(0,0,0,.25)'; + roundRect(1176, 16, 84, 48, 12); ctx.fill(); + text('II', 1218, 50, 26, '#fff'); + + if (game.bannerT > 0) { + ctx.globalAlpha = Math.min(1, game.bannerT); + text(game.banner, W / 2, 360, 54, '#ffd54a', 'center', 700, '#7a4400'); + ctx.globalAlpha = 1; + } + } + + function drawButtons() { + buttons.forEach((b, i) => { + const on = i === focus; + ctx.save(); + if (b.hex) { + const locked = b.level > save.unlocked; + const cx = b.x + b.w / 2, cy = b.y + b.h / 2 - 8; + ctx.fillStyle = locked ? '#9ea7ad' : '#f7a823'; + hexPath(cx, cy, on ? 44 : 40); ctx.fill(); + ctx.lineWidth = on ? 6 : 3; ctx.strokeStyle = on ? '#ffffff' : locked ? '#59636a' : '#a35a00'; ctx.stroke(); + text(locked ? '🔒' : b.label, cx, cy + 10, locked ? 22 : 28, '#fff', 'center', 700, locked ? null : '#a35a00'); + const st = save.stars[b.level] || 0; + if (!locked) text('★'.repeat(st) + '☆'.repeat(3 - st), cx, cy + 50, 18, '#ffd54a', 'center', 700, '#7a4400'); + } else if (b.round) { + ctx.fillStyle = on ? '#ffd54a' : '#f7a823'; + ctx.beginPath(); ctx.arc(b.x + b.w / 2, b.y + b.h / 2, b.w / 2, 0, Math.PI * 2); ctx.fill(); + ctx.lineWidth = on ? 6 : 3; ctx.strokeStyle = on ? '#fff' : '#a35a00'; ctx.stroke(); + text(b.label, b.x + b.w / 2, b.y + b.h / 2 + 12, 32, '#fff', 'center', 700, '#a35a00'); + } else { + if (on) { ctx.shadowColor = 'rgba(255,255,255,.9)'; ctx.shadowBlur = 24; } + const g = ctx.createLinearGradient(0, b.y, 0, b.y + b.h); + g.addColorStop(0, on ? '#ffd27a' : '#fbbf47'); g.addColorStop(1, on ? '#f7a823' : '#e08c10'); + ctx.fillStyle = g; + roundRect(b.x, b.y, b.w, b.h, b.h / 2); ctx.fill(); + ctx.shadowBlur = 0; + ctx.lineWidth = on ? 5 : 3; ctx.strokeStyle = on ? '#ffffff' : '#a35a00'; ctx.stroke(); + if (b.glow) { ctx.globalAlpha = 0.5 + Math.sin(game.time * 5) * 0.3; ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; roundRect(b.x - 6, b.y - 6, b.w + 12, b.h + 12, b.h / 2 + 6); ctx.stroke(); ctx.globalAlpha = 1; } + text(b.label, b.x + b.w / 2, b.y + b.h / 2 + b.h * 0.15, Math.round(b.h * 0.4), '#fff', 'center', 700, '#a35a00'); + } + ctx.restore(); + }); + } + + function drawTitle() { + drawSky(); + const bob = Math.sin(game.time * 2) * 6; + text('Orbit', W / 2, 150 + bob, 110, '#ffc21a', 'center', 700, '#8a4b00'); + text('Hive', W / 2, 260 + bob, 110, '#ffc21a', 'center', 700, '#8a4b00'); + drawBee(900, 210 + Math.sin(game.time * 3) * 10, 2.4, Math.sin(game.time) * 0.1); + for (let i = 0; i < 5; i++) drawFlower(250 + i * 40, 620 + Math.sin(game.time * 2 + i) * 4, i % 5, 1); + text(`Honeybombs: ${save.bombs} • Levels unlocked: ${save.unlocked}/${LEVELS}`, W / 2, 700, 18, '#fff', 'center', 500, 'rgba(0,0,0,.35)'); + drawButtons(); + } + + function drawLevels() { + drawSky(); + text('Level Select', W / 2, 100, 64, '#ffc21a', 'center', 700, '#8a4b00'); + const pages = Math.ceil(LEVELS / PER_PAGE); + let stars = 0; for (const k in save.stars) stars += save.stars[k]; + text(`Page ${save.page + 1} of ${pages} • ★ ${stars}`, W / 2, 140, 20, '#fff', 'center', 500, 'rgba(0,0,0,.35)'); + drawButtons(); + } + + function drawHelp() { + drawSky(); + panel(220, 40, 840, 500); + text('How to play', W / 2, 100, 46, '#ffd54a', 'center', 700, '#7a4400'); + const lines = [ + 'Your bee fires flowers into the honeycomb.', + 'Join three or more of the same colour to burst them.', + 'Flowers that lose their hold on the ceiling fall away for big bonus points.', + 'The ceiling bar fills over time. When it is full, the ceiling pushes down a row.', + 'Clear every flower to win. If they reach the dashed line, you are swarmed!', + 'Finish before the ceiling drops twice for three stars.', + 'Remote: ◀ ▶ aim, OK fires, ▼ swaps your flower, ▲ arms a Honeybomb, BACK pauses.', + 'Change the skin below: Orbit or Classic.', + ]; + lines.forEach((l, i) => text(l, W / 2, 160 + i * 44, 21, '#fff', 'center', 500)); + drawButtons(); + } + + function drawWheel() { + drawSky(); + text('Daily Prize', W / 2, 90, 60, '#ffc21a', 'center', 700, '#8a4b00'); + const cx = W / 2, cy = 340, r = 210; + const seg = PRIZES.length; + ctx.save(); + ctx.translate(cx, cy); + ctx.rotate(wheel.angle); + for (let i = 0; i < seg; i++) { + ctx.fillStyle = i % 2 ? '#f7a823' : '#ffd54a'; + ctx.beginPath(); ctx.moveTo(0, 0); ctx.arc(0, 0, r, (i / seg) * Math.PI * 2, ((i + 1) / seg) * Math.PI * 2); ctx.closePath(); ctx.fill(); + ctx.strokeStyle = '#a35a00'; ctx.lineWidth = 3; ctx.stroke(); + ctx.save(); + ctx.rotate(((i + 0.5) / seg) * Math.PI * 2); + text(`${PRIZES[i]}`, r * 0.68, 12, 36, '#fff', 'center', 700, '#a35a00'); + ctx.restore(); + } + ctx.restore(); + ctx.fillStyle = '#a35a00'; ctx.beginPath(); ctx.arc(cx, cy, 34, 0, Math.PI * 2); ctx.fill(); + drawBomb(cx, cy, 0.9); + ctx.fillStyle = '#e53935'; + ctx.beginPath(); ctx.moveTo(cx - 18, cy - r - 24); ctx.lineTo(cx + 18, cy - r - 24); ctx.lineTo(cx, cy - r + 14); ctx.fill(); + if (wheel.spun) text(`You won ${wheel.prize} Honeybomb${wheel.prize > 1 ? 's' : ''}!`, W / 2, 590, 34, '#fff', 'center', 700, '#7a4400'); + else text('Press OK to spin for Honeybombs', W / 2, 590, 26, '#fff', 'center', 500, 'rgba(0,0,0,.35)'); + drawButtons(); + } + + function drawOverlay(title, sub, colour) { + ctx.fillStyle = 'rgba(20,10,0,.55)'; ctx.fillRect(0, 0, W, H); + panel(400, 150, 480, 520); + text(title, W / 2, 240, 66, colour, 'center', 700, '#7a4400'); + if (sub) sub(); + drawButtons(); + } + + function render() { + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = '#3d7fb8'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.setTransform(scale, 0, 0, scale, ox, oy); + ctx.save(); + ctx.beginPath(); ctx.rect(0, 0, W, H); ctx.clip(); + if (screen === 'title') drawTitle(); + else if (screen === 'levels') drawLevels(); + else if (screen === 'help') drawHelp(); + else if (screen === 'daily') drawWheel(); + else { + const shake = game.effects.find((f) => f.type === 'shake'); + ctx.save(); + if (shake) ctx.translate(0, Math.sin(shake.t * 60) * 4 * (1 - shake.t / 0.3)); + drawSky(); + drawHive(); + drawGrid(); + if (game.shot) { if (game.shot.bomb) drawBomb(game.shot.x, game.shot.y); else drawFlower(game.shot.x, game.shot.y, game.shot.colour); } + drawLauncher(); + drawEffects(); + drawHud(); + ctx.restore(); + if (screen === 'pause') drawOverlay('Paused', () => text(`Level ${game.level} • Score ${game.score}`, W / 2, 280, 22, '#fff', 'center', 500), '#ffd54a'); + if (screen === 'win') drawOverlay('Hive full!', () => { + text('★'.repeat(game.stars) + '☆'.repeat(3 - game.stars), W / 2, 320, 64, '#ffd54a', 'center', 700, '#7a4400'); + text(`Score ${game.score}`, W / 2, 380, 28, '#fff', 'center', 700); + }, '#ffd54a'); + if (screen === 'lose') drawOverlay('Swarmed!', () => { + text('The flowers reached your bee.', W / 2, 310, 24, '#fff', 'center', 500); + text(`Score ${game.score}`, W / 2, 360, 28, '#fff', 'center', 700); + }, '#ff8a65'); + } + ctx.restore(); + } + + // ---------- main loop ---------- + let last = performance.now(); + function frame(now) { + const dt = Math.min(0.05, (now - last) / 1000); + last = now; + update(dt); + render(); + requestAnimationFrame(frame); + } + + addEventListener('resize', resize); + resize(); + game.grid = buildLevel(1).grid; + game.info = levelInfo(1); + setScreen('title'); + canvas.focus(); + if (document.fonts && document.fonts.load) document.fonts.load(`700 20px ${FONT}`).then(() => { skyCache = null; }); + requestAnimationFrame(frame); + + // The app's Back button, once per press however it arrives. + let lastBack = 0; + window.orbitBack = () => { const t = performance.now(); if (t - lastBack > 250) { lastBack = t; onAction('back'); } }; + // Small hook for automated checks. + window.orbitHive = { game, save, startLevel, fire, setScreen, get screen() { return screen; } }; +})(); diff --git a/android/app/src/main/assets/orbithive/index.html b/android/app/src/main/assets/orbithive/index.html new file mode 100644 index 0000000..382f8dc --- /dev/null +++ b/android/app/src/main/assets/orbithive/index.html @@ -0,0 +1,17 @@ + + + + + +OrbitHive + + + + + + + diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt index a171797..0b47c10 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -66,7 +66,7 @@ internal class HttpsEmbyTransport(internal val deviceId: String = UUID.randomUUI connection.connectTimeout = 10_000 connection.readTimeout = request.readTimeoutMs ?: if (request.path == "/Sessions/Logout") 5_000 else 15_000 connection.setRequestProperty("Accept", "application/json") - connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.78.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.79.0\"") request.token?.let { connection.setRequestProperty("X-Emby-Token", it) } if (request.method == "POST") { val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8) diff --git a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt index 1c6bceb..703b626 100644 --- a/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt +++ b/android/app/src/main/java/tv/orbit/classic/OrbitArcade.kt @@ -41,6 +41,9 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import android.webkit.JavascriptInterface +import android.webkit.WebView import androidx.tv.material3.Text import kotlinx.coroutines.delay import kotlin.math.cos @@ -252,17 +255,57 @@ private fun SnakeScreen(best: Int, onBest: (Int) -> Unit, onLeave: () -> Unit) { } } -/** The arcade: the game on the left, and a honeycomb of what's here, with room for more. */ +/** OrbitHive runs as a small web page from inside the app, with no network, and handles the remote itself. */ +@android.annotation.SuppressLint("SetJavaScriptEnabled") +@Composable +private fun OrbitHiveScreen(onLeave: () -> Unit) { + var web by remember { mutableStateOf(null) } + BackHandler { web?.evaluateJavascript("window.orbitBack()", null) } + AndroidView(modifier = Modifier.fillMaxSize(), factory = { context -> + WebView(context).apply { + setBackgroundColor(android.graphics.Color.rgb(7, 22, 50)) + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.blockNetworkLoads = true + settings.mediaPlaybackRequiresUserGesture = false + isFocusable = true; isFocusableInTouchMode = true + addJavascriptInterface(object { + @JavascriptInterface fun leave() { post { onLeave() } } + }, "OrbitApp") + loadUrl("file:///android_asset/orbithive/index.html") + requestFocus() + web = this + } + }, onRelease = { it.destroy() }) +} + +private enum class ArcadeGame(val title: String, val blurb: String) { + Snake("SNAKE", "Eat, grow, and don't hit the walls or yourself."), + Hive("ORBITHIVE", "Fire flowers into the honeycomb, and clear the hive before it reaches you.") +} + +@Composable +private fun GameTile(game: ArcadeGame, focus: FocusRequester?, nav: FocusRequester?, onFocus: () -> Unit, onPlay: () -> Unit) { + var focused by remember { mutableStateOf(false) } + Box((if (focus != null) Modifier.focusRequester(focus) else Modifier).size(135.dp).focusProperties { if (nav != null) up = nav }.onFocusChanged { focused = it.isFocused; if (it.isFocused) onFocus() }.clickable(onClick = onPlay), contentAlignment = Alignment.Center) { + Canvas(Modifier.fillMaxSize()) { drawPath(hexagon(center, size.minDimension / 2), if (focused) Yellow else Panel) } + Text(if (game == ArcadeGame.Snake) "Snake" else "OrbitHive", color = if (focused) Navy else Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) + } +} + +/** The arcade: the chosen game on the left, and a honeycomb of what's here, with room for more. */ @Composable internal fun OrbitArcade(modifier: Modifier, onPlaying: (Boolean) -> Unit) { val context = LocalContext.current val settings = remember { OrbitSettings(context) } var best by remember { mutableIntStateOf(settings.snakeBest) } - var playing by remember { mutableStateOf(false) } - DisposableEffect(playing) { onPlaying(playing); onDispose { onPlaying(false) } } - if (playing) { - SnakeScreen(best, onBest = { best = it; settings.snakeBest = it }, onLeave = { playing = false }) - return + var playing by remember { mutableStateOf(null) } + var chosen by remember { mutableStateOf(ArcadeGame.Snake) } + DisposableEffect(playing) { onPlaying(playing != null); onDispose { onPlaying(false) } } + when (playing) { + ArcadeGame.Snake -> { SnakeScreen(best, onBest = { best = it; settings.snakeBest = it }, onLeave = { playing = null }); return } + ArcadeGame.Hive -> { OrbitHiveScreen(onLeave = { playing = null }); return } + null -> {} } val first = remember { FocusRequester() } LaunchedEffect(Unit) { first.requestFocus() } @@ -272,27 +315,32 @@ internal fun OrbitArcade(modifier: Modifier, onPlaying: (Boolean) -> Unit) { Row(Modifier.fillMaxWidth().weight(1f), horizontalArrangement = Arrangement.spacedBy(20.dp)) { Column(Modifier.weight(1f).fillMaxHeight().background(Panel, RoundedCornerShape(6.dp)).padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Image(painterResource(R.drawable.orbit_arcade_logo), "Orbit Arcade", Modifier.width(256.dp).height(90.dp)) - Box(Modifier.fillMaxWidth().weight(1f).background(Color(0xFF050E2A))) { SnakeField(preview, Modifier.fillMaxSize()) } - Text("SNAKE", color = Honey, fontFamily = Marquee, fontSize = 30.sp) - Text(if (best > 0) "Your best: $best" else "Eat, grow, and don't hit the walls or yourself.", color = Color.White, fontSize = 15.sp) - } - Column(Modifier.width(270.dp).fillMaxHeight(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { - var focused by remember { mutableStateOf(false) } - Box(Modifier.size(170.dp).focusRequester(first).focusProperties { if (nav != null) up = nav }.onFocusChanged { focused = it.isFocused }.clickable { playing = true }, contentAlignment = Alignment.Center) { - Canvas(Modifier.fillMaxSize()) { drawPath(hexagon(center, size.minDimension / 2), if (focused) Yellow else Panel) } - Text("Snake", color = if (focused) Navy else Color.White, fontSize = 20.sp, fontWeight = FontWeight.SemiBold) - } - Spacer(Modifier.height(4.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - repeat(2) { - Box(Modifier.size(110.dp), contentAlignment = Alignment.Center) { - Canvas(Modifier.fillMaxSize()) { drawPath(hexagon(center, size.minDimension / 2), Panel.copy(alpha = .4f)) } - Text("More soon", color = Muted, fontSize = 12.sp) + Box(Modifier.fillMaxWidth().weight(1f).background(Color(0xFF050E2A))) { + if (chosen == ArcadeGame.Snake) SnakeField(preview, Modifier.fillMaxSize()) + else Canvas(Modifier.fillMaxSize()) { + val colours = listOf(Color(0xFFE53935), Color(0xFF1E66D0), Color(0xFF2E9D3A), Color(0xFFF4F4F4), Color(0xFFF5A623), Color(0xFF8E44AD)) + val r = size.height / 9 + for (row in 0..3) for (col in 0 until (size.width / (r * 1.8f)).toInt()) { + val at = Offset(r + col * r * 1.8f + (row % 2) * r * .9f, r + row * r * 1.56f) + drawPath(hexagon(at, r * .92f), colours[(col * 3 + row * 5 + col * row) % colours.size]) } } } + Text(chosen.title, color = Honey, fontFamily = Marquee, fontSize = 30.sp) + Text(if (chosen == ArcadeGame.Snake && best > 0) "Your best: $best" else chosen.blurb, color = Color.White, fontSize = 15.sp, maxLines = 2) + } + Column(Modifier.width(290.dp).fillMaxHeight(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + GameTile(ArcadeGame.Snake, first, nav, { chosen = ArcadeGame.Snake }, { playing = ArcadeGame.Snake }) + GameTile(ArcadeGame.Hive, null, nav, { chosen = ArcadeGame.Hive }, { playing = ArcadeGame.Hive }) + } + Spacer(Modifier.height(4.dp)) + Box(Modifier.size(110.dp), contentAlignment = Alignment.Center) { + Canvas(Modifier.fillMaxSize()) { drawPath(hexagon(center, size.minDimension / 2), Panel.copy(alpha = .4f)) } + Text("More soon", color = Muted, fontSize = 12.sp) + } } } - Text("OK Play Back Main menu", color = Muted, fontSize = 11.sp) + Text("◄► Choose OK Play Back Main menu", color = Muted, fontSize = 11.sp) } } diff --git a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index 4aae5a8..8a34f89 100644 --- a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt +++ b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt @@ -33,7 +33,7 @@ internal class SecureMediaDataSource(private val session: EmbySampleSession) : B next.instanceFollowRedirects = false next.connectTimeout = 10_000 next.readTimeout = 20_000 - next.setRequestProperty("User-Agent", "OrbitTV/0.78.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.79.0") next.setRequestProperty("Accept-Encoding", "identity") session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) } if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) { diff --git a/docs/refinement-notes.md b/docs/refinement-notes.md index 47664e0..a2e04ac 100644 --- a/docs/refinement-notes.md +++ b/docs/refinement-notes.md @@ -33,6 +33,10 @@ Paul's approach, 2 November 2026: build in pieces, learn by using it, and note e - **Check them in motion.** Paul caught what stills hid: planets outside their rings, a burst covering a letter. - **Flat layers look blocky.** Real 3D in Blender is a big step up, and Blender's free. It takes about 25 minutes per ident on a mid-range graphics card. +## Games + +- **OrbitHive recreates Beehive Bedlam's rules.** The names are changed and the art is its own, but check this properly before any public release. + ## Size and upkeep Paul, 2 November 2026: keep an eye on optimisation, size and cache management.