微件:GGLScratchGame:修订间差异

来自Limbo Wiki Mirror
Gaoice留言 | 贡献
无编辑摘要
标签(旧)WikiEditor
Gaoice留言 | 贡献
无编辑摘要
第1行: 第1行:
<!DOCTYPE html>
<div class="scratch-lottery-root" data-config="">
<html>
  <div class="sl-debug"></div>
<head>
    <meta charset="UTF-8">
    <style>
        .scratch-lottery-container {
            font-family: 'Arial', sans-serif;
            max-width: 1100px;
            margin: 20px auto;
            background: #f5f5f5;
            border-radius: 8px;
            padding: 20px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }


        .scratch-lottery-header {
  <div class="sl-header">
            text-align: center;
     <div class="sl-info">
            margin-bottom: 20px;
      <span class="sl-ticket-count"></span>
        }
      <span class="sl-ticket-index"></span>
 
        .scratch-lottery-header h2 {
            margin: 0 0 10px 0;
            color: #333;
            font-size: 24px;
        }
 
        .scratch-lottery-status {
            display: flex;
            justify-content: space-around;
            margin: 15px 0;
            font-size: 14px;
            color: #666;
        }
 
        .status-item {
            text-align: center;
        }
 
        .status-label {
            font-weight: bold;
            color: #333;
            display: block;
            font-size: 12px;
            margin-bottom: 4px;
        }
 
        .status-value {
            font-size: 20px;
            color: #ff6b35;
        }
 
        /* 彩票主容器 */
        .lottery-ticket-wrapper {
            position: relative;
            margin: 20px auto;
            display: inline-block;
            width: 100%;
            max-width: 540px;
            background: white;
            border-radius: 4px;
            overflow: hidden;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
        }
 
        .lottery-bg-img {
            display: block;
            width: 100%;
            height: auto;
            background: #fff;
        }
 
        /* 刮奖区域容器(黄色块位置) */
        .scratchable-area {
            position: absolute;
            left: 10.6%;
            top: 23.6%;
            width: 78.6%;
            height: 53.4%;
            background: transparent;
        }
 
        /* 30 个格子网格 */
        .tile-grid {
            display: grid;
            grid-template-columns: repeat(5, 1fr);
            grid-template-rows: repeat(6, 1fr);
            gap: 0;
            width: 100%;
            height: 100%;
            padding: 0;
        }
 
        /* 单个格子 */
        .tile {
            position: relative;
            aspect-ratio: 1;
            background: #000;
            cursor: pointer;
            border: 1px solid rgba(255,255,255,0.2);
            overflow: hidden;
            transition: all 0.2s ease;
        }
 
        .tile:hover:not(.revealed) {
            background: #1a1a1a;
            box-shadow: inset 0 0 8px rgba(255,255,255,0.3);
        }
 
        .tile-cover {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            background: #000;
            font-size: 24px;
            font-weight: bold;
            color: #666;
            opacity: 1;
            transition: opacity 0.3s ease;
            pointer-events: none;
        }
 
        .tile-cover img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
        }
 
        .tile.revealed .tile-cover {
            opacity: 0;
            pointer-events: none;
        }
 
        .tile-reveal {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            background: #ffd700;
            opacity: 0;
            transition: opacity 0.3s ease;
            pointer-events: none;
        }
 
        .tile-reveal img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
        }
 
        .tile.revealed .tile-reveal {
            opacity: 1;
        }
 
        .tile-text {
            position: absolute;
            inset: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
            color: #333;
            font-weight: bold;
            text-align: center;
            padding: 4px;
            word-break: break-word;
        }
 
        /* 气泡文本 */
        .bubble {
            position: fixed;
            background: white;
            border: 2px solid #333;
            border-radius: 8px;
            padding: 12px 16px;
            font-size: 13px;
            color: #333;
            max-width: 200px;
            word-wrap: break-word;
            z-index: 1000;
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
            animation: bubbleAppear 0.3s ease;
            pointer-events: none;
        }
 
        .bubble::before {
            content: '';
            position: absolute;
            bottom: -8px;
            left: 20px;
            width: 0;
            height: 0;
            border-left: 8px solid transparent;
            border-right: 8px solid transparent;
            border-top: 8px solid #333;
        }
 
        .bubble::after {
            content: '';
            position: absolute;
            bottom: -4px;
            left: 22px;
            width: 0;
            height: 0;
            border-left: 6px solid transparent;
            border-right: 6px solid transparent;
            border-top: 6px solid white;
        }
 
        .bubble.top {
            bottom: auto;
        }
 
        .bubble.top::before {
            bottom: auto;
            top: -8px;
            border-top: none;
            border-bottom: 8px solid #333;
        }
 
        .bubble.top::after {
            bottom: auto;
            top: -4px;
            border-top: none;
            border-bottom: 6px solid white;
        }
 
        @keyframes bubbleAppear {
            from {
                opacity: 0;
                transform: scale(0.8);
            }
            to {
                opacity: 1;
                transform: scale(1);
            }
        }
 
        /* 吉祥物按钮 + 扫描按钮容器 */
        .controls-row {
            display: flex;
            gap: 10px;
            margin-top: 20px;
            width: 100%;
        }
 
        .mascot-button,
        .scan-button {
            flex: 1;
            padding: 16px 20px;
            font-size: 14px;
            font-weight: bold;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 12px;
            transition: all 0.3s ease;
        }
 
        .mascot-button {
            background: #fff3e0;
            color: #333;
            border: 2px solid #ffb74d;
        }
 
        .mascot-button:hover {
            background: #ffe0b2;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(255, 183, 77, 0.3);
        }
 
        .mascot-icon {
            font-size: 24px;
            min-width: 28px;
        }
 
        .mascot-text {
            text-align: left;
            flex: 1;
        }
 
        .scan-button {
            background: #e0e0e0;
            color: #666;
            border: 2px solid #999;
        }
 
        .scan-button:hover {
            background: #d0d0d0;
            transform: translateY(-2px);
        }
 
        .scan-button.active {
            background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
            color: white;
            border-color: #ff6b35;
            box-shadow: 0 4px 16px rgba(255, 107, 53, 0.4);
            animation: pulse 1.5s infinite;
        }
 
        .scan-button.active:hover {
            box-shadow: 0 6px 20px rgba(255, 107, 53, 0.6);
        }
 
        @keyframes pulse {
            0%, 100% {
                transform: scale(1);
            }
            50% {
                transform: scale(1.02);
            }
        }
 
        /* 彩票申领按钮 */
        .draw-button {
            display: block;
            margin: 20px auto;
            padding: 12px 32px;
            font-size: 16px;
            font-weight: bold;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            transition: all 0.3s ease;
        }
 
        .draw-button:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
        }
 
        .draw-button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }
 
        /* 无彩票提示 */
        .no-tickets-message {
            text-align: center;
            padding: 40px 20px;
            color: #999;
            font-size: 14px;
        }
 
        /* 倒数计时器 */
        .countdown {
            display: inline-block;
            font-size: 14px;
            color: #ff6b35;
            font-weight: bold;
            margin-left: 10px;
        }
 
        /* 反色滤镜 (第11张后) */
        .invert-filter {
            filter: invert(1) hue-rotate(180deg);
        }
 
        /* 调试模式 */
        .debug-panel {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(0,0,0,0.85);
            color: #0f0;
            padding: 12px 16px;
            border-radius: 4px;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            z-index: 999;
            max-height: 200px;
            overflow-y: auto;
            display: none;
        }
 
        .debug-panel.active {
            display: block;
        }
 
        .debug-line {
            margin: 4px 0;
        }
 
        /* 响应式设计 */
        @media (max-width: 600px) {
            .scratch-lottery-container {
                padding: 12px;
            }
 
            .lottery-ticket-wrapper {
                max-width: 100%;
            }
 
            .controls-row {
                flex-direction: column;
            }
 
            .scratch-lottery-status {
                flex-direction: column;
                gap: 10px;
            }
        }
 
        /* 整页反色效果 (第11张) */
        .page-invert-crash {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: #000;
            color: #ff0000;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 32px;
            font-weight: bold;
            font-family: 'Courier New', monospace;
            z-index: 2000;
            animation: crashFlash 0.1s infinite;
            pointer-events: none;
        }
 
        @keyframes crashFlash {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.8; }
        }
 
        /* 吉祥物消失效果 */
        .mascot-hidden .mascot-button {
            opacity: 0;
            pointer-events: none;
        }
    </style>
