diff --git a/TUICallKit-Vue3/manifest.json b/TUICallKit-Vue3/manifest.json index 146f31f1..b479fb4f 100644 --- a/TUICallKit-Vue3/manifest.json +++ b/TUICallKit-Vue3/manifest.json @@ -52,14 +52,14 @@ "mp-weixin" : { "appid" : "wx79b9a0bfbfe7cbcd", "setting" : { - "urlCheck" : false + "urlCheck" : false, + "minified" : true }, "optimization" : { "subPackages" : true }, "usingComponents" : true, - "requiredBackgroundModes" : ["audio"], - + "requiredBackgroundModes" : [ "audio" ], "plugins" : { "WechatSI" : { "version" : "0.3.5", diff --git a/TUICallKit-Vue3/static/zs.jpg b/TUICallKit-Vue3/static/zs.jpg index 795f696d..00b3336a 100644 Binary files a/TUICallKit-Vue3/static/zs.jpg and b/TUICallKit-Vue3/static/zs.jpg differ diff --git a/TUICallKit-Vue3/tongji/pages/game.vue b/TUICallKit-Vue3/tongji/pages/game.vue index c5bfc7eb..783d9e4e 100644 --- a/TUICallKit-Vue3/tongji/pages/game.vue +++ b/TUICallKit-Vue3/tongji/pages/game.vue @@ -923,6 +923,9 @@ function initBoard() { commitBoard(next) // 注入本关唯一高糖(解锁前固定 2 个,无法三连) ensureHighPresence() + // 注入高糖可能破坏“有解”性:开局静默兜底,避免出现整盘无法消除的死局 + // (仅打乱类型、保持高糖数量不变) + if (!boardHasSolution()) shuffleTypesUntilPlayable() startHintTimer() } @@ -1423,6 +1426,38 @@ function startHintTimer() { hintTimer = setTimeout(showOperationHint, HINT_DELAY_MS) } +/** + * 就地打乱棋盘上所有方块的“食物类型”(位置/对象不变),直到棋盘“有解”: + * 无现成消除组 + 至少存在一个可用移动。仅交换 type,故高糖数量等多重集不变。 + * 不带任何 UI 反馈,供初始化等静默场景使用。返回是否成功。 + */ +function shuffleTypesUntilPlayable(maxRetry = 80) { + const tiles = [] + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const t = board.value[r]?.[c] + if (t && !t.isRemoved) tiles.push(t) + } + } + if (tiles.length < 3) return false + for (let attempt = 0; attempt < maxRetry; attempt++) { + for (let i = tiles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type] + } + if (findMatches().length === 0 && findPossibleMoves().length > 0) { + bumpLayout() + return true + } + } + return false +} + +/** 当前棋盘是否“有解”(无现成消除组且至少有一个可用移动) */ +function boardHasSolution() { + return findMatches().length === 0 && findPossibleMoves().length > 0 +} + /** * 棋盘死锁自动被测与修复。 * 使用 Fisher-Yates 打乱食物类型,最多重试 maxRetry 次。 @@ -1431,7 +1466,7 @@ function startHintTimer() { async function reshuffleIfStuck() { if (gamePhase.value !== 'playing') return // 已有解(无现成消除组 + 存在可用移动)则无需重排 - if (findMatches().length === 0 && findPossibleMoves().length > 0) return + if (boardHasSolution()) return // 给玩家反馈 playSfx('reshuffle', 0.75) @@ -1444,32 +1479,8 @@ async function reshuffleIfStuck() { setTimeout(() => { floatingVals.value = floatingVals.value.filter(v => v.id !== id) }, 1400) } catch (_) {} - // 收集棋盘上所有有效方块(保持 tile 对象与位置不变,仅打乱其食物类型) - const tiles = [] - for (let r = 0; r < ROWS; r++) { - for (let c = 0; c < COLS; c++) { - const t = board.value[r]?.[c] - if (t && !t.isRemoved) tiles.push(t) - } - } - - // 反复打乱类型并就地试写,直到“有解且无现成三连”; - // tiles 已是 board.value 中的同一批对象,故 findMatches/findPossibleMoves 可直接复用 - const MAX_RETRY = 60 - let ok = false - for (let attempt = 0; attempt < MAX_RETRY; attempt++) { - for (let i = tiles.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type] - } - if (findMatches().length === 0 && findPossibleMoves().length > 0) { - ok = true - break - } - } - - if (!ok) { - // 就地洗牌无法洗出可玩布局,回退到全棋盘重新生成 + // 就地打乱类型直到有解;失败则回退到全棋盘重新生成 + if (!shuffleTypesUntilPlayable(60)) { initBoard() return } @@ -1754,10 +1765,8 @@ async function processSwap(t1, t2, opts = {}) { isProcessing.value = false return } - // 按解锁进度维持高糖数量(到第 7 步起补到可消除的 3 个) - ensureHighPresence() - // 所有连消结束后,检查棋盘是否死锁并自动重排 - await reshuffleIfStuck() + // 落子算法已保证“下落即有可消除走法”;此处仅作极端兜底(静默、无横幅) + if (!boardHasSolution()) shuffleTypesUntilPlayable() } else { await animateSwapOffset(t1, t2) swapTiles(t1, t2) @@ -1831,9 +1840,134 @@ async function clearAndRefill() { return false } +/* ——— 落子前在 next 网格上做的辅助判定(不触动可见的 board.value) ——— */ +function gridIsHigh(grid, r, c) { + const t = grid[r]?.[c] + return t && !t.isRemoved && t.type.gi === 'high' +} + +function gridWouldFormHighRun(grid, r, c) { + let h = 1 + for (let cc = c - 1; cc >= 0 && gridIsHigh(grid, r, cc); cc--) h++ + for (let cc = c + 1; cc < COLS && gridIsHigh(grid, r, cc); cc++) h++ + if (h >= 3) return true + let v = 1 + for (let rr = r - 1; rr >= 0 && gridIsHigh(grid, rr, c); rr--) v++ + for (let rr = r + 1; rr < ROWS && gridIsHigh(grid, rr, c); rr++) v++ + return v >= 3 +} + +/** 在 grid 上是否存在“现成可消除组” */ +function gridHasCurrentMatch(grid) { + const saved = board.value + board.value = grid + const has = findMatches().length > 0 + board.value = saved + return has +} + +/** 在 grid 上是否存在“可交换出消除的走法” */ +function gridHasMove(grid) { + const saved = board.value + board.value = grid + const ok = findPossibleMoves().length > 0 + board.value = saved + return ok +} + +/** + * 仅在 next 网格上调整本局高糖数量(只动“新生成的方块”,玩家尚未看到,故无可见突变): + * 解锁前固定 2 个、解锁后 3~HIGH_COUNT_MAX 个;注入时避免直接连成三连。 + */ +function balanceHighInRefill(next, newTiles) { + const highFood = sessionHighFood.value + if (!highFood) return + const before = playerMoves.value < HIGH_UNLOCK_MOVES + const minTarget = before ? HIGH_COUNT_LOCKED : HIGH_COUNT_UNLOCKED + const maxCap = before ? HIGH_COUNT_LOCKED : HIGH_COUNT_MAX + + let high = 0 + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) if (gridIsHigh(next, r, c)) high++ + } + const lowMidPool = activeFoods.value.filter((f) => f.gi !== 'high') + + if (high > maxCap) { + for (const t of shuffleInPlace([...newTiles])) { + if (high <= maxCap) break + if (t.type.gi !== 'high') continue + const repl = lowMidPool.length ? lowMidPool[Math.floor(Math.random() * lowMidPool.length)] : null + if (repl) { t.type = cloneFoodType(repl); high-- } + } + } + + if (high < minTarget) { + const cands = shuffleInPlace([...newTiles]) + for (const t of cands) { + if (high >= minTarget) break + if (t.type.gi === 'high') continue + if (gridWouldFormHighRun(next, t.r, t.c)) continue + t.type = cloneFoodType(highFood) + high++ + } + for (const t of cands) { + if (high >= minTarget) break + if (t.type.gi === 'high') continue + t.type = cloneFoodType(highFood) + high++ + } + } +} + +/** + * 保证“落下后必有可消除的走法”:只重 roll 新方块里的非高糖类型(落子前完成,无可见重排)。 + * 若 next 已有现成消除组(即将触发连锁),则交给连锁处理,最终落子时再保证。 + */ +function ensureRefillPlayable(next, newTiles) { + if (gridHasCurrentMatch(next)) return + if (gridHasMove(next)) return + + const nonHighPool = activeFoods.value.filter((f) => f.gi !== 'high') + const rerollable = newTiles.filter((t) => t.type.gi !== 'high') + if (nonHighPool.length && rerollable.length) { + for (let attempt = 0; attempt < 100; attempt++) { + rerollable.forEach((t) => { + t.type = cloneFoodType(nonHighPool[Math.floor(Math.random() * nonHighPool.length)]) + }) + if (gridHasMove(next)) return + } + } + + // 极端兜底:静默打乱整盘类型(保持多重集/高糖数量),无任何横幅提示 + silentShuffleGrid(next) +} + +/** 在 next 网格上静默打乱类型直至“无现成消除组且有可用走法”,仅作极端兜底 */ +function silentShuffleGrid(next) { + const tiles = [] + for (let r = 0; r < ROWS; r++) { + for (let c = 0; c < COLS; c++) { + const t = next[r]?.[c] + if (t && !t.isRemoved) tiles.push(t) + } + } + if (tiles.length < 3) return + const saved = board.value + board.value = next + for (let attempt = 0; attempt < 80; attempt++) { + for (let i = tiles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [tiles[i].type, tiles[j].type] = [tiles[j].type, tiles[i].type] + } + if (findMatches().length === 0 && findPossibleMoves().length > 0) break + } + board.value = saved +} + async function applyGravity() { const next = Array.from({ length: ROWS }, () => Array(COLS).fill(null)) const dropTiles = [] + const newTiles = [] for (let c = 0; c < COLS; c++) { const surviving = [] @@ -1861,10 +1995,15 @@ async function applyGravity() { tile.isDropAnimate = false next[r][c] = tile dropTiles.push(tile) + newTiles.push(tile) } } ensureBoardFull(next) + // 落子前只调整“新方块”:①按解锁进度维持高糖数量 ②保证落下后必有可消除走法 + // 这样棋盘随下落即更新,无需“无解时整盘自动重排”。 + balanceHighInRefill(next, newTiles) + ensureRefillPlayable(next, newTiles) commitBoard(next) await nextTick() diff --git a/TUICallKit-Vue3/tongji/pages/weekly.vue b/TUICallKit-Vue3/tongji/pages/weekly.vue index b92012a8..353204ab 100644 --- a/TUICallKit-Vue3/tongji/pages/weekly.vue +++ b/TUICallKit-Vue3/tongji/pages/weekly.vue @@ -193,19 +193,26 @@ × - + - dietAiAskFocused.value || String(dietAiAskText.value || '').length > 0 +) + /** 键盘高度(px):录入弹窗据此整体上移,避免输入框被键盘遮挡 */ const keyboardHeight = ref(0) function onKeyboardHeightChange(res) { diff --git a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss index c7d1160d..f22f33ff 100644 --- a/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss +++ b/TUICallKit-Vue3/tongji/styles/weekly-stitch.scss @@ -740,13 +740,22 @@ .weekly-page .st-float-inner { display: flex; align-items: center; - gap: 24rpx; - padding: 16rpx 16rpx 16rpx 40rpx; + gap: 16rpx; + padding: 16rpx 16rpx 16rpx 28rpx; border-radius: 999rpx; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(20rpx); border: 1rpx solid rgba(0, 108, 73, 0.1); box-shadow: var(--st-shadow-float); + transition: border-radius 0.22s ease, padding 0.22s ease, border-color 0.22s ease; +} + +/* 聚焦/有内容时展开:底部对齐让输入框向上增高,圆角收敛为圆角矩形 */ +.weekly-page .st-float-inner.is-expanded { + align-items: flex-end; + border-radius: 32rpx; + padding: 18rpx 16rpx 18rpx 32rpx; + border-color: rgba(0, 108, 73, 0.28); } .weekly-page .st-float-icon { @@ -763,15 +772,30 @@ .weekly-page .st-float-input { flex: 1; min-width: 0; - height: 64rpx; + width: 100%; + min-height: 64rpx; + max-height: 240rpx; font-size: 28rpx; + line-height: 40rpx; color: var(--on-surface); background: transparent; + padding: 12rpx 0; + box-sizing: border-box; + transition: min-height 0.2s ease; +} + +/* 聚焦时输入框变大;内容超长时由 auto-height 继续向上增高,超过上限可滚动 */ +.weekly-page .st-float-inner.is-expanded .st-float-input { + min-height: 104rpx; } .weekly-page .st-float-placeholder { color: rgba(60, 74, 66, 0.6); - font-size: 28rpx; + font-size: 26rpx; + line-height: 40rpx; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .weekly-page .st-float-mic {