| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- """build_preview.py —— 用清洗后的素材打一个单文件 HTML 可玩预览。
- 把 out/<game>/ 里的角色 + UI 美术(已去棋盘格、真透明)按比例内嵌成
- base64,生成一个 index.html:手机竖屏布局、霓虹框包住果冻网格、可点 SPIN
- 跑"同款聚 5 只就消除→连锁倍数"的玩法。纯前端、双击即玩,无需任何后端。
- 用法: python build_preview.py [game] [输出html路径]
- """
- import base64
- import io
- import json
- import os
- import sys
- from PIL import Image
- HERE = os.path.dirname(os.path.abspath(__file__))
- GAME = sys.argv[1] if len(sys.argv) > 1 else "jelly-candy-slot"
- OUT = sys.argv[2] if len(sys.argv) > 2 else os.path.join(HERE, "out", GAME, f"JellyPop预览.html")
- BASE = os.path.join(HERE, "out", GAME)
- def b64(img, maxside, fmt="PNG"):
- im = img.convert("RGBA")
- w, h = im.size
- s = min(1.0, maxside / max(w, h))
- if s < 1.0:
- im = im.resize((max(1, int(w * s)), max(1, int(h * s))), Image.LANCZOS)
- buf = io.BytesIO()
- im.save(buf, fmt)
- return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
- def load(p):
- return Image.open(p).convert("RGBA")
- lib = json.load(open(os.path.join(BASE, "library.json"), encoding="utf-8"))
- char_ids = [c["id"] for c in lib.get("characters", [])]
- # 内嵌资源(按用途控制尺寸,平衡清晰度与体积)
- A = {}
- for cid in char_ids:
- A[cid] = b64(load(os.path.join(BASE, "characters", f"{cid}.png")), 190)
- ui = os.path.join(BASE, "ui_art")
- A["bg"] = b64(load(os.path.join(ui, "bg_main.png")), 820)
- A["logo"] = b64(load(os.path.join(ui, "logo.png")), 560)
- A["frame"] = b64(load(os.path.join(ui, "reel_frame.png")), 620)
- A["spin"] = b64(load(os.path.join(ui, "btn_spin.png")), 220)
- A["round"] = b64(load(os.path.join(ui, "btn_round.png")), 150)
- SYMS = json.dumps(char_ids, ensure_ascii=False)
- ASSETS = json.dumps(A, ensure_ascii=False)
- HTML = r"""<!DOCTYPE html>
- <html lang="zh">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
- <title>Jelly Pop · 果冻消消乐</title>
- <style>
- *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;margin:0;padding:0}
- html,body{height:100%;background:#0e0a1c;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;
- display:flex;align-items:center;justify-content:center;overflow:hidden}
- #phone{position:relative;height:min(100vh,calc(100vw*1.7778));aspect-ratio:9/16;
- border-radius:26px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.6);user-select:none}
- #bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
- #shade{position:absolute;inset:0;background:radial-gradient(120% 80% at 50% 0%,rgba(0,0,0,0) 40%,rgba(20,8,40,.45) 100%)}
- .layer{position:absolute;left:0;right:0}
- #logo{top:1.5%;height:13%;margin:auto;display:block}
- #logo img{height:100%;display:block;margin:auto;filter:drop-shadow(0 4px 8px rgba(0,0,0,.35))}
- #frameWrap{position:absolute;top:14.5%;height:55%;left:50%;transform:translateX(-50%);aspect-ratio:.737}
- #frameImg{position:absolute;inset:0;width:100%;height:100%}
- /* 霓虹框内孔 ≈ 左右各 15%、上下各 3% */
- #panel{position:absolute;left:15%;right:15%;top:3%;bottom:3%;border-radius:7%/4%;
- background:linear-gradient(180deg,rgba(255,255,255,.30),rgba(220,235,255,.16));
- backdrop-filter:blur(2px)}
- #grid{position:absolute;left:15%;right:15%;top:3%;bottom:3%;display:grid;
- grid-template-columns:repeat(5,1fr);grid-template-rows:repeat(7,1fr);gap:1.5%;padding:2%}
- .cell{position:relative;display:flex;align-items:center;justify-content:center}
- .cell img{width:100%;height:100%;object-fit:contain;
- filter:drop-shadow(0 2px 3px rgba(0,0,0,.28));transition:transform .12s ease}
- .cell.pop img{animation:pop .42s ease both}
- .cell.drop img{animation:drop .34s cubic-bezier(.2,1.3,.5,1) both}
- @keyframes pop{0%{transform:scale(1)}35%{transform:scale(1.28) rotate(6deg)}
- 70%{transform:scale(.2);opacity:.2}100%{transform:scale(.2);opacity:0}}
- @keyframes drop{0%{transform:translateY(-60%) scale(.7);opacity:0}
- 100%{transform:translateY(0) scale(1);opacity:1}}
- #mult{position:absolute;top:33%;left:0;right:0;text-align:center;font-weight:900;
- font-size:7vh;color:#ffd23f;text-shadow:0 3px 0 #c0392b,0 0 18px rgba(255,170,40,.7);
- opacity:0;pointer-events:none;transform:scale(.5)}
- #mult.show{animation:flash .9s ease both}
- @keyframes flash{0%{opacity:0;transform:scale(.4)}25%{opacity:1;transform:scale(1.15)}
- 70%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(1)}}
- #hud{bottom:18.5%;height:8%;display:flex;gap:3%;padding:0 5%;margin:auto}
- .pill{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
- background:linear-gradient(180deg,rgba(86,62,150,.92),rgba(54,38,102,.95));
- border:2px solid rgba(160,140,235,.9);border-radius:999px;color:#fff;
- box-shadow:inset 0 2px 6px rgba(255,255,255,.18),0 4px 10px rgba(0,0,0,.3)}
- .pill .k{font-size:1.5vh;letter-spacing:.1em;color:#cbd6ff;opacity:.95}
- .pill .v{font-size:2.5vh;font-weight:800;line-height:1.1}
- #ctrl{bottom:3%;height:14%;display:flex;align-items:center;justify-content:space-between;
- padding:0 6%;margin:auto}
- .cbtn{display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer}
- .rb{position:relative;display:flex;align-items:center;justify-content:center;color:#fff;
- font-weight:800;background:none;border:none}
- .rb img{position:absolute;inset:0;width:100%;height:100%}
- .rb span{position:relative;z-index:1;text-shadow:0 1px 2px rgba(0,0,0,.4)}
- .small{width:8.5vh;height:8.5vh;font-size:3.2vh}
- #spin{width:13.5vh;height:13.5vh;cursor:pointer;transition:transform .1s}
- #spin:active{transform:scale(.92)}
- #spin.spinning{animation:spin 1s linear infinite}
- @keyframes spin{to{transform:rotate(360deg)}}
- .lab{font-size:1.5vh;color:#fff;letter-spacing:.12em;opacity:.92;font-weight:700}
- .on{color:#ffd23f;text-shadow:0 0 8px rgba(255,200,60,.8)}
- #coins{position:absolute;inset:0;pointer-events:none;overflow:hidden}
- .coin{position:absolute;width:5%;will-change:transform}
- .hint{position:absolute;bottom:.4%;left:0;right:0;text-align:center;color:#fff;
- font-size:1.4vh;opacity:.5}
- </style>
- </head>
- <body>
- <div id="phone">
- <img id="bg">
- <div id="shade"></div>
- <div id="logo" class="layer"><img></div>
- <div id="frameWrap">
- <div id="panel"></div>
- <div id="grid"></div>
- <img id="frameImg">
- </div>
- <div id="mult"></div>
- <div id="hud" class="layer">
- <div class="pill"><div class="k">余额</div><div class="v" id="bal">5000</div></div>
- <div class="pill"><div class="k">下注</div><div class="v" id="bet">50</div></div>
- <div class="pill"><div class="k">本局赢</div><div class="v" id="win">0</div></div>
- </div>
- <div id="ctrl" class="layer">
- <div class="cbtn"><button class="rb small" id="turbo"><img><span>⚡</span></button><div class="lab" id="turboLab">TURBO</div></div>
- <div class="cbtn"><button class="rb small" id="minus"><img><span>−</span></button><div class="lab">BET</div></div>
- <img id="spin">
- <div class="cbtn"><button class="rb small" id="plus"><img><span>+</span></button><div class="lab">BET</div></div>
- <div class="cbtn"><button class="rb small" id="auto"><img><span>▶</span></button><div class="lab" id="autoLab">AUTO</div></div>
- </div>
- <div id="coins"></div>
- <div class="hint">点 ⟳ 旋转 · 同款果冻聚成一片就连锁消除,连锁越多倍数越高</div>
- </div>
- <script>
- const A = __ASSETS__;
- const SYMBOLS = __SYMBOLS__;
- const COLS = 5, ROWS = 7, MIN_MATCH = 7, MAX_CASCADE = 8, START = 5000;
- let bet = 50, balance = START, multiplier = 1, roundWin = 0, spinning = false;
- let cascade = 0, turbo = false, auto = false;
- const $ = s => document.querySelector(s);
- // 贴静态美术
- $('#bg').src = A.bg;
- $('#logo img').src = A.logo;
- $('#frameImg').src = A.frame;
- $('#spin').src = A.spin;
- document.querySelectorAll('.rb img').forEach(i => i.src = A.round);
- // 建网格
- const grid = $('#grid');
- const ids = []; // ids[c][r]
- const cellEls = []; // cellEls[c][r]
- const rand = () => SYMBOLS[Math.floor(Math.random()*SYMBOLS.length)];
- for (let r=0;r<ROWS;r++){
- for (let c=0;c<COLS;c++){
- const el = document.createElement('div'); el.className='cell';
- const img = document.createElement('img');
- el.appendChild(img); grid.appendChild(el);
- (cellEls[c]=cellEls[c]||[])[r]=el; (ids[c]=ids[c]||[])[r]=null;
- }
- }
- function setSym(c,r,id,drop){
- ids[c][r]=id; const el=cellEls[c][r]; const img=el.querySelector('img');
- img.src=A[id]; el.classList.remove('pop');
- if(drop){ el.classList.remove('drop'); void el.offsetWidth; el.classList.add('drop'); }
- }
- for(let c=0;c<COLS;c++) for(let r=0;r<ROWS;r++) setSym(c,r,rand(),false);
- const fmt = n => Math.floor(n).toLocaleString();
- function setHud(){ $('#bet').textContent=bet; $('#win').textContent=fmt(roundWin); }
- function animBalance(){
- const cur=parseInt($('#bal').textContent.replace(/,/g,''))||0;
- const step=(balance-cur)/8;
- if(Math.abs(balance-cur)<1){ $('#bal').textContent=fmt(balance); return; }
- $('#bal').textContent=fmt(cur+step); requestAnimationFrame(animBalance);
- }
- const T = () => turbo?0.45:1; // turbo 缩短节奏
- function spin(){
- if(spinning) return;
- if(balance<bet){ flash('余额不足'); return; }
- spinning=true; $('#spin').classList.add('spinning');
- balance-=bet; animBalance(); multiplier=1; roundWin=0; cascade=0; setHud();
- for(let c=0;c<COLS;c++) for(let r=0;r<ROWS;r++) setSym(c,r,rand(),true);
- setTimeout(resolve, 420*T());
- }
- function resolve(){
- const count={};
- for(let c=0;c<COLS;c++) for(let r=0;r<ROWS;r++){const id=ids[c][r];count[id]=(count[id]||0)+1;}
- const winners=Object.keys(count).filter(k=>count[k]>=MIN_MATCH);
- if(!winners.length){ endRound(); return; }
- let cleared=0;
- for(let c=0;c<COLS;c++) for(let r=0;r<ROWS;r++)
- if(winners.includes(ids[c][r])){ cellEls[c][r].classList.add('pop'); cleared++; }
- const pay=Math.floor(cleared*(bet/MIN_MATCH)*multiplier);
- roundWin+=pay; balance+=pay; setHud(); animBalance();
- flash('x'+multiplier+' +'+pay); coinRain(Math.min(18,cleared));
- cascade++;
- setTimeout(()=>{
- for(let c=0;c<COLS;c++) for(let r=0;r<ROWS;r++)
- if(winners.includes(ids[c][r])) setSym(c,r,rand(),true);
- multiplier++;
- if(cascade>=MAX_CASCADE){ endRound(); return; } // 连锁封顶,确保收束
- setTimeout(resolve, 360*T());
- }, 520*T());
- }
- function endRound(){
- if(roundWin>0) flash('总赢 +'+fmt(roundWin));
- spinning=false; $('#spin').classList.remove('spinning');
- if(auto && balance>=bet) setTimeout(spin, 600*T());
- }
- function flash(t){
- const m=$('#mult'); m.textContent=t; m.classList.remove('show'); void m.offsetWidth; m.classList.add('show');
- }
- function coinRain(n){
- const box=$('#coins');
- for(let i=0;i<n;i++){
- const c=document.createElement('img'); c.src=A['symbol_coin']||A[SYMBOLS[0]]; c.className='coin';
- c.style.left=(10+Math.random()*80)+'%'; c.style.top='28%';
- box.appendChild(c);
- const dx=(Math.random()*2-1)*30, dur=0.9+Math.random()*0.6, rot=(Math.random()*2-1)*360;
- c.animate([{transform:'translate(0,0) rotate(0)',opacity:1},
- {transform:`translate(${dx}vw,70vh) rotate(${rot}deg)`,opacity:.2}],
- {duration:dur*1000,easing:'cubic-bezier(.4,0,.7,1)'}).onfinish=()=>c.remove();
- }
- }
- $('#spin').onclick=spin;
- $('#minus').onclick=()=>{ if(spinning)return; bet=Math.max(10,bet-10); setHud(); };
- $('#plus').onclick=()=>{ if(spinning)return; bet=Math.min(500,bet+10); setHud(); };
- $('#turbo').onclick=()=>{ turbo=!turbo; $('#turboLab').classList.toggle('on',turbo); };
- $('#auto').onclick=()=>{ auto=!auto; $('#autoLab').classList.toggle('on',auto); if(auto&&!spinning) spin(); };
- setHud();
- </script>
- </body>
- </html>
- """
- html = HTML.replace("__ASSETS__", ASSETS).replace("__SYMBOLS__", SYMS)
- os.makedirs(os.path.dirname(OUT), exist_ok=True)
- with open(OUT, "w", encoding="utf-8") as f:
- f.write(html)
- print("wrote", OUT, f"({len(html)//1024} KB)")
|