fix(tongji): stabilize food transitions and mixed weekly rank
This commit is contained in:
@@ -19,9 +19,20 @@
|
||||
<TongjiIcon name="chevron-left" size="md" color="#155e4b" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="eg-title-wrap">
|
||||
<view class="eg-title-wrap" :class="{ 'is-noticing': centerNoticeVisible }">
|
||||
<text class="eg-title">识糖小课堂</text>
|
||||
<text class="eg-subtitle">第{{ themeIndex + 1 }}站 · {{ currentTheme.title }}</text>
|
||||
<view
|
||||
v-if="centerNoticeVisible"
|
||||
:key="centerNoticeKey"
|
||||
class="eg-nav-notice"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<text class="eg-nav-notice-title">{{ centerNoticeTitle }}</text>
|
||||
<text class="eg-nav-notice-detail">{{ centerNoticeDetail }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="eg-nav-actions">
|
||||
<view class="eg-nav-btn eg-nav-btn--small" @tap="toggleSfx">
|
||||
@@ -126,7 +137,12 @@
|
||||
<view class="eg-fire-spark is-two" />
|
||||
<view class="eg-fire-spark is-three" />
|
||||
</view>
|
||||
<image class="eg-food-image" :src="foodOf(tile.key).img" mode="aspectFit" />
|
||||
<image
|
||||
class="eg-food-image"
|
||||
:class="{ 'is-large-square': foodOf(tile.key).largeIcon }"
|
||||
:src="foodOf(tile.key).img"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="eg-food-name">{{ foodOf(tile.key).short }}</text>
|
||||
<view class="eg-sugar-tag" :class="`is-${foodOf(tile.key).level || 'low'}`">
|
||||
{{ foodOf(tile.key).levelLabel || '低糖' }}
|
||||
@@ -271,13 +287,13 @@
|
||||
<TongjiIcon name="refresh" size="lg" color="#ffffff" />
|
||||
</view>
|
||||
<text class="eg-reshuffle-title">{{ reshuffleDone ? '洗牌完成' : '没有可消除组合' }}</text>
|
||||
<text class="eg-reshuffle-desc">{{ reshuffleDone ? '新的消除机会已经出现' : '正在为您重新洗牌,请稍候' }}</text>
|
||||
<text class="eg-reshuffle-desc">{{ reshuffleDone ? (reshuffleImportedFoods ? '洗牌完成,新食物已经加入' : '新的消除机会已经出现') : '正在为您重新洗牌,请稍候' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="stageVisible" class="eg-stage-overlay" @tap.stop>
|
||||
<view class="eg-stage-card">
|
||||
<text class="eg-stage-kicker">新菜单已解锁</text>
|
||||
<text class="eg-stage-kicker">新食物排队加入</text>
|
||||
<text class="eg-stage-number">第 {{ stageMeta.number }} 站</text>
|
||||
<text class="eg-stage-title">{{ stageMeta.title }}</text>
|
||||
<view class="eg-stage-foods">
|
||||
@@ -286,7 +302,7 @@
|
||||
<text>{{ food.short }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="eg-stage-tip">本组橙框食物可用驼乳粉替换</text>
|
||||
<text class="eg-stage-tip">橙框食物已更新,普通食物会随掉落逐步加入</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -320,6 +336,14 @@ import { useGamePlatform } from './composables/useGamePlatform.js'
|
||||
import { useGameAuth } from './composables/useGameAuth.js'
|
||||
|
||||
const FOOD_IMAGE_BASE_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/games/food/'
|
||||
const LOCAL_FOOD_IMAGE_BASE_URL = '/tongji/endless-game/assets/food/'
|
||||
const LOCAL_FOOD_IMAGES = {
|
||||
tofu: `${LOCAL_FOOD_IMAGE_BASE_URL}tofu.jpg`,
|
||||
spinach: `${LOCAL_FOOD_IMAGE_BASE_URL}spinach.jpg`,
|
||||
carrot: `${LOCAL_FOOD_IMAGE_BASE_URL}carrot.jpg`,
|
||||
fish: `${LOCAL_FOOD_IMAGE_BASE_URL}fish.jpg`,
|
||||
redBeanCongee: `${LOCAL_FOOD_IMAGE_BASE_URL}redBeanCongee.jpg`
|
||||
}
|
||||
|
||||
const ROWS = 5
|
||||
const COLS = 4
|
||||
@@ -395,9 +419,20 @@ const FOODS = {
|
||||
riceNoodleSoup: { name: '汤粉', short: '汤粉', emoji: '🍜', color: 'orange', level: 'high', levelLabel: '高糖', high: true, replaceable: true, tip: '汤粉以精制米粉为主,实际影响与分量和配菜有关,应注意搭配。' },
|
||||
noodleSoup: { name: '汤面', short: '汤面', emoji: '🍜', color: 'orange', level: 'high', levelLabel: '高糖', high: true, replaceable: true, tip: '汤面以精细面食为主,汤汁和分量需要注意,具体建议以医生审核为准。' },
|
||||
milkTea: { name: '含糖奶茶', short: '奶茶', emoji: '🧋', color: 'orange', level: 'high', levelLabel: '高糖', high: true, replaceable: true, tip: '含糖奶茶可能同时含有较多添加糖和能量,应关注配方与饮用量。' },
|
||||
tofu: { name: '豆腐', short: '豆腐', emoji: '◻️', color: 'cream', category: '豆制品', level: 'low', levelLabel: '低糖', largeIcon: true },
|
||||
spinach: { name: '菠菜', short: '菠菜', emoji: '🥬', color: 'yellow', category: '蔬菜', level: 'low', levelLabel: '低糖', largeIcon: true },
|
||||
carrot: { name: '胡萝卜', short: '胡萝卜', emoji: '🥕', color: 'red', category: '蔬菜', level: 'low', levelLabel: '低糖', largeIcon: true },
|
||||
fish: { name: '鱼', short: '鱼', emoji: '🐟', color: 'purple', category: '鱼肉蛋类', level: 'low', levelLabel: '低糖', largeIcon: true },
|
||||
redBeanCongee: {
|
||||
name: '软烂红豆粥', short: '红豆粥', emoji: '🥣', color: 'orange', category: '粥类',
|
||||
level: 'high', levelLabel: '高糖', high: true, replaceable: true, largeIcon: true,
|
||||
tip: '本局图标表示长时间熬煮、豆粒开花的红豆粥。食物对血糖的影响与熬煮程度、食用量和搭配有关,糖尿病患者应结合个人情况合理选择。'
|
||||
},
|
||||
camel: { name: '驼乳', short: '驼乳', emoji: '🥛', color: 'milk', level: 'prop', levelLabel: '道具', product: true }
|
||||
}
|
||||
Object.keys(FOODS).forEach((key) => { FOODS[key].img = `${FOOD_IMAGE_BASE_URL}${key}.jpg` })
|
||||
Object.keys(FOODS).forEach((key) => {
|
||||
FOODS[key].img = LOCAL_FOOD_IMAGES[key] || `${FOOD_IMAGE_BASE_URL}${key}.jpg`
|
||||
})
|
||||
const FOOD_GROUPS = [
|
||||
{ title: '中式早餐', safe: ['egg', 'cucumber', 'corn', 'grainMantou'], danger: 'congee' },
|
||||
{ title: '家常粥餐', safe: ['celery', 'mushroom', 'chicken', 'taro'], danger: 'centuryCongee' },
|
||||
@@ -406,6 +441,7 @@ const FOOD_GROUPS = [
|
||||
{ title: '日常饮品', safe: ['milk', 'tea', 'blackCoffee', 'coffee'], danger: 'soda' },
|
||||
{ title: '南方汤粉', safe: ['pepper', 'greenBean', 'shrimp', 'udon'], danger: 'riceNoodleSoup' },
|
||||
{ title: '北方面食', safe: ['onion', 'celery', 'corn', 'grainMantou'], danger: 'noodleSoup' },
|
||||
{ title: '家常餐桌', safe: ['tofu', 'spinach', 'carrot', 'fish'], danger: 'redBeanCongee' },
|
||||
]
|
||||
|
||||
const {
|
||||
@@ -451,12 +487,19 @@ const camelMode = ref(false)
|
||||
const busy = ref(false)
|
||||
const gameOver = ref(false)
|
||||
const themeIndex = ref(0)
|
||||
const activeSafeKeys = ref([...FOOD_GROUPS[0].safe])
|
||||
const foodTransition = ref(null)
|
||||
const queuedSafeGroups = ref([])
|
||||
const endedFood = ref(FOODS.congee)
|
||||
const taskIndex = ref(0)
|
||||
const taskProgress = ref(0)
|
||||
const comboDryTurns = ref(0)
|
||||
const taskRewardVisible = ref(false)
|
||||
const taskRewardKey = ref(0)
|
||||
const centerNoticeVisible = ref(false)
|
||||
const centerNoticeKey = ref(0)
|
||||
const centerNoticeTitle = ref('')
|
||||
const centerNoticeDetail = ref('')
|
||||
const statusText = ref('交换相邻食物,三个相同即可消除')
|
||||
const navStyle = ref({ paddingTop: '20px' })
|
||||
const touch = { x: 0, y: 0, index: -1 }
|
||||
@@ -474,6 +517,7 @@ const stageMeta = ref({ number: 1, title: '', foods: [] })
|
||||
const reshuffleVisible = ref(false)
|
||||
const reshuffleDone = ref(false)
|
||||
const reshuffling = ref(false)
|
||||
const reshuffleImportedFoods = ref(false)
|
||||
const confettiVisible = ref(false)
|
||||
const weeklyRankVisible = ref(false)
|
||||
const restoredGame = ref(false)
|
||||
@@ -498,6 +542,7 @@ let jackpotTimer = null
|
||||
let confettiTimer = null
|
||||
let stageTimer = null
|
||||
let taskRewardTimer = null
|
||||
let centerNoticeTimer = null
|
||||
let flowTimer = null
|
||||
let flowPauseTimer = null
|
||||
let flowPausedRemainingMs = 0
|
||||
@@ -511,7 +556,7 @@ let incomingInviteCode = ''
|
||||
|
||||
const currentTheme = computed(() => FOOD_GROUPS[themeIndex.value % FOOD_GROUPS.length])
|
||||
const currentTask = computed(() => {
|
||||
const safe = currentTheme.value.safe
|
||||
const safe = activeSafeKeys.value.length === 4 ? activeSafeKeys.value : currentTheme.value.safe
|
||||
const templates = [
|
||||
{ key: safe[0], label: `消除 7 个${foodOf(safe[0]).short}`, target: 7, kind: 'food' },
|
||||
{ key: 'special', label: '制造 1 个惊喜棋子', target: 1, kind: 'special' },
|
||||
@@ -586,7 +631,8 @@ const weeklyRangeLabel = computed(() => {
|
||||
if (!start || !end) return getWeekRangeLabel(new Date())
|
||||
return `${formatWeekDate(start)}—${formatWeekDate(end)}`
|
||||
})
|
||||
const weeklySexLabel = computed(() => platformLeaderboard.value.sex_label || '同行')
|
||||
// 前期人数较少,七人榜统一混排,不向玩家展示男女分组。
|
||||
const weeklySexLabel = computed(() => '同行')
|
||||
const weeklyMemberCount = computed(() => {
|
||||
const reported = Number(platformLeaderboard.value.member_count || 0)
|
||||
const remoteCount = Array.isArray(platformLeaderboard.value.players)
|
||||
@@ -686,6 +732,7 @@ onUnload(() => {
|
||||
if (confettiTimer) clearTimeout(confettiTimer)
|
||||
if (stageTimer) clearTimeout(stageTimer)
|
||||
if (taskRewardTimer) clearTimeout(taskRewardTimer)
|
||||
if (centerNoticeTimer) clearTimeout(centerNoticeTimer)
|
||||
clearFlowTimers()
|
||||
})
|
||||
|
||||
@@ -697,6 +744,101 @@ function foodOf(key) {
|
||||
return FOODS[key] || FOODS.corn
|
||||
}
|
||||
|
||||
function isValidSafeGroup(keys) {
|
||||
return Array.isArray(keys)
|
||||
&& keys.length === 4
|
||||
&& new Set(keys).size === 4
|
||||
&& keys.every(key => FOODS[key] && !foodOf(key).high && !foodOf(key).product)
|
||||
}
|
||||
|
||||
function resetFoodTransitions(keys = currentTheme.value.safe) {
|
||||
activeSafeKeys.value = isValidSafeGroup(keys) ? [...keys] : [...currentTheme.value.safe]
|
||||
foodTransition.value = null
|
||||
queuedSafeGroups.value = []
|
||||
}
|
||||
|
||||
function queueSafeGroupTransition(keys) {
|
||||
if (!isValidSafeGroup(keys)) return false
|
||||
const target = [...keys]
|
||||
const lastQueued = queuedSafeGroups.value[queuedSafeGroups.value.length - 1]
|
||||
if (lastQueued?.every((key, index) => key === target[index])) return false
|
||||
queuedSafeGroups.value = [...queuedSafeGroups.value, target]
|
||||
startNextFoodTransition()
|
||||
return true
|
||||
}
|
||||
|
||||
function startNextFoodTransition() {
|
||||
if (foodTransition.value) return false
|
||||
while (queuedSafeGroups.value.length) {
|
||||
const target = queuedSafeGroups.value[0]
|
||||
const slot = target.findIndex((key, index) => activeSafeKeys.value[index] !== key)
|
||||
if (slot < 0) {
|
||||
queuedSafeGroups.value = queuedSafeGroups.value.slice(1)
|
||||
continue
|
||||
}
|
||||
const beforeTask = currentTask.value
|
||||
const from = activeSafeKeys.value[slot]
|
||||
const to = target[slot]
|
||||
const nextActive = [...activeSafeKeys.value]
|
||||
nextActive[slot] = to
|
||||
activeSafeKeys.value = nextActive
|
||||
foodTransition.value = { from, to, slot }
|
||||
if (beforeTask.kind === 'food' && beforeTask.key === from) taskProgress.value = 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function finishFoodTransitionIfReady(silent = false) {
|
||||
let changed = false
|
||||
let lastAdded = ''
|
||||
while (foodTransition.value) {
|
||||
const transition = foodTransition.value
|
||||
const remaining = board.value.filter(tile => tile?.key === transition.from).length
|
||||
// 旧食物停止掉落后,最后 1~2 个已经无法再自然三消。
|
||||
// 直接把这点“尾巴”换成新食物,避免下一组里长期孤零零留着一杯茶等旧图标。
|
||||
if (remaining > 2) break
|
||||
if (remaining > 0) {
|
||||
board.value = board.value.map(tile => (
|
||||
tile?.key === transition.from ? { ...tile, key: transition.to } : tile
|
||||
))
|
||||
}
|
||||
lastAdded = transition.to
|
||||
foodTransition.value = null
|
||||
changed = true
|
||||
startNextFoodTransition()
|
||||
}
|
||||
if (changed && !silent) statusText.value = `${foodOf(lastAdded).short}已加入,下一种食物正在排队`
|
||||
return changed
|
||||
}
|
||||
|
||||
function prepareFoodTransitionForReshuffle(source) {
|
||||
const active = activeSafeKeys.value
|
||||
const activeSet = new Set(active)
|
||||
const counts = Object.fromEntries(active.map(key => [key, 0]))
|
||||
source.forEach(tile => {
|
||||
if (tile && activeSet.has(tile.key)) counts[tile.key] += 1
|
||||
})
|
||||
let imported = 0
|
||||
const transition = foodTransition.value
|
||||
const next = source.map(tile => {
|
||||
if (!tile) return tile
|
||||
const food = foodOf(tile.key)
|
||||
if (food.high || food.product) return { ...tile }
|
||||
if (transition && tile.key === transition.from) {
|
||||
imported += 1
|
||||
counts[transition.to] += 1
|
||||
return { ...tile, key: transition.to }
|
||||
}
|
||||
if (activeSet.has(tile.key)) return { ...tile }
|
||||
const key = [...active].sort((a, b) => counts[a] - counts[b])[0]
|
||||
counts[key] += 1
|
||||
imported += 1
|
||||
return { ...tile, key }
|
||||
})
|
||||
return { board: next, imported }
|
||||
}
|
||||
|
||||
function getWeekRangeLabel(now) {
|
||||
const date = new Date(now)
|
||||
const day = date.getDay() || 7
|
||||
@@ -929,6 +1071,9 @@ function saveActiveGame() {
|
||||
confirmed_session_learned: Math.max(0, Number(confirmedSessionLearned.value) || 0),
|
||||
saved_at: Date.now(),
|
||||
theme_index: themeIndex.value,
|
||||
active_safe_keys: [...activeSafeKeys.value],
|
||||
food_transition: foodTransition.value ? { ...foodTransition.value } : null,
|
||||
queued_safe_groups: queuedSafeGroups.value.map(keys => [...keys]),
|
||||
board: board.value.map(tile => ({ id: tile.id, key: tile.key, special: tile.special || '' })),
|
||||
score: score.value,
|
||||
learned: learned.value,
|
||||
@@ -966,6 +1111,28 @@ function restoreActiveGame() {
|
||||
}
|
||||
|
||||
themeIndex.value = Math.max(0, Math.min(FOOD_GROUPS.length - 1, Number(saved.theme_index) || 0))
|
||||
activeSafeKeys.value = isValidSafeGroup(saved.active_safe_keys)
|
||||
? [...saved.active_safe_keys]
|
||||
: [...currentTheme.value.safe]
|
||||
const savedTransition = saved.food_transition
|
||||
foodTransition.value = savedTransition
|
||||
&& FOODS[savedTransition.from]
|
||||
&& FOODS[savedTransition.to]
|
||||
&& !foodOf(savedTransition.from).high
|
||||
&& !foodOf(savedTransition.from).product
|
||||
&& !foodOf(savedTransition.to).high
|
||||
&& !foodOf(savedTransition.to).product
|
||||
&& Number.isInteger(Number(savedTransition.slot))
|
||||
&& activeSafeKeys.value[Math.max(0, Math.min(3, Number(savedTransition.slot)))] === savedTransition.to
|
||||
? {
|
||||
from: savedTransition.from,
|
||||
to: savedTransition.to,
|
||||
slot: Math.max(0, Math.min(3, Number(savedTransition.slot)))
|
||||
}
|
||||
: null
|
||||
queuedSafeGroups.value = Array.isArray(saved.queued_safe_groups)
|
||||
? saved.queued_safe_groups.filter(isValidSafeGroup).map(keys => [...keys])
|
||||
: []
|
||||
board.value = saved.board.map(tile => ({
|
||||
id: Math.max(1, Number(tile.id) || tileId++),
|
||||
key: tile.key,
|
||||
@@ -988,6 +1155,7 @@ function restoreActiveGame() {
|
||||
busy.value = false
|
||||
gameOver.value = false
|
||||
taskRewardVisible.value = false
|
||||
centerNoticeVisible.value = false
|
||||
clearingIndices.value = []
|
||||
fireClearingIndices.value = []
|
||||
impactText.value = ''
|
||||
@@ -996,12 +1164,14 @@ function restoreActiveGame() {
|
||||
reshuffleVisible.value = false
|
||||
reshuffleDone.value = false
|
||||
reshuffling.value = false
|
||||
reshuffleImportedFoods.value = false
|
||||
confettiVisible.value = false
|
||||
pendingThemeMatchCount = 0
|
||||
taskRewardStatusUntil = 0
|
||||
endedFood.value = foodOf(currentTheme.value.danger)
|
||||
restoredGame.value = true
|
||||
resetDrag()
|
||||
finishFoodTransitionIfReady(true)
|
||||
statusText.value = '已恢复上次未完成的游戏和得分'
|
||||
scheduleHint()
|
||||
return true
|
||||
@@ -1012,6 +1182,7 @@ function startGame() {
|
||||
clearActiveGame()
|
||||
beginPlatformSession()
|
||||
themeIndex.value = 0
|
||||
resetFoodTransitions(currentTheme.value.safe)
|
||||
board.value = buildPlayableBoard(openingKeysForTheme(currentTheme.value))
|
||||
selected.value = -1
|
||||
score.value = 0
|
||||
@@ -1027,6 +1198,7 @@ function startGame() {
|
||||
taskProgress.value = 0
|
||||
comboDryTurns.value = 0
|
||||
taskRewardVisible.value = false
|
||||
centerNoticeVisible.value = false
|
||||
clearingIndices.value = []
|
||||
fireClearingIndices.value = []
|
||||
impactText.value = ''
|
||||
@@ -1035,6 +1207,7 @@ function startGame() {
|
||||
reshuffleVisible.value = false
|
||||
reshuffleDone.value = false
|
||||
reshuffling.value = false
|
||||
reshuffleImportedFoods.value = false
|
||||
confettiVisible.value = false
|
||||
pendingThemeMatchCount = 0
|
||||
taskRewardStatusUntil = 0
|
||||
@@ -1060,7 +1233,7 @@ function goBack() {
|
||||
function showRules() {
|
||||
uni.showModal({
|
||||
title: '玩法说明',
|
||||
content: `当前难度:${GAME_DIFFICULTY}/10\n\n交换相邻食物,三个相同即可消除。四连、直线五连会生成火焰惊喜棋子;横向三个与竖向三个相交的 L/T 形五消,会生成范围爆破棋子,再次消除可清除周围九格。连消×2会计入连消小目标,连续普通消除后会出现一次连消机会;连消×3可获得额外惊喜奖励。完成小目标直接获得 1 罐驼乳粉;驼乳粉可替换本组指定稀食或饮品,三个驼乳连消后进入下一组食品。主动三消“高糖”食物代表吃掉,会结束本局;火焰特效波及高糖食物代表将它移出餐盘,不算吃掉。没有可走步时会自动洗牌。`,
|
||||
content: `当前难度:${GAME_DIFFICULTY}/10\n\n交换相邻食物,三个相同即可消除。四连、直线五连会生成火焰惊喜棋子;横向三个与竖向三个相交的 L/T 形五消,会生成范围爆破棋子,再次消除可清除周围九格。连消×2会计入连消小目标,连续普通消除后会出现一次连消机会;连消×3可获得额外惊喜奖励。完成小目标直接获得 1 罐驼乳粉;驼乳粉可替换本组指定稀食或饮品,三个驼乳连消后,高糖食物立即更新,普通食物会随掉落逐步换入。主动三消“高糖”食物代表吃掉,会结束本局;火焰特效波及高糖食物代表将它移出餐盘,不算吃掉。没有可走步时会自动洗牌,并加快当前新食物换入。`,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
@@ -1371,6 +1544,7 @@ async function resolveMatches(initialGroups, movedIndex, options = {}) {
|
||||
&& comboTaskAtStart
|
||||
&& comboDryTurns.value >= COMBO_PITY_TURNS - 1
|
||||
collapseAndFill(shouldGuaranteeCombo)
|
||||
finishFoodTransitionIfReady()
|
||||
playDropSfx(remove.size)
|
||||
await wait(190)
|
||||
groups = findMatches(board.value)
|
||||
@@ -1430,6 +1604,18 @@ function grantCamelReward(requested) {
|
||||
return reward
|
||||
}
|
||||
|
||||
function showCamelOverflowNotice(overflowScore) {
|
||||
if (centerNoticeTimer) clearTimeout(centerNoticeTimer)
|
||||
centerNoticeKey.value += 1
|
||||
centerNoticeTitle.value = '驼乳粉已满'
|
||||
centerNoticeDetail.value = `多余奖励换 ${overflowScore} 分`
|
||||
centerNoticeVisible.value = true
|
||||
centerNoticeTimer = setTimeout(() => {
|
||||
centerNoticeVisible.value = false
|
||||
centerNoticeTimer = null
|
||||
}, 1050)
|
||||
}
|
||||
|
||||
function rewardMegaCombo(wave) {
|
||||
score.value += MEGA_COMBO_SCORE
|
||||
const camelReward = grantCamelReward(1)
|
||||
@@ -1437,6 +1623,7 @@ function rewardMegaCombo(wave) {
|
||||
statusText.value = `${wave} 连消惊喜:+${MEGA_COMBO_SCORE} 分${rewardText ? ` · ${rewardText}` : ''}`
|
||||
taskRewardStatusUntil = Date.now() + 1800
|
||||
if (camelReward.granted > 0) showTaskCamelReward()
|
||||
if (camelReward.overflow > 0) showCamelOverflowNotice(camelReward.overflowScore)
|
||||
startConfetti()
|
||||
playSfxLayer('comboMega', 'fanfare', 100, .82)
|
||||
vibrate('medium')
|
||||
@@ -1449,7 +1636,6 @@ function rewardMegaCombo(wave) {
|
||||
function camelRewardText(reward) {
|
||||
const parts = []
|
||||
if (reward.granted > 0) parts.push(`驼乳粉 ×${reward.granted}`)
|
||||
if (reward.overflow > 0) parts.push(`满仓多余奖励换 ${reward.overflowScore} 分`)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
@@ -1488,17 +1674,10 @@ function advanceFoodTheme() {
|
||||
const previous = currentTheme.value
|
||||
themeIndex.value = (themeIndex.value + 1) % FOOD_GROUPS.length
|
||||
const nextTheme = currentTheme.value
|
||||
const mapping = {}
|
||||
previous.safe.forEach((key, index) => { mapping[key] = nextTheme.safe[index] })
|
||||
mapping[previous.danger] = nextTheme.danger
|
||||
const mappedBoard = board.value.map(tile => {
|
||||
if (!tile || !mapping[tile.key]) return tile
|
||||
return { ...tile, key: mapping[tile.key] }
|
||||
})
|
||||
board.value = mappedBoard
|
||||
if (findMatches(mappedBoard).length || !hasSafeMoveIn(mappedBoard)) {
|
||||
board.value = buildPlayableBoard(openingKeysForTheme(nextTheme))
|
||||
}
|
||||
board.value = board.value.map(tile => (
|
||||
tile?.key === previous.danger ? { ...tile, key: nextTheme.danger } : tile
|
||||
))
|
||||
queueSafeGroupTransition(nextTheme.safe)
|
||||
taskIndex.value = 0
|
||||
taskProgress.value = 0
|
||||
comboDryTurns.value = 0
|
||||
@@ -1511,7 +1690,7 @@ function advanceFoodTheme() {
|
||||
foods: [...nextTheme.safe, nextTheme.danger].map(key => ({ key, ...foodOf(key) }))
|
||||
}
|
||||
stageVisible.value = true
|
||||
statusText.value = `已进入${nextTheme.title},橙框${foodOf(nextTheme.danger).short}可替换`
|
||||
statusText.value = `已进入${nextTheme.title},新食物会随掉落逐步加入`
|
||||
playSfxLayer('fanfare', 'sparkle', 100, .65)
|
||||
if (stageTimer) clearTimeout(stageTimer)
|
||||
stageTimer = setTimeout(() => { stageVisible.value = false }, 1800)
|
||||
@@ -1652,14 +1831,14 @@ function findForcedDropMatch(snapshot) {
|
||||
}
|
||||
|
||||
function pickForcedDropKey(snapshot, indices) {
|
||||
return currentTheme.value.safe
|
||||
return activeSafeKeys.value
|
||||
.map(key => ({
|
||||
key,
|
||||
nearby: snapshot.reduce((count, tile, index) => (
|
||||
tile?.key === key && indices.some(target => isAdjacent(target, index)) ? count + 1 : count
|
||||
), 0)
|
||||
}))
|
||||
.sort((a, b) => a.nearby - b.nearby)[0]?.key || currentTheme.value.safe[0]
|
||||
.sort((a, b) => a.nearby - b.nearby)[0]?.key || activeSafeKeys.value[0]
|
||||
}
|
||||
|
||||
function pickDropFoodKey(snapshot, index, col) {
|
||||
@@ -1667,9 +1846,9 @@ function pickDropFoodKey(snapshot, index, col) {
|
||||
const key = randomFoodKey(snapshot, col)
|
||||
if (!wouldCreateMatchAt(snapshot, index, key)) return key
|
||||
}
|
||||
const fallback = [...currentTheme.value.safe, currentTheme.value.danger]
|
||||
const fallback = [...activeSafeKeys.value, currentTheme.value.danger]
|
||||
.find(key => !wouldCreateMatchAt(snapshot, index, key))
|
||||
return fallback || currentTheme.value.safe[(index + col) % currentTheme.value.safe.length]
|
||||
return fallback || activeSafeKeys.value[(index + col) % activeSafeKeys.value.length]
|
||||
}
|
||||
|
||||
function wouldCreateMatchAt(source, index, key) {
|
||||
@@ -1689,7 +1868,7 @@ function wouldCreateMatchAt(source, index, key) {
|
||||
function randomFoodKey(snapshot, col = -1) {
|
||||
const highs = snapshot.filter(tile => tile && foodOf(tile.key).high).length
|
||||
if (highs < HIGH_CAP && Math.random() < DANGER_DROP_CHANCE) return currentTheme.value.danger
|
||||
const keys = currentTheme.value.safe
|
||||
const keys = activeSafeKeys.value
|
||||
if (col >= 0 && Math.random() < COLUMN_GROUP_BIAS) return keys[col % keys.length]
|
||||
return keys[Math.floor(Math.random() * keys.length)]
|
||||
}
|
||||
@@ -1747,13 +1926,11 @@ function advanceTask(amount) {
|
||||
if (camelReward.granted > 0) {
|
||||
showTaskCamelReward()
|
||||
statusText.value = '小目标完成,驼乳粉已放入道具栏'
|
||||
uni.showToast({ title: '小目标完成:驼乳粉 +1', icon: 'none' })
|
||||
} else {
|
||||
statusText.value = `驼乳粉已满,多余奖励换 ${camelReward.overflowScore} 分`
|
||||
statusText.value = '小目标完成,继续挑战'
|
||||
showCamelOverflowNotice(camelReward.overflowScore)
|
||||
}
|
||||
uni.showToast({
|
||||
title: camelReward.granted > 0 ? '小目标完成:驼乳粉 +1' : `驼乳粉已满:奖励换 ${camelReward.overflowScore} 分`,
|
||||
icon: 'none'
|
||||
})
|
||||
taskIndex.value = (taskIndex.value + 1) % 4
|
||||
taskProgress.value = 0
|
||||
comboDryTurns.value = 0
|
||||
@@ -1823,6 +2000,7 @@ async function performReshuffle() {
|
||||
reshuffling.value = true
|
||||
busy.value = true
|
||||
reshuffleDone.value = false
|
||||
reshuffleImportedFoods.value = false
|
||||
reshuffleVisible.value = true
|
||||
selected.value = -1
|
||||
camelMode.value = false
|
||||
@@ -1831,7 +2009,9 @@ async function performReshuffle() {
|
||||
vibrate('medium')
|
||||
await wait(760)
|
||||
|
||||
const source = board.value.map(tile => ({ ...tile }))
|
||||
const prepared = prepareFoodTransitionForReshuffle(board.value)
|
||||
const source = prepared.board
|
||||
reshuffleImportedFoods.value = prepared.imported > 0
|
||||
let candidate = null
|
||||
for (let attempt = 0; attempt < 240; attempt += 1) {
|
||||
const shuffled = shuffleTiles(source)
|
||||
@@ -1841,8 +2021,11 @@ async function performReshuffle() {
|
||||
break
|
||||
}
|
||||
board.value = candidate || buildGuaranteedBoard()
|
||||
finishFoodTransitionIfReady(true)
|
||||
reshuffleDone.value = true
|
||||
statusText.value = '洗牌完成,新的消除机会已经出现'
|
||||
statusText.value = reshuffleImportedFoods.value
|
||||
? '洗牌完成,新食物已经加入'
|
||||
: '洗牌完成,新的消除机会已经出现'
|
||||
playSfxLayer('drop', 'sparkle', 70, .55)
|
||||
vibrate('light')
|
||||
await wait(520)
|
||||
@@ -1867,7 +2050,9 @@ function buildGuaranteedBoard() {
|
||||
}
|
||||
|
||||
function openingKeysForTheme(theme) {
|
||||
const [a, b, c, d] = theme.safe
|
||||
const [a, b, c, d] = isValidSafeGroup(activeSafeKeys.value)
|
||||
? activeSafeKeys.value
|
||||
: theme.safe
|
||||
const danger = theme.danger
|
||||
return [
|
||||
a, b, c, d,
|
||||
@@ -1888,7 +2073,9 @@ function buildPlayableBoard(keys) {
|
||||
}
|
||||
|
||||
function forcedPlayableKeysForTheme(theme) {
|
||||
const [a, b, c, d] = theme.safe
|
||||
const [a, b, c, d] = isValidSafeGroup(activeSafeKeys.value)
|
||||
? activeSafeKeys.value
|
||||
: theme.safe
|
||||
const danger = theme.danger
|
||||
return [
|
||||
d, a, danger, c,
|
||||
|
||||
Reference in New Issue
Block a user