@ywkrlerche/richardwho 0.0.0-stage → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +524 -0
- package/config.js +20 -0
- package/games.json +1 -0
- package/index.html +150 -0
- package/logos/1-on-1-soccer.webp +0 -0
- package/logos/1-on-1-tennis.webp +0 -0
- package/logos/10-minutes-till-dawn.webp +0 -0
- package/logos/1v1.lol.webp +0 -0
- package/logos/2048-cupcakes.webp +0 -0
- package/logos/2048.webp +0 -0
- package/logos/3D-flight-simulator.webp +0 -0
- package/logos/3d-free-kick.webp +0 -0
- package/logos/8-ball-classic.webp +0 -0
- package/logos/9007199254740992.webp +0 -0
- package/logos/Alpha_1.2.6.webp +0 -0
- package/logos/Indev.webp +0 -0
- package/logos/agar-io-lite.webp +0 -0
- package/logos/age-of-war-2.webp +0 -0
- package/logos/age-of-war.webp +0 -0
- package/logos/ages-of-conflict.webp +0 -0
- package/logos/among-us.webp +0 -0
- package/logos/angry-birds-space.webp +0 -0
- package/logos/angry-birds.webp +0 -0
- package/logos/awesome-tanks-2.webp +0 -0
- package/logos/awesome-tanks.webp +0 -0
- package/logos/bacon-may-die.webp +0 -0
- package/logos/bad-ice-cream-2.webp +0 -0
- package/logos/bad-ice-cream-3.webp +0 -0
- package/logos/bad-ice-cream.webp +0 -0
- package/logos/bad-parenting.webp +0 -0
- package/logos/bad-piggies.webp +0 -0
- package/logos/bank-robbery-2.webp +0 -0
- package/logos/bank-robbery-3.webp +0 -0
- package/logos/baseball-bros.webp +0 -0
- package/logos/basket-bros.webp +0 -0
- package/logos/basket-random.webp +0 -0
- package/logos/basketball-legends.webp +0 -0
- package/logos/basketball-stars.webp +0 -0
- package/logos/big-ice-tower-tiny-square.webp +0 -0
- package/logos/big-neon-tower-tiny-square.webp +0 -0
- package/logos/big-tower-tiny-square-2.webp +0 -0
- package/logos/big-tower-tiny-square.webp +0 -0
- package/logos/blocky-snakes.webp +0 -0
- package/logos/bloons-TD-2.webp +0 -0
- package/logos/bloons-TD.webp +0 -0
- package/logos/bloxorz.webp +0 -0
- package/logos/blumgi-racers.webp +0 -0
- package/logos/blumgi-rocket.webp +0 -0
- package/logos/bob-the-robber-2.webp +0 -0
- package/logos/bob-the-robber.webp +0 -0
- package/logos/bouncy-motors.webp +0 -0
- package/logos/boxing-random.webp +0 -0
- package/logos/breaking-the-bank.webp +0 -0
- package/logos/bubble-shooter.webp +0 -0
- package/logos/candy-crush.webp +0 -0
- package/logos/capybara-clicker.webp +0 -0
- package/logos/car-drawing.webp +0 -0
- package/logos/chaos-faction-2.webp +0 -0
- package/logos/chess.webp +0 -0
- package/logos/choppy-orc.webp +0 -0
- package/logos/circloO-2.webp +0 -0
- package/logos/circloO.webp +0 -0
- package/logos/clash-of-vikings.webp +0 -0
- package/logos/cleanup-io.webp +0 -0
- package/logos/cluster-rush.webp +0 -0
- package/logos/cookie-clicker.webp +0 -0
- package/logos/coreball.webp +0 -0
- package/logos/crazy-cars.webp +0 -0
- package/logos/crazy-cattle-3D.webp +0 -0
- package/logos/crazy-crash-landing.webp +0 -0
- package/logos/crazy-motorcycle.webp +0 -0
- package/logos/crossy-road.webp +0 -0
- package/logos/cut-the-rope-2.webp +0 -0
- package/logos/cut-the-rope-holiday-gift.webp +0 -0
- package/logos/cut-the-rope-time-travel.webp +0 -0
- package/logos/cut-the-rope.webp +0 -0
- package/logos/dadish-2.webp +0 -0
- package/logos/dadish-3.webp +0 -0
- package/logos/dadish.webp +0 -0
- package/logos/death-chase.webp +0 -0
- package/logos/death-run-3D.webp +0 -0
- package/logos/demolition-derby-crash-racing.webp +0 -0
- package/logos/doodle-jump.webp +0 -0
- package/logos/draw-climber.webp +0 -0
- package/logos/dreadhead-parkour.webp +0 -0
- package/logos/drift-boss.webp +0 -0
- package/logos/drift-hunters-pro.webp +0 -0
- package/logos/drive-mad.webp +0 -0
- package/logos/duck-life.webp +0 -0
- package/logos/ducklings-io.webp +0 -0
- package/logos/eagle-ride.webp +0 -0
- package/logos/earn-to-die-2.webp +0 -0
- package/logos/earn-to-die.webp +0 -0
- package/logos/eggy-car.webp +0 -0
- package/logos/elastic-face.webp +0 -0
- package/logos/escape-road-2.webp +0 -0
- package/logos/escape-road-3.webp +0 -0
- package/logos/escape-road.webp +0 -0
- package/logos/escaping-the-prison.webp +0 -0
- package/logos/eugenes-life.webp +0 -0
- package/logos/evil-glitch.webp +0 -0
- package/logos/fancy-pants-adventure-2.webp +0 -0
- package/logos/fancy-pants-adventure.webp +0 -0
- package/logos/fireblob.webp +0 -0
- package/logos/fireboy-and-watergirl.webp +0 -0
- package/logos/flappy-bird.webp +0 -0
- package/logos/fleeing-the-complex.webp +0 -0
- package/logos/flood-runner-2.webp +0 -0
- package/logos/flood-runner-3.webp +0 -0
- package/logos/flood-runner-4.webp +0 -0
- package/logos/fnaf-2.webp +0 -0
- package/logos/fnaf-3.webp +0 -0
- package/logos/fnaf.webp +0 -0
- package/logos/football-bros.webp +0 -0
- package/logos/football-legends.webp +0 -0
- package/logos/friday-night-funkin.webp +0 -0
- package/logos/fruit-ninja.webp +0 -0
- package/logos/funny-battle.webp +0 -0
- package/logos/funny-mad-racing.webp +0 -0
- package/logos/funny-shooter-2.webp +0 -0
- package/logos/geometry-dash-lite.webp +0 -0
- package/logos/geometry-dash.webp +0 -0
- package/logos/geometry-vibes.webp +0 -0
- package/logos/get-on-top.webp +0 -0
- package/logos/getaway-shootout.webp +0 -0
- package/logos/gladi-hoppers.webp +0 -0
- package/logos/google-baseball.webp +0 -0
- package/logos/google-dino.webp +0 -0
- package/logos/granny.webp +0 -0
- package/logos/guess-their-answer.webp +0 -0
- package/logos/happy-wheels.webp +0 -0
- package/logos/helix-jump.webp +0 -0
- package/logos/highway-traffic.webp +0 -0
- package/logos/hill-climb-racing-lite.webp +0 -0
- package/logos/hole-io.webp +0 -0
- package/logos/house-of-hazards.webp +0 -0
- package/logos/hover-racer-drive.webp +0 -0
- package/logos/ice-dodo.webp +0 -0
- package/logos/idle-breakout.webp +0 -0
- package/logos/idle-dice.webp +0 -0
- package/logos/infiltrating-the-airship.webp +0 -0
- package/logos/iron-snout.webp +0 -0
- package/logos/jacksmith.webp +0 -0
- package/logos/jetpack-joyride.webp +0 -0
- package/logos/johnny-trigger.webp +0 -0
- package/logos/jumping-shell.webp +0 -0
- package/logos/karate-bros.webp +0 -0
- package/logos/kart-bros.webp +0 -0
- package/logos/kittytoy.webp +0 -0
- package/logos/learn-to-fly-2.webp +0 -0
- package/logos/learn-to-fly-3.webp +0 -0
- package/logos/learn-to-fly.webp +0 -0
- package/logos/level-devil.webp +0 -0
- package/logos/little-alchemy-2.webp +0 -0
- package/logos/madalin-stunt-cars-2.webp +0 -0
- package/logos/madness-project-nexus.webp +0 -0
- package/logos/melon-playground.webp +0 -0
- package/logos/merge-round-racers.webp +0 -0
- package/logos/minesweeper.webp +0 -0
- package/logos/monkey-mart.webp +0 -0
- package/logos/monster-tracks.webp +0 -0
- package/logos/motherload.webp +0 -0
- package/logos/moto-x3m-2.webp +0 -0
- package/logos/moto-x3m-3.webp +0 -0
- package/logos/moto-x3m-pool-party.webp +0 -0
- package/logos/moto-x3m-spooky-land.webp +0 -0
- package/logos/moto-x3m-winter.webp +0 -0
- package/logos/moto-x3m.webp +0 -0
- package/logos/murder.webp +0 -0
- package/logos/noob-miner.webp +0 -0
- package/logos/openTTD.webp +0 -0
- package/logos/opposite-day.webp +0 -0
- package/logos/ovo.webp +0 -0
- package/logos/pac-man.webp +0 -0
- package/logos/papas-bakeria.webp +0 -0
- package/logos/papas-burgeria.webp +0 -0
- package/logos/papas-cheeseria.webp +0 -0
- package/logos/papas-cupcakeria.webp +0 -0
- package/logos/papas-donuteria.webp +0 -0
- package/logos/papas-freezeria.webp +0 -0
- package/logos/papas-pancakeria.webp +0 -0
- package/logos/papas-pastaria.webp +0 -0
- package/logos/papas-pizzeria.webp +0 -0
- package/logos/papas-scooperia.webp +0 -0
- package/logos/papas-sushiria.webp +0 -0
- package/logos/papas-taco-mia.webp +0 -0
- package/logos/papas-wingeria.webp +0 -0
- package/logos/paper-io-2.webp +0 -0
- package/logos/parking-fury-2.webp +0 -0
- package/logos/picos-school.webp +0 -0
- package/logos/ping-pong-chaos.webp +0 -0
- package/logos/pixel-speedrun.webp +0 -0
- package/logos/plants-vs-zombies.webp +0 -0
- package/logos/plonky.webp +0 -0
- package/logos/polytrack.webp +0 -0
- package/logos/poor-bunny.webp +0 -0
- package/logos/pou.webp +0 -0
- package/logos/ragdoll-archers.webp +0 -0
- package/logos/ragdoll-hit.webp +0 -0
- package/logos/ragdoll-soccer.webp +0 -0
- package/logos/red-ball-4-vol-1.webp +0 -0
- package/logos/red-ball-4-vol-2.webp +0 -0
- package/logos/red-ball-4-vol-3.webp +0 -0
- package/logos/retro-bowl-college.webp +0 -0
- package/logos/retro-bowl.webp +0 -0
- package/logos/retro-highway.webp +0 -0
- package/logos/retro-ping-pong.webp +0 -0
- package/logos/riddle-school-2.webp +0 -0
- package/logos/riddle-school-3.webp +0 -0
- package/logos/riddle-school-4.webp +0 -0
- package/logos/riddle-school-5.webp +0 -0
- package/logos/riddle-school.webp +0 -0
- package/logos/riddle-transfer.webp +0 -0
- package/logos/rocket-soccer-derby.webp +0 -0
- package/logos/rooftop-run.webp +0 -0
- package/logos/rooftop-snipers-2.webp +0 -0
- package/logos/rooftop-snipers.webp +0 -0
- package/logos/run-2.webp +0 -0
- package/logos/run-3.webp +0 -0
- package/logos/run.webp +0 -0
- package/logos/sandbox-city.webp +0 -0
- package/logos/short-life.webp +0 -0
- package/logos/slope-2-player.webp +0 -0
- package/logos/slope-3.webp +0 -0
- package/logos/slope.webp +0 -0
- package/logos/snail-bob-2.webp +0 -0
- package/logos/snail-bob-3.webp +0 -0
- package/logos/snail-bob-4.webp +0 -0
- package/logos/snail-bob-5.webp +0 -0
- package/logos/snail-bob-6.webp +0 -0
- package/logos/snail-bob-7.webp +0 -0
- package/logos/snail-bob-8.webp +0 -0
- package/logos/snail-bob.webp +0 -0
- package/logos/snow-rider.webp +0 -0
- package/logos/snow-road.webp +0 -0
- package/logos/snowball-io.webp +0 -0
- package/logos/soccer-bros.webp +0 -0
- package/logos/soccer-random.webp +0 -0
- package/logos/space-is-key-2.webp +0 -0
- package/logos/space-is-key.webp +0 -0
- package/logos/space-waves.webp +0 -0
- package/logos/spacebar-clicker.webp +0 -0
- package/logos/speed-stars.webp +0 -0
- package/logos/sprunki.webp +0 -0
- package/logos/stack.webp +0 -0
- package/logos/stacktris.webp +0 -0
- package/logos/state-io.webp +0 -0
- package/logos/stealing-the-diamond.webp +0 -0
- package/logos/stick-archers-battle.webp +0 -0
- package/logos/stick-fighter.webp +0 -0
- package/logos/stick-merge.webp +0 -0
- package/logos/stickman-hook.webp +0 -0
- package/logos/subway-surfers-beijing.webp +0 -0
- package/logos/subway-surfers-havana.webp +0 -0
- package/logos/subway-surfers-san-francisco.webp +0 -0
- package/logos/super-bike-the-champion.webp +0 -0
- package/logos/super-liquid-soccer.webp +0 -0
- package/logos/super-star-car.webp +0 -0
- package/logos/table-tennis-world-tour.webp +0 -0
- package/logos/tag.webp +0 -0
- package/logos/tanuki-sunset.webp +0 -0
- package/logos/tap-road.webp +0 -0
- package/logos/temple-of-boom.webp +0 -0
- package/logos/temple-run-2.webp +0 -0
- package/logos/territorial-io.webp +0 -0
- package/logos/tetrominoes.webp +0 -0
- package/logos/the-impossible-quiz-2.webp +0 -0
- package/logos/the-impossible-quiz.webp +0 -0
- package/logos/they-are-coming.webp +0 -0
- package/logos/this-is-the-only-level-too.webp +0 -0
- package/logos/this-is-the-only-level.webp +0 -0
- package/logos/time-shooter-2.webp +0 -0
- package/logos/time-shooter-3.webp +0 -0
- package/logos/tiny-fishing.webp +0 -0
- package/logos/trap-the-cat.webp +0 -0
- package/logos/trivia-crack.webp +0 -0
- package/logos/tube-jumpers.webp +0 -0
- package/logos/tunnel-rush.webp +0 -0
- package/logos/unicycle-hero.webp +0 -0
- package/logos/vex-4.webp +0 -0
- package/logos/vex-5.webp +0 -0
- package/logos/vex-x3m-2.webp +0 -0
- package/logos/vex-x3m.webp +0 -0
- package/logos/volley-random.webp +0 -0
- package/logos/war-the-knights.webp +0 -0
- package/logos/we-become-what-we-behold.webp +0 -0
- package/logos/wheelie-bike.webp +0 -0
- package/logos/wheely-2.webp +0 -0
- package/logos/wheely-3.webp +0 -0
- package/logos/wheely-4.webp +0 -0
- package/logos/wheely-5.webp +0 -0
- package/logos/wheely-6.webp +0 -0
- package/logos/wheely-7.webp +0 -0
- package/logos/wheely.webp +0 -0
- package/logos/wordle-unlimited.webp +0 -0
- package/logos/worlds-hardest-game-2.webp +0 -0
- package/logos/worlds-hardest-game.webp +0 -0
- package/logos/wrestle-bros.webp +0 -0
- package/logos/yohoho-io.webp +0 -0
- package/logos/zombie-rush.webp +0 -0
- package/package.json +12 -4
- package/style.css +224 -0
- package/README.md +0 -3
package/app.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const CFG = window.RUG_CONFIG || {};
|
|
5
|
+
const SITE_NAME = "Richards Unblocked Games";
|
|
6
|
+
const RECENT_KEY = "rug-recent";
|
|
7
|
+
const CLOCK_KEY = "rug-clock24";
|
|
8
|
+
const THEME_KEY = "rug-theme";
|
|
9
|
+
const PER_ROW_KEY = "rug-per-row";
|
|
10
|
+
const MAX_RECENT = 6;
|
|
11
|
+
const MARQUEE_SPEED = 38; // pixels per second
|
|
12
|
+
|
|
13
|
+
const TAG_EMOJI = {
|
|
14
|
+
Minecraft: "⛏️", Racing: "🏎️", Sports: "⚽", Shooter: "🎯", Horror: "👻", Clicker: "👆",
|
|
15
|
+
Cooking: "🍔", Puzzle: "🧩", Strategy: "🏰", IO: "🌐", Platformer: "🏃", Arcade: "👾",
|
|
16
|
+
Rhythm: "🎵", Adventure: "🗺️", Tools: "🧰",
|
|
17
|
+
};
|
|
18
|
+
// Shown in the "Popular games" strip; any id missing from the catalog is skipped
|
|
19
|
+
const POPULAR = ["1v1.lol", "slope", "retro-bowl", "subway-surfers-san-francisco", "fnaf", "moto-x3m",
|
|
20
|
+
"papas-freezeria", "basket-random", "drift-boss", "monkey-mart", "temple-run-2", "happy-wheels",
|
|
21
|
+
"jetpack-joyride", "cut-the-rope", "friday-night-funkin", "plants-vs-zombies", "crossy-road", "run-3"];
|
|
22
|
+
|
|
23
|
+
const $ = (id) => document.getElementById(id);
|
|
24
|
+
const els = {
|
|
25
|
+
homeView: $("view-home"), gamesView: $("view-games"), homeInput: $("home-input"), homeSub: $("home-sub"),
|
|
26
|
+
grid: $("grid"), search: $("search"), sort: $("sort"), chips: $("chips"), count: $("count"), empty: $("empty"),
|
|
27
|
+
allTitle: $("all-title"), footerCount: $("footer-count"),
|
|
28
|
+
recentBlock: $("recent-block"), recentRow: $("recent-row"),
|
|
29
|
+
popularBlock: $("popular-block"), marquee: $("marquee"), marqueeTrack: $("marquee-track"),
|
|
30
|
+
player: $("player"), frame: $("game-frame"), playerTitle: $("player-title"),
|
|
31
|
+
loader: $("loader"), loaderName: $("loader-name"), loaderDesc: $("loader-desc"), loaderArt: $("loader-art"),
|
|
32
|
+
barFill: $("bar-fill"), loaderStatus: $("loader-status"), retryBtn: $("retry-btn"),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// localStorage can throw (private mode, blocked storage) - never let that break the page
|
|
36
|
+
const store = {
|
|
37
|
+
get(k, fallback) { try { const v = localStorage.getItem(k); return v === null ? fallback : JSON.parse(v); } catch { return fallback; } },
|
|
38
|
+
set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let games = [];
|
|
42
|
+
let byId = new Map();
|
|
43
|
+
let activeTag = "All";
|
|
44
|
+
let currentLoad = null; // { controller, id }
|
|
45
|
+
|
|
46
|
+
// ---------- Helpers ----------
|
|
47
|
+
function hash(str) {
|
|
48
|
+
let h = 2166136261;
|
|
49
|
+
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
50
|
+
return h >>> 0;
|
|
51
|
+
}
|
|
52
|
+
function gradient(game) {
|
|
53
|
+
const h = hash(game.id);
|
|
54
|
+
const a = h % 360, b = (a + 40 + (h >> 9) % 80) % 360;
|
|
55
|
+
return `linear-gradient(135deg, hsl(${a} 65% 42%), hsl(${b} 70% 24%))`;
|
|
56
|
+
}
|
|
57
|
+
function initials(name) {
|
|
58
|
+
const words = name.replace(/\(.*?\)/g, "").split(/\s+/).filter(Boolean);
|
|
59
|
+
if (words.length === 1) return words[0].slice(0, 3).toUpperCase();
|
|
60
|
+
return words.slice(0, 3).map((w) => w[0]).join("").toUpperCase();
|
|
61
|
+
}
|
|
62
|
+
// Our logos ship inside this package under logos/<id>.webp (relative, so it works on unpkg and locally);
|
|
63
|
+
// games from the extra collection carry their own pinned cover URL
|
|
64
|
+
function logoUrl(game) {
|
|
65
|
+
if (game.cover) return game.cover;
|
|
66
|
+
return game.logo ? `logos/${encodeURIComponent(game.id)}.webp` : null;
|
|
67
|
+
}
|
|
68
|
+
function fmtMB(bytes) {
|
|
69
|
+
const mb = bytes / 1048576;
|
|
70
|
+
return mb < 1 ? `${Math.max(1, Math.round(bytes / 1024))} KB` : `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
|
|
71
|
+
}
|
|
72
|
+
function gameUrls(game) {
|
|
73
|
+
if (game.url) return [game.url]; // extra collection: one launch file at a pinned URL
|
|
74
|
+
const base = CFG.localBase
|
|
75
|
+
? `${CFG.localBase}/${game.repo}`
|
|
76
|
+
: `https://cdn.jsdelivr.net/gh/${CFG.githubUser}/${game.repo}@${CFG.branch || "main"}`;
|
|
77
|
+
const url = `${base}/${game.path}`;
|
|
78
|
+
if (!game.parts) return [url];
|
|
79
|
+
return Array.from({ length: game.parts }, (_, i) => `${url}.part${i}`);
|
|
80
|
+
}
|
|
81
|
+
function escapeHtml(s) {
|
|
82
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------- Rendering ----------
|
|
86
|
+
function tileHtml(game, { popular = false, eager = false } = {}) {
|
|
87
|
+
const url = logoUrl(game);
|
|
88
|
+
return `<button class="tile" type="button" data-id="${escapeHtml(game.id)}" title="${escapeHtml(game.desc || game.name)}">
|
|
89
|
+
<span class="tile-art" style="background:${gradient(game)}">${escapeHtml(initials(game.name))}</span>
|
|
90
|
+
${url ? `<span class="tile-art"><img src="${url}" alt="" loading="${eager ? "eager" : "lazy"}" decoding="async" onerror="this.parentNode.remove()"></span>` : ""}
|
|
91
|
+
${popular ? `<span class="badge">Popular</span>` : ""}
|
|
92
|
+
<span class="tile-label">${escapeHtml(game.name)}</span>
|
|
93
|
+
</button>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function tagCounts() {
|
|
97
|
+
const counts = {};
|
|
98
|
+
games.forEach((g) => { counts[g.tag] = (counts[g.tag] || 0) + 1; });
|
|
99
|
+
return counts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderChips() {
|
|
103
|
+
const counts = tagCounts();
|
|
104
|
+
const tags = ["All", ...Object.keys(counts).sort((a, b) => counts[b] - counts[a])];
|
|
105
|
+
els.chips.innerHTML = tags.map((t) =>
|
|
106
|
+
`<button class="tab" role="tab" type="button" data-tag="${escapeHtml(t)}" aria-selected="${t === activeTag}">${t === "All" ? "All games" : (TAG_EMOJI[t] || "") + " " + escapeHtml(t)}<small>${t === "All" ? games.length : counts[t]}</small></button>`
|
|
107
|
+
).join("");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sorted(list) {
|
|
111
|
+
const by = els.sort.value;
|
|
112
|
+
const out = list.slice();
|
|
113
|
+
if (by === "za") out.reverse();
|
|
114
|
+
else if (by === "small") out.sort((a, b) => a.size - b.size);
|
|
115
|
+
else if (by === "big") out.sort((a, b) => b.size - a.size);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderGrid() {
|
|
120
|
+
const raw = els.search.value.trim();
|
|
121
|
+
const terms = raw.toLowerCase().split(/\s+/).filter(Boolean);
|
|
122
|
+
const list = sorted(games.filter((g) => {
|
|
123
|
+
if (activeTag !== "All" && g.tag !== activeTag) return false;
|
|
124
|
+
const hay = `${g.name} ${g.id} ${g.tag} ${g.desc || ""}`.toLowerCase();
|
|
125
|
+
return terms.every((t) => hay.includes(t));
|
|
126
|
+
}));
|
|
127
|
+
els.grid.innerHTML = list.map((g) => tileHtml(g)).join("");
|
|
128
|
+
els.empty.hidden = list.length > 0;
|
|
129
|
+
els.count.textContent = `${list.length} game${list.length === 1 ? "" : "s"}`;
|
|
130
|
+
els.allTitle.textContent = raw ? `Results for “${raw}”` : activeTag === "All" ? "All games" : `${activeTag} games`;
|
|
131
|
+
// Strips are for browsing; hide them while searching or filtering
|
|
132
|
+
const browsing = !raw && activeTag === "All";
|
|
133
|
+
els.popularBlock.hidden = !browsing;
|
|
134
|
+
els.recentBlock.hidden = !browsing || !els.recentRow.children.length;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderRecent() {
|
|
138
|
+
const recent = store.get(RECENT_KEY, []).map((id) => byId.get(id)).filter(Boolean);
|
|
139
|
+
els.recentRow.innerHTML = recent.map((g) => tileHtml(g, { eager: true })).join("");
|
|
140
|
+
els.recentBlock.hidden = recent.length === 0 || !!els.search.value.trim() || activeTag !== "All";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function addRecent(id) {
|
|
144
|
+
const list = store.get(RECENT_KEY, []).filter((x) => x !== id);
|
|
145
|
+
list.unshift(id);
|
|
146
|
+
store.set(RECENT_KEY, list.slice(0, MAX_RECENT));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setTag(tag) {
|
|
150
|
+
activeTag = tag;
|
|
151
|
+
els.chips.querySelectorAll(".tab").forEach((c) => c.setAttribute("aria-selected", c.dataset.tag === tag));
|
|
152
|
+
renderGrid();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function setPerRow(n) {
|
|
156
|
+
document.documentElement.style.setProperty("--per-row", n);
|
|
157
|
+
document.querySelectorAll(".per-row button").forEach((b) => b.classList.toggle("on", Number(b.dataset.per) === n));
|
|
158
|
+
store.set(PER_ROW_KEY, n);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------- Popular games: a smooth, endless sideways glide ----------
|
|
162
|
+
function startMarquee() {
|
|
163
|
+
const list = POPULAR.map((id) => byId.get(id)).filter(Boolean);
|
|
164
|
+
if (!list.length) { els.popularBlock.hidden = true; return; }
|
|
165
|
+
const set = list.map((g) => tileHtml(g, { popular: true, eager: true })).join("");
|
|
166
|
+
els.marqueeTrack.innerHTML = set + set; // two copies so the loop never shows a gap
|
|
167
|
+
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
168
|
+
let offset = 0, last = 0;
|
|
169
|
+
function step(t) {
|
|
170
|
+
const dt = last ? Math.min(64, t - last) : 0;
|
|
171
|
+
last = t;
|
|
172
|
+
// Pause while pointed at or keyboard-focused (checked live, so it can never get stuck paused)
|
|
173
|
+
const paused = els.marquee.matches(":hover") || els.marquee.contains(document.activeElement);
|
|
174
|
+
const half = els.marqueeTrack.scrollWidth / 2; // width of one copy (gap included); 0 while hidden
|
|
175
|
+
if (!paused && !reduce && half > 0 && els.player.hidden) {
|
|
176
|
+
offset = (offset + (MARQUEE_SPEED * dt) / 1000) % half;
|
|
177
|
+
els.marqueeTrack.style.transform = `translate3d(${-offset}px,0,0)`;
|
|
178
|
+
}
|
|
179
|
+
requestAnimationFrame(step);
|
|
180
|
+
}
|
|
181
|
+
requestAnimationFrame(step);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------- Game loading ----------
|
|
185
|
+
// jsDelivr serves .html as text/plain (so it can't be iframed directly), and caps files at 20 MB.
|
|
186
|
+
// So we fetch the file (or all its .partN pieces), join them, and write the HTML into a blank iframe.
|
|
187
|
+
// Writing (instead of a blob: URL) gives the game a normal https:// address, which many games need
|
|
188
|
+
// to work out paths (Unity, Ruffle/Flash and others crash on blob: URLs).
|
|
189
|
+
async function loadGame(game) {
|
|
190
|
+
stopGame();
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
currentLoad = { controller, id: game.id };
|
|
193
|
+
|
|
194
|
+
els.playerTitle.textContent = game.name;
|
|
195
|
+
els.loaderName.textContent = game.name;
|
|
196
|
+
els.loaderDesc.textContent = game.desc || "";
|
|
197
|
+
const url = logoUrl(game);
|
|
198
|
+
els.loaderArt.style.backgroundImage = url ? `url("${url}"), ${gradient(game)}` : gradient(game);
|
|
199
|
+
els.loaderArt.textContent = url ? "" : initials(game.name);
|
|
200
|
+
els.loader.hidden = false;
|
|
201
|
+
els.retryBtn.hidden = true;
|
|
202
|
+
els.barFill.style.width = "0%";
|
|
203
|
+
els.loaderStatus.textContent = "Connecting…";
|
|
204
|
+
document.title = `${game.name} · ${SITE_NAME}`;
|
|
205
|
+
|
|
206
|
+
const urls = gameUrls(game);
|
|
207
|
+
const loaded = new Array(urls.length).fill(0);
|
|
208
|
+
const total = game.size || 1;
|
|
209
|
+
const update = () => {
|
|
210
|
+
const got = loaded.reduce((a, b) => a + b, 0);
|
|
211
|
+
if (game.size) {
|
|
212
|
+
els.barFill.style.width = `${Math.min(100, (got / total) * 100).toFixed(1)}%`;
|
|
213
|
+
els.loaderStatus.textContent = `${fmtMB(got)} / ${fmtMB(total)}${urls.length > 1 ? ` · ${urls.length} parts` : ""}`;
|
|
214
|
+
} else {
|
|
215
|
+
// Size unknown (extra collection): show what has arrived so far
|
|
216
|
+
els.barFill.style.width = "35%";
|
|
217
|
+
els.loaderStatus.textContent = `${fmtMB(got)} loaded`;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const pieces = await Promise.all(urls.map(async (url, i) => {
|
|
223
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
224
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url.split("/").pop()}`);
|
|
225
|
+
if (!res.body) { const b = await res.arrayBuffer(); loaded[i] = b.byteLength; update(); return [b]; }
|
|
226
|
+
const reader = res.body.getReader();
|
|
227
|
+
const chunks = [];
|
|
228
|
+
for (;;) {
|
|
229
|
+
const { done, value } = await reader.read();
|
|
230
|
+
if (done) break;
|
|
231
|
+
chunks.push(value);
|
|
232
|
+
loaded[i] += value.byteLength;
|
|
233
|
+
update();
|
|
234
|
+
}
|
|
235
|
+
return chunks;
|
|
236
|
+
}));
|
|
237
|
+
if (currentLoad?.id !== game.id) return;
|
|
238
|
+
els.loaderStatus.textContent = "Starting…";
|
|
239
|
+
let html = await new Blob(pieces.flat()).text();
|
|
240
|
+
if (currentLoad?.id !== game.id) return;
|
|
241
|
+
if (game.src === "gn") html = stripTracking(html);
|
|
242
|
+
html = guardHistory(html);
|
|
243
|
+
const doc = freshFrame().contentDocument;
|
|
244
|
+
doc.open();
|
|
245
|
+
doc.write(html);
|
|
246
|
+
doc.close();
|
|
247
|
+
els.loader.hidden = true;
|
|
248
|
+
hookFrameKeys();
|
|
249
|
+
try { els.frame.contentWindow.focus(); } catch {}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err.name === "AbortError") return;
|
|
252
|
+
console.error(err);
|
|
253
|
+
els.loaderStatus.textContent = `Couldn't load this game (${err.message}).`;
|
|
254
|
+
els.retryBtn.hidden = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The extra collection's launch files embed Google Analytics; remove it so players aren't tracked
|
|
259
|
+
function stripTracking(html) {
|
|
260
|
+
return html
|
|
261
|
+
.replace(/<script\b[^>]*googletagmanager\.com[^>]*>\s*<\/script>/gi, "")
|
|
262
|
+
.replace(/<script\b[^>]*>(?:(?!<\/script>)[\s\S])*?\bgtag\s*\((?:(?!<\/script>)[\s\S])*?<\/script>/gi, "");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// A written-in game shares this page's address and session history, so a game calling history.back()
|
|
266
|
+
// or pushState() would move the whole tab (and kick the player out). Give games no-op versions first.
|
|
267
|
+
const HISTORY_GUARD = "<script>(function(){try{var h=window.history,n=function(){};" +
|
|
268
|
+
"h.back=h.forward=h.go=h.pushState=h.replaceState=n;}catch(e){}})();<\/script>";
|
|
269
|
+
function guardHistory(html) {
|
|
270
|
+
if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, (m) => m + HISTORY_GUARD);
|
|
271
|
+
if (/<html[^>]*>/i.test(html)) return html.replace(/<html[^>]*>/i, (m) => m + HISTORY_GUARD);
|
|
272
|
+
if (/^\s*<!doctype[^>]*>/i.test(html)) return html.replace(/^\s*<!doctype[^>]*>/i, (m) => m + HISTORY_GUARD);
|
|
273
|
+
return HISTORY_GUARD + html;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function stopGame() {
|
|
277
|
+
if (!currentLoad) return;
|
|
278
|
+
currentLoad.controller.abort();
|
|
279
|
+
currentLoad = null;
|
|
280
|
+
freshFrame();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Swap in a brand-new blank iframe (same attributes, including sandbox) so nothing from the last game lingers
|
|
284
|
+
function freshFrame() {
|
|
285
|
+
const fresh = els.frame.cloneNode(false);
|
|
286
|
+
els.frame.replaceWith(fresh);
|
|
287
|
+
els.frame = fresh;
|
|
288
|
+
return fresh;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// The game frame is same-origin, so we can listen for the panic keys inside the game too
|
|
292
|
+
function hookFrameKeys() {
|
|
293
|
+
try { els.frame.contentWindow.addEventListener("keydown", onKey, true); } catch {}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------- Routing: "" home, #games, #recent, #play/<id> ----------
|
|
297
|
+
function route() {
|
|
298
|
+
const m = location.hash.match(/^#play\/(.+)$/);
|
|
299
|
+
const game = m && byId.get(decodeURIComponent(m[1]));
|
|
300
|
+
if (game) {
|
|
301
|
+
els.player.hidden = false;
|
|
302
|
+
document.body.classList.add("playing");
|
|
303
|
+
addRecent(game.id);
|
|
304
|
+
if (currentLoad?.id !== game.id) loadGame(game);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
stopGame();
|
|
308
|
+
els.player.hidden = true;
|
|
309
|
+
document.body.classList.remove("playing");
|
|
310
|
+
document.title = SITE_NAME;
|
|
311
|
+
const view = location.hash === "#games" || location.hash === "#recent" ? "games" : "home";
|
|
312
|
+
els.homeView.hidden = view !== "home";
|
|
313
|
+
els.gamesView.hidden = view !== "games";
|
|
314
|
+
document.querySelectorAll(".rail-btn[data-nav]").forEach((b) =>
|
|
315
|
+
b.classList.toggle("active", b.dataset.nav === (location.hash === "#recent" ? "recent" : view)));
|
|
316
|
+
if (view === "games") renderRecent();
|
|
317
|
+
if (location.hash === "#recent") requestAnimationFrame(() => els.recentBlock.scrollIntoView({ block: "start" }));
|
|
318
|
+
}
|
|
319
|
+
function play(id) { location.hash = `#play/${encodeURIComponent(id)}`; }
|
|
320
|
+
function go(view) {
|
|
321
|
+
const target = view === "home" ? "" : `#${view}`;
|
|
322
|
+
if (location.hash !== target) history.pushState(null, "", target || location.pathname + location.search);
|
|
323
|
+
route();
|
|
324
|
+
if (view !== "recent") window.scrollTo(0, 0);
|
|
325
|
+
}
|
|
326
|
+
function goBack() {
|
|
327
|
+
if (location.hash.startsWith("#play/")) history.length > 1 ? history.back() : (location.hash = "#games");
|
|
328
|
+
else if (location.hash) go("home");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------- Utilities ----------
|
|
332
|
+
// If this page is ever shown inside a frame, redirect the whole tab
|
|
333
|
+
function panic() {
|
|
334
|
+
const url = CFG.panicUrl || "https://classroom.google.com/";
|
|
335
|
+
try { window.top.location.replace(url); } catch { location.replace(url); }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function randomGame() {
|
|
339
|
+
const pool = !els.gamesView.hidden && activeTag !== "All" ? games.filter((g) => g.tag === activeTag) : games;
|
|
340
|
+
if (pool.length) play(pool[Math.floor(Math.random() * pool.length)].id);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function startClock() {
|
|
344
|
+
let is24 = store.get(CLOCK_KEY, false);
|
|
345
|
+
const timeEl = $("clock-time"), metaEl = $("clock-meta");
|
|
346
|
+
const tick = () => {
|
|
347
|
+
const d = new Date();
|
|
348
|
+
let h = d.getHours();
|
|
349
|
+
const m = String(d.getMinutes()).padStart(2, "0");
|
|
350
|
+
if (is24) { timeEl.textContent = `${String(h).padStart(2, "0")}:${m}`; metaEl.textContent = "24H"; }
|
|
351
|
+
else { const ap = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; timeEl.textContent = `${h}:${m}`; metaEl.textContent = ap; }
|
|
352
|
+
};
|
|
353
|
+
$("clock").addEventListener("click", () => { is24 = !is24; store.set(CLOCK_KEY, is24); tick(); });
|
|
354
|
+
tick();
|
|
355
|
+
setInterval(tick, 1000);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function initTheme() {
|
|
359
|
+
const btn = $("theme-btn");
|
|
360
|
+
const apply = (t) => {
|
|
361
|
+
document.documentElement.dataset.theme = t;
|
|
362
|
+
btn.title = t === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
|
363
|
+
};
|
|
364
|
+
apply(document.documentElement.dataset.theme === "light" ? "light" : "dark");
|
|
365
|
+
btn.addEventListener("click", () => {
|
|
366
|
+
const next = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
|
|
367
|
+
apply(next);
|
|
368
|
+
try { localStorage.setItem(THEME_KEY, next); } catch {}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---------- Arrow keys and controller move focus between tiles and buttons ----------
|
|
373
|
+
function focusables() {
|
|
374
|
+
const scope = els.player.hidden ? document : els.player;
|
|
375
|
+
return [...scope.querySelectorAll(".tile, .rail-btn, .tab, .per-row button, #search, #sort, #home-input, .text-btn, .pill-btn")]
|
|
376
|
+
.filter((el) => el.offsetParent !== null && !el.closest("[hidden]"));
|
|
377
|
+
}
|
|
378
|
+
function moveFocus(dir) {
|
|
379
|
+
const cur = document.activeElement;
|
|
380
|
+
const vw = innerWidth;
|
|
381
|
+
const list = focusables().filter((el) => { const r = el.getBoundingClientRect(); return r.right > 0 && r.left < vw; });
|
|
382
|
+
if (!list.includes(cur)) { (list.find((el) => el.classList.contains("tile")) || list[0])?.focus(); return; }
|
|
383
|
+
const a = cur.getBoundingClientRect();
|
|
384
|
+
const ax = a.left + a.width / 2, ay = a.top + a.height / 2;
|
|
385
|
+
let best = null, bestScore = Infinity;
|
|
386
|
+
for (const el of list) {
|
|
387
|
+
if (el === cur) continue;
|
|
388
|
+
const b = el.getBoundingClientRect();
|
|
389
|
+
const dx = b.left + b.width / 2 - ax, dy = b.top + b.height / 2 - ay;
|
|
390
|
+
const main = { left: -dx, right: dx, up: -dy, down: dy }[dir];
|
|
391
|
+
if (main <= 4) continue;
|
|
392
|
+
const cross = dir === "left" || dir === "right" ? Math.abs(dy) : Math.abs(dx);
|
|
393
|
+
const score = main + cross * 2.5;
|
|
394
|
+
if (score < bestScore) { bestScore = score; best = el; }
|
|
395
|
+
}
|
|
396
|
+
if (best) { best.focus({ preventScroll: true }); best.scrollIntoView({ block: "nearest", behavior: "smooth" }); }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function onKey(e) {
|
|
400
|
+
const keys = CFG.panicKeys || ["`", "]"];
|
|
401
|
+
if (keys.includes(e.key)) { e.preventDefault(); panic(); return; }
|
|
402
|
+
if (!els.player.hidden) {
|
|
403
|
+
if (e.key === "Escape" && document.activeElement !== els.frame) goBack();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const typing = document.activeElement === els.search || document.activeElement === els.homeInput;
|
|
407
|
+
if (e.key === "/" && !typing) {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
(els.homeView.hidden ? els.search : els.homeInput).focus();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (e.key === "Escape") {
|
|
413
|
+
if (typing) { document.activeElement.value = ""; renderGrid(); document.activeElement.blur(); } else goBack();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const dir = { ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", ArrowDown: "down" }[e.key];
|
|
417
|
+
if (dir && !(typing && (dir === "left" || dir === "right"))) { e.preventDefault(); moveFocus(dir); }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Gamepad: d-pad/left stick move, A select, B back, Y random. In a game only View (⧉) leaves,
|
|
421
|
+
// since games use A/B themselves.
|
|
422
|
+
function startGamepad() {
|
|
423
|
+
const prev = {};
|
|
424
|
+
let lastMove = 0;
|
|
425
|
+
const press = (i) => {
|
|
426
|
+
if (!els.player.hidden) { if (i === 8) goBack(); return; }
|
|
427
|
+
if (i === 0) document.activeElement?.click?.();
|
|
428
|
+
else if (i === 1) goBack();
|
|
429
|
+
else if (i === 3) randomGame();
|
|
430
|
+
else if (i === 12) moveFocus("up");
|
|
431
|
+
else if (i === 13) moveFocus("down");
|
|
432
|
+
else if (i === 14) moveFocus("left");
|
|
433
|
+
else if (i === 15) moveFocus("right");
|
|
434
|
+
};
|
|
435
|
+
function poll(t) {
|
|
436
|
+
const pads = navigator.getGamepads ? [...navigator.getGamepads()].filter(Boolean) : [];
|
|
437
|
+
for (const pad of pads) {
|
|
438
|
+
pad.buttons.forEach((b, i) => {
|
|
439
|
+
const key = `${pad.index}:${i}`;
|
|
440
|
+
if (b.pressed && !prev[key]) press(i);
|
|
441
|
+
prev[key] = b.pressed;
|
|
442
|
+
});
|
|
443
|
+
const [x, y] = pad.axes;
|
|
444
|
+
if (els.player.hidden && t - lastMove > 180 && (Math.abs(x) > 0.6 || Math.abs(y) > 0.6)) {
|
|
445
|
+
moveFocus(Math.abs(x) > Math.abs(y) ? (x > 0 ? "right" : "left") : (y > 0 ? "down" : "up"));
|
|
446
|
+
lastMove = t;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (pads.length) requestAnimationFrame(poll);
|
|
450
|
+
}
|
|
451
|
+
window.addEventListener("gamepadconnected", () => requestAnimationFrame(poll));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------- Wire up ----------
|
|
455
|
+
function bind() {
|
|
456
|
+
document.addEventListener("keydown", onKey, true);
|
|
457
|
+
document.addEventListener("click", (e) => {
|
|
458
|
+
const tile = e.target.closest(".tile");
|
|
459
|
+
if (tile) { play(tile.dataset.id); return; }
|
|
460
|
+
const nav = e.target.closest("[data-nav]");
|
|
461
|
+
if (nav) { e.preventDefault(); go(nav.dataset.nav); }
|
|
462
|
+
});
|
|
463
|
+
// Typing on the home search jumps straight into the games list
|
|
464
|
+
const fromHome = () => {
|
|
465
|
+
els.search.value = els.homeInput.value;
|
|
466
|
+
els.homeInput.value = "";
|
|
467
|
+
setTag("All");
|
|
468
|
+
go("games");
|
|
469
|
+
els.search.focus();
|
|
470
|
+
renderGrid();
|
|
471
|
+
};
|
|
472
|
+
els.homeInput.addEventListener("input", () => { if (els.homeInput.value.trim()) fromHome(); });
|
|
473
|
+
$("home-search").addEventListener("submit", (e) => { e.preventDefault(); fromHome(); });
|
|
474
|
+
|
|
475
|
+
els.search.addEventListener("input", renderGrid);
|
|
476
|
+
els.sort.addEventListener("change", renderGrid);
|
|
477
|
+
els.chips.addEventListener("click", (e) => { const tab = e.target.closest(".tab"); if (tab) setTag(tab.dataset.tag); });
|
|
478
|
+
document.querySelectorAll(".per-row button").forEach((b) => b.addEventListener("click", () => setPerRow(Number(b.dataset.per))));
|
|
479
|
+
$("clear-recent").addEventListener("click", () => { store.set(RECENT_KEY, []); renderRecent(); });
|
|
480
|
+
$("random-btn").addEventListener("click", randomGame);
|
|
481
|
+
$("panic-btn").addEventListener("click", panic);
|
|
482
|
+
$("panic-btn-2").addEventListener("click", panic);
|
|
483
|
+
$("back-btn").addEventListener("click", goBack);
|
|
484
|
+
$("reload-btn").addEventListener("click", () => {
|
|
485
|
+
const g = currentLoad && byId.get(currentLoad.id);
|
|
486
|
+
if (g) loadGame(g);
|
|
487
|
+
});
|
|
488
|
+
els.retryBtn.addEventListener("click", () => { const id = currentLoad?.id; if (id) loadGame(byId.get(id)); });
|
|
489
|
+
$("fullscreen-btn").addEventListener("click", () => {
|
|
490
|
+
const el = els.frame;
|
|
491
|
+
(el.requestFullscreen || el.webkitRequestFullscreen)?.call(el);
|
|
492
|
+
});
|
|
493
|
+
window.addEventListener("hashchange", route);
|
|
494
|
+
window.addEventListener("popstate", route);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function init() {
|
|
498
|
+
initTheme();
|
|
499
|
+
startClock();
|
|
500
|
+
startGamepad();
|
|
501
|
+
setPerRow(store.get(PER_ROW_KEY, 6));
|
|
502
|
+
bind();
|
|
503
|
+
try {
|
|
504
|
+
const res = await fetch("games.json", { cache: "no-cache" });
|
|
505
|
+
const data = await res.json();
|
|
506
|
+
games = data.games.slice().sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
|
|
507
|
+
byId = new Map(games.map((g) => [g.id, g]));
|
|
508
|
+
} catch (err) {
|
|
509
|
+
console.error(err);
|
|
510
|
+
els.homeSub.textContent = "Couldn't load the game list. Try refreshing.";
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
els.homeInput.placeholder = `Search ${games.length} games…`;
|
|
514
|
+
els.homeSub.innerHTML = `${games.length} games · press <kbd>/</kbd> to search`;
|
|
515
|
+
els.footerCount.textContent = `${games.length} games`;
|
|
516
|
+
renderChips();
|
|
517
|
+
renderRecent();
|
|
518
|
+
renderGrid();
|
|
519
|
+
startMarquee();
|
|
520
|
+
route();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
init();
|
|
524
|
+
})();
|
package/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// ===== Richards Unblocked Games: settings =====
|
|
2
|
+
// The only file you normally need to edit.
|
|
3
|
+
window.RUG_CONFIG = {
|
|
4
|
+
// Your GitHub username (the account that owns the rug-games-N repos)
|
|
5
|
+
githubUser: "Kingston5390",
|
|
6
|
+
|
|
7
|
+
// Branch the games live on. jsDelivr caches branches for up to 12h;
|
|
8
|
+
// use a tag like "v1" here if you want instant, permanent URLs.
|
|
9
|
+
branch: "main",
|
|
10
|
+
|
|
11
|
+
// Where the panic button sends you
|
|
12
|
+
panicUrl: "https://classroom.google.com/",
|
|
13
|
+
|
|
14
|
+
// Keys that trigger panic (KeyboardEvent.key values)
|
|
15
|
+
panicKeys: ["`", "]"],
|
|
16
|
+
|
|
17
|
+
// For local testing only: load games from a folder on a local server
|
|
18
|
+
// instead of jsDelivr, e.g. "/repos". Leave null when published.
|
|
19
|
+
localBase: null,
|
|
20
|
+
};
|