叠叠乐

点放下

适配设备:RW

作者:giao_114gg

下载量:27 · 点赞:0

可信源码风险提示:保存或下载前请阅读源码和权限声明。
SHA-2564c56df4f9ab581d48e153238fcfe7671529a52c985423411a9b49f9164a3969c
const DATA_KEY = 'stack_joy_data';

const SKINS = {
  default: { name: '经典', price: 0, bg: '#0a0d14',
    colors: ['#ff5252','#ff9800','#ffd600','#8bc34a','#4fc3f7','#7c4dff','#f06292','#4db6ac'] },
  forest: { name: '森林', price: 30, bg: '#0e1e12',
    colors: ['#e8f5e9','#c8e6c9','#a5d6a7','#81c784','#66bb6a','#4caf50','#43a047','#2e7d32'] },
  sunset: { name: '日落', price: 60, bg: '#1f0e08',
    colors: ['#fff3e0','#ffe0b2','#ffcc80','#ffb74d','#ffa726','#ff9800','#fb8c00','#ef6c00'] },
  galaxy: { name: '星空', price: 100, bg: '#0a0a1e',
    colors: ['#ede7f6','#d1c4e9','#b39ddb','#9575cd','#7c4dff','#651fff','#536dfe','#5c6bc0'] }
};
const ITEMS = {
  widen:  { name: '加宽', price: 10 },
  revive: { name: '复活', price: 20 },
  auto:   { name: '对齐', price: 15 }
};

const W = 400, H = 300, BLOCK_H = 15, GAP = 2, BASE_W = 100;
const PERFECT = 13, SNAP = 4;
const SPEED_DEFAULT = 5;
const SPEED_STEP = 0.25;

let DATA = {
  best: 0, points: 0, skin: 'default', owned: ['default'],
  speed: SPEED_DEFAULT,
  cards: { widen: 0, revive: 0, auto: 0 }
};

let tower = [], curX = 0, curW = BASE_W, curColor = '#fff';
let curVx = SPEED_DEFAULT;
let score = 0, streak = 0, moveTimer = null;
let playing = false, over = false;
let effects = { widen: false };
let screen = 'home';

async function save() {
  try {
    const snap = JSON.parse(JSON.stringify(DATA));
    await script.data.set(DATA_KEY, snap);
  } catch (e) { console.error('save fail', e); }
}

async function loadData() {
  try {
    const s = await script.data.get(DATA_KEY, null);
    if (!s || typeof s !== 'object') return;
    DATA = {
      best: Number(s.best) || 0,
      points: Number(s.points) || 0,
      skin: typeof s.skin === 'string' ? s.skin : 'default',
      owned: Array.isArray(s.owned) ? s.owned : ['default'],
      speed: (typeof s.speed === 'number' && isFinite(s.speed)) ? s.speed : SPEED_DEFAULT,
      cards: {
        widen: Number((s.cards || {}).widen) || 0,
        revive: Number((s.cards || {}).revive) || 0,
        auto: Number((s.cards || {}).auto) || 0
      }
    };
    if (!DATA.owned.includes('default')) DATA.owned.push('default');
    if (!DATA.owned.includes(DATA.skin)) DATA.skin = 'default';
  } catch (e) { console.error('load fail', e); }
}

function skin() { return SKINS[DATA.skin] || SKINS.default; }
function stopTimer() { if (moveTimer) { clearInterval(moveTimer); moveTimer = null; } }

function placeNext() {
  if (curVx > 0) curX = 0;
  else if (curVx < 0) curX = W - curW;
  else curX = (W - curW) / 2;
  curColor = skin().colors[tower.length % skin().colors.length];
}

function startGame() {
  stopTimer();
  tower = [{ x: (W - BASE_W) / 2, w: BASE_W, color: skin().colors[0] }];
  curW = BASE_W;
  curVx = DATA.speed;
  if (curVx > 0) curX = 0;
  else if (curVx < 0) curX = W - curW;
  else curX = (W - curW) / 2;
  curColor = skin().colors[1];
  score = 0;
  streak = 0;
  playing = true;
  over = false;
  effects = { widen: false };
  screen = 'game';
  startMove();
  render();
}

function startMove() {
  stopTimer();
  moveTimer = setInterval(() => {
    if (!playing || over) return;

    let nx = curX + curVx;
    // 直接反射:越界多少就反弹多少,不钳制到边缘
    let guard = 0;
    while (guard++ < 50) {
      if (nx <= 0) {
        nx = -nx;
        curVx = Math.abs(curVx);
      } else if (nx + curW >= W) {
        nx = 2 * (W - curW) - nx;
        curVx = -Math.abs(curVx);
      } else {
        break;
      }
    }
    curX = nx;

    if (screen === 'game') render();
  }, 50);
}

async function useCard(type) {
  if (!playing || over) return;
  if ((DATA.cards[type] || 0) <= 0) return;
  if (type === 'widen' && !effects.widen) {
    DATA.cards.widen--;
    effects.widen = true;
    render();
    await save();
  } else if (type === 'auto') {
    DATA.cards.auto--;
    await save();
    autoDrop();
  }
}

