2303 lines
72 KiB
JavaScript
2303 lines
72 KiB
JavaScript
const season = require('../../../data/season')
|
|
const {
|
|
getProgress,
|
|
saveProgress,
|
|
getSettings,
|
|
saveSettings,
|
|
getAudioProgress,
|
|
saveAudioProgress,
|
|
} = require('../../../utils/storage')
|
|
const {
|
|
getChapterLayoutMetrics,
|
|
} = require('../../../utils/layout')
|
|
const {
|
|
getComicArtStageStyle,
|
|
} = require('../../../utils/comicLayout')
|
|
const {
|
|
addMemoryCard,
|
|
} = require('../../../utils/memoryCollection')
|
|
const {
|
|
chapterRoute,
|
|
normalizeChapterNumber,
|
|
} = require('../../../utils/chapterRoute')
|
|
const {
|
|
buildComicPageModel,
|
|
} = require('./chapterPages')
|
|
const {
|
|
applyComicReaderAction,
|
|
buildComicReaderState,
|
|
mergeComicReaderProgress,
|
|
readComicReaderProgress,
|
|
} = require('../../utils/comicReaderState')
|
|
const {
|
|
releaseAssets,
|
|
} = require('../../data/releaseAssetManifest')
|
|
const {
|
|
getApprovedRemotePageAudio,
|
|
} = require('../../data/remotePageAudioManifest')
|
|
const assetReleaseConfig = require('../../data/assetReleaseConfig')
|
|
const {
|
|
createAssetPlatformFacade,
|
|
createAssetManager,
|
|
} = require('../../utils/assetManager')
|
|
const {
|
|
IMAGE_UNAVAILABLE_MESSAGE,
|
|
buildComicImageState,
|
|
buildReviewedAudioState,
|
|
getComicFocusImageStyle,
|
|
getComicPageImageMode,
|
|
getComicPageImageStyle,
|
|
} = require('../../utils/comicPageModel')
|
|
|
|
const SHARE_PREVIEW_IMAGE = '/assets/share/guixiang-story-share-preview-v1.jpg'
|
|
const SHARE_PREVIEW_CACHE_NAME = 'guixiang-story-share-preview-v1.jpg'
|
|
const MEMORY_SHARE_SOURCE = 'chapter-memory'
|
|
const STORY_FIRST_PRESENTATION = 'front-back-comic-v1'
|
|
// C01 full-page tracks live behind real player pages in two sibling
|
|
// subpackages. The reader may navigate to those pages, but must never read a
|
|
// sibling package's MP3 path directly (WeChat forbids cross-package assets).
|
|
const CHAPTER_ONE_AUDIO_COMPANIONS = Object.freeze({
|
|
'S01-C01-P01': Object.freeze({ packageRoot: 'package-audio-c01-a', durationSeconds: 33.679388 }),
|
|
'S01-C01-P02': Object.freeze({ packageRoot: 'package-audio-c01-a', durationSeconds: 49.49424 }),
|
|
'S01-C01-P03': Object.freeze({ packageRoot: 'package-audio-c01-a', durationSeconds: 30.534762 }),
|
|
'S01-C01-P04': Object.freeze({ packageRoot: 'package-audio-c01-a', durationSeconds: 46.79424 }),
|
|
'S01-C01-P05': Object.freeze({ packageRoot: 'package-audio-c01-b', durationSeconds: 32.880567 }),
|
|
'S01-C01-P06': Object.freeze({ packageRoot: 'package-audio-c01-b', durationSeconds: 13.328889 }),
|
|
'S01-C01-P07': Object.freeze({ packageRoot: 'package-audio-c01-b', durationSeconds: 10.466644 }),
|
|
'S01-C01-P08': Object.freeze({ packageRoot: 'package-audio-c01-b', durationSeconds: 39.534467 }),
|
|
})
|
|
const CHAPTER_ONE_EVENT_COPY = Object.freeze({
|
|
'S01-H01': Object.freeze({
|
|
question: '几位老人举着手机,纸名单还压在桌角。您先怎么帮?',
|
|
actionDescription: '小满只把二维码转向陆续进门的老人,纸名单还压在桌角。',
|
|
evidence: '几位老人举着手机半天没动,桌角的大字纸名单还没有铺开。',
|
|
retryHint: '先别急。有人举着手机半天没动,桌角那张纸名单还没铺开。',
|
|
reason: '这下有座可找,也不用求人替自己点。只留扫码口,看不清小字或不熟手机的人,连自己找座的机会都没了。',
|
|
actionAdvice: '扫码、纸名单、有人可问,三样都留着。',
|
|
choiceOptions: Object.freeze([
|
|
Object.freeze({ value: 'safer', label: '先让大家扫着,实在不会再来问。' }),
|
|
Object.freeze({ value: 'caution', label: '把大字纸名单铺开,再问一句要不要帮忙。' }),
|
|
]),
|
|
}),
|
|
'S01-H02': Object.freeze({
|
|
question: '菜已经不少了,赵伯还想再添一道撑场面。您会怎么接这句话?',
|
|
actionDescription: '十个人的菜已经摆得满满当当,赵伯的拇指还停在“再来一道”上。',
|
|
evidence: '十个人已经点了十四道菜;赵伯惦记的是桌面体面,不是有人没吃饱。',
|
|
retryHint: '您再数数:十个人,十四道菜。赵伯惦记的是体面,不是有人没吃饱。',
|
|
reason: '热闹留住了,菜也没白白堆满桌。情分不在菜多菜少,点得太满,反倒挤掉大家真正想吃的。',
|
|
actionAdvice: '先看人数、份量和搭配;真不够,再补也来得及。',
|
|
choiceOptions: Object.freeze([
|
|
Object.freeze({ value: 'safer', label: '难得聚一回,再添一道才显得热闹。' }),
|
|
Object.freeze({ value: 'caution', label: '先问问够不够,不够咱再添。' }),
|
|
]),
|
|
}),
|
|
'S01-H03': Object.freeze({
|
|
question: '地上有新压痕,请柬又写着十五桌。您先怎么查?',
|
|
actionDescription: '乐乐蹲下来一量:四个桌脚印还是新的,拖痕一路朝侧门去了。',
|
|
evidence: '地毯上有四个新压痕,椅脚拖痕通向侧门;请柬和座位图也对不上。',
|
|
retryHint: '只顺着一条拖痕走,容易漏掉请柬和跳号的座位图。',
|
|
reason: '三样东西一放到一块儿,桌子来过没有,就有了眉目。光看手机那张图,还说明不了全貌。',
|
|
actionAdvice: '先拍照留住压痕和拖痕,再查当天的调台记录。',
|
|
choiceOptions: Object.freeze([
|
|
Object.freeze({ value: 'caution', label: '顺着拖痕追过去,先看看桌子搬到哪儿了。' }),
|
|
Object.freeze({ value: 'safer', label: '先拍下压痕,再把请柬和座位图放一块儿对对。' }),
|
|
]),
|
|
}),
|
|
'S01-H04': Object.freeze({
|
|
question: '铜牌卷在没念完的稿纸里。您觉得先怎么办?',
|
|
actionDescription: '秦师傅隔着门问:“十五桌的人齐了吗?”稿纸越攥越紧,脚却往后退。',
|
|
evidence: '铜牌和未念完的道歉稿卷在一起,稿首写着“给十五桌的老伙计们”。',
|
|
retryHint: '铜牌和道歉稿原本卷在一起,看着更像话到嘴边,又咽了回去。',
|
|
reason: '这下有眉目了。事情还没问清,先别急着给谁扣帽子;这些东西也不能证明是谁搬走了桌子。',
|
|
actionAdvice: '别拆散铜牌、纸筒和稿纸,记清来路,再等秦师傅把话说完。',
|
|
choiceOptions: Object.freeze([
|
|
Object.freeze({ value: 'safer', label: '先问秦师傅这份稿子写给谁,再看铜牌怎么来的。' }),
|
|
Object.freeze({ value: 'caution', label: '铜牌既然在这儿,先去问是谁把桌子搬走的。' }),
|
|
]),
|
|
}),
|
|
})
|
|
const CHAPTER_ONE_EMOTION_CHOICES = Object.freeze([
|
|
Object.freeze({
|
|
choiceId: 'S01-EM01-A',
|
|
text: '喊一声“赵伯”,把请柬递给他:“您带我们一块儿看看。”',
|
|
}),
|
|
Object.freeze({
|
|
choiceId: 'S01-EM01-B',
|
|
text: '先拉一把普通椅子,再问他愿不愿意坐下等。',
|
|
}),
|
|
])
|
|
|
|
// The internal values stay locked to the reviewed season data. Only the
|
|
// player-facing question and the two ordinary actions change here, so the
|
|
// interaction reads like a conversation about the scene instead of a quiz.
|
|
const STORY_FIRST_EVENT_COPY = Object.freeze({
|
|
'S01-H05': {
|
|
question: '展签把厂内饭菜票写成全国粮票,您先怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '照着展签,继续说是全国粮票' },
|
|
{ value: 'caution', label: '先看票面,再把展签改准' },
|
|
],
|
|
},
|
|
'S01-H06': {
|
|
question: '赵伯说缺口碗是他的,您先怎么认?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '听他说完,马上写上名字' },
|
|
{ value: 'caution', label: '翻过照片,再对一遍旧账' },
|
|
],
|
|
},
|
|
'S01-H07': {
|
|
question: '合影里露出半张脸和几件旧物,您先记什么?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '把布包和桌沿都记下来' },
|
|
{ value: 'caution', label: '只看半张脸,就把人认定' },
|
|
],
|
|
},
|
|
'S01-H08': {
|
|
question: '铜牌背面有红漆、钉孔和破票,您先怎么留证?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '拍下漆痕孔位,留着再核对' },
|
|
{ value: 'caution', label: '只看一处红漆,就定了来历' },
|
|
],
|
|
},
|
|
'S01-H09': {
|
|
question: '老吕手上还沾着机油,就伸手拿馒头。您会怎么做?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '赶时间,先拿馒头再说' },
|
|
{ value: 'caution', label: '先把手洗净擦干,再吃饭' },
|
|
],
|
|
},
|
|
'S01-H10': {
|
|
question: '同坐一条长凳,有人要起身。您会怎么做?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '不用招呼,直接站起来' },
|
|
{ value: 'caution', label: '先喊坐稳,再慢慢起身' },
|
|
],
|
|
},
|
|
'S01-H11': {
|
|
question: '看见当年工人吃大盆主食,您会怎么说?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '按今天的吃法,笑他们饭多' },
|
|
{ value: 'caution', label: '先看当年供应和下午的重活' },
|
|
],
|
|
},
|
|
'S01-H12': {
|
|
question: '饭菜票和晚班人数单到了手里,您怎么收?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '两种凭据分开夹、分开核对' },
|
|
{ value: 'caution', label: '两种票单混在一起收' },
|
|
],
|
|
},
|
|
'S01-H13': {
|
|
question: '工友又起哄比谁先吃第三碗,您会怎么接?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '跟着起哄,比谁先吃第三碗' },
|
|
{ value: 'caution', label: '不跟着比,先问自己还饿不饿' },
|
|
],
|
|
},
|
|
'S01-H14': {
|
|
question: '赵建国已经吃撑,还要马上抬货。您先怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '嘴上说没事,马上去抬货' },
|
|
{ value: 'caution', label: '先停下添饭,说清哪里难受' },
|
|
],
|
|
},
|
|
'S01-H15': {
|
|
question: '小唐说“别又急又撑”,这句话该怎么听?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '听成主食一口都不能吃' },
|
|
{ value: 'caution', label: '听成别急别撑,再看自己需要' },
|
|
],
|
|
},
|
|
'S01-H16': {
|
|
question: '同伴扶住桌沿,您先怎么给他留面子?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '拉把凳子,先问要不要歇' },
|
|
{ value: 'caution', label: '继续拿他起哄,催他干活' },
|
|
],
|
|
},
|
|
'S01-H17': {
|
|
question: '老吕错过饭点,还想空着肚子去上晚班。您怎么留?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '什么也不说,空着肚子硬扛' },
|
|
{ value: 'caution', label: '告诉带班人,先吃饭休息' },
|
|
],
|
|
},
|
|
'S01-H18': {
|
|
question: '只剩半个馒头,几位晚班工友怎么分?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '都留给最能干的那个人' },
|
|
{ value: 'caution', label: '先数清人数,再一起商量' },
|
|
],
|
|
},
|
|
'S01-H19': {
|
|
question: '午间熟菜放了多久说不清,秦师傅怎么做?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '拿另放妥当的白菜和面现做' },
|
|
{ value: 'caution', label: '把放了多久不清楚的饭再热' },
|
|
],
|
|
},
|
|
'S01-H20': {
|
|
question: '晚班的人终于回来了,这张桌该怎么摆?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '把桌摆好,让晚班人坐下吃' },
|
|
{ value: 'caution', label: '让来晚的人端碗站在门口' },
|
|
],
|
|
},
|
|
'S01-H21': {
|
|
question: '秦师傅签了经营责任书,旧桌就归他了吗?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '签完字,就当旧桌归自己' },
|
|
{ value: 'caution', label: '对着合同和清单逐项核对' },
|
|
],
|
|
},
|
|
'S01-H22': {
|
|
question: '老工友还递饭票,您会怎么帮他?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '笑他没跟上,让他自己琢磨' },
|
|
{ value: 'caution', label: '说清付款办法,陪他办一次' },
|
|
],
|
|
},
|
|
'S01-H23': {
|
|
question: '饭馆开业再忙,员工这顿饭怎么安排?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '先排好替班,让员工坐下吃饭' },
|
|
{ value: 'caution', label: '忙起来,就靠尝菜顶一顿' },
|
|
],
|
|
},
|
|
'S01-H24': {
|
|
question: '客人想拿员工桌拼大桌,秦师傅怎么安排?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '另给客人拼桌,员工桌照留' },
|
|
{ value: 'caution', label: '先撤员工桌,等忙完再说' },
|
|
],
|
|
},
|
|
'S01-H25': {
|
|
question: '孩子已经说饱了,碗里还有饭。您会怎么做?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '再撑也得把碗吃见底' },
|
|
{ value: 'caution', label: '说饱就停,下回先少盛' },
|
|
],
|
|
},
|
|
'S01-H26': {
|
|
question: '赵伯举着饭勺,还想给孩子添饭。您怎么拦?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '不问孩子,拿勺接着添' },
|
|
{ value: 'caution', label: '先问还要不要,同意再添' },
|
|
],
|
|
},
|
|
'S01-H27': {
|
|
question: '孩子的话还没说完,唐守安已经在看表。您怎么做?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '边看表边问,没听完就走' },
|
|
{ value: 'caution', label: '放下表,听孩子把话说完' },
|
|
],
|
|
},
|
|
'S01-H28': {
|
|
question: '饭馆忙起来,员工在哪儿吃、什么时候吃?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '清出员工桌,轮流坐下吃' },
|
|
{ value: 'caution', label: '客人一多,员工就站着吃' },
|
|
],
|
|
},
|
|
'S01-H29': {
|
|
question: '菜已经摆满,主家还怕别人说小气。您怎么劝?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '怕人说小气,再添几道菜' },
|
|
{ value: 'caution', label: '先看人数份量,真缺再添' },
|
|
],
|
|
},
|
|
'S01-H30': {
|
|
question: '剩菜要不要带,主家只怕没面子。您怎么接?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '觉得打包丢脸,剩菜全不要' },
|
|
{ value: 'caution', label: '先问哪些能带,再照提示存' },
|
|
],
|
|
},
|
|
'S01-H31': {
|
|
question: '套餐只写菜名,主家看不出够几个人。您先问什么?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '只看菜名,马上把套餐定下' },
|
|
{ value: 'caution', label: '问清盘量和人数,再决定' },
|
|
],
|
|
},
|
|
'S01-H32': {
|
|
question: '婚宴临时加桌,员工的座位怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '婚宴加桌,先把员工桌挪走' },
|
|
{ value: 'caution', label: '先留员工座位,再安排加桌' },
|
|
],
|
|
},
|
|
'S01-H33': {
|
|
question: '车钥匙还在桌上,酒杯又被推过来。您怎么选?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '还要开车,也尝一口白酒' },
|
|
{ value: 'caution', label: '撤下酒杯,换水再开车' },
|
|
],
|
|
},
|
|
'S01-H34': {
|
|
question: '唐守安已经拒酒,这桌人接下来怎么碰杯?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '递上热茶,照样碰杯聊天' },
|
|
{ value: 'caution', label: '继续劝酒,非喝一口才算' },
|
|
],
|
|
},
|
|
'S01-H35': {
|
|
question: '酒局要开始,赵伯想自己改药。您怎么劝?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '为了酒局,自己把药量改改' },
|
|
{ value: 'caution', label: '照原有安排,有疑问问医生药师' },
|
|
],
|
|
},
|
|
'S01-H36': {
|
|
question: '赵伯出汗、手抖、反应慢,眼前先做什么?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '先停酒移开杯子,留人陪着' },
|
|
{ value: 'caution', label: '还当醉酒,继续劝他喝' },
|
|
],
|
|
},
|
|
'S01-H37': {
|
|
question: '炊事员总在传菜口站着吃,怎么给她留顿饭?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '趁传菜空当,站着几口吃完' },
|
|
{ value: 'caution', label: '清出座位替班,坐下吃完' },
|
|
],
|
|
},
|
|
'S01-H38': {
|
|
question: '赵伯又拿大碗给明远添饭,您怎么接?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '拿大碗添满,夸能吃有本事' },
|
|
{ value: 'caution', label: '先问要不要,再按需要盛' },
|
|
],
|
|
},
|
|
'S01-H39': {
|
|
question: '订席电话又响,明远这顿冷饭怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '电话一响,就放下饭继续接' },
|
|
{ value: 'caution', label: '安排人替接,留出吃饭时间' },
|
|
],
|
|
},
|
|
'S01-H40': {
|
|
question: '嘴上说给员工留桌,桌面却堆满酒箱。怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '酒箱照堆,嘴上说留着就行' },
|
|
{ value: 'caution', label: '清空桌面替班,让人坐下' },
|
|
],
|
|
},
|
|
'S01-H41': {
|
|
question: '“健康、互助”不能只挂墙上,还要落在哪儿?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '只把展板擦亮,服务照旧' },
|
|
{ value: 'caution', label: '把座位、纸单和帮忙安排好' },
|
|
],
|
|
},
|
|
'S01-H42': {
|
|
question: '旧桌有感情,可还没检查。秦师傅先怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '舍不得旧桌,直接给客人用' },
|
|
{ value: 'caution', label: '先停用检查,再决定怎么留' },
|
|
],
|
|
},
|
|
'S01-H43': {
|
|
question: '饭店翻建,材料和行人通道怎么分?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '围好材料区,给人留通道' },
|
|
{ value: 'caution', label: '材料靠通道堆,大家绕着走' },
|
|
],
|
|
},
|
|
'S01-H44': {
|
|
question: '旧桌不能再用,留下来的物件怎么收?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '登记来源,把桌板铜牌存好' },
|
|
{ value: 'caution', label: '签完处置单,旧物随手丢' },
|
|
],
|
|
},
|
|
'S01-H45': {
|
|
question: '库房钥匙在手,开柜前先做什么?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '先问同意,再逐件拍照核对' },
|
|
{ value: 'caution', label: '拿钥匙就开柜,只看一个孔' },
|
|
],
|
|
},
|
|
'S01-H46': {
|
|
question: '饭店用了扫码点餐,老人不熟手机怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '扫码旁摆纸单,留人工点餐和现金' },
|
|
{ value: 'caution', label: '只留扫码牌,让老人自己试' },
|
|
],
|
|
},
|
|
'S01-H47': {
|
|
question: '十个人点了十四道菜,赵伯还嫌数字不好听。怎么劝?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '嫌十四不好听,再加两道菜' },
|
|
{ value: 'caution', label: '看人数份量,真不够再添' },
|
|
],
|
|
},
|
|
'S01-H48': {
|
|
question: '菜单写着“炖、小份”,您会怎么用这些信息?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '看见炖和小份,就当健康保证' },
|
|
{ value: 'caution', label: '把做法份量当信息来选' },
|
|
],
|
|
},
|
|
'S01-H49': {
|
|
question: '三双公筷一起伸向乐乐,您先说什么?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '都别停,一起往孩子碗里夹' },
|
|
{ value: 'caution', label: '先问想不想吃,同意再夹' },
|
|
],
|
|
},
|
|
'S01-H50': {
|
|
question: '大家担心赵伯,座位该怎么安排?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '没问赵伯,就把他挪到侧桌' },
|
|
{ value: 'caution', label: '先问想坐哪,再在同桌调整' },
|
|
],
|
|
},
|
|
'S01-H51': {
|
|
question: '明远叫孩子少喝甜饮,自己却照喝。全家怎么办?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '只管孩子少喝,自己照喝甜饮' },
|
|
{ value: 'caution', label: '全家一起换,让孩子参与选' },
|
|
],
|
|
},
|
|
'S01-H52': {
|
|
question: '小满手里拿着软尺,先量座位还是先问赵伯?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '先问赵伯想坐哪,再搬椅子' },
|
|
{ value: 'caution', label: '替他拿主意,直接挪座位' },
|
|
],
|
|
},
|
|
'S01-H53': {
|
|
question: '有人想把热汤面叫“祖传降糖面”,您怎么接?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '把老汤面说成祖传降糖菜' },
|
|
{ value: 'caution', label: '只讲食材做法,不说能降糖' },
|
|
],
|
|
},
|
|
'S01-H54': {
|
|
question: '票据、铜牌、照片都摆齐了,怎么认这张旧桌?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '票据铜牌照片一起核对' },
|
|
{ value: 'caution', label: '只凭一处红漆就定来历' },
|
|
],
|
|
},
|
|
'S01-H55': {
|
|
question: '秦师傅要说当年的事,唐守安坐哪里、怎么听?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '让秦师傅说完,唐大夫坐旁边听' },
|
|
{ value: 'caution', label: '请唐大夫坐主位,替他说' },
|
|
],
|
|
},
|
|
'S01-H56': {
|
|
question: '这桌人饮品各不相同,怎么碰杯?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '各拿自己的饮品,照样碰杯' },
|
|
{ value: 'caution', label: '把茶换成酒,再劝一轮' },
|
|
],
|
|
},
|
|
'S01-H57': {
|
|
question: '一家人饭量不一样,菜单份量怎么摆?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '摆出三种份量,按人数来选' },
|
|
{ value: 'caution', label: '只留一种大份,大家统一点' },
|
|
],
|
|
},
|
|
'S01-H58': {
|
|
question: '开席前,桌上的饮品怎么摆?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '先摆白水,其他有人要再拿' },
|
|
{ value: 'caution', label: '酒和甜饮先摆满桌' },
|
|
],
|
|
},
|
|
'S01-H59': {
|
|
question: '明远的公筷停在半空,下一步怎么做?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '筷子先停住,问孩子还要不要' },
|
|
{ value: 'caution', label: '不等开口,直接夹进碗' },
|
|
],
|
|
},
|
|
'S01-H60': {
|
|
question: '桌上有剩菜,员工先怎么打包?',
|
|
choiceOptions: [
|
|
{ value: 'safer', label: '不管是什么,剩菜全打包' },
|
|
{ value: 'caution', label: '逐样问清能不能存,再打包' },
|
|
],
|
|
},
|
|
})
|
|
|
|
function chapterOneChoiceOptions(eventId, fallbackOptions = []) {
|
|
const eventCopy = CHAPTER_ONE_EVENT_COPY[String(eventId || '')]
|
|
const fixed = eventCopy && eventCopy.choiceOptions
|
|
if (fixed) return fixed.map((option) => ({ ...option }))
|
|
return (Array.isArray(fallbackOptions) ? fallbackOptions : []).map(
|
|
(option) => ({
|
|
value: option.value,
|
|
label: option.label,
|
|
}),
|
|
)
|
|
}
|
|
|
|
function storyFirstChoiceOptions(eventId, fallbackOptions = []) {
|
|
if (CHAPTER_ONE_EVENT_COPY[String(eventId || '')]) {
|
|
return chapterOneChoiceOptions(eventId, fallbackOptions)
|
|
}
|
|
const eventCopy = STORY_FIRST_EVENT_COPY[String(eventId || '')]
|
|
if (eventCopy && Array.isArray(eventCopy.choiceOptions)) {
|
|
return eventCopy.choiceOptions.map((option) => ({ ...option }))
|
|
}
|
|
return (Array.isArray(fallbackOptions) ? fallbackOptions : []).map(
|
|
(option) => ({
|
|
value: option.value,
|
|
label: option.value === 'safer'
|
|
? '按画里的做法接着办。'
|
|
: '先停一停,再看看手边的情况。',
|
|
}),
|
|
)
|
|
}
|
|
|
|
function storyFirstQuestion(sourceEvent, pageQuestion = '') {
|
|
const fixed = CHAPTER_ONE_EVENT_COPY[
|
|
String(sourceEvent && sourceEvent.hotspotId || '')
|
|
]
|
|
if (fixed && fixed.question) return fixed.question
|
|
const eventCopy = STORY_FIRST_EVENT_COPY[
|
|
String(sourceEvent && sourceEvent.hotspotId || '')
|
|
]
|
|
if (eventCopy && eventCopy.question) return eventCopy.question
|
|
const source = String(
|
|
pageQuestion || sourceEvent && sourceEvent.question || '',
|
|
)
|
|
for (const ending of [',这样更稳妥吗?', ',这样妥当吗?']) {
|
|
if (source.endsWith(ending)) {
|
|
return `${source.slice(0, -ending.length)},您会怎么接着办?`
|
|
}
|
|
}
|
|
return source
|
|
}
|
|
|
|
function chapterOneEventCopy(sourceEvent) {
|
|
if (!sourceEvent) return null
|
|
const fixed = CHAPTER_ONE_EVENT_COPY[String(sourceEvent.hotspotId || '')]
|
|
if (!fixed) return { ...sourceEvent }
|
|
const { choiceOptions, ...copy } = fixed
|
|
return {
|
|
...sourceEvent,
|
|
...copy,
|
|
choiceOptions: chapterOneChoiceOptions(
|
|
sourceEvent.hotspotId,
|
|
sourceEvent.options,
|
|
),
|
|
}
|
|
}
|
|
|
|
function chapterOneDisplayChapter(chapter) {
|
|
if (!chapter || chapter.chapterId !== 'S01-C01') return chapter
|
|
const sourceMoment = chapter.emotionMoment
|
|
if (!sourceMoment) return { ...chapter }
|
|
const sourceChoices = Array.isArray(sourceMoment.playerChoices)
|
|
? sourceMoment.playerChoices
|
|
: []
|
|
return {
|
|
...chapter,
|
|
emotionMoment: {
|
|
...sourceMoment,
|
|
prompt: '赵伯嘴上说没事。您会怎么请他一块儿查?',
|
|
playerChoices: CHAPTER_ONE_EMOTION_CHOICES.map((copy) => ({
|
|
...(sourceChoices.find(
|
|
(item) => item.choiceId === copy.choiceId,
|
|
) || {}),
|
|
...copy,
|
|
})),
|
|
},
|
|
}
|
|
}
|
|
|
|
function memoryCardSharePath(memoryCard) {
|
|
const cardId = String(memoryCard && memoryCard.cardId || '').trim()
|
|
if (!cardId) return ''
|
|
return (
|
|
`/pages/share/share?cardId=${encodeURIComponent(cardId)}`
|
|
+ `&source=${MEMORY_SHARE_SOURCE}`
|
|
)
|
|
}
|
|
|
|
function lifeReportPath(memoryCard) {
|
|
const cardId = String(memoryCard && memoryCard.cardId || '').trim()
|
|
if (!cardId) return ''
|
|
return `/pages/report/report?cardId=${encodeURIComponent(cardId)}`
|
|
}
|
|
|
|
function comicAssetIdForPage(page) {
|
|
const match = String(page && page.pageId).match(
|
|
/^S(\d+)-C(\d+)-P(\d+)$/,
|
|
)
|
|
if (!match) return ''
|
|
return `comic.s${match[1]}.c${match[2]}.p${match[3]}`
|
|
}
|
|
|
|
function formatTime(value) {
|
|
const seconds = Math.max(0, Math.floor(Number(value) || 0))
|
|
const minutes = Math.floor(seconds / 60)
|
|
return `${minutes}:${String(seconds % 60).padStart(2, '0')}`
|
|
}
|
|
|
|
function pageProgressData(readerState) {
|
|
return {
|
|
completedIds: [...readerState.completedEventIds],
|
|
}
|
|
}
|
|
|
|
function textLength(value) {
|
|
return [...String(value || '')].length
|
|
}
|
|
|
|
function pageActor(chapter, page) {
|
|
const people = chapter && Array.isArray(chapter.people)
|
|
? chapter.people
|
|
: []
|
|
const visualActorInstanceId = page
|
|
&& page.playableVisual
|
|
&& page.playableVisual.actorInstanceId
|
|
return people.find(
|
|
(person) => person.instanceId === (
|
|
(page && page.actorInstanceId) || visualActorInstanceId
|
|
),
|
|
) || null
|
|
}
|
|
|
|
function decisionResultNeedsScroll(
|
|
activeEvent,
|
|
resultMode,
|
|
metrics,
|
|
fontScale,
|
|
) {
|
|
if (!activeEvent || !resultMode || !metrics) return false
|
|
const compact = Boolean(metrics.compactHeight)
|
|
const horizontalPadding = compact ? 40 : 64
|
|
const backdropPadding = compact ? 16 : 24
|
|
const modalWidth = Math.max(
|
|
1,
|
|
Number(metrics.windowWidth || 0)
|
|
- Number(metrics.safeLeft || 0)
|
|
- Number(metrics.safeRight || 0)
|
|
- backdropPadding,
|
|
)
|
|
const panelWidth = Math.max(120, modalWidth * 0.4 - horizontalPadding)
|
|
const bodyFontSize = fontScale === 'xlarge'
|
|
? (compact ? 18 : 20)
|
|
: (compact ? 17 : 18)
|
|
const charactersPerLine = Math.max(
|
|
6,
|
|
Math.floor(panelWidth / bodyFontSize),
|
|
)
|
|
const bodyLength = resultMode === 'retry'
|
|
? textLength(activeEvent.retryHint)
|
|
: (
|
|
textLength(activeEvent.reason)
|
|
+ textLength(activeEvent.actionAdvice)
|
|
)
|
|
const estimatedLines = Math.max(
|
|
1,
|
|
Math.ceil(bodyLength / charactersPerLine),
|
|
)
|
|
const footerHeight = compact ? 70 : 82
|
|
const verticalPadding = compact ? 36 : 54
|
|
const availableHeight = Math.max(
|
|
90,
|
|
Number(metrics.windowHeight || 0)
|
|
- Number(metrics.modalTop || 0)
|
|
- Number(metrics.safeBottom || 0)
|
|
- footerHeight
|
|
- verticalPadding,
|
|
)
|
|
const fixedResultHeight = resultMode === 'correct'
|
|
? (compact ? 140 : 118)
|
|
: (compact ? 62 : 80)
|
|
const estimatedHeight = (
|
|
fixedResultHeight
|
|
+ estimatedLines * bodyFontSize * 1.48
|
|
)
|
|
return estimatedHeight > availableHeight
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
chapter: null,
|
|
chapterNumber: 1,
|
|
chapterCount: 15,
|
|
completedIds: [],
|
|
chapterFinished: false,
|
|
activeEvent: null,
|
|
activePerson: null,
|
|
decisionOpen: false,
|
|
textReadingOpen: false,
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
emotionResult: null,
|
|
fontScale: 'large',
|
|
audioPlaying: false,
|
|
audioLoading: false,
|
|
audioError: '',
|
|
audioCurrent: '0:00',
|
|
audioDuration: '0:37',
|
|
audioPercent: 0,
|
|
audioProgressStyle: 'width:0%',
|
|
chapterAudioAvailable: false,
|
|
chapterAudioSrc: '',
|
|
compactHeight: true,
|
|
windowHeight: 390,
|
|
pageStyle: '',
|
|
topbarStyle: '',
|
|
comicPageCount: 0,
|
|
currentPage: null,
|
|
currentPageIndex: 0,
|
|
currentPageNumber: 1,
|
|
currentPageId: '',
|
|
comicImageSrc: '',
|
|
comicImageFallback: '',
|
|
comicImageActorFallback: '',
|
|
comicImageUsingFallback: false,
|
|
comicImageUsesActorFallback: false,
|
|
comicImageError: '',
|
|
comicImageSource: '',
|
|
comicImageShouldResolveRemote: false,
|
|
comicPageImageMode: 'scaleToFill',
|
|
sharePreviewLocalPath: '',
|
|
comicPageImageStyle: '',
|
|
comicFocusImageStyle: '',
|
|
comicArtStageStyle: '',
|
|
comicPreviousDisabled: true,
|
|
comicNextDisabled: false,
|
|
comicNextLocked: false,
|
|
currentEventCompleted: false,
|
|
currentInteractionEnabled: false,
|
|
presentationMode: 'legacy-60-40-v1',
|
|
storyFirstComicMode: false,
|
|
chapterOneComicMode: false,
|
|
chapterOneBackOpen: false,
|
|
chapterOneEmotionOpen: false,
|
|
frontControlsRevealed: false,
|
|
frontHintVisible: false,
|
|
chapterOneReviewEvent: null,
|
|
decisionResultNeedsScroll: false,
|
|
memoryCollected: false,
|
|
memoryFamilyOpen: false,
|
|
},
|
|
|
|
onLoad(options = {}) {
|
|
const progress = getProgress()
|
|
const chapterNumber = normalizeChapterNumber(
|
|
options.chapter || progress.lastChapter || 1,
|
|
)
|
|
const settings = getSettings()
|
|
this._storyFrontTapHintSeen = settings.storyFrontTapHintSeen === true
|
|
this.setData({ fontScale: settings.fontScale || 'large' })
|
|
this.applyLayoutMetrics()
|
|
this.loadChapter(chapterNumber, options.replay === '1')
|
|
this.prepareSharePreview()
|
|
},
|
|
|
|
onResize(event = {}) {
|
|
this.applyLayoutMetrics(event.size)
|
|
},
|
|
|
|
onShow() {
|
|
this._audioPageHidden = false
|
|
if (!this._audioCompanionNavigationLocked) return
|
|
this._audioCompanionNavigationLocked = false
|
|
this.setData({ audioLoading: false })
|
|
},
|
|
|
|
onHide() {
|
|
this._audioPageHidden = true
|
|
if (this.audioContext && this.data.audioLoading) {
|
|
this.destroyAudio()
|
|
return
|
|
}
|
|
this.pauseAudio(true)
|
|
},
|
|
|
|
onUnload() {
|
|
this.destroyAudio()
|
|
},
|
|
|
|
prepareSharePreview() {
|
|
const readyPath = String(this.data.sharePreviewLocalPath || '').trim()
|
|
if (readyPath) return Promise.resolve(readyPath)
|
|
if (this._sharePreviewPromise) return this._sharePreviewPromise
|
|
const userDataPath = String(
|
|
wx.env && wx.env.USER_DATA_PATH || '',
|
|
).trim()
|
|
if (!userDataPath || typeof wx.getFileSystemManager !== 'function') {
|
|
return Promise.resolve('')
|
|
}
|
|
const destinationPath = `${userDataPath}/${SHARE_PREVIEW_CACHE_NAME}`
|
|
const fileSystem = wx.getFileSystemManager()
|
|
this._sharePreviewPromise = new Promise((resolve) => {
|
|
fileSystem.readFile({
|
|
filePath: SHARE_PREVIEW_IMAGE,
|
|
success: ({ data }) => {
|
|
fileSystem.writeFile({
|
|
filePath: destinationPath,
|
|
data,
|
|
success: () => resolve(destinationPath),
|
|
fail: () => resolve(''),
|
|
})
|
|
},
|
|
fail: () => resolve(''),
|
|
})
|
|
}).then((localPath) => {
|
|
this._sharePreviewPromise = null
|
|
if (localPath) this.setData({ sharePreviewLocalPath: localPath })
|
|
return localPath
|
|
})
|
|
return this._sharePreviewPromise
|
|
},
|
|
|
|
loadChapter(chapterNumber, replayFromStart = false) {
|
|
const chapter = chapterOneDisplayChapter(
|
|
season.chapters[chapterNumber - 1],
|
|
)
|
|
const comicModel = buildComicPageModel(chapter, chapterNumber)
|
|
const storyFirstComicMode = (
|
|
comicModel.presentationMode === STORY_FIRST_PRESENTATION
|
|
)
|
|
const chapterOneComicMode = Boolean(
|
|
storyFirstComicMode && chapter.chapterId === 'S01-C01',
|
|
)
|
|
const chapterAudio = buildReviewedAudioState(
|
|
chapter.audio,
|
|
releaseAssets,
|
|
)
|
|
this._comicModel = comicModel
|
|
const storedProgress = getProgress()
|
|
const storedReaderProgress = readComicReaderProgress(
|
|
storedProgress,
|
|
comicModel,
|
|
chapterNumber,
|
|
)
|
|
if (replayFromStart) {
|
|
storedReaderProgress.currentPageId = comicModel.firstPageId
|
|
if (storyFirstComicMode) {
|
|
storedReaderProgress.completedEventIds = []
|
|
storedReaderProgress.chapterFinished = false
|
|
}
|
|
}
|
|
const readerState = buildComicReaderState(
|
|
comicModel,
|
|
storedReaderProgress,
|
|
)
|
|
const normalizedStorage = mergeComicReaderProgress(
|
|
storedProgress,
|
|
comicModel,
|
|
chapterNumber,
|
|
readerState,
|
|
)
|
|
saveProgress(normalizedStorage)
|
|
const collectedMemoryCards = Array.isArray(
|
|
normalizedStorage.collectedMemoryCards,
|
|
)
|
|
? normalizedStorage.collectedMemoryCards
|
|
: []
|
|
const chapterMemoryPage = comicModel.pageSequence.find(
|
|
(page) => page.type === 'memory',
|
|
)
|
|
const chapterMemoryCardId = chapterMemoryPage
|
|
&& chapterMemoryPage.memoryCard
|
|
? chapterMemoryPage.memoryCard.cardId
|
|
: ''
|
|
this.setData({
|
|
chapter,
|
|
chapterNumber,
|
|
chapterCount: season.chapters.length,
|
|
...pageProgressData(readerState),
|
|
chapterFinished: readerState.chapterFinished,
|
|
activeEvent: null,
|
|
activePerson: null,
|
|
decisionOpen: false,
|
|
textReadingOpen: false,
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
decisionResultNeedsScroll: false,
|
|
memoryFamilyOpen: false,
|
|
emotionResult: null,
|
|
audioPlaying: false,
|
|
audioLoading: false,
|
|
audioError: '',
|
|
audioCurrent: '0:00',
|
|
audioDuration: formatTime(
|
|
chapterAudio.available ? chapter.audio.durationSeconds : 0,
|
|
),
|
|
audioPercent: 0,
|
|
audioProgressStyle: 'width:0%',
|
|
chapterAudioAvailable: chapterAudio.available,
|
|
chapterAudioSrc: chapterAudio.src,
|
|
comicPageCount: comicModel.pageSequence.length,
|
|
presentationMode: comicModel.presentationMode || 'legacy-60-40-v1',
|
|
storyFirstComicMode,
|
|
chapterOneComicMode,
|
|
chapterOneBackOpen: false,
|
|
chapterOneEmotionOpen: false,
|
|
frontControlsRevealed: false,
|
|
frontHintVisible: Boolean(
|
|
storyFirstComicMode && !this._storyFrontTapHintSeen,
|
|
),
|
|
memoryCollected: Boolean(
|
|
chapterMemoryCardId
|
|
&& collectedMemoryCards.includes(chapterMemoryCardId),
|
|
),
|
|
})
|
|
this.applyComicReaderState(readerState, false)
|
|
wx.pageScrollTo({ scrollTop: 0, duration: 0 })
|
|
},
|
|
|
|
getComicPageData(pageIndex, readerState = this._readerState) {
|
|
const model = this._comicModel
|
|
if (!model || model.mode !== 'comic' || !model.pageSequence.length) {
|
|
return {}
|
|
}
|
|
const safeIndex = Math.min(
|
|
model.pageSequence.length - 1,
|
|
Math.max(0, Number(pageIndex) || 0),
|
|
)
|
|
const page = model.pageSequence[safeIndex]
|
|
const assetId = comicAssetIdForPage(page)
|
|
const releaseAsset = assetId ? releaseAssets[assetId] : null
|
|
const imageState = buildComicImageState(
|
|
page,
|
|
releaseAsset,
|
|
this.data,
|
|
)
|
|
const currentPageActor = pageActor(this.data.chapter, page)
|
|
// C02-C15 full-page narration is remote-only and fail-closed. Legacy
|
|
// cue-level seeds may remain as provenance, but do not expose them as a
|
|
// complete page track. A page gets a player route only after both the page
|
|
// manifest and immutable release asset have independently been approved.
|
|
const chapterNumber = Number(this.data.chapterNumber) || 1
|
|
const useRemotePageAudio = chapterNumber >= 2
|
|
const pageAudio = useRemotePageAudio
|
|
? { available: false, src: '' }
|
|
: buildReviewedAudioState(
|
|
{
|
|
status: page.audioStatus,
|
|
assetId: page.audioAssetId,
|
|
src: page.audioSrc,
|
|
},
|
|
releaseAssets,
|
|
)
|
|
const localAudioCompanion = (
|
|
CHAPTER_ONE_AUDIO_COMPANIONS[page.pageId] || null
|
|
)
|
|
const remoteAudioCompanion = useRemotePageAudio
|
|
? getApprovedRemotePageAudio(page.pageId, releaseAssets)
|
|
: null
|
|
const audioCompanion = localAudioCompanion || remoteAudioCompanion
|
|
const audioCompanionPath = localAudioCompanion
|
|
? `/${localAudioCompanion.packageRoot}/pages/player/player?pageId=${page.pageId}`
|
|
: (remoteAudioCompanion
|
|
? `/package-audio-player/pages/player/player?pageId=${encodeURIComponent(page.pageId)}&chapterNumber=${chapterNumber}`
|
|
: '')
|
|
const audioCueCount = Array.isArray(page.audioCueIds)
|
|
? page.audioCueIds.length
|
|
: 0
|
|
const completedEventIds = readerState
|
|
? readerState.completedEventIds
|
|
: this.data.completedIds
|
|
const eventCompleted = page.type === 'event'
|
|
? completedEventIds.includes(page.eventId)
|
|
: false
|
|
const activeInteraction = readerState
|
|
? readerState.activeInteraction
|
|
: null
|
|
const currentInteractionEnabled = Boolean(
|
|
page.type === 'event'
|
|
&& activeInteraction
|
|
&& activeInteraction.type === 'event'
|
|
&& activeInteraction.pageId === page.pageId
|
|
&& activeInteraction.eventId === page.eventId
|
|
)
|
|
const sourceReviewEvent = page.type === 'event'
|
|
? (this.data.chapter.events || []).find(
|
|
(item) => item.hotspotId === page.eventId,
|
|
) || null
|
|
: null
|
|
const chapterOneReviewEvent = (
|
|
sourceReviewEvent && this.data.chapterOneComicMode
|
|
)
|
|
? chapterOneEventCopy(sourceReviewEvent)
|
|
: sourceReviewEvent
|
|
return {
|
|
currentPage: {
|
|
...page,
|
|
illustrationAssetId: assetId,
|
|
audioAvailable: pageAudio.available,
|
|
audioSrc: pageAudio.src,
|
|
audioCompanionAvailable: Boolean(audioCompanion),
|
|
audioCompanionPath,
|
|
audioCueCount,
|
|
audioPlanned: audioCueCount > 0,
|
|
audioDurationLabel: formatTime(
|
|
audioCompanion
|
|
? audioCompanion.durationSeconds
|
|
: page.audioDurationSeconds,
|
|
),
|
|
eventCompleted,
|
|
displayCaption: (
|
|
eventCompleted && page.resolvedCaption
|
|
? page.resolvedCaption
|
|
: page.caption
|
|
),
|
|
},
|
|
currentPageIndex: safeIndex,
|
|
currentPageNumber: safeIndex + 1,
|
|
currentPageId: page.pageId,
|
|
comicImageSrc: imageState.src,
|
|
comicImageFallback: imageState.fallback,
|
|
comicImageActorFallback: imageState.actorFallback,
|
|
comicImageUsingFallback: imageState.usingFallback,
|
|
comicImageUsesActorFallback: imageState.usingActorFallback,
|
|
comicImageError: imageState.error,
|
|
comicImageSource: imageState.source,
|
|
comicImageShouldResolveRemote: imageState.shouldResolveRemote,
|
|
comicPageImageMode: getComicPageImageMode(
|
|
currentPageActor,
|
|
imageState.usingActorFallback,
|
|
),
|
|
comicPageImageStyle: getComicPageImageStyle(
|
|
currentPageActor,
|
|
imageState.usingActorFallback,
|
|
),
|
|
comicArtStageStyle: getComicArtStageStyle(
|
|
this._layoutMetrics,
|
|
page,
|
|
),
|
|
comicPreviousDisabled: readerState
|
|
? !readerState.canGoPrevious
|
|
: safeIndex <= 0,
|
|
comicNextDisabled: safeIndex >= model.pageSequence.length - 1,
|
|
comicNextLocked: readerState
|
|
? readerState.nextPageLocked
|
|
: false,
|
|
currentEventCompleted: eventCompleted,
|
|
currentInteractionEnabled,
|
|
chapterOneReviewEvent,
|
|
}
|
|
},
|
|
|
|
applyComicReaderState(readerState, persist = true) {
|
|
if (!readerState || !readerState.valid) return
|
|
const pageData = this.getComicPageData(
|
|
readerState.currentPageIndex,
|
|
readerState,
|
|
)
|
|
if (!pageData.currentPage) return
|
|
const pageChanged = Boolean(
|
|
this.data.currentPageId
|
|
&& this.data.currentPageId !== pageData.currentPageId
|
|
)
|
|
const nextAudioKey = `page:${pageData.currentPage.pageId}`
|
|
if (
|
|
this._audioProgressKey
|
|
&& this._audioProgressKey.startsWith('page:')
|
|
&& this._audioProgressKey !== nextAudioKey
|
|
) {
|
|
this.destroyAudio()
|
|
}
|
|
this._readerState = readerState
|
|
this._readerProgress = {
|
|
currentPageId: readerState.currentPageId,
|
|
completedEventIds: [...readerState.completedEventIds],
|
|
chapterFinished: readerState.chapterFinished,
|
|
}
|
|
this.setData({
|
|
...pageData,
|
|
...pageProgressData(readerState),
|
|
chapterFinished: readerState.chapterFinished,
|
|
textReadingOpen: false,
|
|
...(pageChanged ? {
|
|
chapterOneBackOpen: false,
|
|
chapterOneEmotionOpen: false,
|
|
frontControlsRevealed: false,
|
|
frontHintVisible: false,
|
|
chapterOneReviewEvent: pageData.chapterOneReviewEvent || null,
|
|
decisionOpen: false,
|
|
activeEvent: null,
|
|
activePerson: null,
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
emotionResult: null,
|
|
decisionResultNeedsScroll: false,
|
|
comicFocusImageStyle: '',
|
|
} : {}),
|
|
})
|
|
if (pageData.comicImageShouldResolveRemote) {
|
|
this.resolveComicPageAsset(
|
|
pageData.currentPage,
|
|
readerState.currentPageIndex,
|
|
)
|
|
}
|
|
if (persist) {
|
|
saveProgress(mergeComicReaderProgress(
|
|
getProgress(),
|
|
this._comicModel,
|
|
this.data.chapterNumber,
|
|
this._readerProgress,
|
|
))
|
|
}
|
|
this.ensureCurrentMemoryCollected(pageData.currentPage)
|
|
},
|
|
|
|
applyReaderAction(action, persist = true) {
|
|
if (!this._comicModel || !this._readerProgress) {
|
|
return this._readerState
|
|
}
|
|
const nextProgress = applyComicReaderAction(
|
|
this._comicModel,
|
|
this._readerProgress,
|
|
action,
|
|
)
|
|
const nextState = buildComicReaderState(
|
|
this._comicModel,
|
|
nextProgress,
|
|
)
|
|
this.applyComicReaderState(nextState, persist)
|
|
return nextState
|
|
},
|
|
|
|
refreshComicPage() {
|
|
if (!this._readerState) return
|
|
this.setData(this.getComicPageData(
|
|
this._readerState.currentPageIndex,
|
|
this._readerState,
|
|
))
|
|
},
|
|
|
|
getAssetManager() {
|
|
if (!this._assetManager) {
|
|
this._assetManager = createAssetManager({
|
|
assetPlatform: createAssetPlatformFacade(),
|
|
manifest: releaseAssets,
|
|
...assetReleaseConfig,
|
|
})
|
|
}
|
|
return this._assetManager
|
|
},
|
|
|
|
async resolveComicPageAsset(page, pageIndex) {
|
|
const assetId = comicAssetIdForPage(page)
|
|
const releaseAsset = assetId ? releaseAssets[assetId] : null
|
|
if (
|
|
!releaseAsset
|
|
|| releaseAsset.kind !== 'image'
|
|
|| releaseAsset.localSeed
|
|
|| !releaseAsset.remotePath
|
|
) return
|
|
const pageId = page.pageId
|
|
try {
|
|
const resolved = await this.getAssetManager().resolve(assetId)
|
|
if (
|
|
this.data.currentPageId === pageId
|
|
&& resolved.available
|
|
&& resolved.uri
|
|
) {
|
|
this.setData({
|
|
comicImageSrc: resolved.uri,
|
|
comicImageUsingFallback: false,
|
|
comicImageUsesActorFallback: false,
|
|
comicImageError: '',
|
|
comicImageSource: resolved.source,
|
|
comicPageImageMode: 'scaleToFill',
|
|
comicPageImageStyle: '',
|
|
comicFocusImageStyle: '',
|
|
})
|
|
} else if (
|
|
this.data.currentPageId === pageId
|
|
&& !this.data.comicImageSrc
|
|
) {
|
|
this.setData({
|
|
comicImageError: IMAGE_UNAVAILABLE_MESSAGE,
|
|
comicImageSource: 'text-fallback',
|
|
})
|
|
}
|
|
} catch (error) {
|
|
if (
|
|
this.data.currentPageId === pageId
|
|
&& !this.data.comicImageSrc
|
|
) {
|
|
this.setData({
|
|
comicImageError: IMAGE_UNAVAILABLE_MESSAGE,
|
|
comicImageSource: 'text-fallback',
|
|
})
|
|
}
|
|
}
|
|
|
|
const model = this._comicModel
|
|
if (!model || this.data.currentPageId !== pageId) return
|
|
const ahead = Math.max(
|
|
0,
|
|
Number(assetReleaseConfig.prefetchAhead) || 0,
|
|
)
|
|
const nextAssetIds = model.pageSequence
|
|
.slice(Number(pageIndex) + 1, Number(pageIndex) + 1 + ahead)
|
|
.map(comicAssetIdForPage)
|
|
.filter((nextAssetId) => (
|
|
releaseAssets[nextAssetId]
|
|
&& !releaseAssets[nextAssetId].localSeed
|
|
&& releaseAssets[nextAssetId].remotePath
|
|
))
|
|
if (nextAssetIds.length) {
|
|
this.getAssetManager().prefetch(nextAssetIds).catch(() => {})
|
|
}
|
|
},
|
|
|
|
onComicImageLoad() {
|
|
if (this.data.comicImageError) {
|
|
this.setData({ comicImageError: '' })
|
|
}
|
|
},
|
|
|
|
onComicImageError() {
|
|
const actorFallback = this.data.comicImageActorFallback
|
|
const fallback = this.data.comicImageFallback
|
|
const currentActor = pageActor(
|
|
this.data.chapter,
|
|
this.data.currentPage,
|
|
)
|
|
if (
|
|
actorFallback
|
|
&& this.data.comicImageSrc !== actorFallback
|
|
&& !this.data.comicImageUsesActorFallback
|
|
) {
|
|
this.setData({
|
|
comicImageSrc: actorFallback,
|
|
comicImageUsingFallback: true,
|
|
comicImageUsesActorFallback: true,
|
|
comicImageError: '',
|
|
comicImageSource: 'actor-fallback',
|
|
comicPageImageMode: getComicPageImageMode(
|
|
currentActor,
|
|
true,
|
|
),
|
|
comicPageImageStyle: getComicPageImageStyle(
|
|
currentActor,
|
|
true,
|
|
),
|
|
comicFocusImageStyle: getComicFocusImageStyle(
|
|
this.data.activePerson
|
|
|| pageActor(this.data.chapter, this.data.currentPage),
|
|
true,
|
|
),
|
|
})
|
|
return
|
|
}
|
|
if (
|
|
fallback
|
|
&& this.data.comicImageSrc !== fallback
|
|
&& (
|
|
!this.data.comicImageUsingFallback
|
|
|| this.data.comicImageUsesActorFallback
|
|
)
|
|
) {
|
|
this.setData({
|
|
comicImageSrc: fallback,
|
|
comicImageUsingFallback: true,
|
|
comicImageUsesActorFallback: false,
|
|
comicImageError: '',
|
|
comicImageSource: 'local-fallback',
|
|
comicPageImageMode: 'scaleToFill',
|
|
comicPageImageStyle: '',
|
|
comicFocusImageStyle: '',
|
|
})
|
|
return
|
|
}
|
|
this.setData({
|
|
comicImageSrc: '',
|
|
comicImageUsesActorFallback: false,
|
|
comicImageError: IMAGE_UNAVAILABLE_MESSAGE,
|
|
comicImageSource: 'text-fallback',
|
|
comicPageImageMode: 'scaleToFill',
|
|
comicPageImageStyle: '',
|
|
comicFocusImageStyle: '',
|
|
})
|
|
},
|
|
|
|
applyLayoutMetrics(resizeInfo = null) {
|
|
let windowInfo = {}
|
|
try {
|
|
windowInfo = wx.getWindowInfo
|
|
? wx.getWindowInfo()
|
|
: wx.getSystemInfoSync()
|
|
} catch (error) {
|
|
windowInfo = {}
|
|
}
|
|
if (resizeInfo) {
|
|
windowInfo = {
|
|
...windowInfo,
|
|
windowWidth: resizeInfo.windowWidth || windowInfo.windowWidth,
|
|
windowHeight: resizeInfo.windowHeight || windowInfo.windowHeight,
|
|
}
|
|
}
|
|
|
|
let menuRect = {}
|
|
try {
|
|
menuRect = wx.getMenuButtonBoundingClientRect
|
|
? wx.getMenuButtonBoundingClientRect()
|
|
: {}
|
|
} catch (error) {
|
|
menuRect = {}
|
|
}
|
|
|
|
const metrics = getChapterLayoutMetrics(windowInfo, menuRect)
|
|
this._layoutMetrics = metrics
|
|
const pageStyle = [
|
|
`height:${metrics.windowHeight}px`,
|
|
`--safe-left:${metrics.safeLeft}px`,
|
|
`--safe-right:${metrics.safeRight}px`,
|
|
`--safe-bottom:${metrics.safeBottom}px`,
|
|
`--modal-top:${metrics.modalTop}px`,
|
|
`--topbar-height:${metrics.topbarHeight}px`,
|
|
].join(';')
|
|
const topbarStyle = [
|
|
`height:${metrics.topbarHeight}px`,
|
|
`padding-top:${metrics.topPadding}px`,
|
|
`padding-right:${metrics.capsuleReserve}px`,
|
|
`padding-bottom:${metrics.bottomPadding}px`,
|
|
`padding-left:${metrics.leftInset}px`,
|
|
].join(';')
|
|
const nextData = {
|
|
compactHeight: metrics.compactHeight,
|
|
windowHeight: metrics.windowHeight,
|
|
pageStyle,
|
|
topbarStyle,
|
|
comicArtStageStyle: getComicArtStageStyle(
|
|
metrics,
|
|
this.data.currentPage,
|
|
),
|
|
decisionResultNeedsScroll: decisionResultNeedsScroll(
|
|
this.data.activeEvent,
|
|
this.data.resultMode,
|
|
metrics,
|
|
this.data.fontScale,
|
|
),
|
|
}
|
|
this.setData(nextData)
|
|
},
|
|
|
|
openCurrentFrontAction() {
|
|
const page = this.data.currentPage
|
|
if (
|
|
!this.data.storyFirstComicMode
|
|
|| !page
|
|
|| this.data.chapterOneBackOpen
|
|
|| this.data.decisionOpen
|
|
|| this.data.chapterOneEmotionOpen
|
|
|| !this.data.frontControlsRevealed
|
|
) return
|
|
if (page.type === 'event' && this.data.currentInteractionEnabled) {
|
|
this.openComicActor()
|
|
return
|
|
}
|
|
if (page.type === 'emotion') {
|
|
this.openChapterOneEmotion()
|
|
return
|
|
}
|
|
this.openChapterOneBack()
|
|
},
|
|
|
|
revealCurrentFrontControls() {
|
|
if (
|
|
!this.data.storyFirstComicMode
|
|
|| !this.data.currentPage
|
|
|| this.data.chapterOneBackOpen
|
|
|| this.data.decisionOpen
|
|
|| this.data.chapterOneEmotionOpen
|
|
|| this.data.frontControlsRevealed
|
|
) return
|
|
this.setData({
|
|
frontControlsRevealed: true,
|
|
frontHintVisible: false,
|
|
})
|
|
const settings = getSettings()
|
|
if (settings.storyFrontTapHintSeen === true) return
|
|
settings.storyFrontTapHintSeen = true
|
|
saveSettings(settings)
|
|
this._storyFrontTapHintSeen = true
|
|
},
|
|
|
|
openComicActor() {
|
|
const page = this.data.currentPage
|
|
const interaction = this._readerState
|
|
? this._readerState.activeInteraction
|
|
: null
|
|
if (
|
|
!page
|
|
|| page.type !== 'event'
|
|
|| !interaction
|
|
|| interaction.type !== 'event'
|
|
|| interaction.pageId !== page.pageId
|
|
|| interaction.eventId !== page.eventId
|
|
) return
|
|
if (this.data.storyFirstComicMode) {
|
|
if (!this.data.frontControlsRevealed) return
|
|
this.openChapterOneBack()
|
|
return
|
|
}
|
|
const person = this.data.chapter.people.find(
|
|
(item) => item.instanceId === page.actorInstanceId,
|
|
)
|
|
if (!person) return
|
|
this.openDecision(page.eventId, person, { question: page.question })
|
|
},
|
|
|
|
openChapterOneBack() {
|
|
const page = this.data.currentPage
|
|
if (
|
|
!this.data.storyFirstComicMode
|
|
|| !page
|
|
|| page.type === 'emotion'
|
|
|| !this.data.frontControlsRevealed
|
|
) return
|
|
this.setData({ chapterOneBackOpen: true })
|
|
},
|
|
|
|
startChapterOneDecisionFromBack() {
|
|
const page = this.data.currentPage
|
|
if (
|
|
!this.data.storyFirstComicMode
|
|
|| !this.data.chapterOneBackOpen
|
|
|| !page
|
|
|| page.type !== 'event'
|
|
|| this.data.currentEventCompleted
|
|
) return
|
|
const person = this.data.chapter.people.find(
|
|
(item) => item.instanceId === page.actorInstanceId,
|
|
)
|
|
if (!person) return
|
|
this.setData({ chapterOneBackOpen: false })
|
|
this.openDecision(page.eventId, person, { question: page.question })
|
|
},
|
|
|
|
closeChapterOneBack() {
|
|
this.setData({
|
|
chapterOneBackOpen: false,
|
|
frontControlsRevealed: false,
|
|
frontHintVisible: false,
|
|
})
|
|
},
|
|
|
|
nextChapterOnePageFromBack() {
|
|
this.setData({ chapterOneBackOpen: false })
|
|
this.nextComicPage()
|
|
},
|
|
|
|
openChapterOneEmotion() {
|
|
const page = this.data.currentPage
|
|
const readerState = this._readerState
|
|
const interaction = readerState ? readerState.activeInteraction : null
|
|
if (
|
|
!this.data.storyFirstComicMode
|
|
|| !page
|
|
|| page.type !== 'emotion'
|
|
|| !this.data.frontControlsRevealed
|
|
) return
|
|
if (
|
|
interaction
|
|
&& interaction.type === 'emotion'
|
|
&& interaction.pageId === page.pageId
|
|
) {
|
|
this.setData({ chapterOneEmotionOpen: true })
|
|
return
|
|
}
|
|
// 已完成章节回看 P07 时,画面仍应是一条真实可走的翻页路径。
|
|
// 不能继续绑定一个已经没有 activeInteraction 的空操作。
|
|
if (readerState && readerState.chapterFinished && readerState.canGoNext) {
|
|
this.applyReaderAction({ type: 'next-page' })
|
|
}
|
|
},
|
|
|
|
closeChapterOneEmotion() {
|
|
this.setData({
|
|
chapterOneEmotionOpen: false,
|
|
emotionResult: null,
|
|
frontControlsRevealed: false,
|
|
frontHintVisible: false,
|
|
})
|
|
},
|
|
|
|
openTextReading() {
|
|
if (!this.data.currentPage) return
|
|
this.setData({ textReadingOpen: true })
|
|
},
|
|
|
|
closeTextReading() {
|
|
this.setData({ textReadingOpen: false })
|
|
},
|
|
|
|
openDecision(eventId, person, displayOverrides = {}) {
|
|
const interaction = this._readerState
|
|
? this._readerState.activeInteraction
|
|
: null
|
|
if (
|
|
!interaction
|
|
|| interaction.type !== 'event'
|
|
|| interaction.eventId !== eventId
|
|
|| interaction.pageId !== this.data.currentPageId
|
|
) return
|
|
const sourceEvent = this.data.chapter.events.find(
|
|
(item) => item.hotspotId === eventId,
|
|
)
|
|
if (!sourceEvent) return
|
|
const activeEvent = {
|
|
...(this.data.chapterOneComicMode
|
|
? chapterOneEventCopy(sourceEvent)
|
|
: sourceEvent),
|
|
...displayOverrides,
|
|
...(this.data.storyFirstComicMode
|
|
? {
|
|
question: storyFirstQuestion(
|
|
sourceEvent,
|
|
displayOverrides.question,
|
|
),
|
|
}
|
|
: {}),
|
|
choiceOptions: this.data.storyFirstComicMode
|
|
? storyFirstChoiceOptions(
|
|
sourceEvent.hotspotId,
|
|
sourceEvent.options,
|
|
)
|
|
: [],
|
|
}
|
|
this.setData({
|
|
activeEvent,
|
|
activePerson: person,
|
|
decisionOpen: true,
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
decisionResultNeedsScroll: false,
|
|
comicFocusImageStyle: getComicFocusImageStyle(
|
|
person,
|
|
this.data.comicImageUsesActorFallback,
|
|
),
|
|
})
|
|
},
|
|
|
|
answer(event) {
|
|
const value = event.currentTarget.dataset.value
|
|
const activeEvent = this.data.activeEvent
|
|
const interaction = this._readerState
|
|
? this._readerState.activeInteraction
|
|
: null
|
|
if (
|
|
!activeEvent
|
|
|| this.data.resultMode
|
|
|| !interaction
|
|
|| interaction.type !== 'event'
|
|
|| interaction.eventId !== activeEvent.hotspotId
|
|
|| interaction.pageId !== this.data.currentPageId
|
|
) return
|
|
|
|
if (value !== activeEvent.correctAnswer) {
|
|
this.setData({
|
|
resultMode: 'retry',
|
|
decisionResultNeedsScroll: decisionResultNeedsScroll(
|
|
activeEvent,
|
|
'retry',
|
|
this._layoutMetrics,
|
|
this.data.fontScale,
|
|
),
|
|
})
|
|
wx.vibrateShort({ type: 'light' })
|
|
return
|
|
}
|
|
|
|
const needsSafetyDetail = activeEvent.hotspotId === 'S01-H36'
|
|
if (!needsSafetyDetail && !this.data.storyFirstComicMode) {
|
|
const nextState = this.applyReaderAction({
|
|
type: 'complete-event',
|
|
eventId: activeEvent.hotspotId,
|
|
})
|
|
if (!nextState.completedEventIds.includes(activeEvent.hotspotId)) return
|
|
}
|
|
this.setData({
|
|
resultMode: 'correct',
|
|
resultDetailOpen: false,
|
|
decisionResultNeedsScroll: needsSafetyDetail
|
|
? false
|
|
: decisionResultNeedsScroll(
|
|
activeEvent,
|
|
'correct',
|
|
this._layoutMetrics,
|
|
this.data.fontScale,
|
|
),
|
|
})
|
|
wx.vibrateShort({ type: 'light' })
|
|
},
|
|
|
|
retryAnswer() {
|
|
this.setData({
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
decisionResultNeedsScroll: false,
|
|
})
|
|
},
|
|
|
|
openResultDetail() {
|
|
const activeEvent = this.data.activeEvent
|
|
if (
|
|
this.data.resultMode !== 'correct'
|
|
|| !activeEvent
|
|
|| activeEvent.hotspotId !== 'S01-H36'
|
|
) return
|
|
this.setData({
|
|
resultDetailOpen: true,
|
|
decisionResultNeedsScroll: decisionResultNeedsScroll(
|
|
activeEvent,
|
|
'correct',
|
|
this._layoutMetrics,
|
|
this.data.fontScale,
|
|
),
|
|
})
|
|
},
|
|
|
|
continueAfterDecision() {
|
|
if (
|
|
this.data.storyFirstComicMode
|
|
&& this.data.resultMode === 'correct'
|
|
) {
|
|
if (
|
|
this.data.activeEvent
|
|
&& this.data.activeEvent.hotspotId === 'S01-H36'
|
|
&& !this.data.resultDetailOpen
|
|
) {
|
|
this.openResultDetail()
|
|
return
|
|
}
|
|
this.finishChapterOneDecision(false)
|
|
return
|
|
}
|
|
const activeEvent = this.data.activeEvent
|
|
if (
|
|
this.data.resultMode === 'correct'
|
|
&& activeEvent
|
|
&& activeEvent.hotspotId === 'S01-H36'
|
|
) {
|
|
if (!this.data.resultDetailOpen) {
|
|
this.openResultDetail()
|
|
return
|
|
}
|
|
const nextState = this.applyReaderAction({
|
|
type: 'complete-event',
|
|
eventId: activeEvent.hotspotId,
|
|
})
|
|
if (!nextState.completedEventIds.includes(activeEvent.hotspotId)) return
|
|
}
|
|
this.setData({
|
|
decisionOpen: false,
|
|
activeEvent: null,
|
|
activePerson: null,
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
decisionResultNeedsScroll: false,
|
|
comicFocusImageStyle: '',
|
|
})
|
|
this.refreshComicPage()
|
|
},
|
|
|
|
closeDecision() {
|
|
if (
|
|
this.data.storyFirstComicMode
|
|
&& this.data.resultMode === 'correct'
|
|
) {
|
|
if (
|
|
this.data.activeEvent
|
|
&& this.data.activeEvent.hotspotId === 'S01-H36'
|
|
&& !this.data.resultDetailOpen
|
|
) {
|
|
this.openResultDetail()
|
|
return
|
|
}
|
|
this.finishChapterOneDecision(false)
|
|
return
|
|
}
|
|
if (this.data.resultMode === 'correct') {
|
|
if (
|
|
this.data.activeEvent
|
|
&& this.data.activeEvent.hotspotId === 'S01-H36'
|
|
&& !this.data.resultDetailOpen
|
|
) {
|
|
this.openResultDetail()
|
|
return
|
|
}
|
|
this.continueAfterDecision()
|
|
return
|
|
}
|
|
this.setData({
|
|
decisionOpen: false,
|
|
activeEvent: null,
|
|
activePerson: null,
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
decisionResultNeedsScroll: false,
|
|
comicFocusImageStyle: '',
|
|
chapterOneBackOpen: false,
|
|
frontControlsRevealed: false,
|
|
frontHintVisible: false,
|
|
})
|
|
},
|
|
|
|
finishChapterOneDecision(goNext) {
|
|
const activeEvent = this.data.activeEvent
|
|
if (
|
|
!this.data.storyFirstComicMode
|
|
|| this.data.resultMode !== 'correct'
|
|
|| !activeEvent
|
|
|| (
|
|
activeEvent.hotspotId === 'S01-H36'
|
|
&& !this.data.resultDetailOpen
|
|
)
|
|
) return
|
|
const nextState = this.applyReaderAction({
|
|
type: 'complete-event',
|
|
eventId: activeEvent.hotspotId,
|
|
})
|
|
if (!nextState.completedEventIds.includes(activeEvent.hotspotId)) return
|
|
this.setData({
|
|
decisionOpen: false,
|
|
activeEvent: null,
|
|
activePerson: null,
|
|
resultMode: '',
|
|
resultDetailOpen: false,
|
|
decisionResultNeedsScroll: false,
|
|
comicFocusImageStyle: '',
|
|
chapterOneBackOpen: false,
|
|
frontControlsRevealed: false,
|
|
frontHintVisible: false,
|
|
})
|
|
if (goNext) this.applyReaderAction({ type: 'next-page' })
|
|
else this.refreshComicPage()
|
|
},
|
|
|
|
returnFromChapterOneDecision() {
|
|
this.finishChapterOneDecision(false)
|
|
},
|
|
|
|
nextFromChapterOneDecision() {
|
|
this.finishChapterOneDecision(true)
|
|
},
|
|
|
|
chooseEmotion(event) {
|
|
const choiceId = event.currentTarget.dataset.choice
|
|
const moment = this.data.chapter.emotionMoment
|
|
const interaction = this._readerState
|
|
? this._readerState.activeInteraction
|
|
: null
|
|
if (
|
|
!interaction
|
|
|| interaction.type !== 'emotion'
|
|
|| interaction.emotionMomentId !== moment.emotionMomentId
|
|
|| interaction.pageId !== this.data.currentPageId
|
|
) return
|
|
const choice = moment.playerChoices.find((item) => item.choiceId === choiceId)
|
|
if (!choice) return
|
|
this.setData({
|
|
emotionResult: {
|
|
feedback: choice.characterFeedback,
|
|
echo: moment.tableEcho,
|
|
category: moment.echoCategory,
|
|
},
|
|
})
|
|
},
|
|
|
|
finishChapterOneEmotionAndTurn() {
|
|
const moment = this.data.chapter && this.data.chapter.emotionMoment
|
|
if (
|
|
!this.data.storyFirstComicMode
|
|
|| !moment
|
|
|| !this.data.emotionResult
|
|
) return
|
|
const nextState = this.applyReaderAction({
|
|
type: 'complete-emotion',
|
|
emotionMomentId: moment.emotionMomentId,
|
|
})
|
|
if (!nextState || !nextState.chapterFinished) return
|
|
if (nextState.canGoNext) {
|
|
this.applyReaderAction({ type: 'next-page' })
|
|
}
|
|
},
|
|
|
|
finishEmotion() {
|
|
const moment = this.data.chapter.emotionMoment
|
|
const nextState = this.applyReaderAction({
|
|
type: 'complete-emotion',
|
|
emotionMomentId: moment.emotionMomentId,
|
|
})
|
|
if (!nextState || !nextState.chapterFinished) return
|
|
// Completion unlocks P08 but deliberately leaves the reader on P07. The
|
|
// memory card opens only after the user's next explicit tap or page turn.
|
|
if (typeof wx.vibrateShort === 'function') {
|
|
wx.vibrateShort({ type: 'light' })
|
|
}
|
|
},
|
|
|
|
finishComicEmotion() {
|
|
if (!this.data.emotionResult && !this.data.chapterFinished) {
|
|
wx.showToast({ title: '先选一种陪伴方式', icon: 'none' })
|
|
return
|
|
}
|
|
if (this.data.chapterFinished) {
|
|
this.applyReaderAction({ type: 'next-page' })
|
|
return
|
|
}
|
|
this.finishEmotion()
|
|
},
|
|
|
|
previousComicPage() {
|
|
this.applyReaderAction({ type: 'previous-page' })
|
|
},
|
|
|
|
nextComicPage() {
|
|
const page = this.data.currentPage
|
|
if (!page) return
|
|
if (this._readerState && this._readerState.nextPageLocked) {
|
|
if (page.type === 'event') {
|
|
wx.showToast({
|
|
title: page.playableVisual.kind === 'evidence-composite'
|
|
? '先看看桌上的东西'
|
|
: '先看看画里发生了什么',
|
|
icon: 'none',
|
|
})
|
|
return
|
|
}
|
|
if (page.type === 'emotion') {
|
|
wx.showToast({ title: '先收下这一桌的回声', icon: 'none' })
|
|
return
|
|
}
|
|
}
|
|
if (
|
|
!this._readerState
|
|
|| !this._readerState.canGoNext
|
|
) {
|
|
return
|
|
}
|
|
this.applyReaderAction({ type: 'next-page' })
|
|
},
|
|
|
|
onComicTouchStart(event) {
|
|
const touch = event && event.touches && event.touches[0]
|
|
if (!touch) return
|
|
this._comicTouchStart = {
|
|
x: Number(touch.clientX) || 0,
|
|
y: Number(touch.clientY) || 0,
|
|
at: Date.now(),
|
|
}
|
|
},
|
|
|
|
onComicTouchEnd(event) {
|
|
const start = this._comicTouchStart
|
|
this._comicTouchStart = null
|
|
const touch = event && event.changedTouches && event.changedTouches[0]
|
|
if (!start || !touch) return
|
|
const deltaX = (Number(touch.clientX) || 0) - start.x
|
|
const deltaY = (Number(touch.clientY) || 0) - start.y
|
|
const elapsed = Date.now() - start.at
|
|
if (
|
|
elapsed > 900
|
|
|| Math.abs(deltaX) < 64
|
|
|| Math.abs(deltaX) < Math.abs(deltaY) * 1.35
|
|
) return
|
|
if (deltaX > 0) this.previousComicPage()
|
|
else this.nextComicPage()
|
|
},
|
|
|
|
collectMemoryCard() {
|
|
const page = this.data.currentPage
|
|
const card = page && page.memoryCard
|
|
if (!card || !card.cardId) return
|
|
const chapter = this.data.chapter || {}
|
|
const progress = addMemoryCard(getProgress(), card, {
|
|
chapterId: chapter.chapterId,
|
|
chapterNumber: this.data.chapterNumber || chapter.chapterNumber,
|
|
chapterTitle: chapter.title,
|
|
})
|
|
saveProgress(progress)
|
|
this.setData({ memoryCollected: true })
|
|
if (typeof wx.showToast === 'function') {
|
|
wx.showToast({
|
|
title: '已收进封面的桂香岁月',
|
|
icon: 'none',
|
|
})
|
|
}
|
|
if (typeof wx.vibrateShort === 'function') {
|
|
wx.vibrateShort({ type: 'light' })
|
|
}
|
|
},
|
|
|
|
ensureCurrentMemoryCollected(page) {
|
|
const card = page && page.type === 'memory' && page.memoryCard
|
|
if (!card || !card.cardId) return false
|
|
const existing = getProgress()
|
|
const collected = Array.isArray(existing.collectedMemoryCards)
|
|
? existing.collectedMemoryCards
|
|
: []
|
|
const snapshots = (
|
|
existing.memoryCardSnapshots
|
|
&& typeof existing.memoryCardSnapshots === 'object'
|
|
&& !Array.isArray(existing.memoryCardSnapshots)
|
|
)
|
|
? existing.memoryCardSnapshots
|
|
: {}
|
|
const alreadyComplete = (
|
|
collected.includes(card.cardId)
|
|
&& snapshots[card.cardId]
|
|
&& snapshots[card.cardId].cardId === card.cardId
|
|
)
|
|
if (alreadyComplete) {
|
|
if (!this.data.memoryCollected) {
|
|
this.setData({ memoryCollected: true })
|
|
}
|
|
return false
|
|
}
|
|
const chapter = this.data.chapter || {}
|
|
const progress = addMemoryCard(existing, card, {
|
|
chapterId: chapter.chapterId,
|
|
chapterNumber: this.data.chapterNumber || chapter.chapterNumber,
|
|
chapterTitle: chapter.title,
|
|
})
|
|
saveProgress(progress)
|
|
this.setData({ memoryCollected: true })
|
|
return true
|
|
},
|
|
|
|
openLifeReport() {
|
|
const page = this.data.currentPage
|
|
const card = page && page.type === 'memory' && page.memoryCard
|
|
const url = lifeReportPath(card)
|
|
if (!url) return
|
|
wx.navigateTo({ url })
|
|
},
|
|
|
|
toggleMemoryFamily() {
|
|
this.setData({ memoryFamilyOpen: !this.data.memoryFamilyOpen })
|
|
},
|
|
|
|
toggleComicPageAudio() {
|
|
const page = this.data.currentPage
|
|
if (page && page.audioCompanionAvailable && page.audioCompanionPath) {
|
|
if (this._audioCompanionNavigationLocked) return
|
|
this._audioCompanionNavigationLocked = true
|
|
this.setData({ audioLoading: true, audioError: '' })
|
|
wx.navigateTo({
|
|
url: page.audioCompanionPath,
|
|
fail: () => {
|
|
this._audioCompanionNavigationLocked = false
|
|
this.setData({
|
|
audioLoading: false,
|
|
audioError: '有声夹页暂时没有打开,请稍后再试。',
|
|
})
|
|
if (typeof wx.showToast === 'function') {
|
|
wx.showToast({
|
|
title: '有声夹页没打开,请稍后再试',
|
|
icon: 'none',
|
|
})
|
|
}
|
|
},
|
|
})
|
|
return
|
|
}
|
|
if (!page || !page.audioAvailable || !page.audioSrc) return
|
|
this.toggleAudioSource(
|
|
page.audioSrc,
|
|
`page:${page.pageId}`,
|
|
Number(page.audioDurationSeconds) || 0,
|
|
{
|
|
restartFromBeginning: true,
|
|
resumeSavedProgress: false,
|
|
persistProgress: false,
|
|
},
|
|
)
|
|
},
|
|
|
|
toggleFont() {
|
|
const fontScale = this.data.fontScale === 'large' ? 'xlarge' : 'large'
|
|
const settings = getSettings()
|
|
settings.fontScale = fontScale
|
|
saveSettings(settings)
|
|
this.setData({
|
|
fontScale,
|
|
decisionResultNeedsScroll: decisionResultNeedsScroll(
|
|
this.data.activeEvent,
|
|
this.data.resultMode,
|
|
this._layoutMetrics,
|
|
fontScale,
|
|
),
|
|
})
|
|
},
|
|
|
|
toggleAudio() {
|
|
const chapter = this.data.chapter
|
|
if (
|
|
!chapter
|
|
|| !this.data.chapterAudioAvailable
|
|
|| !this.data.chapterAudioSrc
|
|
) return
|
|
this.toggleAudioSource(
|
|
this.data.chapterAudioSrc,
|
|
chapter.chapterId,
|
|
Number(chapter.audio.durationSeconds) || 0,
|
|
)
|
|
},
|
|
|
|
toggleAudioSource(src, progressKey, durationSeconds, options = {}) {
|
|
if (!src || !progressKey) return
|
|
if (
|
|
this.audioContext
|
|
&& (
|
|
this._audioProgressKey !== progressKey
|
|
|| this._audioSrc !== src
|
|
)
|
|
) {
|
|
this.destroyAudio()
|
|
}
|
|
if (!this.audioContext) {
|
|
this.createAudioContext({
|
|
src,
|
|
progressKey,
|
|
durationSeconds,
|
|
resumeSavedProgress: options.resumeSavedProgress,
|
|
persistProgress: options.persistProgress,
|
|
})
|
|
this.setData({
|
|
audioCurrent: '0:00',
|
|
audioDuration: formatTime(durationSeconds),
|
|
audioPercent: 0,
|
|
audioProgressStyle: 'width:0%',
|
|
})
|
|
}
|
|
if (this.data.audioPlaying) {
|
|
this.audioContext.pause()
|
|
return
|
|
}
|
|
if (
|
|
options.restartFromBeginning
|
|
&& this.audioContext
|
|
&& Number(this.data.audioPercent) > 0
|
|
) {
|
|
this.audioContext.seek(0)
|
|
this.setData({
|
|
audioCurrent: '0:00',
|
|
audioPercent: 0,
|
|
audioProgressStyle: 'width:0%',
|
|
})
|
|
}
|
|
this.setData({ audioLoading: true, audioError: '' })
|
|
this.audioContext.play()
|
|
},
|
|
|
|
createAudioContext(options = {}) {
|
|
const src = String(options.src || '')
|
|
const progressKey = String(options.progressKey || '')
|
|
const durationSeconds = Math.max(
|
|
0,
|
|
Number(options.durationSeconds) || 0,
|
|
)
|
|
if (!src || !progressKey) return
|
|
const context = wx.createInnerAudioContext()
|
|
context.autoplay = false
|
|
context.src = src
|
|
this._audioProgressKey = progressKey
|
|
this._audioSrc = src
|
|
this._audioDurationSeconds = durationSeconds
|
|
this._audioResumeSavedProgress = options.resumeSavedProgress !== false
|
|
this._audioPersistProgress = options.persistProgress !== false
|
|
const isCurrentContext = () => this.audioContext === context
|
|
|
|
context.onCanplay(() => {
|
|
if (!isCurrentContext()) return
|
|
const saved = getAudioProgress()
|
|
const resumeAt = Number(saved[progressKey] || 0)
|
|
if (
|
|
this._audioResumeSavedProgress
|
|
&&
|
|
resumeAt > 1
|
|
&& durationSeconds > 2
|
|
&& resumeAt < durationSeconds - 2
|
|
) {
|
|
context.seek(resumeAt)
|
|
}
|
|
this.setData({ audioLoading: false, audioError: '' })
|
|
})
|
|
context.onPlay(() => {
|
|
if (!isCurrentContext()) {
|
|
context.pause()
|
|
return
|
|
}
|
|
if (this._audioPageHidden) {
|
|
context.pause()
|
|
this.setData({
|
|
audioPlaying: false,
|
|
audioLoading: false,
|
|
})
|
|
return
|
|
}
|
|
this.setData({ audioPlaying: true, audioLoading: false, audioError: '' })
|
|
})
|
|
context.onPause(() => {
|
|
if (!isCurrentContext()) return
|
|
this.setData({ audioPlaying: false, audioLoading: false })
|
|
this.saveCurrentAudioTime()
|
|
})
|
|
context.onStop(() => {
|
|
if (!isCurrentContext()) return
|
|
this.setData({ audioPlaying: false, audioLoading: false })
|
|
this.saveCurrentAudioTime()
|
|
})
|
|
context.onEnded(() => {
|
|
if (!isCurrentContext()) return
|
|
const saved = getAudioProgress()
|
|
if (this._audioPersistProgress) saved[progressKey] = 0
|
|
else delete saved[progressKey]
|
|
saveAudioProgress(saved)
|
|
const completedDuration = context.duration || durationSeconds
|
|
this.setData({
|
|
audioPlaying: false,
|
|
audioLoading: false,
|
|
audioCurrent: formatTime(completedDuration),
|
|
audioDuration: formatTime(completedDuration),
|
|
audioPercent: 100,
|
|
audioProgressStyle: 'width:100%',
|
|
})
|
|
})
|
|
context.onTimeUpdate(() => {
|
|
if (!isCurrentContext()) return
|
|
const duration = context.duration || durationSeconds
|
|
const current = context.currentTime || 0
|
|
this.setData({
|
|
audioCurrent: formatTime(current),
|
|
audioDuration: formatTime(duration),
|
|
audioPercent: duration > 0 ? Math.min(100, current / duration * 100) : 0,
|
|
audioProgressStyle: `width:${
|
|
duration > 0 ? Math.min(100, current / duration * 100) : 0
|
|
}%`,
|
|
})
|
|
})
|
|
context.onError(() => {
|
|
if (!isCurrentContext()) return
|
|
this.setData({
|
|
audioPlaying: false,
|
|
audioLoading: false,
|
|
audioError: '音频暂时没有打开,文字游戏仍可继续。',
|
|
})
|
|
})
|
|
this.audioContext = context
|
|
},
|
|
|
|
saveCurrentAudioTime() {
|
|
if (
|
|
!this.audioContext
|
|
|| !this._audioProgressKey
|
|
|| !this._audioPersistProgress
|
|
) return
|
|
const saved = getAudioProgress()
|
|
saved[this._audioProgressKey] = this.audioContext.currentTime || 0
|
|
saveAudioProgress(saved)
|
|
},
|
|
|
|
pauseAudio(force = false) {
|
|
if (!this.audioContext) return
|
|
if (force || this.data.audioPlaying || this.data.audioLoading) {
|
|
this.audioContext.pause()
|
|
this.setData({
|
|
audioPlaying: false,
|
|
audioLoading: false,
|
|
})
|
|
}
|
|
},
|
|
|
|
destroyAudio() {
|
|
if (!this.audioContext) return
|
|
this.saveCurrentAudioTime()
|
|
this.audioContext.destroy()
|
|
this.audioContext = null
|
|
this._audioProgressKey = ''
|
|
this._audioSrc = ''
|
|
this._audioDurationSeconds = 0
|
|
this._audioResumeSavedProgress = true
|
|
this._audioPersistProgress = true
|
|
this.setData({
|
|
audioPlaying: false,
|
|
audioLoading: false,
|
|
audioError: '',
|
|
audioCurrent: '0:00',
|
|
audioDuration: '0:00',
|
|
audioPercent: 0,
|
|
audioProgressStyle: 'width:0%',
|
|
})
|
|
},
|
|
|
|
previousChapter() {
|
|
if (this.data.chapterNumber <= 1) return
|
|
this.destroyAudio()
|
|
wx.redirectTo({
|
|
url: chapterRoute(this.data.chapterNumber - 1),
|
|
})
|
|
},
|
|
|
|
nextChapter() {
|
|
if (this.data.chapterNumber >= this.data.chapterCount) return
|
|
this.destroyAudio()
|
|
wx.redirectTo({
|
|
url: chapterRoute(this.data.chapterNumber + 1),
|
|
})
|
|
},
|
|
|
|
goCatalog() {
|
|
this.destroyAudio()
|
|
const pages = getCurrentPages()
|
|
const previous = pages[pages.length - 2]
|
|
if (previous && previous.route === 'pages/catalog/catalog') {
|
|
wx.navigateBack()
|
|
return
|
|
}
|
|
wx.redirectTo({ url: '/pages/catalog/catalog' })
|
|
},
|
|
|
|
onShareAppMessage(options = {}) {
|
|
const memoryCard = (
|
|
options.from === 'button'
|
|
&& this.data.currentPage
|
|
&& this.data.currentPage.type === 'memory'
|
|
)
|
|
? this.data.currentPage.memoryCard
|
|
: null
|
|
const sharePayload = {
|
|
title: memoryCard && memoryCard.familyLine
|
|
? `带回家的一句话:${memoryCard.familyLine}`
|
|
: `唐侦探·第${this.data.chapterNumber}回:${this.data.chapter.title}`,
|
|
path: memoryCardSharePath(memoryCard)
|
|
|| chapterRoute(this.data.chapterNumber),
|
|
}
|
|
const localPath = String(this.data.sharePreviewLocalPath || '').trim()
|
|
if (localPath) {
|
|
return {
|
|
...sharePayload,
|
|
imageUrl: localPath,
|
|
}
|
|
}
|
|
return {
|
|
...sharePayload,
|
|
promise: this.prepareSharePreview().then((resolvedPath) => (
|
|
resolvedPath
|
|
? { ...sharePayload, imageUrl: resolvedPath }
|
|
: sharePayload
|
|
)),
|
|
}
|
|
},
|
|
|
|
onShareTimeline() {
|
|
return {
|
|
title: `唐侦探·第${this.data.chapterNumber}回:${this.data.chapter.title}`,
|
|
query: `chapter=${this.data.chapterNumber}`,
|
|
}
|
|
},
|
|
})
|