</head>
<body>
 
<div class="scratch-lottery-container" id="scratchLotteryRoot">
     <div class="scratch-lottery-header">
        <h2>🎰 刮刮乐抽奖</h2>
        <div class="scratch-lottery-status">
            <div class="status-item">
                <span class="status-label">剩余彩票</span>
                <span class="status-value" id="ticketCount">3</span>
            </div>
            <div class="status-item">
                <span class="status-label">当前票号</span>
                <span class="status-value" id="currentTicketId">-</span>
            </div>
            <div class="status-item">
                <span class="status-label">已刮格子</span>
                <span class="status-value" id="revealedCount">0/30</span>
            </div>
        </div>
     </div>
     </div>
    <button class="sl-open-btn">来一张彩票</button>
  </div>


    <!-- 主彩票区域 -->
  <div class="sl-stage">
     <div id="ticketContainer" style="text-align: center;">
     <div class="sl-canvas">
        <button class="draw-button" id="drawButton">来一张彩票</button>
      <img class="sl-bg" />
        <div class="no-tickets-message" id="noTicketsMsg" style="display: none;">
      <div class="sl-area"></div>
            没有更多彩票了!所有剧情已解锁。
        </div>
     </div>
     </div>
  </div>


    <!-- 控制按钮 -->
  <div class="sl-controls">
    <div class="controls-row" id="controlsRow" style="display: none;">
    <button class="sl-mascot">(◕‿◕) …</button>
        <div class="mascot-button" id="mascotButton">
    <button class="sl-scan">扫描结果</button>
            <span class="mascot-icon" id="mascotIcon">🎲</span>
  </div>
            <div class="mascot-text" id="mascotText">点击格子开始</div>