function autoDrop() {
  if (!playing || over) return;
  const last = tower[tower.length - 1];
  const dL = Math.abs(curX - last.x);
  const dR = Math.abs(curX + curW - last.x - last.w);
  curX = dL <= dR ? last.x : last.x + last.w - curW;
  drop();
}

async function drop() {
  if (!playing || over) return;
  const last = tower[tower.length - 1];
  const dL = Math.abs(curX - last.x);
  const dR = Math.abs(curX + curW - last.x - last.w);
  const minDiff = Math.min(dL, dR);

  if (minDiff <= PERFECT) {
    const perfectX = dL <= dR ? last.x : last.x + last.w - curW;
    tower.push({ x: perfectX, w: curW, color: curColor });
    streak++;
    score += 1 + streak;
    placeNext();
    if (tower.length % 5 === 0 && curVx !== 0) {
      curVx += curVx > 0 ? SPEED_STEP : -SPEED_STEP;
    }
    render();
    return;
  }

  let snapX = curX;
  if (minDiff <= SNAP) snapX = dL <= dR ? last.x : last.x + last.w - curW;
  let left = Math.max(snapX, last.x);
  let right = Math.min(snapX + curW, last.x + last.w);
  let ow = right - left;

  if (effects.widen && ow > 0.5 && ow < curW - 0.1) {
    const cut = curW - ow;
    left -= cut / 4;
    right += cut / 4;
    ow = right - left;
    effects.widen = false;
  }

  if (ow <= 0.5) {
    if ((DATA.cards.revive || 0) > 0) {
      DATA.cards.revive--;
      await save();
      curW = Math.max(curW, 40);
      placeNext();
      streak = 0;
      render();
      return;
    }
    await endGame();
    return;
  }

  tower.push({ x: left, w: ow, color: curColor });
  curW = ow;
  streak = 0;
  score += 1;
  placeNext();
  if (tower.length % 5 === 0 && curVx !== 0) {
    curVx += curVx > 0 ? SPEED_STEP : -SPEED_STEP;
  }
  render();
}

async function endGame() {
  over = true;
  playing = false;
  stopTimer();
  if (score > DATA.best) DATA.best = score;
  DATA.points += score;
  screen = 'over';
  render();
  await save();
}

function renderBoard() {
  const n = tower.length;
  const nodes = [];
  for (let i = 0; i < n; i++) {
    const b = tower[i];
    const y = H - BLOCK_H * (i + 1);
    if (y < -BLOCK_H || y > H) continue;
    nodes.push(ui.stack([], {
      x: b.x, y: y, width: b.w, height: BLOCK_H - GAP,
      background: b.color, radius: 2
    }));
  }
  if (!over && n > 0) {
    const y = H - BLOCK_H * (n + 1);
    if (y > -BLOCK_H && y < H) {
      nodes.push(ui.stack([], {
        x: curX, y: y, width: curW, height: BLOCK_H - GAP,
        background: curColor, radius: 2
      }));
    }
  }
  return ui.stack(nodes, { width: W, height: H, background: skin().bg, radius: 6 });
}

async function editSpeed() {
  const res = await dialog.number({
    title: '速度设置',
    message: '任意数值(可为负数或 0)。正数向右开始,负数向左开始,0 静止',
    value: DATA.speed,
    decimals: 2
  });
  if (res.action !== 'confirm') return;
  const v = Number(res.value);
  if (!isFinite(v)) { script.toast('无效数值'); return; }
  DATA.speed = v;
  render();
  await save();
}

function renderHome() {
  ui.render([
    ui.heading('叠叠乐', { align: 'center', lines: 1 }),
    ui.spacer(10),
    ui.column([
      ui.text('最佳 ' + DATA.best, { align: 'center', color: '#ffd75e', size: 18, lines: 1 }),
      ui.text('积分 ' + DATA.points, { align: 'center', color: '#52c41a', size: 18, lines: 1 }),
      ui.text('速度 ' + DATA.speed, { align: 'center', color: '#aaddff', size: 14, lines: 1 })
    ], { background: '#1c1c1e', radius: 8, padding: 12, justify: 'center' }),
    ui.spacer(10),
    ui.button('开始游戏', startGame, { tone: 'primary', height: 50 }),
    ui.spacer(6),
    ui.button('速度设置', editSpeed, { tone: 'warning', height: 44 }),
    ui.spacer(6),
    ui.button('积分商店', () => { screen = 'shop'; render(); }, { tone: 'neutral', height: 44 })
  ]);
}

