Files
wechat-minigame/js/main.js
2026-03-29 00:49:03 +08:00

954 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createContentSystem, createDefaultPlayerState } from './content/index.js'
import { createSceneStore } from './ui/scene-store.js'
const {
windowWidth,
windowHeight,
pixelRatio = 1,
} = wx.getSystemInfoSync()
const canvas = wx.createCanvas()
canvas.width = windowWidth * pixelRatio
canvas.height = windowHeight * pixelRatio
const ctx = canvas.getContext('2d')
ctx.scale(pixelRatio, pixelRatio)
ctx.textBaseline = 'middle'
const STORAGE_KEY = 'player_state'
const contentSystem = createContentSystem()
const playerState = loadPlayerState()
const sceneStore = createSceneStore({ contentSystem, playerState })
const hitTargets = []
let transientMessage = ''
let transientMessageUntil = 0
function loadPlayerState() {
try {
const stored = wx.getStorageSync(STORAGE_KEY)
if (!stored) {
return createDefaultPlayerState()
}
return {
...createDefaultPlayerState(),
...stored,
inventory: {
...createDefaultPlayerState().inventory,
...(stored.inventory ?? {}),
},
settings: {
...createDefaultPlayerState().settings,
...(stored.settings ?? {}),
},
stats: {
...createDefaultPlayerState().stats,
...(stored.stats ?? {}),
},
cityTeam: {
...createDefaultPlayerState().cityTeam,
...(stored.cityTeam ?? {}),
},
}
} catch {
return createDefaultPlayerState()
}
}
function savePlayerState() {
try {
wx.setStorageSync(STORAGE_KEY, playerState)
} catch {
// Ignore storage write failures in the MVP shell.
}
}
function triggerInviteShare() {
playerState.stats.totalShareCount += 1
savePlayerState()
if (typeof wx.shareAppMessage === 'function') {
try {
wx.shareAppMessage({
title: '一起来抓猫猫!',
query: 'from=invite',
})
showTransientMessage('已打开分享卡片')
return
} catch {
// Fall through to local feedback.
}
}
showTransientMessage('分享入口已触发,真机再校验分享卡片')
}
function drawRoundedRect(x, y, width, height, radius) {
const clampedRadius = Math.min(radius, width / 2, height / 2)
ctx.beginPath()
ctx.moveTo(x + clampedRadius, y)
ctx.lineTo(x + width - clampedRadius, y)
ctx.arcTo(x + width, y, x + width, y + clampedRadius, clampedRadius)
ctx.lineTo(x + width, y + height - clampedRadius)
ctx.arcTo(x + width, y + height, x + width - clampedRadius, y + height, clampedRadius)
ctx.lineTo(x + clampedRadius, y + height)
ctx.arcTo(x, y + height, x, y + height - clampedRadius, clampedRadius)
ctx.lineTo(x, y + clampedRadius)
ctx.arcTo(x, y, x + clampedRadius, y, clampedRadius)
ctx.closePath()
}
function drawBackground() {
const gradient = ctx.createLinearGradient(0, 0, windowWidth, windowHeight)
gradient.addColorStop(0, '#fff7ef')
gradient.addColorStop(1, '#ffe1d6')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, windowWidth, windowHeight)
}
function registerHitTarget(target) {
hitTargets.push(target)
}
function resetHitTargets() {
hitTargets.length = 0
}
function showTransientMessage(message, durationMs = 1800) {
transientMessage = message
transientMessageUntil = Date.now() + durationMs
}
function hashStringToHue(value) {
let hash = 0
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) % 360
}
return hash
}
function getElementStyle(elementId) {
const hue = hashStringToHue(elementId)
return {
fill: `hsl(${hue} 82% 87%)`,
border: `hsl(${hue} 58% 52%)`,
}
}
function hitTest(x, y) {
for (let index = hitTargets.length - 1; index >= 0; index -= 1) {
const target = hitTargets[index]
if (
x >= target.x
&& x <= target.x + target.width
&& y >= target.y
&& y <= target.y + target.height
) {
return target
}
}
return null
}
function drawHeader(title, subtitle) {
ctx.fillStyle = '#231f2a'
ctx.font = 'bold 30px sans-serif'
ctx.textAlign = 'left'
ctx.fillText(title, 24, 36)
if (subtitle) {
ctx.fillStyle = '#6b6474'
ctx.font = '15px sans-serif'
ctx.fillText(subtitle, 24, 64)
}
}
function drawButton({ x, y, width, height, label, onTap, fillStyle = '#231f2a', textColor = '#ffffff' }) {
ctx.fillStyle = fillStyle
drawRoundedRect(x, y, width, height, 14)
ctx.fill()
ctx.fillStyle = textColor
ctx.font = 'bold 16px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(label, x + width / 2, y + height / 2)
if (onTap) {
registerHitTarget({ x, y, width, height, onTap })
}
}
function drawBackButton(onTap) {
drawButton({
x: 24,
y: windowHeight - 58,
width: 88,
height: 38,
label: '返回',
onTap,
fillStyle: '#ffffff',
textColor: '#231f2a',
})
}
function drawSidebarCard({ x, y, width, height, titleLines, subtitle, onTap, accentColor = '#231f2a' }) {
ctx.fillStyle = '#ffffff'
ctx.strokeStyle = accentColor
ctx.lineWidth = 2
drawRoundedRect(x, y, width, height, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = accentColor
ctx.font = 'bold 14px sans-serif'
ctx.textAlign = 'center'
titleLines.forEach((line, index) => {
ctx.fillText(line, x + width / 2, y + 34 + index * 18)
})
if (subtitle) {
ctx.fillStyle = '#6b6474'
ctx.font = '11px sans-serif'
ctx.fillText(subtitle, x + width / 2, y + height - 20)
}
if (onTap) {
registerHitTarget({ x, y, width, height, onTap })
}
}
function drawHomeScene() {
const playerStateSnapshot = sceneStore.getPlayerState()
drawHeader('城市抓猫猫', '世界主页面 · 3×3 入口')
const homeTiles = contentSystem.getHomeTiles(playerStateSnapshot)
const playerGiftAlbums = contentSystem.getGiftAlbums(playerStateSnapshot)
const teamCity = playerStateSnapshot.cityTeam?.teamCityId
? contentSystem.getCity(playerStateSnapshot.cityTeam.teamCityId)
: null
const totalGiftCount = playerGiftAlbums.reduce((sum, album) => sum + album.collectedCount, 0)
const sidebarWidth = Math.max(58, Math.min(70, Math.floor(windowWidth * 0.18)))
const sidebarGap = 8
const centerX = 24 + sidebarWidth + sidebarGap
const centerWidth = windowWidth - 48 - sidebarWidth * 2 - sidebarGap * 2
const columns = 3
const cardWidth = (centerWidth - 12 * (columns - 1)) / columns
const cardHeight = Math.min(118, Math.max(96, Math.floor(windowHeight * 0.15)))
const startY = 96
drawSidebarCard({
x: 24,
y: startY,
width: sidebarWidth,
height: cardHeight,
titleLines: ['邀请', '好友'],
subtitle: '分享卡片',
accentColor: '#FF8A65',
onTap() {
triggerInviteShare()
render()
},
})
drawSidebarCard({
x: 24,
y: startY + cardHeight + 12,
width: sidebarWidth,
height: cardHeight,
titleLines: ['开房', 'PK'],
subtitle: 'V1.1+',
accentColor: '#7E57C2',
onTap() {
showTransientMessage('开房间 PK 需要实时同步V1.1+ 接入')
render()
},
})
drawSidebarCard({
x: windowWidth - 24 - sidebarWidth,
y: startY,
width: sidebarWidth,
height: cardHeight,
titleLines: ['礼物', '区'],
subtitle: `${totalGiftCount} 个收藏`,
accentColor: '#26A69A',
onTap() {
sceneStore.openGiftZone()
render()
},
})
drawSidebarCard({
x: windowWidth - 24 - sidebarWidth,
y: startY + cardHeight + 12,
width: sidebarWidth,
height: cardHeight,
titleLines: ['排行', '榜'],
subtitle: teamCity ? `${teamCity.display.name} 战队` : '选择战队',
accentColor: '#42A5F5',
onTap() {
sceneStore.openCityTeamSelect()
render()
},
})
homeTiles.forEach((tile, index) => {
const row = Math.floor(index / columns)
const column = index % columns
const x = centerX + column * (cardWidth + 12)
const y = startY + row * (cardHeight + 12)
const isLocked = !tile.isUnlocked && tile.id !== 'coming_soon'
ctx.fillStyle = tile.isUnlocked ? '#ffffff' : '#f0ebe5'
ctx.strokeStyle = tile.themeColor
ctx.lineWidth = 2
drawRoundedRect(x, y, cardWidth, cardHeight, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = tile.themeColor
ctx.beginPath()
ctx.arc(x + cardWidth / 2, y + 34, 22, 0, Math.PI * 2)
ctx.fill()
ctx.fillStyle = '#ffffff'
ctx.font = 'bold 16px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(String(index + 1), x + cardWidth / 2, y + 34)
ctx.fillStyle = '#231f2a'
ctx.font = tile.name.length >= 5 ? 'bold 13px sans-serif' : 'bold 16px sans-serif'
ctx.fillText(tile.name, x + cardWidth / 2, y + 76)
ctx.fillStyle = '#6b6474'
ctx.font = '12px sans-serif'
let subtitle = '后续开放'
if (tile.id === 'asia') {
subtitle = 'MVP 已开放'
} else if (tile.id === 'mashup') {
subtitle = tile.isUnlocked ? '随机混搭' : '通关 2 城解锁'
} else if (tile.id === 'coming_soon') {
subtitle = '新玩法预告'
} else if (!isLocked) {
subtitle = '可进入'
}
ctx.fillText(subtitle, x + cardWidth / 2, y + 100)
if (isLocked) {
ctx.fillStyle = '#8d8694'
ctx.font = 'bold 12px sans-serif'
ctx.fillText('LOCK', x + cardWidth / 2, y + 116)
}
registerHitTarget({
x,
y,
width: cardWidth,
height: cardHeight,
onTap() {
const result = sceneStore.openHomeTile(tile.id)
if (!result.opened) {
if (result.reason === 'locked') {
showTransientMessage(tile.id === 'mashup' ? '通关 2 个城市后解锁主题大混战' : `${tile.name} 后续开放`)
} else if (result.reason === 'coming-soon') {
showTransientMessage('新玩法即将上线,敬请期待!')
} else if (result.reason === 'mode-unavailable') {
showTransientMessage('主题大混战入口已预留,玩法实现下一步接入')
} else {
showTransientMessage('该入口暂不可用')
}
}
render()
},
})
})
}
function drawCitySelectScene(scene) {
drawHeader('城市抓猫猫', '亚洲城市页 · 点击已解锁城市')
const cityCards = contentSystem.listCityCards(scene.continentId, sceneStore.getPlayerState())
const columns = 3
const cardWidth = (windowWidth - 24 * 2 - 12 * (columns - 1)) / columns
const cardHeight = 128
const startY = 96
cityCards.forEach((card, index) => {
const row = Math.floor(index / columns)
const column = index % columns
const x = 24 + column * (cardWidth + 12)
const y = startY + row * (cardHeight + 12)
ctx.fillStyle = card.isUnlocked ? '#ffffff' : '#ebe5df'
ctx.strokeStyle = card.isUnlocked ? card.bgColor : '#d3ccc4'
ctx.lineWidth = 2
drawRoundedRect(x, y, cardWidth, cardHeight, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = card.bgColor
ctx.beginPath()
ctx.arc(x + cardWidth / 2, y + 34, 22, 0, Math.PI * 2)
ctx.fill()
ctx.fillStyle = '#231f2a'
ctx.font = 'bold 18px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(card.name, x + cardWidth / 2, y + 76)
ctx.fillStyle = '#6b6474'
ctx.font = '13px sans-serif'
ctx.fillText(
card.isUnlocked ? `${card.completedLevels}/${card.totalLevels}` : '未解锁',
x + cardWidth / 2,
y + 100,
)
if (card.isUnlocked) {
registerHitTarget({
x,
y,
width: cardWidth,
height: cardHeight,
onTap() {
sceneStore.openCity(card.cityId)
render()
},
})
}
})
}
function drawLevelSelectScene(scene) {
const city = contentSystem.getCity(scene.cityId)
drawHeader(city.display.name, `${city.display.tagline} · 选择关卡`)
const columns = 2
const cardWidth = (windowWidth - 24 * 2 - 14) / columns
const cardHeight = 100
const startY = 104
scene.levels.forEach((level, index) => {
const row = Math.floor(index / columns)
const column = index % columns
const x = 24 + column * (cardWidth + 14)
const y = startY + row * (cardHeight + 14)
ctx.fillStyle = level.isUnlocked ? '#ffffff' : '#ebe5df'
ctx.strokeStyle = level.isCompleted ? city.display.bgColor : '#d7d0c8'
ctx.lineWidth = 2
drawRoundedRect(x, y, cardWidth, cardHeight, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = level.isCompleted ? city.display.bgColor : '#231f2a'
ctx.font = 'bold 22px sans-serif'
ctx.textAlign = 'left'
ctx.fillText(`关卡 ${level.levelId}`, x + 18, y + 28)
ctx.fillStyle = '#6b6474'
ctx.font = '14px sans-serif'
ctx.fillText(
level.isUnlocked ? (level.isCompleted ? `${'★'.repeat(level.stars || 3)}` : '点击开始') : '未解锁',
x + 18,
y + 64,
)
if (level.isUnlocked) {
registerHitTarget({
x,
y,
width: cardWidth,
height: cardHeight,
onTap() {
sceneStore.openLevel(scene.cityId, level.levelId)
render()
},
})
}
})
drawBackButton(() => {
sceneStore.goBack()
render()
})
}
function drawGiftZoneScene(scene) {
const playerStateSnapshot = sceneStore.getPlayerState()
const albums = contentSystem.getGiftAlbums(playerStateSnapshot)
const entries = contentSystem.getGiftAlbumEntries(scene.selectedTab, playerStateSnapshot)
const teamCity = playerStateSnapshot.cityTeam?.teamCityId
? contentSystem.getCity(playerStateSnapshot.cityTeam.teamCityId)
: null
drawHeader('城市主题礼物区', 'MVP 收集册 · 冰箱贴 / 邮票 / 猫猫')
const summaryX = 24
const summaryY = 92
const summaryWidth = windowWidth - 48
const summaryHeight = 52
ctx.fillStyle = '#ffffff'
ctx.strokeStyle = '#d7d0c8'
ctx.lineWidth = 2
drawRoundedRect(summaryX, summaryY, summaryWidth, summaryHeight, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = '#231f2a'
ctx.font = 'bold 14px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(
albums.map((album) => `${album.name} ${album.collectedCount}/${album.totalCount}`).join(' · '),
summaryX + summaryWidth / 2,
summaryY + summaryHeight / 2,
)
const teamCardY = 156
const teamCardHeight = 58
ctx.fillStyle = '#ffffff'
ctx.strokeStyle = teamCity ? teamCity.display.bgColor : '#8aa3ff'
ctx.lineWidth = 2
drawRoundedRect(summaryX, teamCardY, summaryWidth, teamCardHeight, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = '#231f2a'
ctx.font = 'bold 15px sans-serif'
ctx.textAlign = 'left'
ctx.fillText(
teamCity ? `${teamCity.display.name} 战队` : '还没加入城市战队',
summaryX + 16,
teamCardY + 22,
)
ctx.fillStyle = '#6b6474'
ctx.font = '12px sans-serif'
ctx.fillText(
teamCity ? '本地已锁定,排行榜继续占位' : '先选已解锁城市,本地状态先走通',
summaryX + 16,
teamCardY + 42,
)
drawButton({
x: summaryX + summaryWidth - 116,
y: teamCardY + 12,
width: 100,
height: 34,
label: teamCity ? '查看战队' : '选择战队',
onTap() {
sceneStore.openCityTeamSelect()
render()
},
fillStyle: teamCity ? teamCity.display.bgColor : '#231f2a',
textColor: '#ffffff',
})
const tabGap = 12
const tabWidth = (windowWidth - 24 * 2 - tabGap * (albums.length - 1)) / albums.length
const tabsY = 228
albums.forEach((album, index) => {
drawButton({
x: 24 + index * (tabWidth + 12),
y: tabsY,
width: tabWidth,
height: 42,
label: album.name,
fillStyle: scene.selectedTab === album.id ? '#231f2a' : '#ffffff',
textColor: scene.selectedTab === album.id ? '#ffffff' : '#231f2a',
onTap() {
sceneStore.selectGiftTab(album.id)
render()
},
})
})
const columns = 2
const cardWidth = (windowWidth - 24 * 2 - 14) / columns
const cardHeight = 82
const startY = 286
entries.forEach((entry, index) => {
const row = Math.floor(index / columns)
const column = index % columns
const x = 24 + column * (cardWidth + 14)
const y = startY + row * (cardHeight + 12)
ctx.fillStyle = '#ffffff'
ctx.strokeStyle = entry.themeColor
ctx.lineWidth = 2
drawRoundedRect(x, y, cardWidth, cardHeight, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = entry.themeColor
ctx.font = 'bold 17px sans-serif'
ctx.textAlign = 'left'
ctx.fillText(entry.cityName, x + 16, y + 24)
ctx.fillStyle = '#6b6474'
ctx.font = '13px sans-serif'
if (scene.selectedTab === 'magnets') {
ctx.fillText(`冰箱贴 ${entry.collectedCount}/${entry.totalCount}`, x + 16, y + 52)
} else if (scene.selectedTab === 'stamps') {
ctx.fillText(entry.isCollected ? '邮票已收集' : '邮票未收集', x + 16, y + 52)
} else {
ctx.fillText(entry.isCollected ? `${entry.catName} 已收集` : '通关城市后收集', x + 16, y + 52)
}
})
drawBackButton(() => {
sceneStore.goBack()
render()
})
}
function drawCityTeamSelectScene(scene) {
const isLocked = Boolean(scene.selectedTeamCityId)
const selectedCity = isLocked ? contentSystem.getCity(scene.selectedTeamCityId) : null
drawHeader(
'选择城市战队',
selectedCity ? `当前所属 ${selectedCity.display.name} 战队 · 本地先锁定` : '从已解锁城市里选择一个战队',
)
const columns = 2
const cardWidth = (windowWidth - 24 * 2 - 14) / columns
const cardHeight = 94
const startY = 104
scene.options.forEach((entry, index) => {
const row = Math.floor(index / columns)
const column = index % columns
const x = 24 + column * (cardWidth + 14)
const y = startY + row * (cardHeight + 12)
const isSelected = scene.selectedTeamCityId === entry.cityId
ctx.fillStyle = '#ffffff'
ctx.strokeStyle = entry.themeColor
ctx.lineWidth = 2
drawRoundedRect(x, y, cardWidth, cardHeight, 16)
ctx.fill()
ctx.stroke()
ctx.fillStyle = entry.themeColor
ctx.font = 'bold 18px sans-serif'
ctx.textAlign = 'left'
ctx.fillText(entry.cityName, x + 16, y + 24)
ctx.fillStyle = '#6b6474'
ctx.font = '13px sans-serif'
ctx.fillText(`猫猫 ${entry.catName}`, x + 16, y + 48)
ctx.fillText(isSelected ? '已加入,当前不可更换' : '可加入本地战队', x + 16, y + 72)
registerHitTarget({
x,
y,
width: cardWidth,
height: cardHeight,
onTap() {
const selected = sceneStore.chooseCityTeam(entry.cityId)
if (selected) {
savePlayerState()
showTransientMessage(`已加入 ${entry.cityName} 战队`)
} else if (isLocked) {
showTransientMessage('当前版本先锁定首次选队')
} else {
showTransientMessage('只能选择已解锁城市')
}
render()
},
})
})
drawBackButton(() => {
sceneStore.goBack()
render()
})
}
function drawGameplayScene(scene) {
const isMashup = scene.mode === 'mashup'
const city = isMashup ? null : contentSystem.getCity(scene.cityId)
const session = scene.session
const boardState = session.getBoardState()
const state = session.getState()
const clickableIds = new Set(session.getClickablePieces().map((piece) => piece.id))
const activePieces = boardState.pieces
.filter((piece) => !piece.removed)
.sort((left, right) => left.layer - right.layer)
const elementNameMap = isMashup
? new Map(scene.elementDefinitions.map((element) => [element.elementId, element.name]))
: new Map(city.elements.map((element) => [element.id, element.name]))
const title = isMashup ? scene.title : `${city.display.name} · 关卡 ${scene.levelId}`
const subtitle = isMashup
? `${scene.subtitle ?? '随机混搭'} · ${state.slot.length}/7 槽位`
: `${state.slot.length}/7 槽位`
drawHeader(title, subtitle)
drawButton({
x: windowWidth - 112,
y: 22,
width: 88,
height: 34,
label: '重开',
onTap() {
sceneStore.restartCurrentLevel()
render()
},
fillStyle: '#ffffff',
textColor: '#231f2a',
})
activePieces.forEach((piece) => {
const style = getElementStyle(piece.elementId)
const isClickable = clickableIds.has(piece.id)
ctx.fillStyle = style.fill
ctx.strokeStyle = isClickable ? style.border : '#c9c1ba'
ctx.lineWidth = isClickable ? 2 : 1
ctx.globalAlpha = isClickable ? 1 : 0.5
drawRoundedRect(piece.x, piece.y, piece.width, piece.height, 12)
ctx.fill()
ctx.stroke()
ctx.globalAlpha = 1
ctx.fillStyle = '#231f2a'
ctx.font = 'bold 12px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(
(elementNameMap.get(piece.elementId) ?? '').slice(0, 2),
piece.x + piece.width / 2,
piece.y + piece.height / 2,
)
if (isClickable && state.status === 'playing') {
registerHitTarget({
x: piece.x,
y: piece.y,
width: piece.width,
height: piece.height,
onTap() {
const result = session.pickPiece(piece.id)
if (result.status === 'won') {
sceneStore.completeGameplayVictory(3)
savePlayerState()
}
render()
},
})
}
})
const slotY = windowHeight - 126
const slotWidth = (windowWidth - 24 * 2 - 6 * 8) / 7
for (let index = 0; index < 7; index += 1) {
const x = 24 + index * (slotWidth + 8)
const slotEntry = state.slot[index]
ctx.fillStyle = '#ffffff'
ctx.strokeStyle = '#d5cec7'
ctx.lineWidth = 2
drawRoundedRect(x, slotY, slotWidth, 54, 12)
ctx.fill()
ctx.stroke()
if (slotEntry) {
const label = elementNameMap.get(slotEntry.elementId) ?? ''
const style = getElementStyle(slotEntry.elementId)
ctx.fillStyle = style.border
ctx.font = 'bold 12px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(label.slice(0, 2), x + slotWidth / 2, slotY + 27)
}
}
drawBackButton(() => {
sceneStore.goBack()
render()
})
if (state.status === 'won' || state.status === 'failed') {
drawResultOverlay(scene, state.status, isMashup ? scene.accentColor : city.display.bgColor)
}
}
function drawResultOverlay(scene, status, accentColor) {
const isMashup = scene.mode === 'mashup'
const rewardSummary = scene.rewardSummary
const title = status === 'won' ? '通关成功' : '挑战失败'
let detail = '可以重开当前关卡,继续测试核心循环'
if (status === 'won' && isMashup) {
if (rewardSummary?.type === 'magnet') {
const rewardCity = contentSystem.getCity(rewardSummary.cityId)
detail = `奖励:${rewardCity?.display.name ?? rewardSummary.cityId} 冰箱贴 ${rewardSummary.levelId}`
} else if (rewardSummary?.type === 'inventory') {
detail = '奖励Shuffle +1'
} else {
detail = '已结算本地混战奖励'
}
} else if (status === 'won') {
detail = '已记录进度,返回关卡页继续'
}
ctx.fillStyle = 'rgba(35, 31, 42, 0.45)'
ctx.fillRect(0, 0, windowWidth, windowHeight)
const boxWidth = windowWidth - 48
const boxHeight = 180
const x = 24
const y = (windowHeight - boxHeight) / 2
ctx.fillStyle = '#ffffff'
ctx.strokeStyle = accentColor
ctx.lineWidth = 2
drawRoundedRect(x, y, boxWidth, boxHeight, 20)
ctx.fill()
ctx.stroke()
ctx.fillStyle = '#231f2a'
ctx.font = 'bold 28px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(title, x + boxWidth / 2, y + 48)
ctx.fillStyle = '#6b6474'
ctx.font = '15px sans-serif'
ctx.fillText(detail, x + boxWidth / 2, y + 86)
drawButton({
x: x + 20,
y: y + 120,
width: (boxWidth - 50) / 2,
height: 42,
label: status === 'won' ? (isMashup ? '返回主页' : '返回关卡') : '返回',
onTap() {
sceneStore.goBack()
render()
},
fillStyle: '#ffffff',
textColor: '#231f2a',
})
drawButton({
x: x + 30 + (boxWidth - 50) / 2,
y: y + 120,
width: (boxWidth - 50) / 2,
height: 42,
label: status === 'won' ? (isMashup ? '再来一局' : '下一步再做') : '重新挑战',
onTap() {
if (status === 'won') {
if (isMashup) {
sceneStore.restartCurrentLevel()
} else {
sceneStore.goBack()
}
} else {
sceneStore.restartCurrentLevel()
}
render()
},
fillStyle: '#231f2a',
textColor: '#ffffff',
})
}
function drawTransientMessage() {
if (!transientMessage || transientMessageUntil <= Date.now()) {
transientMessage = ''
transientMessageUntil = 0
return
}
const width = windowWidth - 48
const height = 44
const x = 24
const y = windowHeight - 116
ctx.fillStyle = 'rgba(35, 31, 42, 0.88)'
drawRoundedRect(x, y, width, height, 14)
ctx.fill()
ctx.fillStyle = '#ffffff'
ctx.font = '14px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(transientMessage, x + width / 2, y + height / 2)
}
function render() {
resetHitTargets()
drawBackground()
const scene = sceneStore.getScene()
if (scene.type === 'home-select') {
drawHomeScene()
drawTransientMessage()
return
}
if (scene.type === 'city-select') {
drawCitySelectScene(scene)
drawTransientMessage()
return
}
if (scene.type === 'level-select') {
drawLevelSelectScene(scene)
drawTransientMessage()
return
}
if (scene.type === 'gift-zone') {
drawGiftZoneScene(scene)
drawTransientMessage()
return
}
if (scene.type === 'city-team-select') {
drawCityTeamSelectScene(scene)
drawTransientMessage()
return
}
if (scene.type === 'gameplay') {
drawGameplayScene(scene)
drawTransientMessage()
}
}
function handleTouchStart(event) {
const touch = event.touches?.[0]
if (!touch) {
return
}
const target = hitTest(touch.clientX, touch.clientY)
if (target) {
target.onTap()
}
}
if (typeof wx.onTouchStart === 'function') {
wx.onTouchStart(handleTouchStart)
}
render()