</div>
        </div>
        <button class="scan-button" id="scanButton" style="display: none;">
            扫描结果
        </button>
    </div>


    <!-- 调试面板 -->
<style>
    <div class="debug-panel" id="debugPanel"></div>
.scratch-lottery-root {
</div>
  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>
<script>
// ==================== 配置与数据 ====================
(function () {
const CONFIG = {
  const root = document.currentScript.parentElement;
    // 图片设置(可选)
  const cfg = JSON.parse(root.dataset.config || '{}');
    useImages: true,  // 改为 true 以使用自定义背景图
    bgImage: 'https://wm.gaoice.run/images/thumb/b/b6/%E5%9B%BE%E7%89%871.png/180px-%E5%9B%BE%E7%89%871.png',      // 背景图路径(1075×1911 px)
    coverImage: 'https://wm.gaoice.run/images/thumb/5/5a/%E5%88%AE%E5%BC%80%E5%89%8D.png/180px-%E5%88%AE%E5%BC%80%E5%89%8D.png',    // 刮开前覆盖图(169×169 px)
    revealImage: 'https://wm.gaoice.run/images/thumb/4/4a/%E5%88%AE%E5%BC%80%E5%90%8E.jpg/180px-%E5%88%AE%E5%BC%80%E5%90%8E.jpg',  // 刮开后揭示图(169×169 px)
   
    // 布局配置
    bg: 'lottery-bg.png',
    area: { x: 114.5, y: 450, width: 846, height: 1020 },
    cols: 5,
    rows: 6,
    tileSize: 169.2,
    coverSize: 559,
    scale: 0.3027
};


const STATES = {
  const bg = root.querySelector('.sl-bg');
    1: { type: 'normal', text: '', emoji: '😐' },
  const area = root.querySelector('.sl-area');
    2: { type: 'emoji', text: '(◕‿◕) 彩票扫描中…', emoji: '😊' },
  const mascot = root.querySelector('.sl-mascot');
    3: { type: 'emoji', text: '(≧◡≦) 太棒了!', emoji: '🎉' },
  const scanBtn = root.querySelector('.sl-scan');
    4: { type: 'emoji', text: '(•̀ᴗ•́) 思考中…', emoji: '🤔' },
  const openBtn = root.querySelector('.sl-open-btn');
    5: { type: 'emoji', text: '(⊙_⊙) 等等,好像不对', emoji: '😲' },
  const debugBox = root.querySelector('.sl-debug');
    6: { type: 'meta', text: '你看见了什么?', emoji: '👻' },
    7: { type: 'glitch', text: '…▒▒▒▒▒', emoji: '❌' },
    8: { type: 'text', text: '哦嗯嘿哈… 别挠我痒痒', emoji: '😅' },
    9: { type: 'text', text: '你是谁?', emoji: '❓' },
    10: { type: 'text', text: '停下来。', emoji: '🛑' },
    11: { type: 'text', text: '平安喜乐。你知道的太多了。', emoji: '⚫' }
};


// 11张预设票序数据 (每张 30 格的状态)
  const tickets = cfg.tickets || [];
const TICKETS = [
  const states = cfg.states || {};
    {
  let ticketIndex = 0;
        id: 1,
  let playerTickets = cfg.initialTickets || 3;
        outcomes: [2,1,1,3,1, 1,2,1,1,1, 1,1,2,1,1, 1,1,1,2,1, 1,1,1,1,1, 1,1,1,1,1],
  let currentTicket = null;
        notes: '第1张:介绍规则,吉祥物出场',
  let revealed = new Set();
        extraTickets: 0,
        storyTrigger: 'intro'
    },
    {
        id: 2,
        outcomes: [3,1,1,1,3, 1,1,3,1,1, 3,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1],
        notes: '第2张:连中四次,额外发放 2 张',
        extraTickets: 2,
        storyTrigger: 'bonus'
    },
    {
        id: 3,
        outcomes: [1,2,1,1,1, 1,1,2,1,1, 1,1,1,1,2, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1],
        notes: '第3张:中两次 data',
        extraTickets: 0,
        storyTrigger: 'combo'
    },
    {
        id: 4,
        outcomes: [1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,3],
        notes: '第4张:本来没中,颜文字改了一个字后中了',
        extraTickets: 0,
        storyTrigger: 'plot_twist'
    },
    {
        id: 5,
        outcomes: [2,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1],
        notes: '第5张:中 data',
        extraTickets: 0,
        storyTrigger: 'normal'
    },
    {
        id: 6,
        outcomes: [1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1],
        notes: '第6张:什么都没有(空白)',
        extraTickets: 0,
        storyTrigger: 'empty'
    },
    {
        id: 7,
        outcomes: [1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,3,1, 1,1,1,1,1],
        notes: '第7张:中了一张',
        extraTickets: 0,
        storyTrigger: 'single'
    },
    {
        id: 8,
        outcomes: [2,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,1,1, 1,1,1,3,1, 1,1,1,3,1],
        notes: '第8张:中 data,并中两张',
        extraTickets: 0,
        storyTrigger: 'double'
    },
    {
        id: 9,
        outcomes: [2,2,2,2,2, 2,2,2,2,2, 2,2,2,2,2, 2,2,2,2,2, 2,2,2,2,2, 2,2,2,2,2],
        notes: '第9张:全页 data',
        extraTickets: 0,
        storyTrigger: 'full_page'
    },
    {
        id: 10,
        outcomes: [10,10,10,10,10, 10,10,10,10,10, 10,10,10,10,10, 10,10,10,10,10, 10,10,10,10,10, 10,10,10,10,10],
        notes: '第10张:全页停下来,扫描按钮高亮',
        extraTickets: 0,
        storyTrigger: 'scan_ready'
    },
    {
        id: 11,
        outcomes: [11,11,11,11,11, 11,11,11,11,11, 11,11,11,11,11, 11,11,11,11,11, 11,11,11,11,11, 11,11,11,11,11],
        notes: '第11张:反色滤镜,伪闪退跳转',
        extraTickets: 0,
        storyTrigger: 'final'
    }
];


// ==================== 游戏状态机 ====================
  bg.src = cfg.config.bg;
let gameState = {
    playerTickets: 3,
    currentTicketIndex: -1,
    currentTicket: null,
    revealedTiles: new Set(),
    isTicketOpen: false,
    isFinal: false,
    debugMode: false
};


// ==================== DOM 元素缓存 ====================
  function layout() {
const elements = {
    const scale = bg.clientWidth / 1075;
     root: null,
     area.style.left = (cfg.config.area.x * scale) + 'px';
     ticketContainer: null,
     area.style.top = (cfg.config.area.y * scale) + 'px';
     drawButton: null,
     area.style.width = (cfg.config.area.width * scale) + 'px';
     noTicketsMsg: null,
     area.style.height = (cfg.config.area.height * scale) + 'px';
    controlsRow: null,
  }
    mascotButton: null,
  bg.onload = layout;
    mascotIcon: null,
  window.addEventListener('resize', layout);
    mascotText: null,
    scanButton: null,
    ticketCount: null,
    currentTicketId: null,
    revealedCount: null,
    debugPanel: null
};


// ==================== 初始化 ====================
  function updateInfo() {
function init() {
     root.querySelector('.sl-ticket-count').textContent = '🎟️ 票数:' + playerTickets;
     cacheElements();
     root.querySelector('.sl-ticket-index').textContent =
     attachEventListeners();
      currentTicket ? '当前票:' + currentTicket.id : '当前票:-';
    updateUI();
    logDebug('Game initialized');
}


function cacheElements() {
     if (cfg.debug) {
     elements.root = document.getElementById('scratchLotteryRoot');
      debugBox.textContent =
    elements.ticketContainer = document.getElementById('ticketContainer');
        'ticketIndex=' + ticketIndex +
    elements.drawButton = document.getElementById('drawButton');
        ' revealed=' + revealed.size +
    elements.noTicketsMsg = document.getElementById('noTicketsMsg');
        ' playerTickets=' + playerTickets;
    elements.controlsRow = document.getElementById('controlsRow');
     }
    elements.mascotButton = document.getElementById('mascotButton');
  }
    elements.mascotIcon = document.getElementById('mascotIcon');
    elements.mascotText = document.getElementById('mascotText');
    elements.scanButton = document.getElementById('scanButton');
    elements.ticketCount = document.getElementById('ticketCount');
     elements.currentTicketId = document.getElementById('currentTicketId');
    elements.revealedCount = document.getElementById('revealedCount');
    elements.debugPanel = document.getElementById('debugPanel');
}


function attachEventListeners() {
  function openTicket() {
     elements.drawButton.addEventListener('click', drawTicket);
     if (playerTickets <= 0 || ticketIndex >= tickets.length) return;
     elements.mascotButton.addEventListener('click', mascotButtonClick);
    currentTicket = tickets[ticketIndex++];
     elements.scanButton.addEventListener('click', scanButtonClick);
    playerTickets--;
      
    revealed.clear();
     // 键盘快捷键
     scanBtn.classList.remove('highlight');
    document.addEventListener('keydown', handleKeyboard);
     mascot.style.visibility = 'visible';
}
     renderTiles();
     updateInfo();
  }


// ==================== 核心游戏函数 ====================
  function renderTiles() {
function drawTicket() {
     area.innerHTML = '';
     if (gameState.playerTickets <= 0) {
    currentTicket.outcomes.forEach((stateId, i) => {
        showMessage('没有更多彩票了!');
      const tile = document.createElement('div');
        return;
      tile.className = 'sl-tile';
    }
      tile.tabIndex = 0;
      tile.setAttribute('aria-label', '刮奖格子 ' + (i + 1));


    gameState.currentTicketIndex++;
      const col = i % cfg.config.cols;
    if (gameState.currentTicketIndex >= TICKETS.length) {
      const row = Math.floor(i / cfg.config.cols);
        showMessage('所有剧情已完成!');
      tile.style.left = (col * cfg.config.tileSize) + 'px';
        return;
      tile.style.top = (row * cfg.config.tileSize) + 'px';
    }


    gameState.currentTicket = TICKETS[gameState.currentTicketIndex];
      const cover = document.createElement('div');
    gameState.playerTickets--;
      cover.className = 'sl-cover';
    gameState.revealedTiles.clear();
    gameState.isTicketOpen = true;


    renderTicket();
      const coverImg = document.createElement('img');
    updateUI();
      coverImg.src = cfg.config.cover;
    logDebug(`Ticket ${gameState.currentTicket.id} opened`);
      cover.appendChild(coverImg);
      tile.appendChild(cover);


    // 第10张后高亮扫描按钮
      tile.onclick = () => revealTile(i, tile);
    if (gameState.currentTicketIndex === 9) {
      tile.onkeydown = e => {
         elements.scanButton.classList.add('active');
         if (e.key === 'Enter' || e.key === ' ') revealTile(i, tile);
    }
      };
}


function renderTicket() {
      area.appendChild(tile);
    const ticket = gameState.currentTicket;
     });
   
  }
    // 清空容器
    elements.ticketContainer.innerHTML = '';
   
    // 创建彩票HTML
    let ticketHTML;
    if (CONFIG.useImages) {
        // 图片模式:使用背景图
        ticketHTML = `
            <div class="lottery-ticket-wrapper">
                <img id="bgImage" src="" style="display: block; width: 100%; border-radius: 4px; max-width: 100%; height: auto;">
                <div class="scratchable-area">
                    <div class="tile-grid" id="tileGrid"></div>
                </div>
            </div>
        `;
    } else {
        // Canvas 模式:使用 Canvas 绘制(默认)
        ticketHTML = `
            <div class="lottery-ticket-wrapper">
                <canvas id="lotteryCanvas" style="display: block; width: 100%; border-radius: 4px;"></canvas>
                <div class="scratchable-area">
                    <div class="tile-grid" id="tileGrid"></div>
                </div>
            </div>
        `;
    }
   
    elements.ticketContainer.insertAdjacentHTML('beforeend', ticketHTML);
   
    if (CONFIG.useImages) {
        // 图片模式:加载背景图
        const bgImg = document.getElementById('bgImage');
        // 直接使用提供的 URL(支持外部 URL 和 MediaWiki 路径)
        bgImg.src = CONFIG.bgImage;
        bgImg.onerror = () => {
            // 如果图片加载失败,回退到 Canvas 模式
            logDebug('Background image failed to load, falling back to canvas');
            CONFIG.useImages = false;
            renderTicket();
        };
        bgImg.onload = () => {
            setupTileGrid(ticket);
            logDebug('Background image loaded successfully');
        };
     } else {
        // Canvas 模式:使用原有的 Canvas 绘制
        const canvas = document.getElementById('lotteryCanvas');
        const ctx = canvas.getContext('2d');
       
        // 设置 canvas 尺寸(宽高比按照背景图)
        const containerWidth = elements.ticketContainer.offsetWidth - 20;
        const aspectRatio = 1075 / 1911;
        canvas.width = containerWidth;
        canvas.height = containerWidth / aspectRatio;
       
        // 绘制背景色(灰白色作为背景)
        ctx.fillStyle = '#f0f0f0';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
       
        // 绘制黄色刮奖区域
        const areaLeft = (CONFIG.area.x / 1075) * canvas.width;
        const areaTop = (CONFIG.area.y / 1911) * canvas.height;
        const areaWidth = (CONFIG.area.width / 1075) * canvas.width;
        const areaHeight = (CONFIG.area.height / 1911) * canvas.height;
       
        ctx.fillStyle = '#ffd700';
        ctx.fillRect(areaLeft, areaTop, areaWidth, areaHeight);
       
        // 绘制黑色分割线
        ctx.strokeStyle = '#ccc';
        ctx.lineWidth = 2;
        ctx.strokeRect(areaLeft, areaTop, areaWidth, areaHeight);
       
        setupTileGrid(ticket);
    }
   
    elements.controlsRow.style.display = 'flex';
    updateMascotMessage();
}


function setupTileGrid(ticket) {
  function revealTile(i, tile) {
    const tileGrid = document.getElementById('tileGrid');
     if (revealed.has(i)) return;
    const container = elements.ticketContainer;
    revealed.add(i);
   
     tile.innerHTML = '';
    // 计算刮奖区域的实际宽度
    let areaWidth;
     if (CONFIG.useImages) {
        // 使用图片模式时,从背景图宽度计算
        const bgImg = document.getElementById('bgImage');
        if (bgImg && bgImg.width > 0) {
            areaWidth = (CONFIG.area.width / 1075) * bgImg.width;
        } else {
            areaWidth = container.offsetWidth * (CONFIG.area.width / 1075);
        }
     } else {
        // Canvas 模式
        const canvas = document.getElementById('lotteryCanvas');
        areaWidth = (CONFIG.area.width / 1075) * canvas.width;
    }
   
    const tileSize = areaWidth / CONFIG.cols;
   
    for (let i = 0; i < 30; i++) {
        const state = ticket.outcomes[i];
        const tile = createTile(i, state, tileSize);
        tileGrid.appendChild(tile);
    }
}


function createTile(index, stateNum, tileSize) {
    const tile = document.createElement('div');
    tile.className = 'tile';
    tile.dataset.index = index;
    tile.setAttribute('aria-label', `格子 ${index + 1}`);
    tile.tabIndex = 0;
   
    const stateInfo = STATES[stateNum] || STATES[1];
   
    // 覆盖层 (黑色)
    const cover = document.createElement('div');
    cover.className = 'tile-cover';
    cover.textContent = '🔲';
    tile.appendChild(cover);
   
    // 揭示层 (黄色)
     const reveal = document.createElement('div');
     const reveal = document.createElement('div');
     reveal.className = 'tile-reveal';
     reveal.className = 'sl-reveal';
   
 
    if (stateInfo.type === 'emoji') {
     const img = document.createElement('img');
        reveal.textContent = stateInfo.emoji;
    img.src = cfg.config.reveal;
    } else if (stateInfo.type === 'normal') {
     reveal.appendChild(img);
        reveal.textContent = '✨';
     } else if (stateInfo.type === 'text') {
        const textDiv = document.createElement('div');
        textDiv.style.fontSize = '11px';
        textDiv.style.wordBreak = 'break-word';
        textDiv.textContent = stateInfo.text.substring(0, 8);
        reveal.appendChild(textDiv);
     } else if (stateInfo.type === 'glitch') {
        reveal.textContent = '▒▒';
        reveal.style.color = '#333';
    } else if (stateInfo.type === 'meta') {
        reveal.textContent = '?';
        reveal.style.fontSize = '32px';
    }
   
     tile.appendChild(reveal);
     tile.appendChild(reveal);
   
    // 点击事件
    tile.addEventListener('click', () => revealTile(index, stateNum));
    tile.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            revealTile(index, stateNum);
        }
    });
   
    return tile;
}


function revealTile(index, stateNum) {
     const state = states[currentTicket.outcomes[i]];
    if (gameState.revealedTiles.has(index)) {
     showBubble(tile, state?.text || '');
        return;
     handleState(currentTicket.outcomes[i]);
    }
     checkCompletion();
   
  }
    gameState.revealedTiles.add(index);
     const tiles = document.querySelectorAll('.tile');
    tiles[index].classList.add('revealed');
   
    // 显示气泡
    const stateInfo = STATES[stateNum] || STATES[1];
     if (stateInfo.text) {
        showBubble(index, stateInfo.text);
     }
   
    // 触发状态效果
    handleStateEffect(stateNum);
      
    // 更新UI
    updateUI();
   
    // 检查是否完成
    checkTicketCompletion();
   
    logDebug(`Tile ${index} revealed: state ${stateNum}`);
}