function renderGame() {
  const n = tower.length;
  const children = [
    ui.row([
      ui.text('分 ' + score, { flex: 1, size: 13, color: '#aaddff', align: 'center', lines: 1 }),
      ui.text('层 ' + Math.max(0, n - 1), { flex: 1, size: 13, color: '#aaddff', align: 'center', lines: 1 }),
      ui.text('积分 ' + DATA.points, { flex: 1, size: 13, color: '#52c41a', align: 'center', lines: 1 }),
      ui.text('速 ' + (Math.round(curVx * 100) / 100), { flex: 1, size: 13, color: '#ffd75e', align: 'center', lines: 1 })
    ], { gap: 4 }),
    ui.row([renderBoard()], { justify: 'center' })
  ];

  if (streak >= 2) {
    children.push(ui.text('完美连击 x' + streak, { align: 'center', color: '#ffd75e', size: 13, lines: 1 }));
  }

  if (over) {
    children.push(ui.text('没接住 · 得分 ' + score + ' 已入积分', { align: 'center', color: '#ff453a', size: 14, lines: 1 }));
    children.push(ui.button('再来一局', startGame, { tone: 'primary', height: 44 }));
    children.push(ui.button('返回主页', () => { screen = 'home'; render(); }, { tone: 'neutral', height: 36 }));
  } else {
    children.push(ui.button('放下', drop, { tone: 'primary', height: 46 }));
    const btns = [];
    if (DATA.cards.widen > 0 && !effects.widen)
      btns.push(ui.button('加宽 x' + DATA.cards.widen, () => useCard('widen'), { flex: 1, tone: 'warning', height: 36 }));
    if (DATA.cards.auto > 0)
      btns.push(ui.button('对齐 x' + DATA.cards.auto, () => useCard('auto'), { flex: 1, tone: 'primary', height: 36 }));
    if (btns.length > 0) children.push(ui.row(btns, { gap: 6 }));
    if (effects.widen)
      children.push(ui.text('加宽中', { align: 'center', color: '#ffd75e', size: 12, lines: 1 }));
  }

  ui.render(children);
}

function renderShop() {
  const children = [
    ui.heading('积分商店', { align: 'center', lines: 1 }),
    ui.text('积分 ' + DATA.points, { align: 'center', color: '#52c41a', size: 16, lines: 1 }),
    ui.spacer(6),
    ui.text('皮肤', { size: 15, color: '#4fc3f7', lines: 1 })
  ];

  for (const id in SKINS) {
    const sk = SKINS[id];
    const owned = DATA.owned.includes(id);
    const equipped = DATA.skin === id;
    let actionBtn;
    if (owned) {
      if (equipped) {
        actionBtn = (id === 'default')
          ? ui.text('使用中', { size: 13, color: '#888888', flex: 1, align: 'center', lines: 1 })
          : ui.button('卸下', async () => {
              DATA.skin = 'default';
              render();
              await save();
            }, { size: 13, tone: 'neutral', flex: 1, height: 36 });
      } else {
        actionBtn = ui.button('装备', async () => {
          DATA.skin = id;
          render();
          await save();
        }, { size: 13, tone: 'primary', flex: 1, height: 36 });
      }
    } else {
      actionBtn = ui.button('买 ' + sk.price, async () => {
        if (DATA.points >= sk.price) {
          DATA.points -= sk.price;
          DATA.owned.push(id);
          DATA.skin = id;
          render();
          await save();
        } else { script.toast('积分不足'); }
      }, { size: 13, tone: 'primary', flex: 1, height: 36 });
    }
    children.push(ui.row([
      ui.text(sk.name, { size: 14, width: 70, lines: 1 }),
      actionBtn
    ], { gap: 8 }));
  }

  children.push(ui.spacer(6));
  children.push(ui.text('道具', { size: 15, color: '#4fc3f7', lines: 1 }));

  for (const id in ITEMS) {
    const it = ITEMS[id];
    const cnt = DATA.cards[id] || 0;
    children.push(ui.row([
      ui.text(it.name + ' x' + cnt, { size: 14, flex: 1, lines: 1 }),
      ui.button('买 ' + it.price, async () => {
        if (DATA.points >= it.price) {
          DATA.points -= it.price;
          DATA.cards[id] = (DATA.cards[id] || 0) + 1;
          render();
          await save();
        } else { script.toast('积分不足'); }
      }, { size: 13, tone: 'primary', flex: 1, height: 36 })
    ], { gap: 8 }));
  }

  children.push(ui.spacer(8));
  children.push(ui.button('返回主页', () => { screen = 'home'; render(); }, { tone: 'neutral', height: 40 }));
  children.push(ui.button('重置存档', async () => {
    const res = await dialog.confirm({
      title: '重置', message: '确定要清空所有数据吗?',
      confirmText: '清空', cancelText: '取消'
    });
    if (res.action === 'confirm') {
      DATA = {
        best: 0, points: 0, skin: 'default', owned: ['default'],
        speed: SPEED_DEFAULT,
        cards: { widen: 0, revive: 0, auto: 0 }
      };
      render();
      await save();
    }
  }, { tone: 'danger', height: 36 }));

  ui.render(children);
}

function render() {
  if (screen === 'home') renderHome();
  else if (screen === 'shop') renderShop();
  else renderGame();
}

async function main() {
  await loadData();
  render();
}

return main();