feat: add gift zone city team and mashup mode

This commit is contained in:
manpengan
2026-03-29 00:49:03 +08:00
parent 5c2b4f40f9
commit 92bf1f5070
12 changed files with 862 additions and 60 deletions

View File

@@ -44,6 +44,7 @@ export function createContentSystem() {
const giftAlbumDefinitions = [
{ id: 'magnets', name: '冰箱贴册' },
{ id: 'stamps', name: '邮票册' },
{ id: 'cats', name: '猫猫册' },
]
function getCompletedCityCount(playerState) {
@@ -115,6 +116,35 @@ export function createContentSystem() {
})
}
function getCatEntries(playerState) {
const collectedCats = new Set(playerState.collectedCats ?? [])
return cityRegistry.getAllCities().map((city) => ({
cityId: city.id,
cityName: city.display.name,
cityNameEn: city.display.nameEn,
themeColor: city.display.bgColor,
catId: city.cat.id,
catName: city.cat.name,
catThumb: city.cover.catThumb,
isCollected: collectedCats.has(city.id),
}))
}
function summarizeAlbumEntries(albumId, entries) {
if (albumId === 'magnets') {
return {
collectedCount: entries.reduce((sum, entry) => sum + entry.collectedCount, 0),
totalCount: entries.reduce((sum, entry) => sum + entry.totalCount, 0),
}
}
return {
collectedCount: entries.filter((entry) => entry.isCollected).length,
totalCount: entries.length,
}
}
function getGiftAlbumEntries(albumId, playerState) {
if (albumId === 'magnets') {
return getMagnetEntries(playerState)
@@ -124,23 +154,22 @@ export function createContentSystem() {
return getStampEntries(playerState)
}
if (albumId === 'cats') {
return getCatEntries(playerState)
}
return []
}
function getGiftAlbums(playerState) {
return giftAlbumDefinitions.map((definition) => {
const entries = getGiftAlbumEntries(definition.id, playerState)
const collectedCount = definition.id === 'magnets'
? entries.reduce((sum, entry) => sum + entry.collectedCount, 0)
: entries.filter((entry) => entry.isCollected).length
const totalCount = definition.id === 'magnets'
? entries.reduce((sum, entry) => sum + entry.totalCount, 0)
: entries.length
const summary = summarizeAlbumEntries(definition.id, entries)
return {
...definition,
collectedCount,
totalCount,
collectedCount: summary.collectedCount,
totalCount: summary.totalCount,
}
})
}

View File

@@ -105,6 +105,38 @@ function placePieces(piecePool, levelPreset, rng) {
return placedPieces
}
export function generateBoardFromDefinition({
boardId,
cityId,
levelId,
seed,
elements,
levelPreset,
extraState = {},
}) {
const effectiveSeed = seed ?? levelPreset.seedBase
const rng = createRng(effectiveSeed)
const selectedElements = [...elements]
const counts = normalizePiecesPerElement(levelPreset.piecesPerElement, levelPreset.elementCount)
const piecePool = createPiecePool(selectedElements, counts)
const pieces = placePieces(shuffleWithRng(piecePool, rng), levelPreset, rng)
const overlapGraph = buildOverlapGraph(pieces)
const boardState = {
boardId: boardId ?? `${cityId}-${levelId}-${effectiveSeed}`,
cityId,
levelId,
seed: effectiveSeed,
pieces,
overlapGraph,
metrics: {},
...extraState,
}
boardState.metrics = evaluateBoard(boardState, levelPreset)
return boardState
}
export function generateBoard({ cityId, levelId, seed, contentSystem }) {
const city = contentSystem.getCity(cityId)
const levelPreset = contentSystem.getLevelPreset(cityId, levelId)
@@ -116,23 +148,14 @@ export function generateBoard({ cityId, levelId, seed, contentSystem }) {
const effectiveSeed = seed ?? levelPreset.seedBase
const rng = createRng(effectiveSeed)
const selectedElements = pickElements(city, levelPreset, rng)
const counts = normalizePiecesPerElement(levelPreset.piecesPerElement, levelPreset.elementCount)
const piecePool = createPiecePool(selectedElements, counts)
const pieces = placePieces(shuffleWithRng(piecePool, rng), levelPreset, rng)
const overlapGraph = buildOverlapGraph(pieces)
const boardState = {
boardId: `${cityId}-${levelId}-${effectiveSeed}`,
return generateBoardFromDefinition({
cityId,
levelId,
seed: effectiveSeed,
pieces,
overlapGraph,
metrics: {},
}
boardState.metrics = evaluateBoard(boardState, levelPreset)
return boardState
elements: selectedElements,
levelPreset,
})
}
export default generateBoard

View File

@@ -0,0 +1,99 @@
import { generateBoardFromDefinition } from './generate-board.js'
import { createRng, shuffleWithRng } from './random.js'
function createMashupLevelPreset(elementCount) {
return {
id: 1,
seedBase: 31001,
elementCount,
piecesPerElement: Array.from({ length: elementCount }, () => 3),
layers: 4,
density: 'medium_high',
}
}
function getSourceCities(cityIds, contentSystem) {
return cityIds
.map((cityId) => contentSystem.getCity(cityId))
.filter(Boolean)
}
function createCityElementPools(cities, rng) {
return cities.map((city) => ({
cityId: city.id,
cursor: 0,
elements: shuffleWithRng(
city.elements.map((element) => ({
...element,
sourceCityId: city.id,
sourceCityName: city.display.name,
})),
rng,
),
}))
}
function pickMashupElements(cities, targetCount, rng) {
const pools = createCityElementPools(cities, rng)
const selected = []
while (selected.length < targetCount) {
let didAdd = false
for (const pool of pools) {
const element = pool.elements[pool.cursor]
if (!element) {
continue
}
selected.push(element)
pool.cursor += 1
didAdd = true
if (selected.length === targetCount) {
break
}
}
if (!didAdd) {
break
}
}
return selected
}
export function generateMashupBoard({ cityIds, seed, contentSystem }) {
const cities = getSourceCities(cityIds, contentSystem)
if (cities.length === 0) {
throw new Error('Mashup mode requires at least one source city')
}
const effectiveSeed = seed ?? 31001
const rng = createRng(effectiveSeed)
const maxElementCount = cities.reduce((sum, city) => sum + city.elements.length, 0)
const elementCount = Math.min(8, maxElementCount)
const levelPreset = createMashupLevelPreset(elementCount)
const selectedElements = pickMashupElements(cities, elementCount, rng)
return generateBoardFromDefinition({
boardId: `mashup-${effectiveSeed}`,
cityId: 'mashup',
levelId: 1,
seed: effectiveSeed,
elements: selectedElements,
levelPreset,
extraState: {
sourceCityIds: cities.map((city) => city.id),
elementDefinitions: selectedElements.map((element) => ({
elementId: element.id,
name: element.name,
sourceCityId: element.sourceCityId,
sourceCityName: element.sourceCityName,
})),
},
})
}
export default generateMashupBoard

View File

@@ -1,6 +1,7 @@
export { classifyDeadlock } from './classify-deadlock.js'
export { evaluateBoard } from './evaluate-board.js'
export { generateBoard } from './generate-board.js'
export { generateMashupBoard } from './generate-mashup-board.js'
export {
buildOverlapGraph,
getClickablePieces,

View File

@@ -65,6 +65,7 @@ export function createGameSession({
seed,
contentSystem,
boardState,
restartFactory,
}) {
const resolvedBoard = boardState ? cloneBoardState(boardState) : generateBoard({ cityId, levelId, seed, contentSystem })
const state = {
@@ -160,6 +161,10 @@ export function createGameSession({
}
function restart(nextSeed = seed ?? resolvedBoard.seed) {
if (restartFactory) {
return restartFactory(nextSeed)
}
const freshSession = createGameSession({
cityId,
levelId,

View File

@@ -223,10 +223,15 @@ function drawSidebarCard({ x, y, width, height, titleLines, subtitle, onTap, acc
}
function drawHomeScene() {
const playerStateSnapshot = sceneStore.getPlayerState()
drawHeader('城市抓猫猫', '世界主页面 · 3×3 入口')
const homeTiles = contentSystem.getHomeTiles(sceneStore.getPlayerState())
const playerGiftAlbums = contentSystem.getGiftAlbums(sceneStore.getPlayerState())
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
@@ -270,7 +275,7 @@ function drawHomeScene() {
width: sidebarWidth,
height: cardHeight,
titleLines: ['礼物', '区'],
subtitle: `${playerGiftAlbums[0].collectedCount + playerGiftAlbums[1].collectedCount} 个收藏`,
subtitle: `${totalGiftCount} 个收藏`,
accentColor: '#26A69A',
onTap() {
sceneStore.openGiftZone()
@@ -284,10 +289,10 @@ function drawHomeScene() {
width: sidebarWidth,
height: cardHeight,
titleLines: ['排行', '榜'],
subtitle: '城市战队',
subtitle: teamCity ? `${teamCity.display.name} 战队` : '选择战队',
accentColor: '#42A5F5',
onTap() {
showTransientMessage('城市排行榜依赖服务端V1.1+ 接入')
sceneStore.openCityTeamSelect()
render()
},
})
@@ -483,8 +488,11 @@ 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 收集册 · 冰箱贴 / 邮票')
drawHeader('城市主题礼物区', 'MVP 收集册 · 冰箱贴 / 邮票 / 猫猫')
const summaryX = 24
const summaryY = 92
@@ -507,8 +515,50 @@ function drawGiftZoneScene(scene) {
summaryY + summaryHeight / 2,
)
const tabWidth = (windowWidth - 24 * 2 - 12) / 2
const tabsY = 158
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({
@@ -529,7 +579,7 @@ function drawGiftZoneScene(scene) {
const columns = 2
const cardWidth = (windowWidth - 24 * 2 - 14) / columns
const cardHeight = 82
const startY = 216
const startY = 286
entries.forEach((entry, index) => {
const row = Math.floor(index / columns)
@@ -553,8 +603,10 @@ function drawGiftZoneScene(scene) {
ctx.font = '13px sans-serif'
if (scene.selectedTab === 'magnets') {
ctx.fillText(`冰箱贴 ${entry.collectedCount}/${entry.totalCount}`, x + 16, y + 52)
} else {
} else if (scene.selectedTab === 'stamps') {
ctx.fillText(entry.isCollected ? '邮票已收集' : '邮票未收集', x + 16, y + 52)
} else {
ctx.fillText(entry.isCollected ? `${entry.catName} 已收集` : '通关城市后收集', x + 16, y + 52)
}
})
@@ -564,8 +616,73 @@ function drawGiftZoneScene(scene) {
})
}
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 city = contentSystem.getCity(scene.cityId)
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()
@@ -573,10 +690,15 @@ function drawGameplayScene(scene) {
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 槽位`
const elementNameMap = new Map(city.elements.map((element) => [element.id, element.name]))
drawHeader(`${city.display.name} · 关卡 ${scene.levelId}`, `${state.slot.length}/7 槽位`)
drawHeader(title, subtitle)
drawButton({
x: windowWidth - 112,
@@ -624,11 +746,7 @@ function drawGameplayScene(scene) {
const result = session.pickPiece(piece.id)
if (result.status === 'won') {
sceneStore.completeLevel({
cityId: scene.cityId,
levelId: scene.levelId,
stars: 3,
})
sceneStore.completeGameplayVictory(3)
savePlayerState()
}
@@ -668,11 +786,29 @@ function drawGameplayScene(scene) {
})
if (state.status === 'won' || state.status === 'failed') {
drawResultOverlay(state.status, city.display.bgColor)
drawResultOverlay(scene, state.status, isMashup ? scene.accentColor : city.display.bgColor)
}
}
function drawResultOverlay(status, accentColor) {
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)
@@ -691,22 +827,18 @@ function drawResultOverlay(status, accentColor) {
ctx.fillStyle = '#231f2a'
ctx.font = 'bold 28px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(status === 'won' ? '通关成功' : '挑战失败', x + boxWidth / 2, y + 48)
ctx.fillText(title, x + boxWidth / 2, y + 48)
ctx.fillStyle = '#6b6474'
ctx.font = '15px sans-serif'
ctx.fillText(
status === 'won' ? '已记录进度,返回关卡页继续' : '可以重开当前关卡,继续测试核心循环',
x + boxWidth / 2,
y + 86,
)
ctx.fillText(detail, x + boxWidth / 2, y + 86)
drawButton({
x: x + 20,
y: y + 120,
width: (boxWidth - 50) / 2,
height: 42,
label: status === 'won' ? '返回关卡' : '返回',
label: status === 'won' ? (isMashup ? '返回主页' : '返回关卡') : '返回',
onTap() {
sceneStore.goBack()
render()
@@ -720,10 +852,14 @@ function drawResultOverlay(status, accentColor) {
y: y + 120,
width: (boxWidth - 50) / 2,
height: 42,
label: status === 'won' ? '下一步再做' : '重新挑战',
label: status === 'won' ? (isMashup ? '再来一局' : '下一步再做') : '重新挑战',
onTap() {
if (status === 'won') {
sceneStore.goBack()
if (isMashup) {
sceneStore.restartCurrentLevel()
} else {
sceneStore.goBack()
}
} else {
sceneStore.restartCurrentLevel()
}
@@ -786,6 +922,12 @@ function render() {
return
}
if (scene.type === 'city-team-select') {
drawCityTeamSelectScene(scene)
drawTransientMessage()
return
}
if (scene.type === 'gameplay') {
drawGameplayScene(scene)
drawTransientMessage()

View File

@@ -1,4 +1,5 @@
import { createGameSession } from '../gameplay/session/index.js'
import { generateMashupBoard } from '../gameplay/difficulty/index.js'
function createHomeSelectScene() {
return {
@@ -20,6 +21,41 @@ function createGiftZoneScene(selectedTab = 'magnets') {
}
}
function createCityTeamSelectScene(options, selectedTeamCityId) {
return {
type: 'city-team-select',
options,
selectedTeamCityId,
}
}
function createGameplayScene({
cityId,
levelId,
session,
mode = 'city',
title = null,
subtitle = null,
accentColor = null,
sourceCityIds = [],
elementDefinitions = [],
rewardSummary = null,
}) {
return {
type: 'gameplay',
cityId,
levelId,
session,
mode,
title,
subtitle,
accentColor,
sourceCityIds,
elementDefinitions,
rewardSummary,
}
}
function createLevelProgress(city, playerState) {
const cityProgress = playerState.levelProgress[city.id] ?? {}
@@ -57,6 +93,35 @@ function createAcquiredDate(now) {
return new Date(now()).toISOString()
}
function createCityTeamOptions(contentSystem, playerState) {
return playerState.unlockedCities
.map((cityId) => {
const city = contentSystem.getCity(cityId)
if (!city) {
return null
}
return {
cityId: city.id,
cityName: city.display.name,
catName: city.cat.name,
themeColor: city.display.bgColor,
}
})
.filter(Boolean)
}
function createMashupRewardCandidates(contentSystem, playerState) {
return playerState.unlockedCities
.map((cityId) => contentSystem.getCity(cityId))
.filter(Boolean)
.flatMap((city) => city.levelPresets.map((preset) => ({
magnetId: `magnet_${city.id}_${preset.id}`,
cityId: city.id,
levelId: preset.id,
})))
}
function awardLevelMagnet(playerState, cityId, levelId, now) {
const magnets = ensureCollection(playerState, 'collectedMagnets')
const magnetId = `magnet_${cityId}_${levelId}`
@@ -73,6 +138,41 @@ function awardLevelMagnet(playerState, cityId, levelId, now) {
})
}
function awardMashupReward(contentSystem, playerState, seed, now) {
const rewardCandidates = createMashupRewardCandidates(contentSystem, playerState)
const magnets = ensureCollection(playerState, 'collectedMagnets')
if (rewardCandidates.length === 0) {
playerState.inventory.shuffle = (playerState.inventory.shuffle ?? 0) + 1
return {
type: 'inventory',
itemId: 'shuffle',
amount: 1,
}
}
const reward = rewardCandidates[seed % rewardCandidates.length]
if (magnets.some((entry) => entry.magnetId === reward.magnetId)) {
playerState.inventory.shuffle = (playerState.inventory.shuffle ?? 0) + 1
return {
type: 'inventory',
itemId: 'shuffle',
amount: 1,
}
}
magnets.push({
...reward,
acquiredDate: createAcquiredDate(now),
})
return {
type: 'magnet',
...reward,
}
}
function markCityCompletion(contentSystem, playerState, cityId, now) {
if (!playerState.collectedCats.includes(cityId)) {
playerState.collectedCats.push(cityId)
@@ -105,6 +205,40 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
const history = []
let currentScene = createHomeSelectScene()
function createMashupSession(seed = now()) {
const boardState = generateMashupBoard({
cityIds: playerState.unlockedCities,
seed,
contentSystem,
})
return createGameSession({
cityId: 'mashup',
levelId: 1,
seed,
contentSystem,
boardState,
restartFactory: (nextSeed) => createMashupSession(nextSeed),
})
}
function createMashupGameplayScene(seed = now()) {
const session = createMashupSession(seed)
const boardState = session.getBoardState()
return createGameplayScene({
cityId: 'mashup',
levelId: 1,
session,
mode: 'mashup',
title: '主题大混战',
subtitle: `${boardState.sourceCityIds.length} 城混搭`,
accentColor: '#E74C3C',
sourceCityIds: boardState.sourceCityIds ?? [],
elementDefinitions: boardState.elementDefinitions ?? [],
})
}
function getScene() {
return currentScene
}
@@ -130,7 +264,9 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
}
if (tile.id === 'mashup') {
return { opened: false, reason: 'mode-unavailable' }
history.push(currentScene)
currentScene = createMashupGameplayScene()
return { opened: true }
}
return { opened: false, reason: 'unavailable' }
@@ -167,7 +303,7 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
return false
}
if (!['magnets', 'stamps'].includes(tabId)) {
if (!['magnets', 'stamps', 'cats'].includes(tabId)) {
return false
}
@@ -179,6 +315,40 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
return true
}
function openCityTeamSelect() {
history.push(currentScene)
currentScene = createCityTeamSelectScene(
createCityTeamOptions(contentSystem, playerState),
playerState.cityTeam?.teamCityId ?? null,
)
return true
}
function chooseCityTeam(cityId) {
if (currentScene.type !== 'city-team-select') {
return false
}
if (playerState.cityTeam?.teamCityId) {
return false
}
if (!playerState.unlockedCities.includes(cityId)) {
return false
}
playerState.cityTeam = {
...playerState.cityTeam,
teamCityId: cityId,
joinedDate: createAcquiredDate(now),
lastSwitchDate: null,
}
currentScene = history.pop() ?? createHomeSelectScene()
return true
}
function openLevel(cityId, levelId) {
if (!playerState.unlockedCities.includes(cityId)) {
return false
@@ -197,8 +367,7 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
}
history.push(currentScene)
currentScene = {
type: 'gameplay',
currentScene = createGameplayScene({
cityId,
levelId,
session: createGameSession({
@@ -206,7 +375,7 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
levelId,
contentSystem,
}),
}
})
return true
}
@@ -246,6 +415,33 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
return true
}
function completeGameplayVictory(stars = 3) {
if (currentScene.type !== 'gameplay') {
return null
}
if (currentScene.mode === 'mashup') {
const reward = awardMashupReward(contentSystem, playerState, currentScene.session.seed, now)
currentScene = {
...currentScene,
rewardSummary: reward,
}
return reward
}
completeLevel({
cityId: currentScene.cityId,
levelId: currentScene.levelId,
stars,
})
return {
type: 'city',
cityId: currentScene.cityId,
levelId: currentScene.levelId,
}
}
function goBack() {
if (history.length === 0) {
currentScene = createHomeSelectScene()
@@ -274,8 +470,11 @@ export function createSceneStore({ contentSystem, playerState, now = () => Date.
openCity,
openGiftZone,
selectGiftTab,
openCityTeamSelect,
chooseCityTeam,
openLevel,
completeLevel,
completeGameplayVictory,
restartCurrentLevel,
goBack,
}