function showBubble(tileIndex, text) {
  function showBubble(tile, text) {
     // 获取格子位置
     if (!text) return;
    const tiles = document.querySelectorAll('.tile');
    const tile = tiles[tileIndex];
    const rect = tile.getBoundingClientRect();
   
     const bubble = document.createElement('div');
     const bubble = document.createElement('div');
     bubble.className = 'bubble';
     bubble.className = 'sl-bubble';
     bubble.textContent = text;
     bubble.textContent = text;
     document.body.appendChild(bubble);
     tile.appendChild(bubble);
   
    // 自动定位(避免超出视口)
    bubble.style.left = (rect.left + rect.width / 2 - 100) + 'px';
    bubble.style.top = (rect.top - 60) + 'px';
   
    // 检查是否超出视口
    const bubbleRect = bubble.getBoundingClientRect();
    if (bubbleRect.top < 0) {
        bubble.classList.add('top');
        bubble.style.top = (rect.bottom + 20) + 'px';
    }
   
    // 3秒后移除
    setTimeout(() => bubble.remove(), 3000);
}


function handleStateEffect(stateNum) {
     const rect = tile.getBoundingClientRect();
     switch (stateNum) {
    bubble.style.top = rect.bottom > window.innerHeight - 80 ? '-2.4em' : '100%';
        case 2: // 播报
    bubble.style.left = '0';
            updateMascotMessage();
            break;
        case 3: // 欢呼
            elements.mascotIcon.textContent = '🎉';
            break;
        case 5: // 惊讶
            elements.mascotIcon.textContent = '😲';
            break;
        case 6: // 消失
            elements.mascotButton.style.opacity = '0.3';
            break;
        case 11: // 最终
            triggerFinalSequence();
            break;
    }
}


function checkTicketCompletion() {
     setTimeout(() => bubble.remove(), 3000);
    if (gameState.revealedTiles.size === 30) {
  }
        const ticket = gameState.currentTicket;
       
        logDebug(`Ticket ${ticket.id} completed - Story: ${ticket.storyTrigger}`);
       
        // 额外发放彩票
        if (ticket.extraTickets > 0) {
            gameState.playerTickets += ticket.extraTickets;
            showMessage(`✨ 额外获得 ${ticket.extraTickets} 张彩票!`);
        }
       
        // 延迟后显示下一张按钮
        setTimeout(() => {
            updateUI();
        }, 1000);
    }
}
 
