You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

802 lines
30 KiB

<template>
<view class="boost">
<view class="nav" :style="{paddingTop: statusBarHeight + 'px'}">
<view class="nav-back" @tap="goBack"><text></text></view>
<view class="nav-title">学院助推赛</view>
</view>
<scroll-view scroll-y class="page" :style="{paddingTop: (statusBarHeight + 58) + 'px'}">
<view class="hero">
<view class="hero-kicker">COLLEGE ROCKET</view>
<view class="hero-title">今天为 {{collegeName}} 推一把</view>
<view class="college-picker" @tap="openCollegePicker">
<text>{{college ? '已选择学院' : '先选择学院'}}</text>
<text>{{collegeName}} </text>
</view>
<view class="rocket">
<view class="rocket-body">🚀</view>
<view class="rocket-line">
<view class="rocket-progress" :style="{width: rocketPercent + '%'}"></view>
</view>
</view>
<view class="hero-sub">每天1关通关贡献学院推进值21:30结算前三学院成员分别获得3/2/1张星球券</view>
</view>
<view class="rank-card">
<view class="section-head">
<text>学院推进榜</text>
<text>我的学院第 {{home.myRankNo || '-'}} </text>
</view>
<view class="college-row" v-for="(item,i) in collegeRanks" :key="i">
<view class="medal">{{i + 1}}</view>
<view class="college-main">
<text>{{item.college || item.nickname || '未知学院'}}</text>
<view class="bar"><view :style="{width: (item.percent || 8) + '%'}"></view></view>
</view>
<view class="score">{{item.score || 0}}</view>
</view>
<view class="empty" v-if="!collegeRanks.length">今天还没有学院上榜快来抢第一棒</view>
</view>
<view class="game-card">
<view class="section-head">
<text>今日关卡</text>
<text>{{cleared ? '已通关' : '约2分钟'}}</text>
</view>
<view class="start-strip" v-if="!playing && !cleared" @tap="startLevel">
<text>开始助推</text>
<text>先点这里,再开始消除卡牌</text>
</view>
<view class="start-strip done" v-if="cleared">
<text>今日已助推</text>
<text>明天再来为学院继续推进</text>
</view>
<view class="tips">
<text>规则:点选未被压住的卡牌,底部槽内3张相同自动消除,宇宙垃圾2张会触发清理。</text>
<text>陷阱难度 · 剩余 {{remainingCount}} 张 · 当前可点 {{availableCount}} 张 · {{riskText}}</text>
</view>
<view class="debug-panel">
<view><text>{{availableCount}}</text><text>可点</text></view>
<view><text>{{slots.length}}/{{slotLimit}}</text><text>槽位</text></view>
<view><text>{{directComboCount}}</text><text>可消</text></view>
<view><text>{{trapGroupCount}}</text><text>陷阱组</text></view>
</view>
<view class="board">
<view
class="tile"
v-for="card in visibleCards"
:key="card.id"
:class="card.className"
:style="card.style"
@tap="pickCard(card)">
<text>{{card.displayIcon}}</text>
</view>
</view>
<view class="slot-wrap">
<view class="slot-title">助推槽 {{slots.length}}/{{slotLimit}}</view>
<view class="slots">
<view class="slot" v-for="cell in slotCells" :key="cell.key">
<text v-if="cell.icon">{{cell.icon}}</text>
</view>
</view>
</view>
<view class="actions">
<view class="sub-btn" @tap="resetLevel">重开本关</view>
<view class="main-btn" :class="{disabled: cleared || playing}" @tap="startLevel">
{{cleared ? '今日已助推' : (playing ? '助推中' : '开始助推')}}
</view>
</view>
</view>
</scroll-view>
<view v-if="modal.show" class="modal">
<view class="modal-card">
<view class="modal-title">{{modal.title}}</view>
<view class="modal-sub">{{modal.sub}}</view>
<view class="main-btn" @tap="closeModal">知道了</view>
</view>
</view>
<view v-if="collegeModal" class="modal">
<view class="modal-card college-modal">
<view class="modal-title">选择你的学院</view>
<view class="modal-sub">学院来自当前校区后台配置,可随时修改。当天通关后会为所选学院贡献推进值。</view>
<scroll-view scroll-y class="college-list">
<view
class="college-option"
v-for="(item,i) in colleges"
:key="i"
:class="{active: college === item.collegeName}"
@tap="selectCollege(item)">
<text>{{item.icon || '🎓'}}</text>
<view>
<text>{{item.collegeName}}</text>
<text>{{item.shortName || '学院战队'}}</text>
</view>
</view>
</scroll-view>
<view class="empty" v-if="!colleges.length">当前校区还没有配置学院,请联系后台先维护学院配置</view>
<view class="main-btn" @tap="closeCollegePicker">确定</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
statusBarHeight: 20,
userId: '',
regionId: '',
nickname: '',
avatar: '',
college: '',
colleges: [],
collegeModal: false,
home: {},
session: null,
playing: false,
cleared: false,
startTs: 0,
cards: [],
slots: [],
slotLimit: 7,
moveCount: 0,
modal: { show: false, title: '', sub: '' }
}
},
computed: {
collegeName() {
return this.college || '我的学院'
},
collegeRanks() {
const list = this.home.rankList || []
let max = 1
list.forEach(item => {
if ((item.score || 0) > max) max = item.score || 0
})
return list.map(item => {
item.percent = Math.max(8, Math.round((item.score || 0) * 100 / max))
return item
})
},
rocketPercent() {
let mine = null
for (let i = 0; i < this.collegeRanks.length; i++) {
const item = this.collegeRanks[i]
if ((item.college || item.nickname) === this.college) {
mine = item
break
}
}
const top = this.collegeRanks[0] || {}
if (!mine || !top.score) return 16
return Math.max(16, Math.min(100, Math.round((mine.score || 0) * 100 / top.score)))
},
visibleCards() {
return this.cards.filter(item => !item.removed && !item.selected)
},
remainingCount() {
return this.cards.filter(item => !item.removed && !item.selected).length
},
availableCount() {
return this.cards.filter(item => !item.removed && !item.selected && !item.locked).length
},
directComboCount() {
return this.collectDifficultyStats(this.cards.filter(item => !item.removed && !item.selected)).directComboCount
},
trapGroupCount() {
return this.collectDifficultyStats(this.cards).trapGroupCount
},
slotCells() {
const cells = []
for (let i = 0; i < this.slotLimit; i++) {
const card = this.slots[i]
cells.push({
key: 'slot_' + i,
icon: card ? (card.displayIcon || this.displayIcon(card.icon)) : ''
})
}
return cells
},
riskText() {
if (this.slots.length >= this.slotLimit - 1) return '危险:差1格就满'
if (this.slots.length >= this.slotLimit - 2) return '注意槽位'
return '先看上层再下手'
}
},
onLoad() {
const sys = uni.getSystemInfoSync()
this.statusBarHeight = sys.statusBarHeight || 20
this.userId = uni.getStorageSync('id') || ''
this.nickname = uni.getStorageSync('nickName') || uni.getStorageSync('nickname') || ''
this.avatar = uni.getStorageSync('avatarUrl') || uni.getStorageSync('avatar') || ''
try {
const area = uni.getStorageSync('area')
if (area) this.regionId = JSON.parse(area).id || ''
} catch (e) {}
this.college = uni.getStorageSync('planetCollege_' + this.regionId) || ''
this.loadColleges()
this.loadHome()
this.buildLevel()
},
methods: {
loadColleges() {
this.tui.request('/app/planet/college/list', 'POST', {
regionId: this.regionId
}, false, false, true).then(res => {
if (res.code == 200) {
this.colleges = res.result || []
const exists = this.colleges.some(item => item.collegeName === this.college)
if (!exists) {
this.college = ''
uni.removeStorageSync('planetCollege_' + this.regionId)
}
if (!this.college && this.colleges.length) {
this.collegeModal = true
}
}
})
},
loadHome() {
this.tui.request('/app/planet/adventure/home', 'POST', {
userId: this.userId,
regionId: this.regionId,
college: this.college
}, false, false, true).then(res => {
if (res.code == 200 && res.result) {
this.home = res.result
this.cleared = !res.result.freeAvailable
}
})
},
startLevel() {
if (this.cleared || this.playing) return
if (!this.college) {
this.openCollegePicker()
return
}
this.tui.request('/app/planet/adventure/start', 'POST', {
userId: this.userId,
regionId: this.regionId,
nickname: this.nickname,
avatar: this.avatar,
college: this.college
}).then(res => {
if (res.code != 200 || !res.result) {
this.tui.toast(res.message)
return
}
this.session = res.result
this.playing = true
this.startTs = Date.now()
this.buildLevel()
if (!this.cards.length) this.buildLevel()
})
},
buildLevel() {
const icons = ['🍔', '🥤', '🥟', '🏀', '🎧', '🚲', '🍜', '🔥', '☕', '🍢']
const garbageIcons = ['🚀', '🪐', '🛸']
let cards = []
for (let attempt = 0; attempt < 100; attempt++) {
const seed = this.levelSeed() + '_try_' + attempt
const positions = this.buildCardPositions(seed)
cards = this.assignTrapLayoutIcons(positions, icons, garbageIcons, seed)
if (cards.length && this.checkDifficultyMetrics(cards)) break
cards = []
}
if (!cards.length) {
cards = this.assignTrapLayoutIcons(this.buildCardPositions(this.levelSeed() + '_fallback'), icons, garbageIcons, this.levelSeed() + '_fallback', true)
}
if (!cards.length) {
cards = this.assignEmergencyIcons(this.buildCardPositions(this.levelSeed() + '_emergency'), icons, garbageIcons)
}
this.cards = cards
this.slots = []
this.moveCount = 0
this.normalizeCards(icons)
this.refreshCardState()
},
buildCardPositions(seed) {
const cards = []
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 zone = template.zones[layer]
const col = i % zone.cols
const row = Math.floor(i / zone.cols)
const dx = Math.floor(rand() * 6)
const dy = Math.floor(rand() * 6)
const card = {
id: 'c' + idx,
order: idx,
icon: '',
layer,
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,
locked: false,
className: '',
trap: false,
keyCard: false
}
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};`
cards.push(card)
idx++
}
})
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: 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 }
]
}
}
return templates[type] || templates.hell
},
assignTrapLayoutIcons(positions, icons, garbageIcons, seed, forceBuild) {
const rand = this.seededRandom(seed + '_layout')
const cards = positions.slice()
cards.forEach(card => {
card.icon = ''
card.trap = false
card.keyCard = false
card.garbage = 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')
const top = this.seededShuffle(cards.filter(card => card.layer === 2), seed + '_top')
const bottomCenter = this.seededShuffle(bottom.filter(card => this.isCenterCard(card)), seed + '_bottom_center')
const topOuter = this.seededShuffle(top.filter(card => !this.isCenterCard(card)), seed + '_top_outer')
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))
for (let group = 0; group < groupCount; group++) {
const icon = icons[group % icons.length]
let chosen = []
if (group < trapCount) {
chosen = [
this.takeNext(topOuter, assigned) || this.takeNext(top, assigned) || this.takeAny(cards, assigned),
this.takeNext(midOuter, assigned) || this.takeNext(middle, assigned) || this.takeAny(cards, assigned),
this.takeNext(bottomCenter, assigned) || this.takeNext(bottom, assigned) || this.takeAny(cards, assigned)
]
} else {
chosen = [
this.takeNext(top, assigned) || this.takeAny(cards, assigned),
this.takeNext(middle, assigned) || this.takeAny(cards, assigned),
this.takeNext(bottom, assigned) || this.takeAny(cards, assigned)
]
}
if (chosen.some(card => !card)) {
if (!forceBuild) return []
break
}
chosen.forEach((card, index) => {
card.icon = icon
card.trap = group < trapCount
card.keyCard = group < trapCount && index === 2
assigned[card.id] = true
})
}
this.repositionKeyCovers(cards, seed)
const freeCards = cards.filter(card => !assigned[card.id])
const garbageCards = this.seededShuffle(freeCards.slice(), seed + '_garbage').slice(0, 6)
garbageCards.forEach((card, index) => {
card.icon = garbageIcons[Math.floor(index / 2) % garbageIcons.length]
card.garbage = true
assigned[card.id] = true
})
cards.forEach((card, index) => {
if (!card.icon) {
card.icon = icons[(index + Math.floor(rand() * icons.length)) % icons.length]
}
card.displayIcon = this.displayIcon(card.icon)
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};`
})
return cards
},
assignEmergencyIcons(positions, icons, garbageIcons) {
positions.forEach((card, index) => {
card.icon = index < 6 ? garbageIcons[Math.floor(index / 2) % garbageIcons.length] : icons[Math.floor((index - 6) / 3) % icons.length]
card.displayIcon = this.displayIcon(card.icon)
card.trap = index >= 6 && index < 42
card.keyCard = card.trap && card.layer === 0 && this.isCenterCard(card)
card.garbage = index < 6
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};`
})
return positions
},
repositionKeyCovers(cards, seed) {
const rand = this.seededRandom(seed + '_cover')
const keys = cards.filter(card => card.keyCard && card.layer === 0 && this.isCenterCard(card)).slice(0, 10)
const covers = this.seededShuffle(cards.filter(card => !card.keyCard && card.layer > 0), seed + '_cover_cards')
keys.forEach(key => {
for (let i = 0; i < 2; i++) {
const cover = covers.shift()
if (!cover) return
cover.x = key.x + (i === 0 ? 18 : -4) + Math.floor(rand() * 8)
cover.y = key.y + (i === 0 ? 18 : -4) + Math.floor(rand() * 8)
cover.style = `left:${cover.x}rpx;top:${cover.y}rpx;z-index:${this.cardZIndex(cover)};`
}
})
},
takeNext(pool, assigned) {
while (pool.length) {
const card = pool.shift()
if (!assigned[card.id]) return card
}
return null
},
takeAny(cards, assigned) {
for (let i = 0; i < cards.length; i++) {
if (!assigned[cards[i].id]) return cards[i]
}
return null
},
checkDifficultyMetrics(cards) {
const stats = this.collectDifficultyStats(cards)
return stats.visibleTypeCount >= 8 &&
stats.directComboCount <= 2 &&
stats.trapGroupCount >= 10 &&
stats.deepKeyCount >= 10
},
collectDifficultyStats(cards) {
const visible = cards.filter(card => !this.isBlockedIn(card, cards))
const typeMap = {}
const countMap = {}
visible.forEach(card => {
typeMap[card.icon] = true
countMap[card.icon] = (countMap[card.icon] || 0) + 1
})
const trapMap = {}
let deepKeyCount = 0
cards.forEach(card => {
if (card.trap && card.icon) trapMap[card.icon] = true
if (card.keyCard && card.layer === 0 && this.coverCount(card, cards) >= 2) deepKeyCount++
})
return {
visibleTypeCount: Object.keys(typeMap).length,
directComboCount: Object.keys(countMap).filter(icon => countMap[icon] >= 3 && !this.isGarbageIcon(icon)).length,
trapGroupCount: Object.keys(trapMap).length,
deepKeyCount
}
},
coverCount(card, cards) {
return cards.filter(other => {
if (other.id === card.id || other.layer <= card.layer) return false
return this.isOverlap(card, other)
}).length
},
isGarbageIcon(icon) {
return icon === '🚀' || icon === '🪐' || icon === '🛸'
},
isCenterCard(card) {
return card.x >= 160 && card.x <= 400 && card.y >= 120 && card.y <= 440
},
isBlockedIn(card, list) {
return list.some(other => {
if (other.id === card.id || other.layer <= card.layer) return false
return this.isOverlap(card, other)
})
},
isOverlap(a, b) {
return Math.abs(a.x - b.x) < 74 && Math.abs(a.y - b.y) < 74
},
cardZIndex(card) {
return (card.layer || 0) * 100 + (card.order || 0) + 1
},
normalizeCards(icons) {
this.cards.forEach((card, index) => {
if (!card.icon) card.icon = icons[index % icons.length]
card.displayIcon = this.displayIcon(card.icon)
card.locked = false
card.className = ''
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};`
})
},
displayIcon(icon) {
if (!icon) return '?'
if (this.isGarbageIcon(icon)) return icon
return icon
},
refreshCardState() {
this.cards.forEach(card => {
const locked = !card.removed && !card.selected && this.isLocked(card)
card.locked = locked
card.className = [
locked ? 'locked' : '',
card.selected ? 'selected' : ''
].filter(Boolean).join(' ')
})
},
pickCard(card) {
if (this.cleared) {
this.tui.toast('今日已助推,明天再来')
return
}
if (!this.playing) {
this.tui.toast('先点击开始助推')
return
}
this.refreshCardState()
if (card.removed || card.selected || card.locked) {
this.tui.toast('这张还被压住,先消上面的牌')
return
}
if (this.slots.length >= this.slotLimit) return
card.displayIcon = this.displayIcon(card.icon)
card.selected = true
this.slots.push(card)
this.moveCount++
this.tryClear(card.icon)
this.refreshCardState()
if (this.cards.every(item => item.removed || item.selected)) {
this.finishClear()
} else if (this.slots.length >= this.slotLimit) {
this.failLevel()
}
},
tryClear(icon) {
if (this.isGarbageIcon(icon)) {
const garbage = this.slots.filter(item => item.icon === icon)
if (garbage.length < 2) return
let removedGarbage = 0
this.slots = this.slots.filter(item => {
if (item.icon === icon && removedGarbage < 2) {
item.removed = true
removedGarbage++
return false
}
return true
})
return
}
const same = this.slots.filter(item => item.icon === icon)
if (same.length < 3) return
let removed = 0
this.slots = this.slots.filter(item => {
if (item.icon === icon && removed < 3) {
item.removed = true
removed++
return false
}
return true
})
},
isLocked(card) {
return this.cards.some(other => {
if (other.removed || other.selected || other.layer <= card.layer) return false
return this.isOverlap(card, other)
})
},
finishClear() {
const duration = Math.max(20, Math.floor((Date.now() - this.startTs) / 1000))
const progress = Math.max(100, 260 - duration + Math.max(0, 80 - this.moveCount))
this.playing = false
this.tui.request('/app/planet/adventure/submit', 'POST', {
userId: this.userId,
regionId: this.regionId,
sessionId: this.session.sessionId,
normalCount: this.cards.length,
goldenCount: 1,
diamondCount: 0,
rainbowCount: 0,
ticketBagCount: 0,
propUseCount: 0,
hitCount: 0,
score: progress,
durationSeconds: duration
}).then(res => {
if (res.code == 200) {
this.cleared = true
this.modal = { show: true, title: '助推成功', sub: `${this.collegeName} 获得 ${progress} 点推进值,晚上21:30争夺学院荣誉。` }
this.loadHome()
} else {
this.tui.toast(res.message)
}
})
},
failLevel() {
this.playing = false
this.modal = { show: true, title: '槽位满了', sub: '本关失败,不消耗次数。换个顺序再推一次。' }
},
resetLevel() {
if (this.cleared) return
this.playing = false
this.buildLevel()
if (!this.cards.length) this.buildLevel()
},
closeModal() {
this.modal.show = false
},
openCollegePicker() {
this.collegeModal = true
},
closeCollegePicker() {
if (!this.college) {
this.tui.toast('请先选择学院')
return
}
this.collegeModal = false
this.loadHome()
},
selectCollege(item) {
this.college = item.collegeName
uni.setStorageSync('planetCollege_' + this.regionId, this.college)
},
levelSeed() {
const d = new Date()
const p = n => (n < 10 ? '0' + n : n)
return `${this.regionId}_${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_college_boost_v2`
},
seededRandom(seed) {
let h = 2166136261
for (let i = 0; i < seed.length; i++) {
h ^= seed.charCodeAt(i)
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)
}
return function() {
h += 0x6D2B79F5
let t = h
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
},
seededShuffle(arr, seed) {
const rand = this.seededRandom(seed)
return this.shuffleWithRandom(arr, rand)
},
shuffleWithRandom(arr, rand) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1))
const t = arr[i]
arr[i] = arr[j]
arr[j] = t
}
return arr
},
buildFallbackPositions() {
const cards = []
let idx = 0
for (let layer = 0; layer < 3; layer++) {
for (let i = 0; i < 27; i++) {
const col = i % 9
const row = Math.floor(i / 9)
const card = {
id: 'f' + idx,
order: idx,
icon: '',
layer,
x: 10 + col * 68 + (layer * 6),
y: 28 + row * 94 + (layer * 34),
style: '',
removed: false,
selected: false,
locked: false,
className: '',
trap: false,
keyCard: false
}
card.style = `left:${card.x}rpx;top:${card.y}rpx;z-index:${this.cardZIndex(card)};`
cards.push(card)
idx++
}
}
return cards
},
goBack() {
uni.navigateBack()
}
}
}
</script>
<style lang="scss" scoped>
.boost { min-height: 100vh; background: linear-gradient(155deg, #F3FFF4 0%, #EAF8FF 46%, #FFF7DE 100%); color: #12342F; }
.nav { position: fixed; top: 0; left: 0; right: 0; height: 44px; z-index: 200; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 8rpx; background: rgba(243,255,244,0.86); }
.nav-back { position: absolute; left: 20rpx; bottom: 0; width: 70rpx; height: 44px; display: flex; align-items: center; justify-content: center; color: #12342F; font-size: 54rpx; }
.nav-title { color: #12342F; font-size: 34rpx; font-weight: 900; }
.page { height: 100vh; box-sizing: border-box; padding-left: 24rpx; padding-right: 24rpx; }
.hero, .rank-card, .game-card { margin-bottom: 24rpx; border-radius: 42rpx; background: rgba(255,255,255,0.82); padding: 30rpx; box-shadow: 0 16rpx 42rpx rgba(79,183,255,0.10); }
.hero-kicker { color: #FF9C42; font-size: 22rpx; font-weight: 900; letter-spacing: 2rpx; }
.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; }
.rocket-body { font-size: 56rpx; }
.rocket-line { flex: 1; height: 18rpx; border-radius: 999rpx; background: #E6F2EF; overflow: hidden; }
.rocket-progress { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #35D6A6, #4FB7FF, #FFB84D); }
.section-head { display: flex; justify-content: space-between; align-items: center; font-size: 24rpx; color: #6B817D; }
.section-head text:first-child { color: #12342F; font-size: 31rpx; font-weight: 900; }
.start-strip { margin-top: 20rpx; padding: 20rpx 24rpx; border-radius: 28rpx; background: linear-gradient(135deg, #35D6A6, #4FB7FF); color: #fff; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 14rpx 34rpx rgba(53,214,166,0.2); }
.start-strip text:first-child { font-size: 30rpx; font-weight: 900; }
.start-strip text:last-child { font-size: 22rpx; opacity: .88; }
.start-strip.done { background: rgba(255,255,255,0.8); color: #6B817D; box-shadow: none; }
.college-row { margin-top: 18rpx; display: flex; align-items: center; gap: 16rpx; }
.medal { width: 48rpx; height: 48rpx; line-height: 48rpx; border-radius: 50%; text-align: center; background: #FFF1C7; color: #B87932; font-weight: 900; }
.college-main { flex: 1; min-width: 0; }
.college-main text { font-size: 25rpx; font-weight: 800; }
.bar { margin-top: 10rpx; height: 12rpx; background: #EEF7F4; border-radius: 999rpx; overflow: hidden; }
.bar view { height: 100%; border-radius: 999rpx; background: linear-gradient(90deg, #35D6A6, #4FB7FF); }
.score { width: 92rpx; text-align: right; color: #22B889; font-size: 28rpx; font-weight: 900; }
.empty { margin-top: 18rpx; color: #8AA09C; font-size: 24rpx; }
.board { margin-top: 22rpx; height: 620rpx; border-radius: 36rpx; background: linear-gradient(155deg, rgba(234,248,255,0.9), rgba(255,248,222,0.72)); position: relative; overflow: hidden; z-index: 1; }
.tile { position: absolute; width: 76rpx; height: 76rpx; border-radius: 22rpx; background: #fff; display: flex; align-items: center; justify-content: center; box-shadow: 0 10rpx 22rpx rgba(66,99,94,0.12); border: 2rpx solid rgba(255,255,255,0.9); transition: transform .12s ease, opacity .12s ease; }
.tile text { font-size: 42rpx; }
.tile.locked {
opacity:.42
}
.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; }
.slots { display: flex; gap: 10rpx; }
.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; }
.main-btn { background: linear-gradient(135deg, #35D6A6, #4FB7FF); color: #fff; box-shadow: 0 14rpx 34rpx rgba(53,214,166,0.2); }
.main-btn.disabled { opacity: .45; }
.modal { position: fixed; inset: 0; z-index: 9999; background: rgba(18,52,47,0.2); display: flex; align-items: center; justify-content: center; padding: 46rpx; }
.modal-card { width: 100%; border-radius: 42rpx; background: #fff; padding: 42rpx; text-align: center; }
.modal-title { font-size: 40rpx; font-weight: 900; }
.modal-sub { margin: 18rpx 0 30rpx; color: #6B817D; font-size: 26rpx; line-height: 1.55; }
.college-modal { text-align: left; }
.college-modal .modal-title, .college-modal .modal-sub { text-align: center; }
.college-list { max-height: 560rpx; margin-bottom: 24rpx; }
.college-option { margin-bottom: 14rpx; padding: 20rpx; border-radius: 26rpx; background: #F5FBFA; display: flex; align-items: center; gap: 18rpx; border: 2rpx solid transparent; }
.college-option.active { border-color: #35D6A6; background: #EEFFF9; }
.college-option > text { font-size: 42rpx; width: 56rpx; text-align: center; }
.college-option view { display: flex; flex-direction: column; }
.college-option view text:first-child { color: #12342F; font-size: 28rpx; font-weight: 900; }
.college-option view text:last-child { color: #6B817D; font-size: 22rpx; margin-top: 6rpx; }
</style>