299 lines
7.4 KiB
JavaScript
299 lines
7.4 KiB
JavaScript
const { releaseAssets } = require('./releaseAssetManifest')
|
|
|
|
const PLAYABLE_VISUAL_REVIEW_STATUS = 'approved-readable'
|
|
const PROVISIONAL_VISUAL_REVIEW_STATUS = 'experience-provisional'
|
|
const RUNTIME_VISUAL_TIERS = Object.freeze({
|
|
FORMAL: 'formal',
|
|
PROVISIONAL: 'experience-provisional',
|
|
FALLBACK: 'fallback',
|
|
})
|
|
|
|
// This set deliberately mirrors `usable` in
|
|
// docs/production/art-release-gate-s01.json. Registered page art outside this
|
|
// set may be shown only as experience-provisional and never counts as formal.
|
|
const FORMAL_PAGE_ART_IDS = new Set([
|
|
'S01-C01-P01',
|
|
'S01-C01-P02',
|
|
'S01-C01-P03',
|
|
'S01-C01-P04',
|
|
'S01-C01-P05',
|
|
'S01-C01-P07',
|
|
'S01-C01-P08',
|
|
'S01-C02-P01',
|
|
'S01-C02-P02',
|
|
'S01-C02-P03',
|
|
'S01-C02-P04',
|
|
'S01-C02-P05',
|
|
'S01-C02-P06',
|
|
'S01-C02-P07',
|
|
'S01-C02-P08',
|
|
'S01-C03-P01',
|
|
'S01-C03-P02',
|
|
'S01-C03-P03',
|
|
'S01-C03-P04',
|
|
'S01-C03-P05',
|
|
'S01-C03-P07',
|
|
'S01-C03-P08',
|
|
'S01-C04-P01',
|
|
'S01-C04-P02',
|
|
'S01-C04-P03',
|
|
'S01-C04-P05',
|
|
'S01-C04-P06',
|
|
'S01-C04-P07',
|
|
'S01-C04-P08',
|
|
'S01-C05-P01',
|
|
'S01-C05-P02',
|
|
'S01-C05-P03',
|
|
'S01-C05-P04',
|
|
'S01-C05-P05',
|
|
'S01-C05-P06',
|
|
'S01-C05-P07',
|
|
'S01-C05-P08',
|
|
'S01-C06-P01',
|
|
'S01-C06-P02',
|
|
'S01-C06-P03',
|
|
'S01-C06-P04',
|
|
'S01-C06-P05',
|
|
'S01-C06-P06',
|
|
'S01-C06-P07',
|
|
'S01-C06-P08',
|
|
'S01-C07-P01',
|
|
'S01-C07-P02',
|
|
'S01-C07-P03',
|
|
'S01-C07-P04',
|
|
'S01-C07-P05',
|
|
'S01-C07-P06',
|
|
'S01-C07-P07',
|
|
'S01-C07-P08',
|
|
'S01-C08-P02',
|
|
'S01-C08-P03',
|
|
'S01-C08-P04',
|
|
'S01-C08-P05',
|
|
'S01-C08-P06',
|
|
'S01-C08-P07',
|
|
'S01-C08-P08',
|
|
'S01-C09-P02',
|
|
'S01-C09-P03',
|
|
'S01-C09-P04',
|
|
'S01-C09-P07',
|
|
'S01-C09-P08',
|
|
'S01-C10-P01',
|
|
'S01-C10-P02',
|
|
'S01-C10-P03',
|
|
'S01-C10-P04',
|
|
'S01-C10-P05',
|
|
'S01-C10-P06',
|
|
'S01-C10-P07',
|
|
'S01-C10-P08',
|
|
'S01-C11-P01',
|
|
'S01-C11-P02',
|
|
'S01-C11-P03',
|
|
'S01-C11-P04',
|
|
'S01-C11-P05',
|
|
'S01-C11-P06',
|
|
'S01-C11-P07',
|
|
'S01-C11-P08',
|
|
'S01-C12-P01',
|
|
'S01-C12-P02',
|
|
'S01-C12-P03',
|
|
'S01-C12-P04',
|
|
'S01-C12-P05',
|
|
'S01-C12-P06',
|
|
'S01-C12-P07',
|
|
'S01-C12-P08',
|
|
'S01-C13-P01',
|
|
'S01-C13-P02',
|
|
'S01-C13-P03',
|
|
'S01-C13-P05',
|
|
'S01-C13-P06',
|
|
'S01-C13-P07',
|
|
'S01-C13-P08',
|
|
'S01-C14-P01',
|
|
'S01-C14-P02',
|
|
'S01-C14-P03',
|
|
'S01-C14-P04',
|
|
'S01-C14-P05',
|
|
'S01-C14-P06',
|
|
'S01-C14-P07',
|
|
'S01-C14-P08',
|
|
'S01-C15-P01',
|
|
'S01-C15-P02',
|
|
'S01-C15-P03',
|
|
'S01-C15-P04',
|
|
'S01-C15-P05',
|
|
'S01-C15-P06',
|
|
'S01-C15-P07',
|
|
'S01-C15-P08',
|
|
])
|
|
|
|
function clean(value) {
|
|
return typeof value === 'string' ? value.trim() : ''
|
|
}
|
|
|
|
function comicAssetIdForPageId(pageId) {
|
|
const match = clean(pageId).match(/^S(\d+)-C(\d+)-P(\d+)$/)
|
|
if (!match) return ''
|
|
return `comic.s${match[1]}.c${match[2]}.p${match[3]}`
|
|
}
|
|
|
|
function registeredPageArtFor(page) {
|
|
const assetId = comicAssetIdForPageId(page && page.pageId)
|
|
const releaseAsset = assetId ? releaseAssets[assetId] : null
|
|
return Boolean(
|
|
releaseAsset
|
|
&& releaseAsset.kind === 'image'
|
|
&& clean(releaseAsset.localSeed)
|
|
&& clean(releaseAsset.localSeed) === clean(page.illustrationAsset),
|
|
)
|
|
}
|
|
|
|
function createPageArtVisual(page, chapter, runtimeTier) {
|
|
const formal = runtimeTier === RUNTIME_VISUAL_TIERS.FORMAL
|
|
return {
|
|
kind: 'page-art',
|
|
asset: clean(page.illustrationAsset),
|
|
subject: clean(page.actorName || page.headline || chapter.title),
|
|
runtimeTier,
|
|
formalReleaseEligible: formal,
|
|
reviewStatus: formal
|
|
? PLAYABLE_VISUAL_REVIEW_STATUS
|
|
: PROVISIONAL_VISUAL_REVIEW_STATUS,
|
|
reviewBasis: formal
|
|
? 'art-release-gate-s01-usable'
|
|
: 'registered-immutable-asset-experience-only',
|
|
}
|
|
}
|
|
|
|
function personByInstanceId(chapter, instanceId) {
|
|
return (chapter.people || []).find(
|
|
(person) => (
|
|
person.instanceId === instanceId
|
|
&& clean(person.portrait)
|
|
),
|
|
) || null
|
|
}
|
|
|
|
function personByName(chapter, name) {
|
|
const cleanName = clean(name)
|
|
if (!cleanName) return null
|
|
return (chapter.people || []).find(
|
|
(person) => person.name === cleanName && clean(person.portrait),
|
|
) || null
|
|
}
|
|
|
|
function featuredPersonForPage(page, chapter) {
|
|
const direct = personByInstanceId(chapter, page.actorInstanceId)
|
|
if (direct) return direct
|
|
|
|
if (page.type === 'memory') {
|
|
const memoryPerson = personByName(
|
|
chapter,
|
|
page.memoryCard && page.memoryCard.characterName,
|
|
)
|
|
if (memoryPerson) return memoryPerson
|
|
}
|
|
|
|
if (page.type === 'emotion') {
|
|
const emotionPerson = personByName(
|
|
chapter,
|
|
chapter.emotionMoment
|
|
&& chapter.emotionMoment.character
|
|
&& chapter.emotionMoment.character.name,
|
|
)
|
|
if (emotionPerson) return emotionPerson
|
|
}
|
|
|
|
return (chapter.people || []).find(
|
|
(person) => clean(person.portrait),
|
|
) || null
|
|
}
|
|
|
|
function createPlayableVisual(page, chapter) {
|
|
if (FORMAL_PAGE_ART_IDS.has(page.pageId)) {
|
|
return createPageArtVisual(page, chapter, RUNTIME_VISUAL_TIERS.FORMAL)
|
|
}
|
|
|
|
if (registeredPageArtFor(page)) {
|
|
return createPageArtVisual(
|
|
page,
|
|
chapter,
|
|
RUNTIME_VISUAL_TIERS.PROVISIONAL,
|
|
)
|
|
}
|
|
|
|
if (page.type === 'event') {
|
|
const portrait = clean(page.actorPortrait)
|
|
if (portrait) {
|
|
return {
|
|
kind: 'actor-portrait',
|
|
asset: portrait,
|
|
subject: clean(page.actorName),
|
|
actorInstanceId: clean(page.actorInstanceId),
|
|
runtimeTier: RUNTIME_VISUAL_TIERS.FALLBACK,
|
|
formalReleaseEligible: false,
|
|
reviewStatus: PLAYABLE_VISUAL_REVIEW_STATUS,
|
|
reviewBasis: 'season-character-continuity-sheet',
|
|
}
|
|
}
|
|
|
|
return {
|
|
kind: 'evidence-composite',
|
|
asset: clean(page.fallbackAsset),
|
|
subject: clean(page.actorName),
|
|
evidenceLabel: clean(page.headline || '看看手边证据'),
|
|
evidenceDetail: clean(
|
|
page.secondaryCaption || page.caption || page.question,
|
|
),
|
|
runtimeTier: RUNTIME_VISUAL_TIERS.FALLBACK,
|
|
formalReleaseEligible: false,
|
|
reviewStatus: PLAYABLE_VISUAL_REVIEW_STATUS,
|
|
reviewBasis: 'chapter-era-scene-with-reviewed-evidence-copy',
|
|
}
|
|
}
|
|
|
|
const featuredPerson = featuredPersonForPage(page, chapter)
|
|
if (featuredPerson) {
|
|
return {
|
|
kind: 'chapter-character',
|
|
asset: clean(featuredPerson.portrait),
|
|
subject: clean(featuredPerson.name),
|
|
actorInstanceId: clean(featuredPerson.instanceId),
|
|
runtimeTier: RUNTIME_VISUAL_TIERS.FALLBACK,
|
|
formalReleaseEligible: false,
|
|
reviewStatus: PLAYABLE_VISUAL_REVIEW_STATUS,
|
|
reviewBasis: 'season-character-continuity-sheet',
|
|
}
|
|
}
|
|
|
|
// Defensive last resort. The season currently never reaches this branch,
|
|
// but if a future chapter lacks a portrait it still gets explicit evidence
|
|
// instead of an unexplained empty room.
|
|
return {
|
|
kind: 'evidence-composite',
|
|
asset: clean(page.fallbackAsset),
|
|
subject: clean(page.headline || chapter.title),
|
|
evidenceLabel: clean(page.headline || chapter.title),
|
|
evidenceDetail: clean(page.caption || chapter.narration),
|
|
runtimeTier: RUNTIME_VISUAL_TIERS.FALLBACK,
|
|
formalReleaseEligible: false,
|
|
reviewStatus: PLAYABLE_VISUAL_REVIEW_STATUS,
|
|
reviewBasis: 'chapter-era-scene-with-reviewed-evidence-copy',
|
|
}
|
|
}
|
|
|
|
function attachPlayableVisuals(pageSequence, chapter) {
|
|
return (Array.isArray(pageSequence) ? pageSequence : []).map((page) => ({
|
|
...page,
|
|
playableVisual: createPlayableVisual(page, chapter),
|
|
}))
|
|
}
|
|
|
|
module.exports = {
|
|
FORMAL_PAGE_ART_IDS,
|
|
PLAYABLE_VISUAL_REVIEW_STATUS,
|
|
PROVISIONAL_VISUAL_REVIEW_STATUS,
|
|
RUNTIME_VISUAL_TIERS,
|
|
attachPlayableVisuals,
|
|
createPlayableVisual,
|
|
}
|