function triggerFinalSequence() {
    if (gameState.isFinal) return;
    gameState.isFinal = true;
   
    // 创建反色闪退效果
    const crash = document.createElement('div');
    crash.className = 'page-invert-crash';
    crash.textContent = '扫描异常…';
    document.body.appendChild(crash);
   
    // 1秒后跳转到词条页面
     setTimeout(() => {
        // 这里可以替换为实际的跳转逻辑
        showMessage('剧情结束!所有秘密已解锁。');
        crash.remove();
    }, 2000);
}
 
function scanButtonClick() {
    if (gameState.currentTicketIndex !== 9) {
        showMessage('尚未解锁此功能');
        return;
    }
   
    logDebug('Scan button clicked - Releasing ticket 11');
    gameState.playerTickets = 1; // 发放最后一张票
    updateUI();
}


function mascotButtonClick() {
  function handleState(id) {
     showMessage('吉祥物说:' + (elements.mascotText.textContent || '点击格子看看吧!'));
     const s = states[id];
}
    if (!s) return;


function updateMascotMessage() {
     if (s.type === 'emoji') mascot.textContent = s.text;
     if (gameState.currentTicket) {
    if (id === 6) mascot.style.visibility = 'hidden';
        const storyText = {
    if (id === 10) scanBtn.classList.add('highlight');
            intro: '欢迎来到刮刮乐!点击格子开始吧!',
    if (id === 11) endGame();
            bonus: '哇,连中了!你很幸运呢!',
  }
            combo: '组合效果!再来一张?',
            plot_twist: '等等... 这个结果不对劲',
            normal: '继续加油!',
            empty: '哎呀,什么都没有...',
            single: '再接再厉!',
            double: '双倍幸运!',
            full_page: '全中了!!!',
            scan_ready: '...',
            final: '你看到太多了...'
        };
       
        elements.mascotText.textContent = storyText[gameState.currentTicket.storyTrigger] || '...';
        elements.mascotIcon.textContent = STATES[2].emoji;
    }
}


