From 33434c3ba4f6b400f4fec785c77cc38d140a403c Mon Sep 17 00:00:00 2001 From: wangfukang <15630117759@163.com> Date: Tue, 2 Jun 2026 14:01:47 +0800 Subject: [PATCH] 1 --- package1/planet/adventure.vue | 205 ++++++++++++++++++++++++++++++++-- 1 file changed, 193 insertions(+), 12 deletions(-) diff --git a/package1/planet/adventure.vue b/package1/planet/adventure.vue index cbe14bf..6e58c4b 100644 --- a/package1/planet/adventure.vue +++ b/package1/planet/adventure.vue @@ -54,7 +54,14 @@ 规则:点选未被压住的卡牌,底部槽内3张相同自动消除,槽满则失败。 - 地狱难度 · 剩余 {{remainingCount}} 张 · 当前可点 {{availableCount}} 张 · {{riskText}} + 陷阱难度 · 剩余 {{remainingCount}} 张 · 当前可点 {{availableCount}} 张 · {{riskText}} + + + + {{availableCount}}可点 + {{slots.length}}/{{slotLimit}}槽位 + {{directComboCount}}可消 + {{trapGroupCount}}陷阱组 @@ -139,7 +146,7 @@ startTs: 0, cards: [], slots: [], - slotLimit: 5, + slotLimit: 7, moveCount: 0, modal: { show: false, title: '', sub: '' } } @@ -181,6 +188,22 @@ availableCount() { return this.cards.filter(item => !item.removed && !item.selected && !this.isLocked(item)).length }, + directComboCount() { + const map = {} + this.cards.forEach(item => { + if (!item.removed && !item.selected && !this.isLocked(item)) { + map[item.icon] = (map[item.icon] || 0) + 1 + } + }) + return Object.keys(map).filter(k => map[k] >= 3).length + }, + trapGroupCount() { + const map = {} + this.cards.forEach(item => { + if (item.trap) map[item.icon] = true + }) + return Object.keys(map).length + }, riskText() { if (this.slots.length >= this.slotLimit - 1) return '危险:差1格就满' if (this.slots.length >= this.slotLimit - 2) return '注意槽位' @@ -256,12 +279,12 @@ }) }, buildLevel() { - const icons = ['🍔', '🥤', '📚', '🏀', '🎧', '🚲', '🍜', '📦', '☕', '🧋', '🎮', '🌟', '🍟', '🥪'] + const icons = ['🍔', '🥤', '📚', '🏀', '🎧', '🚲', '🍜', '📦', '☕', '🧋'] let cards = [] for (let attempt = 0; attempt < 8; attempt++) { const seed = this.levelSeed() + '_try_' + attempt const positions = this.buildCardPositions(seed) - cards = this.assignSolvableIcons(positions, icons, seed) + cards = this.assignTrapLevelIcons(positions, icons, seed) if (cards.length) break } if (!cards.length) { @@ -273,25 +296,29 @@ }, buildCardPositions(seed) { const cards = [] - const layerCounts = [42, 36, 30, 18] + const template = this.levelTemplate('hell') + const layerCounts = template.layerCounts const rand = this.seededRandom(seed + '_pos') let idx = 0 layerCounts.forEach((count, layer) => { for (let i = 0; i < count; i++) { - const col = i % 7 - const row = Math.floor(i / 7) + const zone = template.zones[layer] + const col = i % zone.cols + const row = Math.floor(i / zone.cols) const dx = Math.floor(rand() * 18) const dy = Math.floor(rand() * 14) const card = { id: 'c' + idx, icon: '', layer, - x: 12 + col * 70 + (layer * 16) + ((row % 2) * 10) + dx, - y: 16 + row * 62 + (layer * 34) + dy, + x: zone.x + col * zone.gapX + (layer * zone.offsetX) + ((row % 2) * zone.stagger) + dx, + y: zone.y + row * zone.gapY + (layer * zone.offsetY) + dy, style: '', removed: false, selected: false, - solutionStep: 0 + solutionStep: 0, + trap: false, + keyCard: false } card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${card.layer + 1};` cards.push(card) @@ -300,6 +327,43 @@ }) return cards }, + levelTemplate(type) { + const templates = { + simple: { + layerCounts: [36, 30, 24], + zones: [ + { x: 20, y: 18, cols: 6, gapX: 82, gapY: 72, offsetX: 8, offsetY: 18, stagger: 8 }, + { x: 70, y: 72, cols: 5, gapX: 78, gapY: 70, offsetX: 10, offsetY: 22, stagger: 10 }, + { x: 96, y: 110, cols: 5, gapX: 74, gapY: 66, offsetX: 12, offsetY: 24, stagger: 10 } + ] + }, + normal: { + layerCounts: [36, 30, 24], + zones: [ + { x: 18, y: 18, cols: 6, gapX: 80, gapY: 70, offsetX: 12, offsetY: 24, stagger: 10 }, + { x: 78, y: 76, cols: 5, gapX: 76, gapY: 66, offsetX: 14, offsetY: 28, stagger: 12 }, + { x: 110, y: 116, cols: 4, gapX: 74, gapY: 62, offsetX: 16, offsetY: 30, stagger: 12 } + ] + }, + difficult: { + layerCounts: [36, 30, 24], + zones: [ + { x: 14, y: 16, cols: 6, gapX: 80, gapY: 70, offsetX: 14, offsetY: 28, stagger: 12 }, + { x: 92, y: 76, cols: 5, gapX: 70, gapY: 62, offsetX: 18, offsetY: 32, stagger: 14 }, + { x: 132, y: 122, cols: 4, gapX: 66, gapY: 58, offsetX: 20, offsetY: 34, stagger: 14 } + ] + }, + hell: { + layerCounts: [36, 30, 24], + zones: [ + { x: 12, y: 16, cols: 6, gapX: 82, gapY: 72, offsetX: 10, offsetY: 22, stagger: 10 }, + { x: 86, y: 72, cols: 5, gapX: 70, gapY: 60, offsetX: 18, offsetY: 34, stagger: 16 }, + { x: 126, y: 112, cols: 4, gapX: 64, gapY: 54, offsetX: 22, offsetY: 38, stagger: 16 } + ] + } + } + return templates[type] || templates.hell + }, assignSolvableIcons(positions, icons, seed) { // 上线关卡不能随机发牌。这里先模拟一条真实可点击的消除路径, // 再按路径给三张牌分配同一图案,保证每日关卡至少存在一条解法。 @@ -330,8 +394,121 @@ }) return positions }, + assignTrapLevelIcons(positions, icons, seed) { + const rand = this.seededRandom(seed + '_trap') + const cards = positions.slice() + const trapIcons = this.seededShuffle(icons.slice(), seed + '_trap_icons').slice(0, 4) + const remaining = cards.slice() + const groups = [] + const trapPlans = this.buildTrapPlans(cards, trapIcons, seed) + if (trapPlans.length < 4) return [] + + while (remaining.length) { + const available = remaining.filter(card => !this.isBlockedIn(card, remaining)) + if (available.length < 3) return [] + const readyTrap = trapPlans.find(plan => { + return !plan.used && plan.cards.every(card => remaining.indexOf(card) >= 0) && !this.isBlockedIn(plan.key, remaining) + }) + const normalAvailable = available.filter(card => !card.reservedTrap) + const picked = readyTrap ? readyTrap.cards : this.pickSolutionTriple(normalAvailable, rand) + if (!picked || picked.length < 3) return [] + groups.push({ + cards: picked, + trapIcon: readyTrap ? readyTrap.icon : '' + }) + if (readyTrap) readyTrap.used = true + picked.forEach(card => { + const idx = remaining.indexOf(card) + if (idx >= 0) remaining.splice(idx, 1) + }) + } + + if (trapPlans.some(plan => !plan.used)) return [] + const trapIndexes = [] + groups.forEach((group, index) => { + if (group.trapIcon) trapIndexes.push(index) + }) + const groupIcons = this.buildPressureIconOrder(groups.length, icons, trapIcons, trapIndexes, seed) + groups.forEach((group, step) => { + const icon = group.trapIcon || groupIcons[step] || icons[step % icons.length] + group.cards.forEach(card => { + card.icon = icon + card.solutionStep = step + 1 + if (group.trapIcon) { + card.trap = true + } + }) + }) + + cards.forEach(card => { + card.reservedTrap = false + card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${card.layer + 1};` + }) + return cards + }, + buildTrapPlans(cards, trapIcons, seed) { + const startAvailable = cards.filter(card => !this.isBlockedIn(card, cards)) + const baitPool = this.seededShuffle(startAvailable.filter(card => !this.isCenterCard(card)), seed + '_bait') + const keyPool = this.seededShuffle(cards.filter(card => card.layer === 0 && this.isCenterCard(card)), seed + '_key') + const plans = [] + trapIcons.forEach(icon => { + const baitA = baitPool.shift() + const baitB = baitPool.shift() + const key = keyPool.shift() + if (!baitA || !baitB || !key) return + ;[baitA, baitB, key].forEach(card => { + card.reservedTrap = true + card.trap = true + }) + key.keyCard = true + plans.push({ + icon, + cards: [baitA, baitB, key], + key, + used: false + }) + }) + return plans + }, + buildPressureIconOrder(count, icons, trapIcons, trapIndexes, seed) { + const rand = this.seededRandom(seed + '_pressure') + const safeIcons = icons.filter(icon => trapIcons.indexOf(icon) < 0) + const order = new Array(count) + trapIndexes.forEach((idx, i) => { + order[idx] = trapIcons[i % trapIcons.length] + }) + const usage = {} + trapIcons.forEach(icon => { + usage[icon] = 1 + }) + for (let i = 0; i < count; i++) { + if (order[i]) continue + const pool = i < 12 ? icons : safeIcons + let icon = pool[i % pool.length] + let guard = 0 + while ((usage[icon] || 0) >= 3 && guard < pool.length + icons.length) { + icon = pool[(i + guard + 1) % pool.length] + guard++ + } + if ((usage[icon] || 0) >= 3) { + icon = icons.find(item => (usage[item] || 0) < 3) || icons[i % icons.length] + } + order[i] = icon + usage[icon] = (usage[icon] || 0) + 1 + } + for (let i = 0; i < order.length; i += 6) { + const part = order.slice(i, i + 6) + this.shuffleWithRandom(part, rand) + for (let j = 0; j < part.length; j++) order[i + j] = part[j] + } + return order + }, + isCenterCard(card) { + return card.x >= 160 && card.x <= 400 && card.y >= 120 && card.y <= 440 + }, pickSolutionTriple(available, rand) { const pool = available.slice() + if (pool.length < 3) return [] this.shuffleWithRandom(pool, rand) const first = pool.shift() pool.sort((a, b) => this.cardDistanceScore(b, first) - this.cardDistanceScore(a, first)) @@ -525,6 +702,10 @@ .hero-title { margin-top: 10rpx; font-size: 42rpx; font-weight: 900; } .hero-sub, .tips { margin-top: 16rpx; color: #6B817D; font-size: 24rpx; line-height: 1.55; } .tips { display: flex; flex-direction: column; gap: 6rpx; } + .debug-panel { margin-top: 16rpx; display: flex; gap: 10rpx; } + .debug-panel view { flex: 1; border-radius: 20rpx; padding: 12rpx 6rpx; background: rgba(255,255,255,0.76); text-align: center; border: 1rpx solid rgba(53,214,166,0.16); } + .debug-panel text:first-child { display: block; color: #22B889; font-size: 28rpx; font-weight: 900; } + .debug-panel text:last-child { display: block; margin-top: 4rpx; color: #8AA09C; font-size: 20rpx; } .college-picker { margin-top: 18rpx; padding: 18rpx 22rpx; border-radius: 26rpx; background: rgba(255,255,255,0.72); display: flex; align-items: center; justify-content: space-between; color: #42635E; font-size: 24rpx; } .college-picker text:last-child { color: #22B889; font-weight: 900; } .rocket { margin-top: 22rpx; display: flex; align-items: center; gap: 16rpx; } @@ -553,8 +734,8 @@ .slot-wrap { margin-top: 24rpx; } .slot-title { color: #6B817D; font-size: 24rpx; margin-bottom: 14rpx; } .slots { display: flex; gap: 10rpx; } - .slot { flex: 1; height: 74rpx; border-radius: 20rpx; background: rgba(255,255,255,0.8); border: 2rpx dashed rgba(53,214,166,0.28); display: flex; align-items: center; justify-content: center; } - .slot text { font-size: 38rpx; } + .slot { flex: 1; height: 66rpx; border-radius: 18rpx; background: rgba(255,255,255,0.8); border: 2rpx dashed rgba(53,214,166,0.28); display: flex; align-items: center; justify-content: center; } + .slot text { font-size: 34rpx; } .actions { margin-top: 28rpx; display: flex; gap: 18rpx; } .sub-btn, .main-btn { flex: 1; height: 82rpx; line-height: 82rpx; text-align: center; border-radius: 999rpx; font-size: 28rpx; font-weight: 900; } .sub-btn { background: rgba(255,255,255,0.78); color: #42635E; }