|
|
|
@ -433,11 +433,11 @@ |
|
|
|
] |
|
|
|
}, |
|
|
|
hell: { |
|
|
|
layerCounts: [36, 30, 24], |
|
|
|
layerCounts: [52, 40, 28], |
|
|
|
zones: [ |
|
|
|
{ x: 58, y: 18, cols: 6, gapX: 86, gapY: 82, offsetX: 0, offsetY: 0, stagger: 4 }, |
|
|
|
{ x: 102, y: 58, cols: 5, gapX: 86, gapY: 82, offsetX: 0, offsetY: 0, stagger: 4 }, |
|
|
|
{ x: 144, y: 98, cols: 4, gapX: 86, gapY: 82, offsetX: 0, offsetY: 0, stagger: 4 } |
|
|
|
{ x: 44, y: 14, cols: 7, gapX: 76, gapY: 72, offsetX: 0, offsetY: 0, stagger: 4 }, |
|
|
|
{ x: 72, y: 42, cols: 6, gapX: 76, gapY: 72, offsetX: 0, offsetY: 0, stagger: 4 }, |
|
|
|
{ x: 34, y: 18, cols: 7, gapX: 76, gapY: 88, offsetX: 0, offsetY: 0, stagger: 0 } |
|
|
|
] |
|
|
|
} |
|
|
|
} |
|
|
|
@ -450,6 +450,7 @@ |
|
|
|
card.trap = false |
|
|
|
card.keyCard = false |
|
|
|
card.garbage = false |
|
|
|
card.openingSafe = false |
|
|
|
}) |
|
|
|
const bottom = this.seededShuffle(cards.filter(card => card.layer === 0), seed + '_bottom') |
|
|
|
const middle = this.seededShuffle(cards.filter(card => card.layer === 1), seed + '_middle') |
|
|
|
@ -459,7 +460,7 @@ |
|
|
|
const midOuter = this.seededShuffle(middle.filter(card => !this.isCenterCard(card)), seed + '_mid_outer') |
|
|
|
const assigned = {} |
|
|
|
const groupCount = Math.floor((cards.length - 6) / 3) |
|
|
|
const trapCount = Math.max(10,Math.ceil(groupCount*0.5)) |
|
|
|
const trapCount = Math.max(20,Math.ceil(groupCount*0.62)) |
|
|
|
for (let group = 0; group < groupCount; group++) { |
|
|
|
const icon = icons[group % icons.length] |
|
|
|
let chosen = [] |
|
|
|
@ -543,8 +544,7 @@ |
|
|
|
}) |
|
|
|
}, |
|
|
|
shouldBuildTrapLayout() { |
|
|
|
const rand = this.seededRandom((this.roundSeed || this.levelSeed()) + '_win_rate') |
|
|
|
return rand() < 0.85 |
|
|
|
return true |
|
|
|
}, |
|
|
|
applyTrapLayout(cards) { |
|
|
|
this.cards = cards |
|
|
|
@ -558,49 +558,168 @@ |
|
|
|
if (!candidates.length) return |
|
|
|
const rand = this.seededRandom((this.roundSeed || this.levelSeed()) + '_trap_layout') |
|
|
|
this.shuffleWithRandom(candidates, rand) |
|
|
|
const trapIcons = candidates.slice(0, Math.min(candidates.length, 12 + Math.floor(rand() * 5))) |
|
|
|
const trapIcons = candidates.slice(0, Math.min(candidates.length, 8 + Math.floor(rand() * 2))) |
|
|
|
trapIcons.forEach((targetIcon, trapIndex) => { |
|
|
|
const group = iconMap[targetIcon].slice(0, 3) |
|
|
|
const group = this.seededShuffle(iconMap[targetIcon].slice(), (this.roundSeed || this.levelSeed()) + '_trap_icon_' + trapIndex).slice(0, 3) |
|
|
|
if (group.length < 3) return |
|
|
|
const key = group[0] |
|
|
|
const pair = group.slice(1, 3) |
|
|
|
this.placeDeepKeyCard(key, trapIndex, rand) |
|
|
|
this.exposePairEarly(pair, trapIndex) |
|
|
|
this.placeDeepKeyCard(key, trapIndex, rand) |
|
|
|
this.buildCoverChain(key, cards, trapIndex, rand) |
|
|
|
}) |
|
|
|
this.exposeOpeningCombos(cards, candidates, rand) |
|
|
|
this.tightenOpeningPressure(cards, rand) |
|
|
|
}, |
|
|
|
placeDeepKeyCard(card, index, rand) { |
|
|
|
card.layer = 0 |
|
|
|
card.keyCard = true |
|
|
|
card.trap = true |
|
|
|
card.x = 170 + Math.floor(rand() * 200) |
|
|
|
card.y = 130 + Math.floor(rand() * 300) |
|
|
|
card.order = 20 + index |
|
|
|
card.x = 188 + Math.floor(rand() * 150) |
|
|
|
card.y = 150 + Math.floor(rand() * 250) |
|
|
|
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};` |
|
|
|
this.refreshCardState() |
|
|
|
}, |
|
|
|
exposePairEarly(pair, index) { |
|
|
|
const baseX = 56 + (index % 5) * 84 |
|
|
|
const baseY = 18 + Math.floor(index / 5) * 76 |
|
|
|
const slots = [ |
|
|
|
{ x: 34, y: 16 }, { x: 138, y: 16 }, { x: 242, y: 16 }, { x: 346, y: 16 }, { x: 450, y: 16 }, |
|
|
|
{ x: 34, y: 114 }, { x: 138, y: 114 }, { x: 242, y: 114 }, { x: 346, y: 114 }, { x: 450, y: 114 }, |
|
|
|
{ x: 34, y: 314 }, { x: 138, y: 314 }, { x: 242, y: 314 }, { x: 346, y: 314 }, { x: 450, y: 314 }, |
|
|
|
{ x: 34, y: 412 }, { x: 138, y: 412 }, { x: 242, y: 412 }, { x: 346, y: 412 }, { x: 450, y: 412 } |
|
|
|
] |
|
|
|
const first = slots[index % 10] |
|
|
|
const second = slots[index % 10 + 10] |
|
|
|
pair.forEach((card, i) => { |
|
|
|
card.layer = 2 |
|
|
|
card.order = 900 + index * 4 + i |
|
|
|
card.trap = true |
|
|
|
card.x = baseX + i * 8 |
|
|
|
card.y = baseY + i * 58 |
|
|
|
card.x = i === 0 ? first.x : second.x |
|
|
|
card.y = i === 0 ? first.y : second.y |
|
|
|
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};` |
|
|
|
}) |
|
|
|
this.refreshCardState() |
|
|
|
}, |
|
|
|
exposeOpeningCombos(cards, candidates, rand) { |
|
|
|
const slots = [ |
|
|
|
{ x: 38, y: 16 }, { x: 122, y: 16 }, { x: 206, y: 16 }, |
|
|
|
{ x: 310, y: 16 }, { x: 394, y: 16 }, { x: 478, y: 16 }, |
|
|
|
{ x: 38, y: 118 }, { x: 122, y: 118 }, { x: 206, y: 118 }, |
|
|
|
{ x: 310, y: 118 }, { x: 394, y: 118 }, { x: 478, y: 118 }, |
|
|
|
{ x: 38, y: 408 }, { x: 122, y: 408 }, { x: 206, y: 408 } |
|
|
|
] |
|
|
|
const safeIcons = candidates.slice(-4) |
|
|
|
let slotIndex = 0 |
|
|
|
safeIcons.forEach((icon, groupIndex) => { |
|
|
|
const group = cards.filter(card => card.icon === icon && !card.garbage && !card.keyCard && !card.trap).slice(0, 3) |
|
|
|
if (group.length < 3 || slotIndex + 2 >= slots.length) return |
|
|
|
group.forEach(card => { |
|
|
|
const slot = slots[slotIndex++] |
|
|
|
card.layer = 2 |
|
|
|
card.trap = false |
|
|
|
card.keyCard = false |
|
|
|
card.openingSafe = true |
|
|
|
card.order = 1100 + groupIndex * 8 + slotIndex |
|
|
|
card.x = slot.x + Math.floor(rand() * 4) |
|
|
|
card.y = slot.y + Math.floor(rand() * 4) |
|
|
|
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};` |
|
|
|
}) |
|
|
|
}) |
|
|
|
this.refreshCardState() |
|
|
|
}, |
|
|
|
buildCoverChain(key, cards, index, rand) { |
|
|
|
const covers = cards.filter(card => { |
|
|
|
return card.id !== key.id && !card.garbage && !card.keyCard && !card.trap && card.layer > 0 |
|
|
|
return card.id !== key.id && !card.garbage && !card.keyCard && !card.trap && !card.openingSafe && card.layer > 0 |
|
|
|
}) |
|
|
|
this.shuffleWithRandom(covers, rand) |
|
|
|
const count = 5 + (index % 3) |
|
|
|
const count = 7 + (index % 3) |
|
|
|
const offsets = [ |
|
|
|
{ x: 0, y: 0 }, |
|
|
|
{ x: 8, y: 8 }, |
|
|
|
{ x: -8, y: 10 }, |
|
|
|
{ x: 10, y: -8 }, |
|
|
|
{ x: -10, y: -6 }, |
|
|
|
{ x: 2, y: 14 }, |
|
|
|
{ x: 14, y: 2 }, |
|
|
|
{ x: -14, y: 4 }, |
|
|
|
{ x: 4, y: -14 }, |
|
|
|
{ x: -4, y: 16 }, |
|
|
|
{ x: 16, y: -4 }, |
|
|
|
{ x: -16, y: -2 } |
|
|
|
] |
|
|
|
for (let i = 0; i < count && i < covers.length; i++) { |
|
|
|
const cover = covers[i] |
|
|
|
cover.layer = i % 2 === 0 ? 1 : 2 |
|
|
|
cover.x = key.x + (i % 2 === 0 ? 14 : -14) + Math.floor(rand() * 10) |
|
|
|
cover.y = key.y + (i % 3 === 0 ? 14 : -14) + Math.floor(rand() * 10) |
|
|
|
const offset = offsets[i % offsets.length] |
|
|
|
cover.layer = i < 5 ? 1 : 2 |
|
|
|
cover.order = 520 + index * 28 + i |
|
|
|
cover.x = key.x + offset.x + Math.floor(rand() * 5) |
|
|
|
cover.y = key.y + offset.y + Math.floor(rand() * 5) |
|
|
|
cover.style = `left:${cover.x}rpx;top:${cover.y}rpx;z-index:${this.cardZIndex(cover)};` |
|
|
|
} |
|
|
|
this.refreshCardState() |
|
|
|
}, |
|
|
|
tightenOpeningPressure(cards, rand) { |
|
|
|
let buryIndex = 0 |
|
|
|
for (let round = 0; round < 3; round++) { |
|
|
|
this.refreshCardState() |
|
|
|
const visibleMap = {} |
|
|
|
cards.forEach(card => { |
|
|
|
if (card.removed || card.selected || card.coverDepth > 0 || this.isGarbageIcon(card.icon)) return |
|
|
|
if (!visibleMap[card.icon]) visibleMap[card.icon] = [] |
|
|
|
visibleMap[card.icon].push(card) |
|
|
|
}) |
|
|
|
let changed = false |
|
|
|
Object.keys(visibleMap).forEach(icon => { |
|
|
|
const list = visibleMap[icon].sort((a, b) => { |
|
|
|
if (a.openingSafe !== b.openingSafe) return a.openingSafe ? -1 : 1 |
|
|
|
if (a.trap !== b.trap) return a.trap ? -1 : 1 |
|
|
|
return this.getRealZ(b) - this.getRealZ(a) |
|
|
|
}) |
|
|
|
const keepCount = list.some(card => card.openingSafe) ? 3 : (list.some(card => !card.trap) ? 3 : 2) |
|
|
|
list.slice(keepCount).forEach(card => { |
|
|
|
changed = true |
|
|
|
this.buryOpeningCard(card, cards, buryIndex++, rand) |
|
|
|
}) |
|
|
|
}) |
|
|
|
if (!changed) break |
|
|
|
} |
|
|
|
this.refreshCardState() |
|
|
|
const visible = cards.filter(card => !card.removed && !card.selected && card.coverDepth === 0 && !this.isGarbageIcon(card.icon) && !card.openingSafe) |
|
|
|
visible |
|
|
|
.sort((a, b) => { |
|
|
|
if (a.trap !== b.trap) return a.trap ? -1 : 1 |
|
|
|
return this.getRealZ(b) - this.getRealZ(a) |
|
|
|
}) |
|
|
|
.slice(30) |
|
|
|
.forEach(card => { |
|
|
|
this.buryOpeningCard(card, cards, buryIndex++, rand) |
|
|
|
}) |
|
|
|
this.refreshCardState() |
|
|
|
}, |
|
|
|
buryOpeningCard(card, cards, index, rand) { |
|
|
|
card.layer = 0 |
|
|
|
card.trap = true |
|
|
|
card.keyCard = true |
|
|
|
card.order = 40 + index |
|
|
|
card.x = 170 + (index % 5) * 44 + Math.floor(rand() * 10) |
|
|
|
card.y = 150 + Math.floor(index / 5) * 54 + Math.floor(rand() * 12) |
|
|
|
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};` |
|
|
|
this.buildCoverChain(card, cards, 20 + index, rand) |
|
|
|
if (this.coverCount(card, cards) < 3) { |
|
|
|
this.forceCoverBuriedCard(card, cards, index, rand) |
|
|
|
} |
|
|
|
}, |
|
|
|
forceCoverBuriedCard(card, cards, index, rand) { |
|
|
|
const covers = cards.filter(item => { |
|
|
|
return item.id !== card.id && !item.garbage && !item.keyCard && !item.trap && !item.openingSafe && item.layer > 0 |
|
|
|
}) |
|
|
|
this.shuffleWithRandom(covers, rand) |
|
|
|
for (let i = 0; i < 3 && i < covers.length; i++) { |
|
|
|
const cover = covers[i] |
|
|
|
cover.layer = i === 0 ? 1 : 2 |
|
|
|
cover.order = 980 + index * 8 + i |
|
|
|
cover.x = card.x + (i === 0 ? 0 : (i === 1 ? 12 : -12)) + Math.floor(rand() * 4) |
|
|
|
cover.y = card.y + (i === 0 ? 0 : (i === 1 ? 10 : -10)) + Math.floor(rand() * 4) |
|
|
|
cover.style = `left:${cover.x}rpx;top:${cover.y}rpx;z-index:${this.cardZIndex(cover)};` |
|
|
|
} |
|
|
|
this.refreshCardState() |
|
|
|
@ -664,7 +783,7 @@ |
|
|
|
coverCount(card, cards) { |
|
|
|
return cards.filter(other => { |
|
|
|
if (other.id === card.id || this.getRealZ(other) <= this.getRealZ(card)) return false |
|
|
|
return this.isOverlap(card, other) |
|
|
|
return this.covers(other, card) |
|
|
|
}).length |
|
|
|
}, |
|
|
|
isGarbageIcon(icon) { |
|
|
|
@ -676,7 +795,7 @@ |
|
|
|
isBlockedIn(card, list) { |
|
|
|
return list.some(other => { |
|
|
|
if (other.id === card.id || this.getRealZ(other) <= this.getRealZ(card)) return false |
|
|
|
return this.isOverlap(card, other) |
|
|
|
return this.covers(other, card) |
|
|
|
}) |
|
|
|
}, |
|
|
|
isOverlap(a, b) { |
|
|
|
@ -690,6 +809,13 @@ |
|
|
|
overlapY >= minAxis && |
|
|
|
(overlapX * overlapY) >= size * size * minAreaRatio |
|
|
|
}, |
|
|
|
// upper 是否真正压住 lower:同层不会重叠,故只要两轴都有实质重叠即判定被压 |
|
|
|
covers(upper, lower) { |
|
|
|
const size = 76 |
|
|
|
const overlapX = Math.min(upper.x + size, lower.x + size) - Math.max(upper.x, lower.x) |
|
|
|
const overlapY = Math.min(upper.y + size, lower.y + size) - Math.max(upper.y, lower.y) |
|
|
|
return overlapX >= 10 && overlapY >= 10 |
|
|
|
}, |
|
|
|
getRealZ(card) { |
|
|
|
return (card.layer || 0) * 1000 + (card.order || 0) |
|
|
|
}, |
|
|
|
@ -719,7 +845,7 @@ |
|
|
|
card.locked = !card.removed && !card.selected && depth > 0 |
|
|
|
card.coverDepth = depth |
|
|
|
card.displayState = depth === 0 ? 'top' : (depth === 1 ? 'middle' : 'deep') |
|
|
|
card.renderIcon = card.displayState === 'deep' ? '' : (card.displayIcon || this.displayIcon(card.icon)) |
|
|
|
card.renderIcon = card.displayState === 'deep' ? '?' : (card.displayIcon || this.displayIcon(card.icon)) |
|
|
|
card.className = [ |
|
|
|
depth === 0 ? 'active' : '', |
|
|
|
depth === 1 ? 'locked' : '', |
|
|
|
@ -733,7 +859,7 @@ |
|
|
|
return this.cards.filter(other => { |
|
|
|
if (other.id === card.id) return false |
|
|
|
if (other.removed || other.selected) return false |
|
|
|
return this.getRealZ(other) > this.getRealZ(card) && this.isOverlap(card, other) |
|
|
|
return this.getRealZ(other) > this.getRealZ(card) && this.covers(other, card) |
|
|
|
}).length |
|
|
|
}, |
|
|
|
pickCard(id) { |
|
|
|
@ -1008,10 +1134,9 @@ |
|
|
|
.tile text { font-size: 42rpx; } |
|
|
|
.tile .debug-layer { position: absolute; right: 6rpx; bottom: 4rpx; font-size: 16rpx; color: #8AA09C; line-height: 1; } |
|
|
|
.tile.active { background: #fff; box-shadow: 0 12rpx 24rpx rgba(66,99,94,0.14); } |
|
|
|
.tile.locked { opacity: .45; filter: grayscale(100%); background: #F3F6F5; box-shadow: 0 6rpx 14rpx rgba(66,99,94,0.08); pointer-events: none; } |
|
|
|
.tile.hidden-card { opacity: .32; background: #fff; border-radius: 22rpx; box-shadow: none; color: #BFC9D4; pointer-events: none; } |
|
|
|
.tile.hidden-card text:first-child { display: none; } |
|
|
|
.tile.hidden-card::after { content: '?'; font-size: 36rpx; color: #BFC9D4; } |
|
|
|
.tile.locked { opacity: .45; filter: grayscale(100%); background: #F3F6F5; box-shadow: 0 6rpx 14rpx rgba(66,99,94,0.08); } |
|
|
|
.tile.hidden-card { opacity: .32; background: #fff; border-radius: 22rpx; box-shadow: none; color: #BFC9D4; } |
|
|
|
.tile.hidden-card text:first-child { display: block; font-size: 36rpx; color: #BFC9D4; } |
|
|
|
.tile.selected { opacity: 0; transform: scale(.5); pointer-events: none; } |
|
|
|
.slot-wrap { margin-top: 24rpx; } |
|
|
|
.slot-title { color: #6B817D; font-size: 24rpx; margin-bottom: 14rpx; } |
|
|
|
|