gengxin
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user