function updateUI() {
  function checkCompletion() {
     elements.ticketCount.textContent = gameState.playerTickets;
     if (revealed.size === 30 && currentTicket.id === 10) {
    elements.currentTicketId.textContent = gameState.currentTicket
      scanBtn.classList.add('highlight');
        ? gameState.currentTicket.id  
        : '-';
    elements.revealedCount.textContent =  
        `${gameState.revealedTiles.size}/30`;
   
    // 控制按钮显示
    if (gameState.isTicketOpen) {
        elements.drawButton.style.display = 'none';
        elements.noTicketsMsg.style.display = 'none';
    } else {
        elements.drawButton.style.display = 'block';
        elements.drawButton.textContent = gameState.playerTickets > 0
            ? '来一张彩票'
            : '没有更多彩票了';
        elements.drawButton.disabled = gameState.playerTickets <= 0;
        elements.noTicketsMsg.style.display = gameState.playerTickets <= 0 ? 'block' : 'none';
     }
     }
}
  }


function handleKeyboard(e) {
  function endGame() {
     if (e.ctrlKey && e.key === 'd') {
     root.classList.add('sl-invert');
        gameState.debugMode = !gameState.debugMode;
    setTimeout(() => {
        elements.debugPanel.classList.toggle('active');
      root.style.display = 'none';
        logDebug('Debug mode toggled: ' + gameState.debugMode);
      location.hash = '#词条正文';
     }
     }, 1500);
}
  }


function showMessage(msg) {
  openBtn.onclick = openTicket;
    alert(msg);
  scanBtn.onclick = () => {
}
     if (currentTicket && currentTicket.id === 10) openTicket();
 
  };
function logDebug(msg) {
     if (gameState.debugMode) {
        const line = document.createElement('div');
        line.className = 'debug-line';
        line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
        elements.debugPanel.appendChild(line);
        elements.debugPanel.scrollTop = elements.debugPanel.scrollHeight;
    }
    console.log('[ScratchLottery]', msg);
}


// ==================== 入口 ====================
  updateInfo();
window.addEventListener('DOMContentLoaded', init);
})();
 
// 如果在现有 DOM 中,立即初始化
if (document.readyState !== 'loading') {
    init();
}
</script>
</script>
</body>
</html>

2026年2月1日 (日) 16:44的版本

     
     
   <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>