微件:GGLScratchGame
来自Limbo Wiki Mirror
<button class="sl-open-btn">来一张彩票</button>
Error: src attribute required.
<button class="sl-mascot">(◕‿◕) …</button> <button class="sl-scan">扫描结果</button>
<style> .scratch-lottery-root {
max-width: 520px; margin: 2em auto; font-family: system-ui, sans-serif; user-select: none;
} .sl-header {
display: flex; justify-content: space-between; align-items: center; margin-bottom: .5em;
} .sl-open-btn { padding: .4em .8em; } .sl-stage { position: relative; } .sl-canvas { position: relative; width: 100%; } .sl-bg { width: 100%; display: block; } .sl-area { position: absolute; } .sl-tile {
position: absolute; width: 169px; height: 169px; cursor: pointer; outline: none;
} .sl-cover img, .sl-reveal img {
width: 100%; height: 100%; object-fit: cover;
} .sl-bubble {
position: absolute; background: #fff; border: 1px solid #000; padding: .4em .6em; font-size: 13px; z-index: 50;
} .sl-controls {
display: flex; margin-top: .5em;
} .sl-controls button {
flex: 1; padding: .6em;
} .sl-scan.highlight {
background: #ffe600; animation: pulse 1s infinite;
} @keyframes pulse {
from { box-shadow: 0 0 0 rgba(255,230,0,.8); }
to { box-shadow: 0 0 12px rgba(255,230,0,0); }
} .sl-invert {
filter: invert(1) hue-rotate(180deg);
} .sl-debug {
font-size: 11px; color: #666; margin-bottom: .5em;
} </style>
<script> (function () {
const root = document.currentScript.parentElement;
const cfg = JSON.parse(root.dataset.config || '{}');
const bg = root.querySelector('.sl-bg');
const area = root.querySelector('.sl-area');
const mascot = root.querySelector('.sl-mascot');
const scanBtn = root.querySelector('.sl-scan');
const openBtn = root.querySelector('.sl-open-btn');
const debugBox = root.querySelector('.sl-debug');
const tickets = cfg.tickets || [];
const states = cfg.states || {};
let ticketIndex = 0;
let playerTickets = cfg.initialTickets || 3;
let currentTicket = null;
let revealed = new Set();
bg.src = cfg.config.bg;
function layout() {
const scale = bg.clientWidth / 1075;
area.style.left = (cfg.config.area.x * scale) + 'px';
area.style.top = (cfg.config.area.y * scale) + 'px';
area.style.width = (cfg.config.area.width * scale) + 'px';
area.style.height = (cfg.config.area.height * scale) + 'px';
}
bg.onload = layout;
window.addEventListener('resize', layout);
function updateInfo() {
root.querySelector('.sl-ticket-count').textContent = '🎟️ 票数:' + playerTickets;
root.querySelector('.sl-ticket-index').textContent =
currentTicket ? '当前票:' + currentTicket.id : '当前票:-';
if (cfg.debug) {
debugBox.textContent =
'ticketIndex=' + ticketIndex +
' revealed=' + revealed.size +
' playerTickets=' + playerTickets;
}
}
function openTicket() {
if (playerTickets <= 0 || ticketIndex >= tickets.length) return;
currentTicket = tickets[ticketIndex++];
playerTickets--;
revealed.clear();
scanBtn.classList.remove('highlight');
mascot.style.visibility = 'visible';
renderTiles();
updateInfo();
}
function renderTiles() {
area.innerHTML = ;
currentTicket.outcomes.forEach((stateId, i) => {
const tile = document.createElement('div');
tile.className = 'sl-tile';
tile.tabIndex = 0;
tile.setAttribute('aria-label', '刮奖格子 ' + (i + 1));
const col = i % cfg.config.cols;
const row = Math.floor(i / cfg.config.cols);
tile.style.left = (col * cfg.config.tileSize) + 'px';
tile.style.top = (row * cfg.config.tileSize) + 'px';
const cover = document.createElement('div');
cover.className = 'sl-cover';
const coverImg = document.createElement('img');
coverImg.src = cfg.config.cover;
cover.appendChild(coverImg);
tile.appendChild(cover);
tile.onclick = () => revealTile(i, tile);
tile.onkeydown = e => {
if (e.key === 'Enter' || e.key === ' ') revealTile(i, tile);
};
area.appendChild(tile); }); }
function revealTile(i, tile) {
if (revealed.has(i)) return;
revealed.add(i);
tile.innerHTML = ;
const reveal = document.createElement('div');
reveal.className = 'sl-reveal';
const img = document.createElement('img');
img.src = cfg.config.reveal;
reveal.appendChild(img);
tile.appendChild(reveal);
const state = states[currentTicket.outcomes[i]]; showBubble(tile, state?.text || ); handleState(currentTicket.outcomes[i]); checkCompletion(); }
function showBubble(tile, text) {
if (!text) return;
const bubble = document.createElement('div');
bubble.className = 'sl-bubble';
bubble.textContent = text;
tile.appendChild(bubble);
const rect = tile.getBoundingClientRect(); bubble.style.top = rect.bottom > window.innerHeight - 80 ? '-2.4em' : '100%'; bubble.style.left = '0';
setTimeout(() => bubble.remove(), 3000); }
function handleState(id) {
const s = states[id];
if (!s) return;
if (s.type === 'emoji') mascot.textContent = s.text;
if (id === 6) mascot.style.visibility = 'hidden';
if (id === 10) scanBtn.classList.add('highlight');
if (id === 11) endGame();
}
function checkCompletion() {
if (revealed.size === 30 && currentTicket.id === 10) {
scanBtn.classList.add('highlight');
}
}
function endGame() {
root.classList.add('sl-invert');
setTimeout(() => {
root.style.display = 'none';
location.hash = '#词条正文';
}, 1500);
}
openBtn.onclick = openTicket;
scanBtn.onclick = () => {
if (currentTicket && currentTicket.id === 10) openTicket();
};
updateInfo();
})(); </script>
