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