@oh-story/dsh 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/lib/client.js +85 -35
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +41 -36
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
- package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
- package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
- package/lib/index.js +16 -7
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/README.md +98 -44
- package/lib/novel-to-game/README_ZH.md +100 -46
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
- package/lib/novel-to-game/manifest.json +303 -78
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
- package/lib/types/client/drama-production-view.d.ts +11 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +1 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +7 -2
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +108 -1
- package/lib/video-recap/README.en.md +18 -14
- package/lib/video-recap/README.md +18 -14
- package/lib/video-recap/manifest.json +297 -321
- package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
- package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
- package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
- package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
- package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
- package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
- package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
- package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
- package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
- package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
- package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
- package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
- package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
- package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
- package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
- package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
- package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
- package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
- package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
- package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
- package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
- package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
- package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
- package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
- package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
- package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
- package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
- package/lib/video-recap/skills/video-script/SKILL.md +9 -7
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
- package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
- package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
- package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
- package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
- package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
- package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
- package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
- package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
- package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
- package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
- package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
- package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
- package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
- package/package.json +45 -45
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
- /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// 覆盖层菜单:回看记录、存读档、设置、系统菜单与心意、鉴赏(CG/回想/音乐/结局)、退出。
|
|
2
|
+
import { CAST, CG, MUSIC, ENDINGS, ENDING_KIND } from './story/index.js';
|
|
3
|
+
import { HEROINES, ROUTE_MIN } from './adv.js';
|
|
4
|
+
import { config, saveConfig, global, loadSaves, deleteSave, SLOT_PAGES, SLOTS_PER_PAGE, DEFAULT_CONFIG } from './store.js';
|
|
5
|
+
import { audio } from './audio.js';
|
|
6
|
+
import { cgVisible, cgClass } from './stage.js';
|
|
7
|
+
import { story, toTitle, loadSnapshot, saveTo, startReplay, setAuto, setSkip, afterMenuClose, applyBoxAlpha } from './main.js';
|
|
8
|
+
|
|
9
|
+
const $ = (sel) => document.querySelector(sel);
|
|
10
|
+
const overlay = () => $('#overlay');
|
|
11
|
+
let current = null;
|
|
12
|
+
const trail = []; // 从哪一层进来的:Esc 与「返回」退回上一层
|
|
13
|
+
let savePage = 1;
|
|
14
|
+
let extrasTab = 'cg';
|
|
15
|
+
const backlog = [];
|
|
16
|
+
export let closedAt = 0;
|
|
17
|
+
|
|
18
|
+
export const menuOpen = () => current !== null;
|
|
19
|
+
export function pushBacklog(entry) { backlog.push(entry); if (backlog.length > 300) backlog.shift(); }
|
|
20
|
+
export function clearBacklog(entries = []) { backlog.splice(0, backlog.length, ...entries); }
|
|
21
|
+
export const backlogTail = () => backlog.slice(-80);
|
|
22
|
+
|
|
23
|
+
export function closeMenu() {
|
|
24
|
+
const was = current;
|
|
25
|
+
current = null;
|
|
26
|
+
trail.length = 0;
|
|
27
|
+
clearInterval(preview.timer);
|
|
28
|
+
overlay().hidden = true;
|
|
29
|
+
overlay().innerHTML = '';
|
|
30
|
+
closedAt = performance.now();
|
|
31
|
+
if (was === null) return;
|
|
32
|
+
audio.play(ingame() ? story.stage.bgm : 'title');
|
|
33
|
+
if (ingame()) afterMenuClose();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function openMenu(kind, sub) {
|
|
37
|
+
setAuto(false);
|
|
38
|
+
setSkip(false);
|
|
39
|
+
trail.length = 0;
|
|
40
|
+
if (kind === 'extras' && sub) extrasTab = sub;
|
|
41
|
+
if (['backlog', 'save', 'load'].includes(kind)) audio.se('page');
|
|
42
|
+
go(kind, false);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function go(kind, remember = true) {
|
|
46
|
+
if (remember && current) trail.push(current);
|
|
47
|
+
current = kind;
|
|
48
|
+
overlay().hidden = false;
|
|
49
|
+
overlay().className = `menu-${kind}`;
|
|
50
|
+
render();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function backMenu() {
|
|
54
|
+
const prev = trail.pop();
|
|
55
|
+
if (prev) { current = prev; overlay().className = `menu-${prev}`; render(); } else closeMenu();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function confirmBox(text, yes) {
|
|
59
|
+
confirmState.text = text;
|
|
60
|
+
confirmState.yes = yes;
|
|
61
|
+
if (current) go('confirm');
|
|
62
|
+
else { setAuto(false); setSkip(false); go('confirm', false); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ingame = () => document.body.dataset.screen === 'game';
|
|
66
|
+
const frame = (title, body, extra = '', cls = '') => `
|
|
67
|
+
<div class="panel ${cls}" role="dialog" aria-label="${title}">
|
|
68
|
+
<header><h2>${title}</h2>${extra}<button class="close" data-act="close" aria-label="返回">返回</button></header>
|
|
69
|
+
<div class="panel-body">${body}</div>
|
|
70
|
+
</div>`;
|
|
71
|
+
|
|
72
|
+
function render() {
|
|
73
|
+
const o = overlay();
|
|
74
|
+
clearInterval(preview.timer);
|
|
75
|
+
if (current === 'backlog') o.innerHTML = frame('回看', `<ol class="backlog">${backlog.map((b) => `<li class="${b.choice ? 'chosen' : ''}">${b.name ? `<b style="color:${b.color ?? '#6b4b2e'}">${b.name}</b>` : ''}<p>${b.text}</p></li>`).join('') || '<li><p>还没有读过的句子。</p></li>'}</ol>`);
|
|
76
|
+
else if (current === 'save' || current === 'load') o.innerHTML = frame(current === 'save' ? '存档' : '读档', slotsHtml(), pagesHtml(), 'slots-panel');
|
|
77
|
+
else if (current === 'config') o.innerHTML = frame('设置', configHtml());
|
|
78
|
+
else if (current === 'system') o.innerHTML = frame('菜单', systemHtml());
|
|
79
|
+
else if (current === 'peek') o.innerHTML = frame('心意', peekHtml());
|
|
80
|
+
else if (current === 'extras') o.innerHTML = frame('鉴赏', extrasHtml(), tabsHtml(), 'tall');
|
|
81
|
+
else if (current === 'exit') o.innerHTML = `<div class="panel small"><h2>已离开</h2><p>浏览器不允许网页自行关闭窗口,你可以直接关掉这个标签页。</p><div class="row"><button class="btn primary" data-act="close">回到标题</button></div></div>`;
|
|
82
|
+
else if (current === 'confirm') o.innerHTML = `<div class="panel small"><p>${confirmState.text}</p><div class="row"><button class="btn primary" data-act="yes">确定</button><button class="btn" data-act="close">取消</button></div></div>`;
|
|
83
|
+
else if (current === 'cgview') o.innerHTML = `<figure class="cgview" data-act="view"><div class="cg-frame"><img class="${cgClass(viewing)}" src="${CG[viewing].src}" alt="${CG[viewing].name}"></div><figcaption>${CG[viewing].name} · ← → 翻看 点中间关闭</figcaption></figure>`;
|
|
84
|
+
if (current === 'backlog') { const body = o.querySelector('.panel-body'); body.scrollTop = body.scrollHeight; }
|
|
85
|
+
if (current === 'config') startPreview();
|
|
86
|
+
o.querySelector('[data-focus], .panel button:not(.close), .panel button')?.focus({ preventScroll: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------- 存读档 ----------
|
|
90
|
+
function pagesHtml() {
|
|
91
|
+
const pages = [...Array(SLOT_PAGES)].map((_, i) => `<button class="tab ${savePage === i + 1 ? 'on' : ''}" data-page="${i + 1}">${i + 1}</button>`).join('');
|
|
92
|
+
return `<nav class="tabs">${pages}<button class="tab ${savePage === 'q' ? 'on' : ''}" data-page="q">快存/自动</button></nav>`;
|
|
93
|
+
}
|
|
94
|
+
const slotName = (id) => (id === 'quick' ? '快速存档' : id === 'auto' ? '自动存档' : `No.${id.padStart(2, '0')}`);
|
|
95
|
+
function slotsHtml() {
|
|
96
|
+
const saves = loadSaves();
|
|
97
|
+
const ids = savePage === 'q' ? ['quick', 'auto'] : [...Array(SLOTS_PER_PAGE)].map((_, i) => String((savePage - 1) * SLOTS_PER_PAGE + i + 1));
|
|
98
|
+
return `<div class="slots">${ids.map((id) => {
|
|
99
|
+
const s = saves[id];
|
|
100
|
+
const locked = current === 'save' && (id === 'auto' || !ingame() || story.replay);
|
|
101
|
+
return `<button class="slot ${s ? '' : 'empty'}" data-slot="${id}" ${(current === 'load' && !s) || locked ? 'disabled' : ''}>
|
|
102
|
+
<span class="thumb" ${s?.thumb ? `style="background-image:url(${s.thumb})"` : ''}>${s ? '' : '空'}</span>
|
|
103
|
+
<span class="meta"><b>${slotName(id)}</b><em>${s ? s.chapter : '——'}</em><small>${s ? s.date : ''}</small><q>${s?.text ?? ''}</q></span>
|
|
104
|
+
</button>`;
|
|
105
|
+
}).join('')}</div><p class="count slot-hint">右键点一份存档,可以删掉它。</p>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------- 设置 ----------
|
|
109
|
+
const preview = { timer: null };
|
|
110
|
+
const PREVIEW_LINE = '金莲:「官人,这字走得快些,还是慢些?」';
|
|
111
|
+
function startPreview() {
|
|
112
|
+
clearInterval(preview.timer);
|
|
113
|
+
const el = $('#cfg-type');
|
|
114
|
+
if (!el) return;
|
|
115
|
+
let start = performance.now();
|
|
116
|
+
preview.timer = setInterval(() => {
|
|
117
|
+
if (config.textSpeed >= 100) { el.textContent = PREVIEW_LINE; return; }
|
|
118
|
+
const shown = Math.floor(((performance.now() - start) * config.textSpeed) / 1000);
|
|
119
|
+
el.textContent = PREVIEW_LINE.slice(0, shown);
|
|
120
|
+
if (shown > PREVIEW_LINE.length + config.textSpeed) start = performance.now(); // 停一秒再重打
|
|
121
|
+
}, 16);
|
|
122
|
+
}
|
|
123
|
+
function configHtml() {
|
|
124
|
+
const out = (key) => (key === 'textSpeed' && config[key] >= 100 ? '瞬间' : config[key]);
|
|
125
|
+
const range = (key, label, min = 0, max = 100) => `<label class="cfg"><span>${label}</span><input type="range" min="${min}" max="${max}" value="${config[key]}" data-cfg="${key}"><output>${out(key)}</output></label>`;
|
|
126
|
+
const toggle = (key, label, hint) => `<label class="cfg toggle"><span>${label}<small>${hint}</small></span><input type="checkbox" ${config[key] ? 'checked' : ''} data-cfg="${key}"></label>`;
|
|
127
|
+
return `<div class="cfg-grid">
|
|
128
|
+
${range('textSpeed', '文字速度', 10)}
|
|
129
|
+
${range('autoSpeed', '自动速度')}
|
|
130
|
+
${range('bgm', '背景音乐')}
|
|
131
|
+
${range('se', '音效')}
|
|
132
|
+
${range('boxAlpha', '文字框浓淡', 40)}
|
|
133
|
+
${toggle('skipUnread', '快进未读文字', '关闭时,快进只跳过读过的句子')}
|
|
134
|
+
${toggle('adult', '成人画面', '关闭后,亲密 CG 以帘幕代替')}
|
|
135
|
+
</div>
|
|
136
|
+
<p class="cfg-preview"><span id="cfg-type"></span></p>
|
|
137
|
+
<p class="cfg-keys">点击推进 右键隐藏 滚轮上翻回看 按住 Ctrl 快进 A 自动 ← 上一选项 Esc 菜单</p>
|
|
138
|
+
<div class="row"><button class="btn" data-act="fullscreen">全屏切换</button><button class="btn" data-act="defaults">恢复默认</button></div>`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------- 系统菜单与心意 ----------
|
|
142
|
+
function systemHtml() {
|
|
143
|
+
return `<div class="sys">
|
|
144
|
+
<button class="btn" data-go="save" ${story.replay ? 'disabled' : ''}>存档</button>
|
|
145
|
+
<button class="btn" data-go="load">读档</button>
|
|
146
|
+
<button class="btn" data-go="config">设置</button>
|
|
147
|
+
<button class="btn" data-go="peek" ${story.replay ? 'disabled' : ''}>心意</button>
|
|
148
|
+
<button class="btn" data-go="backlog">回看记录</button>
|
|
149
|
+
<button class="btn" data-act="title">回到标题</button>
|
|
150
|
+
<button class="btn primary" data-act="close" data-focus>继续游戏</button>
|
|
151
|
+
</div>`;
|
|
152
|
+
}
|
|
153
|
+
// 心意一瞥只给她的神色和院门灯,不给档位
|
|
154
|
+
function peekHtml() {
|
|
155
|
+
return `<p class="peek-note">只是一眼。清明席散之后,只有还为你亮着灯的门,才会开。</p><div class="peek">${HEROINES.map((h) => {
|
|
156
|
+
const v = story.vars.aff[h] ?? 0;
|
|
157
|
+
const face = v >= 6 ? 'blush' : v >= 1 ? 'smile' : 'neutral';
|
|
158
|
+
const lit = v >= ROUTE_MIN;
|
|
159
|
+
return `<div class="peek-card" style="--accent:${CAST[h].color}"><img src="assets/sprite/${h}/${face}.webp" alt=""><b>${CAST[h].full}</b><span class="lantern ${lit ? 'lit' : 'dim'}">${lit ? '灯亮着' : '灯将熄'}</span></div>`;
|
|
160
|
+
}).join('')}</div>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------- 鉴赏 ----------
|
|
164
|
+
const TABS = { cg: 'CG', scene: '回想', music: '音乐', ending: '结局' };
|
|
165
|
+
function tabsHtml() {
|
|
166
|
+
return `<nav class="tabs">${Object.entries(TABS).map(([k, v]) => `<button class="tab ${extrasTab === k ? 'on' : ''}" data-tab="${k}">${v}</button>`).join('')}</nav>`;
|
|
167
|
+
}
|
|
168
|
+
const SCENES = [
|
|
169
|
+
['yue_night', 'yue_good', 'yue', '月下焚香'], ['pan_night', 'pan_good', 'pan', '琵琶不冷'], ['pinger_night', 'pinger_good', 'pinger', '钥匙在她手里'],
|
|
170
|
+
['meng_night', 'meng_good', 'meng', '她自己开的门'], ['xuee_night', 'xuee_good', 'xuee', '天亮前的两碗粥'],
|
|
171
|
+
];
|
|
172
|
+
const cgGroup = (id) => CG[id].group ?? HEROINES.find((h) => id.startsWith(`${h}_`)) ?? 'common';
|
|
173
|
+
const GROUPS = ['common', ...HEROINES];
|
|
174
|
+
// 鉴赏里的顺序:先共通,再按五人分组;大图左右翻看也按这个顺序
|
|
175
|
+
const galleryIds = () => Object.keys(CG).filter((id) => id !== 'title').sort((a, b) => GROUPS.indexOf(cgGroup(a)) - GROUPS.indexOf(cgGroup(b)));
|
|
176
|
+
const viewable = () => galleryIds().filter((id) => global.cg.has(id) && cgVisible(id));
|
|
177
|
+
function extrasHtml() {
|
|
178
|
+
if (extrasTab === 'cg') {
|
|
179
|
+
const ids = galleryIds();
|
|
180
|
+
const open = ids.filter((id) => global.cg.has(id)).length;
|
|
181
|
+
const groups = GROUPS.map((g) => {
|
|
182
|
+
const members = ids.filter((id) => cgGroup(id) === g);
|
|
183
|
+
const got = members.filter((id) => global.cg.has(id)).length;
|
|
184
|
+
return `<h3 class="cg-group">${g === 'common' ? '共通' : CAST[g].name}<small>${got} / ${members.length}</small></h3><div class="cg-grid">${members.map((id) => {
|
|
185
|
+
const unlocked = global.cg.has(id);
|
|
186
|
+
const show = unlocked && cgVisible(id);
|
|
187
|
+
return `<button class="cg-cell ${unlocked ? '' : 'locked'}" data-cg="${id}" ${show ? '' : 'disabled'}>${show ? `<span class="cg-thumb"><img loading="lazy" class="${cgClass(id)}" src="${CG[id].src}" alt=""></span>` : `<span>${unlocked ? '帘' : '?'}</span>`}<small>${unlocked ? CG[id].name : '未解锁'}</small></button>`;
|
|
188
|
+
}).join('')}</div>`;
|
|
189
|
+
}).join('');
|
|
190
|
+
return `<p class="count">已收集 ${open} / ${ids.length}</p>${groups}`;
|
|
191
|
+
}
|
|
192
|
+
if (extrasTab === 'scene') {
|
|
193
|
+
return `<div class="scene-list">${SCENES.map(([id, label, who, title]) => {
|
|
194
|
+
const ok = global.scene.has(id);
|
|
195
|
+
return `<button class="scene-cell" style="--accent:${CAST[who].color}" data-replay="${label}" ${ok ? '' : 'disabled'}><img src="assets/sprite/${who}/${ok ? 'blush' : 'neutral'}.webp" alt=""><b>${ok ? title : '???'}</b><small>${CAST[who].full} · ${ok ? '良缘之夜' : '走到她的良缘结局后解锁'}</small></button>`;
|
|
196
|
+
}).join('')}</div>`;
|
|
197
|
+
}
|
|
198
|
+
if (extrasTab === 'music') {
|
|
199
|
+
return `<ol class="music">${Object.entries(MUSIC).map(([id, name], i) => {
|
|
200
|
+
const ok = global.music.has(id);
|
|
201
|
+
return `<li><button class="btn ${ok && audio.track?.id === id ? 'primary' : ''}" data-music="${id}" ${ok ? '' : 'disabled'}>${String(i + 1).padStart(2, '0')} ${ok ? name : '???'}</button></li>`;
|
|
202
|
+
}).join('')}</ol>`;
|
|
203
|
+
}
|
|
204
|
+
const cell = (id) => {
|
|
205
|
+
const e = ENDINGS[id];
|
|
206
|
+
const ok = global.ending.has(id);
|
|
207
|
+
return `<div class="end-cell ${e.kind} ${ok ? 'ok' : ''}"><small>${ENDING_KIND[e.kind]}</small><b>${ok ? e.title : '???'}</b></div>`;
|
|
208
|
+
};
|
|
209
|
+
const total = Object.keys(ENDINGS).length;
|
|
210
|
+
return `<p class="count">已到达 ${[...global.ending].filter((id) => ENDINGS[id]).length} / ${total}</p>
|
|
211
|
+
<div class="flow"><div class="flow-root">共通线 · 序章~清明荷亭</div>
|
|
212
|
+
<div class="flow-routes">${HEROINES.map((h) => `<div class="flow-col" style="--accent:${CAST[h].color}"><div class="flow-head">${CAST[h].full}篇</div>${['good', 'normal', 'bad'].map((k) => cell(`${h}_${k}`)).join('')}</div>`).join('')}
|
|
213
|
+
<div class="flow-col lonely"><div class="flow-head">无人留灯</div>${cell('lonely')}</div></div></div>`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------- 交互 ----------
|
|
217
|
+
const confirmState = { text: '', yes: null };
|
|
218
|
+
let viewing = null;
|
|
219
|
+
function stepCg(dir) {
|
|
220
|
+
const ids = viewable();
|
|
221
|
+
if (!ids.length) return;
|
|
222
|
+
viewing = ids[(ids.indexOf(viewing) + dir + ids.length) % ids.length];
|
|
223
|
+
render();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
overlay().addEventListener('click', async (e) => {
|
|
227
|
+
const t = e.target.closest('button, figure');
|
|
228
|
+
if (!t) return;
|
|
229
|
+
audio.se('click');
|
|
230
|
+
const d = t.dataset;
|
|
231
|
+
if (d.act === 'close') backMenu();
|
|
232
|
+
else if (d.act === 'view') { const x = e.clientX / window.innerWidth; if (x < 1 / 3) stepCg(-1); else if (x > 2 / 3) stepCg(1); else backMenu(); }
|
|
233
|
+
else if (d.act === 'yes') { const fn = confirmState.yes; confirmState.yes = null; await fn?.(); }
|
|
234
|
+
else if (d.act === 'title') confirmBox('回到标题?未存档的进度会丢失。', toTitle);
|
|
235
|
+
else if (d.act === 'fullscreen') { if (document.fullscreenElement) document.exitFullscreen(); else document.documentElement.requestFullscreen?.(); }
|
|
236
|
+
else if (d.act === 'defaults') { Object.assign(config, DEFAULT_CONFIG, { adult: config.adult }); saveConfig(); audio.setVolumes(config.bgm, config.se); applyBoxAlpha(); render(); }
|
|
237
|
+
else if (d.go) go(d.go);
|
|
238
|
+
else if (d.page) { savePage = d.page === 'q' ? 'q' : Number(d.page); render(); }
|
|
239
|
+
else if (d.tab) { extrasTab = d.tab; render(); }
|
|
240
|
+
else if (d.slot) {
|
|
241
|
+
const record = loadSaves()[d.slot];
|
|
242
|
+
if (current === 'load') loadSnapshot(record);
|
|
243
|
+
else if (record) confirmBox(`覆盖 ${slotName(d.slot)} 的存档?`, async () => { await saveTo(d.slot); backMenu(); });
|
|
244
|
+
else { await saveTo(d.slot); render(); }
|
|
245
|
+
} else if (d.cg) { viewing = d.cg; go('cgview'); }
|
|
246
|
+
else if (d.replay) startReplay(d.replay);
|
|
247
|
+
else if (d.music) { audio.play(d.music); render(); }
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
overlay().addEventListener('contextmenu', (e) => {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
const slot = e.target.closest('[data-slot]')?.dataset.slot;
|
|
253
|
+
if (slot && loadSaves()[slot] && (current === 'save' || current === 'load')) {
|
|
254
|
+
confirmBox(`删除 ${slotName(slot)} 的存档?`, () => { deleteSave(slot); backMenu(); });
|
|
255
|
+
} else backMenu();
|
|
256
|
+
});
|
|
257
|
+
overlay().addEventListener('wheel', (e) => {
|
|
258
|
+
if (current !== 'backlog' || e.deltaY <= 0) return;
|
|
259
|
+
const body = overlay().querySelector('.panel-body');
|
|
260
|
+
if (body.scrollTop + body.clientHeight >= body.scrollHeight - 2) closeMenu();
|
|
261
|
+
}, { passive: true });
|
|
262
|
+
overlay().addEventListener('input', (e) => {
|
|
263
|
+
const key = e.target.dataset.cfg;
|
|
264
|
+
if (!key) return;
|
|
265
|
+
config[key] = e.target.type === 'checkbox' ? e.target.checked : Number(e.target.value);
|
|
266
|
+
saveConfig();
|
|
267
|
+
audio.setVolumes(config.bgm, config.se);
|
|
268
|
+
if (key === 'boxAlpha') applyBoxAlpha();
|
|
269
|
+
if (key === 'textSpeed') startPreview();
|
|
270
|
+
const out = e.target.parentElement.querySelector('output');
|
|
271
|
+
if (out) out.textContent = key === 'textSpeed' && config[key] >= 100 ? '瞬间' : config[key];
|
|
272
|
+
});
|
|
273
|
+
document.addEventListener('keydown', (e) => {
|
|
274
|
+
if (current !== 'cgview') return;
|
|
275
|
+
if (e.key === 'ArrowLeft') stepCg(-1);
|
|
276
|
+
else if (e.key === 'ArrowRight') stepCg(1);
|
|
277
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// 标题飘落桃花瓣(canvas),并让标题底图随指针轻微视差。系统要求减少动效时只画静止花瓣。
|
|
2
|
+
let raf = 0;
|
|
3
|
+
let petals = [];
|
|
4
|
+
const reduce = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
5
|
+
|
|
6
|
+
function petal(w, h, fresh) {
|
|
7
|
+
return {
|
|
8
|
+
x: Math.random() * w,
|
|
9
|
+
y: fresh ? Math.random() * h : -20,
|
|
10
|
+
r: 5 + Math.random() * 7,
|
|
11
|
+
vx: 0.3 + Math.random() * 0.8,
|
|
12
|
+
vy: 0.6 + Math.random() * 1.1,
|
|
13
|
+
a: Math.random() * Math.PI * 2,
|
|
14
|
+
va: (Math.random() - 0.5) * 0.04,
|
|
15
|
+
hue: 340 + Math.random() * 18,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function startPetals(canvas) {
|
|
20
|
+
stopPetals();
|
|
21
|
+
const ctx = canvas.getContext('2d');
|
|
22
|
+
const fit = () => {
|
|
23
|
+
canvas.width = canvas.clientWidth * devicePixelRatio;
|
|
24
|
+
canvas.height = canvas.clientHeight * devicePixelRatio;
|
|
25
|
+
};
|
|
26
|
+
fit();
|
|
27
|
+
window.addEventListener('resize', fit);
|
|
28
|
+
petals = Array.from({ length: 46 }, () => petal(canvas.width, canvas.height, true));
|
|
29
|
+
const draw = () => {
|
|
30
|
+
const { width: w, height: h } = canvas;
|
|
31
|
+
ctx.clearRect(0, 0, w, h);
|
|
32
|
+
for (const p of petals) {
|
|
33
|
+
if (!reduce()) {
|
|
34
|
+
p.x += p.vx * devicePixelRatio + Math.sin(p.a) * 0.4;
|
|
35
|
+
p.y += p.vy * devicePixelRatio;
|
|
36
|
+
p.a += p.va;
|
|
37
|
+
if (p.y > h + 20 || p.x > w + 20) Object.assign(p, petal(w, h, false), { x: Math.random() * w * 0.8 - w * 0.1 });
|
|
38
|
+
}
|
|
39
|
+
ctx.save();
|
|
40
|
+
ctx.translate(p.x, p.y);
|
|
41
|
+
ctx.rotate(p.a);
|
|
42
|
+
ctx.scale(1, 0.62 + Math.sin(p.a * 2) * 0.25);
|
|
43
|
+
ctx.fillStyle = `hsla(${p.hue}, 78%, 86%, 0.85)`;
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
ctx.ellipse(0, 0, p.r * devicePixelRatio, p.r * 0.62 * devicePixelRatio, 0, 0, Math.PI * 2);
|
|
46
|
+
ctx.fill();
|
|
47
|
+
ctx.restore();
|
|
48
|
+
}
|
|
49
|
+
raf = reduce() ? 0 : requestAnimationFrame(draw);
|
|
50
|
+
};
|
|
51
|
+
draw();
|
|
52
|
+
const bg = canvas.parentElement.querySelector('.title-bg');
|
|
53
|
+
stopPetals.move = (e) => {
|
|
54
|
+
const x = e.clientX / innerWidth - 0.5;
|
|
55
|
+
const y = e.clientY / innerHeight - 0.5;
|
|
56
|
+
bg.style.transform = `scale(1.06) translate(${-x * 14}px, ${-y * 10}px)`;
|
|
57
|
+
};
|
|
58
|
+
window.addEventListener('pointermove', stopPetals.move);
|
|
59
|
+
stopPetals.fit = fit;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function stopPetals() {
|
|
63
|
+
cancelAnimationFrame(raf);
|
|
64
|
+
raf = 0;
|
|
65
|
+
if (stopPetals.move) window.removeEventListener('pointermove', stopPetals.move);
|
|
66
|
+
if (stopPetals.fit) window.removeEventListener('resize', stopPetals.fit);
|
|
67
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// 舞台:背景交叉淡入、立绘进出场与差分、说话人高亮、CG 与成人画面门控、存档缩略图合成。
|
|
2
|
+
import { BG, CG, CAST, EXPRESSIONS } from './story/index.js';
|
|
3
|
+
import { config, ageConfirmed } from './store.js';
|
|
4
|
+
|
|
5
|
+
const $ = (sel) => document.querySelector(sel);
|
|
6
|
+
const POS_X = { left: 0.24, center: 0.5, right: 0.76 };
|
|
7
|
+
const FOCUS_ZOOM = 1.3;
|
|
8
|
+
|
|
9
|
+
export const spriteSrc = (who, expr) => `assets/sprite/${who}/${EXPRESSIONS.includes(expr) ? expr : 'neutral'}.webp`;
|
|
10
|
+
// 构图偏右、左侧留白的 CG:按 focus 裁到人物那一侧(舞台、鉴赏大图、缩略图共用)
|
|
11
|
+
export const cgClass = (id) => (CG[id]?.focus ? `focus-${CG[id].focus}` : '');
|
|
12
|
+
export const cgVisible = (id) => Boolean(id && CG[id] && (!CG[id].adult || (config.adult && ageConfirmed())));
|
|
13
|
+
|
|
14
|
+
let shownBg = null;
|
|
15
|
+
let activeBgLayer = 'a';
|
|
16
|
+
let shownCg = null;
|
|
17
|
+
|
|
18
|
+
export function syncStage(stage, { instant = false, speaker = null } = {}) {
|
|
19
|
+
// 背景
|
|
20
|
+
if (stage.bg !== shownBg) {
|
|
21
|
+
shownBg = stage.bg;
|
|
22
|
+
const next = activeBgLayer === 'a' ? 'b' : 'a';
|
|
23
|
+
const nextEl = $(`#bg-${next}`);
|
|
24
|
+
const prevEl = $(`#bg-${activeBgLayer}`);
|
|
25
|
+
nextEl.style.backgroundImage = stage.bg && BG[stage.bg] ? `url("${BG[stage.bg]}")` : 'none';
|
|
26
|
+
nextEl.dataset.bg = stage.bg ?? '';
|
|
27
|
+
nextEl.classList.toggle('night', /night|chamber|rain/.test(stage.bg ?? ''));
|
|
28
|
+
const fast = instant || stage.bgFx === 'cut';
|
|
29
|
+
nextEl.style.transitionDuration = prevEl.style.transitionDuration = fast ? '0ms' : '';
|
|
30
|
+
nextEl.classList.add('on');
|
|
31
|
+
prevEl.classList.remove('on');
|
|
32
|
+
activeBgLayer = next;
|
|
33
|
+
if (stage.bgFx === 'flash' && !instant) flash();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 立绘:按 id 增量更新,新登场者淡入上滑,离场者淡出
|
|
37
|
+
const layer = $('#sprites');
|
|
38
|
+
const want = stage.sprites;
|
|
39
|
+
layer.dataset.count = Object.keys(want).length;
|
|
40
|
+
for (const el of [...layer.children]) {
|
|
41
|
+
if (!want[el.dataset.who] && !el.classList.contains('leaving')) {
|
|
42
|
+
el.classList.add('leaving');
|
|
43
|
+
setTimeout(() => el.remove(), instant ? 0 : 420);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const [who, { pos, expr }] of Object.entries(want)) {
|
|
47
|
+
let el = layer.querySelector(`.sprite[data-who="${who}"]:not(.leaving)`);
|
|
48
|
+
if (!el) {
|
|
49
|
+
el = document.createElement('div');
|
|
50
|
+
el.className = `sprite entering${instant ? ' no-anim' : ''}`;
|
|
51
|
+
el.dataset.who = who;
|
|
52
|
+
el.innerHTML = `<img alt="${CAST[who].full}" draggable="false">`;
|
|
53
|
+
el.querySelector('img').src = spriteSrc(who, expr);
|
|
54
|
+
el.dataset.expr = expr;
|
|
55
|
+
layer.append(el);
|
|
56
|
+
requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove('entering')));
|
|
57
|
+
}
|
|
58
|
+
el.dataset.pos = pos;
|
|
59
|
+
if (el.dataset.expr !== expr) {
|
|
60
|
+
// 差分交叉淡入:新图叠在上面淡入后替换
|
|
61
|
+
const prev = el.querySelector('img:last-child');
|
|
62
|
+
const img = document.createElement('img');
|
|
63
|
+
img.alt = CAST[who].full;
|
|
64
|
+
img.draggable = false;
|
|
65
|
+
img.className = 'swap';
|
|
66
|
+
img.src = spriteSrc(who, expr);
|
|
67
|
+
el.append(img);
|
|
68
|
+
requestAnimationFrame(() => img.classList.add('in'));
|
|
69
|
+
setTimeout(() => { prev.remove(); img.className = ''; }, instant ? 0 : 260);
|
|
70
|
+
el.dataset.expr = expr;
|
|
71
|
+
if (expr === 'surprised' && !instant) jolt(el);
|
|
72
|
+
}
|
|
73
|
+
const speaking = speaker && speaker === who;
|
|
74
|
+
el.classList.toggle('speaking', Boolean(speaking));
|
|
75
|
+
el.classList.toggle('dim', Boolean(speaker && CAST[speaker] && !speaking));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$('#weather').className = stage.weather ?? '';
|
|
79
|
+
|
|
80
|
+
syncCg(stage);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// CG:成人 CG 仅在年龄确认且设置开启时加载;否则以帘幕代替
|
|
84
|
+
function syncCg(stage) {
|
|
85
|
+
if (stage.cg === shownCg) return;
|
|
86
|
+
shownCg = stage.cg;
|
|
87
|
+
const box = $('#cg');
|
|
88
|
+
if (!stage.cg) { box.classList.remove('on'); return; }
|
|
89
|
+
const visible = cgVisible(stage.cg);
|
|
90
|
+
const img = box.querySelector('img');
|
|
91
|
+
img.src = visible ? CG[stage.cg].src : '';
|
|
92
|
+
img.className = visible ? cgClass(stage.cg) : '';
|
|
93
|
+
box.classList.toggle('veiled', !visible);
|
|
94
|
+
box.classList.add('on');
|
|
95
|
+
if (!visible) onVeil?.();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let onVeil = null;
|
|
99
|
+
export const setVeilHook = (fn) => { onVeil = fn; };
|
|
100
|
+
// 设置里改了「成人画面」:正在显示的 CG 立刻按新设置重画
|
|
101
|
+
export function refreshCg(stage) {
|
|
102
|
+
shownCg = undefined;
|
|
103
|
+
syncCg(stage);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resetStage() {
|
|
107
|
+
shownBg = undefined;
|
|
108
|
+
shownCg = undefined;
|
|
109
|
+
$('#sprites').innerHTML = '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function jolt(el) {
|
|
113
|
+
el.classList.remove('jolt');
|
|
114
|
+
void el.offsetWidth;
|
|
115
|
+
el.classList.add('jolt');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function flash() {
|
|
119
|
+
const f = $('#flash');
|
|
120
|
+
f.classList.remove('go');
|
|
121
|
+
void f.offsetWidth;
|
|
122
|
+
f.classList.add('go');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function shake() {
|
|
126
|
+
const s = $('#stage');
|
|
127
|
+
s.classList.remove('shake');
|
|
128
|
+
void s.offsetWidth;
|
|
129
|
+
s.classList.add('shake');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const imageCache = new Map();
|
|
133
|
+
function loadImage(src) {
|
|
134
|
+
if (!imageCache.has(src)) {
|
|
135
|
+
imageCache.set(src, new Promise((resolve) => {
|
|
136
|
+
const img = new Image();
|
|
137
|
+
img.onload = () => resolve(img);
|
|
138
|
+
img.onerror = () => resolve(null);
|
|
139
|
+
img.src = src;
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
return imageCache.get(src);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 存档缩略图:用当前背景+立绘(或非成人 CG)重新合成,绝不截入成人画面
|
|
146
|
+
export async function thumbnail(stage) {
|
|
147
|
+
const W = 320;
|
|
148
|
+
const H = 180;
|
|
149
|
+
const canvas = document.createElement('canvas');
|
|
150
|
+
canvas.width = W;
|
|
151
|
+
canvas.height = H;
|
|
152
|
+
const ctx = canvas.getContext('2d');
|
|
153
|
+
ctx.fillStyle = '#1d1a24';
|
|
154
|
+
ctx.fillRect(0, 0, W, H);
|
|
155
|
+
const cover = (img, zoom = 0) => {
|
|
156
|
+
// zoom>0:与舞台 .focus-right 相同,只取右侧 1/zoom 宽、居中 1/zoom 高
|
|
157
|
+
const sw = zoom ? img.width / zoom : img.width;
|
|
158
|
+
const sh = zoom ? img.height / zoom : img.height;
|
|
159
|
+
const sx = img.width - sw;
|
|
160
|
+
const sy = (img.height - sh) / 2;
|
|
161
|
+
const s = Math.max(W / sw, H / sh);
|
|
162
|
+
ctx.drawImage(img, sx, sy, sw, sh, (W - sw * s) / 2, (H - sh * s) / 2, sw * s, sh * s);
|
|
163
|
+
};
|
|
164
|
+
const cg = stage.cg && CG[stage.cg] && !CG[stage.cg].adult ? await loadImage(CG[stage.cg].src) : null;
|
|
165
|
+
if (cg) cover(cg, CG[stage.cg].focus ? FOCUS_ZOOM : 0);
|
|
166
|
+
else {
|
|
167
|
+
const bg = stage.bg && BG[stage.bg] ? await loadImage(BG[stage.bg]) : null;
|
|
168
|
+
if (bg) cover(bg);
|
|
169
|
+
if (!stage.cg) {
|
|
170
|
+
for (const [who, { pos, expr }] of Object.entries(stage.sprites)) {
|
|
171
|
+
const img = await loadImage(spriteSrc(who, expr));
|
|
172
|
+
if (!img) continue;
|
|
173
|
+
const h = H * 1.15;
|
|
174
|
+
const w = img.width * (h / img.height);
|
|
175
|
+
ctx.drawImage(img, W * POS_X[pos] - w / 2, H * 0.06, w, h);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
try { return canvas.toDataURL('image/jpeg', 0.72); } catch { return null; }
|
|
180
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 本地持久化:设置、跨周目解锁(CG/回想/曲目/结局/已读/已选)、存档槽。全部容错,存储不可用时照常可玩。
|
|
2
|
+
const KEYS = { config: 'jpm2_config', global: 'jpm2_global', saves: 'jpm2_saves' };
|
|
3
|
+
export const AGE_KEY = 'jpm_age_confirmed';
|
|
4
|
+
export const SLOT_PAGES = 4;
|
|
5
|
+
export const SLOTS_PER_PAGE = 9;
|
|
6
|
+
|
|
7
|
+
const read = (key, fallback) => {
|
|
8
|
+
try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; }
|
|
9
|
+
};
|
|
10
|
+
const write = (key, value) => {
|
|
11
|
+
try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_CONFIG = Object.freeze({
|
|
15
|
+
textSpeed: 45, // 字/秒;100 = 瞬间
|
|
16
|
+
autoSpeed: 50, // 0 慢 … 100 快
|
|
17
|
+
bgm: 60,
|
|
18
|
+
se: 70,
|
|
19
|
+
skipUnread: false,
|
|
20
|
+
adult: true,
|
|
21
|
+
boxAlpha: 90, // 文字框浓淡 40…100
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const config = { ...DEFAULT_CONFIG, ...read(KEYS.config, {}) };
|
|
25
|
+
export const saveConfig = () => write(KEYS.config, config);
|
|
26
|
+
|
|
27
|
+
const g = read(KEYS.global, {});
|
|
28
|
+
export const global = {
|
|
29
|
+
cg: new Set(g.cg ?? []),
|
|
30
|
+
scene: new Set(g.scene ?? []),
|
|
31
|
+
music: new Set(g.music ?? []),
|
|
32
|
+
ending: new Set(g.ending ?? []),
|
|
33
|
+
read: new Set(g.read ?? []),
|
|
34
|
+
chosen: new Set(g.chosen ?? []), // 「标签:选项文字」,后续周目给走过的选项标「已选」
|
|
35
|
+
};
|
|
36
|
+
let globalTimer = null;
|
|
37
|
+
export function saveGlobal(now = false) {
|
|
38
|
+
const flush = () => {
|
|
39
|
+
globalTimer = null;
|
|
40
|
+
write(KEYS.global, Object.fromEntries(Object.entries(global).map(([k, v]) => [k, [...v]])));
|
|
41
|
+
};
|
|
42
|
+
if (now) { clearTimeout(globalTimer); flush(); } else if (!globalTimer) globalTimer = setTimeout(flush, 800);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ageConfirmed() {
|
|
46
|
+
try { return sessionStorage.getItem(AGE_KEY) === 'yes'; } catch { return false; }
|
|
47
|
+
}
|
|
48
|
+
export function confirmAge() {
|
|
49
|
+
try { sessionStorage.setItem(AGE_KEY, 'yes'); } catch { /* 本次会话内仍按已确认处理 */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 存档:{ [slotId]: { snap, chapter, text, date, thumb } };slotId 为 1..36、'quick'、'auto'
|
|
53
|
+
export const loadSaves = () => read(KEYS.saves, {});
|
|
54
|
+
export function writeSave(slot, record) {
|
|
55
|
+
const saves = loadSaves();
|
|
56
|
+
saves[slot] = record;
|
|
57
|
+
if (write(KEYS.saves, saves)) return true;
|
|
58
|
+
// 空间不足时先丢掉缩略图再试一次
|
|
59
|
+
saves[slot] = { ...record, thumb: null };
|
|
60
|
+
return write(KEYS.saves, saves);
|
|
61
|
+
}
|
|
62
|
+
export function deleteSave(slot) {
|
|
63
|
+
const saves = loadSaves();
|
|
64
|
+
delete saves[slot];
|
|
65
|
+
write(KEYS.saves, saves);
|
|
66
|
+
}
|
|
67
|
+
export function latestSave() {
|
|
68
|
+
const saves = loadSaves();
|
|
69
|
+
return Object.entries(saves).sort((a, b) => (b[1].time ?? 0) - (a[1].time ?? 0))[0] ?? null;
|
|
70
|
+
}
|