484 lines
11 KiB
JavaScript
484 lines
11 KiB
JavaScript
import { createGameSession } from '../gameplay/session/index.js'
|
|
import { generateMashupBoard } from '../gameplay/difficulty/index.js'
|
|
|
|
function createHomeSelectScene() {
|
|
return {
|
|
type: 'home-select',
|
|
}
|
|
}
|
|
|
|
function createCitySelectScene() {
|
|
return {
|
|
type: 'city-select',
|
|
continentId: 'asia',
|
|
}
|
|
}
|
|
|
|
function createGiftZoneScene(selectedTab = 'magnets') {
|
|
return {
|
|
type: 'gift-zone',
|
|
selectedTab,
|
|
}
|
|
}
|
|
|
|
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] ?? {}
|
|
|
|
return city.levelPresets.map((preset) => {
|
|
const levelState = cityProgress[preset.id] ?? null
|
|
const previousLevel = preset.id - 1
|
|
const previousCompleted = previousLevel <= 0 || cityProgress[previousLevel]?.completed === true
|
|
|
|
return {
|
|
levelId: preset.id,
|
|
isUnlocked: previousCompleted,
|
|
isCompleted: levelState?.completed === true,
|
|
stars: levelState?.stars ?? 0,
|
|
}
|
|
})
|
|
}
|
|
|
|
function ensureCityProgress(playerState, cityId) {
|
|
if (!playerState.levelProgress[cityId]) {
|
|
playerState.levelProgress[cityId] = {}
|
|
}
|
|
|
|
return playerState.levelProgress[cityId]
|
|
}
|
|
|
|
function ensureCollection(playerState, key) {
|
|
if (!Array.isArray(playerState[key])) {
|
|
playerState[key] = []
|
|
}
|
|
|
|
return playerState[key]
|
|
}
|
|
|
|
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}`
|
|
|
|
if (magnets.some((entry) => entry.magnetId === magnetId)) {
|
|
return
|
|
}
|
|
|
|
magnets.push({
|
|
magnetId,
|
|
cityId,
|
|
levelId,
|
|
acquiredDate: createAcquiredDate(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)
|
|
}
|
|
|
|
if (!playerState.passportStamps.includes(cityId)) {
|
|
playerState.passportStamps.push(cityId)
|
|
}
|
|
|
|
const city = contentSystem.getCity(cityId)
|
|
const stamps = ensureCollection(playerState, 'collectedStamps')
|
|
const stampId = city?.passport?.stampId ?? `stamp_${cityId}`
|
|
|
|
if (!stamps.some((entry) => entry.stampId === stampId)) {
|
|
stamps.push({
|
|
stampId,
|
|
cityId,
|
|
acquiredDate: createAcquiredDate(now),
|
|
})
|
|
}
|
|
|
|
const nextCityId = city?.unlockAfterCityId
|
|
|
|
if (nextCityId && !playerState.unlockedCities.includes(nextCityId)) {
|
|
playerState.unlockedCities.push(nextCityId)
|
|
}
|
|
}
|
|
|
|
export function createSceneStore({ contentSystem, playerState, now = () => Date.now() }) {
|
|
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
|
|
}
|
|
|
|
function openHomeTile(tileId) {
|
|
const tile = contentSystem.getHomeTile(tileId, playerState)
|
|
if (!tile) {
|
|
return { opened: false, reason: 'missing' }
|
|
}
|
|
|
|
if (tile.id === 'coming_soon') {
|
|
return { opened: false, reason: 'coming-soon' }
|
|
}
|
|
|
|
if (!tile.isUnlocked) {
|
|
return { opened: false, reason: 'locked' }
|
|
}
|
|
|
|
if (tile.id === 'asia') {
|
|
history.push(currentScene)
|
|
currentScene = createCitySelectScene()
|
|
return { opened: true }
|
|
}
|
|
|
|
if (tile.id === 'mashup') {
|
|
history.push(currentScene)
|
|
currentScene = createMashupGameplayScene()
|
|
return { opened: true }
|
|
}
|
|
|
|
return { opened: false, reason: 'unavailable' }
|
|
}
|
|
|
|
function openCity(cityId) {
|
|
if (!playerState.unlockedCities.includes(cityId)) {
|
|
return false
|
|
}
|
|
|
|
const city = contentSystem.getCity(cityId)
|
|
if (!city) {
|
|
return false
|
|
}
|
|
|
|
history.push(currentScene)
|
|
currentScene = {
|
|
type: 'level-select',
|
|
cityId,
|
|
levels: createLevelProgress(city, playerState),
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function openGiftZone(initialTab = 'magnets') {
|
|
history.push(currentScene)
|
|
currentScene = createGiftZoneScene(initialTab)
|
|
return true
|
|
}
|
|
|
|
function selectGiftTab(tabId) {
|
|
if (currentScene.type !== 'gift-zone') {
|
|
return false
|
|
}
|
|
|
|
if (!['magnets', 'stamps', 'cats'].includes(tabId)) {
|
|
return false
|
|
}
|
|
|
|
currentScene = {
|
|
...currentScene,
|
|
selectedTab: tabId,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const city = contentSystem.getCity(cityId)
|
|
if (!city) {
|
|
return false
|
|
}
|
|
|
|
const levelCards = createLevelProgress(city, playerState)
|
|
const targetLevel = levelCards.find((level) => level.levelId === levelId)
|
|
|
|
if (!targetLevel || !targetLevel.isUnlocked) {
|
|
return false
|
|
}
|
|
|
|
history.push(currentScene)
|
|
currentScene = createGameplayScene({
|
|
cityId,
|
|
levelId,
|
|
session: createGameSession({
|
|
cityId,
|
|
levelId,
|
|
contentSystem,
|
|
}),
|
|
})
|
|
|
|
return true
|
|
}
|
|
|
|
function completeLevel({ cityId, levelId, stars = 3 }) {
|
|
const city = contentSystem.getCity(cityId)
|
|
if (!city) {
|
|
return false
|
|
}
|
|
|
|
const cityProgress = ensureCityProgress(playerState, cityId)
|
|
awardLevelMagnet(playerState, cityId, levelId, now)
|
|
cityProgress[levelId] = {
|
|
...(cityProgress[levelId] ?? {}),
|
|
completed: true,
|
|
stars: Math.max(cityProgress[levelId]?.stars ?? 0, stars),
|
|
}
|
|
|
|
const allLevelsCompleted = city.levelPresets.every((preset) => cityProgress[preset.id]?.completed === true)
|
|
if (allLevelsCompleted) {
|
|
markCityCompletion(contentSystem, playerState, cityId, now)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function restartCurrentLevel() {
|
|
if (currentScene.type !== 'gameplay') {
|
|
return false
|
|
}
|
|
|
|
currentScene = {
|
|
...currentScene,
|
|
session: currentScene.session.restart(),
|
|
}
|
|
|
|
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()
|
|
return currentScene
|
|
}
|
|
|
|
currentScene = history.pop()
|
|
|
|
if (currentScene.type === 'level-select') {
|
|
const city = contentSystem.getCity(currentScene.cityId)
|
|
currentScene = {
|
|
...currentScene,
|
|
levels: createLevelProgress(city, playerState),
|
|
}
|
|
}
|
|
|
|
return currentScene
|
|
}
|
|
|
|
return {
|
|
getScene,
|
|
getPlayerState() {
|
|
return playerState
|
|
},
|
|
openHomeTile,
|
|
openCity,
|
|
openGiftZone,
|
|
selectGiftTab,
|
|
openCityTeamSelect,
|
|
chooseCityTeam,
|
|
openLevel,
|
|
completeLevel,
|
|
completeGameplayVictory,
|
|
restartCurrentLevel,
|
|
goBack,
|
|
}
|
|
}
|
|
|
|
export default createSceneStore
|