├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github
├── issue-checker.yml
└── workflows
│ ├── codeql.yml
│ ├── issue-checker.yml
│ ├── npm-publish-beta.yml
│ └── npm-publish.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .prettierrc.json
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── docs
├── contribute.md
├── img
│ ├── player.png
│ └── text-position.png
├── layers.md
└── layers
│ ├── characterLayer.md
│ └── effectLayer.md
├── index.html
├── lib
├── BaStoryPlayer.vue
├── eventBus.ts
├── index.ts
├── layers
│ ├── bgLayer
│ │ └── index.ts
│ ├── characterLayer
│ │ ├── actionPlayer.ts
│ │ ├── emotionPlayer.ts
│ │ ├── fxPlayer.ts
│ │ ├── index.ts
│ │ └── options
│ │ │ ├── actionOptions.ts
│ │ │ ├── emotionOptions.ts
│ │ │ └── fxOptions.ts
│ ├── effectLayer
│ │ ├── bgEffectHandlers.ts
│ │ ├── effectFunctions
│ │ │ ├── BG_Dust_L.ts
│ │ │ ├── BG_Flash_Sound.ts
│ │ │ ├── BG_FocusLine.ts
│ │ │ ├── BG_Love_L.ts
│ │ │ ├── BG_Love_L_BGOff.ts
│ │ │ ├── BG_Rain_L.ts
│ │ │ ├── BG_SandStorm_L.ts
│ │ │ ├── BG_Shining_L.ts
│ │ │ ├── BG_Shining_L_BGOff.ts
│ │ │ └── BG_UnderFire.ts
│ │ ├── emitterConfigs
│ │ │ ├── dust_fire.json
│ │ │ ├── fire.json
│ │ │ ├── fireline.json
│ │ │ ├── focusline.json
│ │ │ ├── love_heart.json
│ │ │ ├── love_ring.json
│ │ │ ├── rain.json
│ │ │ ├── shining_flare.json
│ │ │ ├── shining_ring.json
│ │ │ └── smoke.json
│ │ ├── emitterUtils.ts
│ │ ├── index.ts
│ │ └── resourcesUtils.ts
│ ├── l2dLayer
│ │ ├── L2D.ts
│ │ └── l2dConfig.ts
│ ├── soundLayer
│ │ └── index.ts
│ ├── textLayer
│ │ ├── BaDialog.vue
│ │ ├── assets
│ │ │ ├── text-next.webp
│ │ │ ├── title_border__lower_left.svg
│ │ │ ├── title_border__lower_right.svg
│ │ │ ├── title_border__upper_left.svg
│ │ │ └── title_border__upper_right.svg
│ │ ├── index.d.ts
│ │ └── responsive-video-background.d.ts
│ ├── translationLayer
│ │ ├── index.ts
│ │ └── utils.ts
│ └── uiLayer
│ │ ├── BaUI.vue
│ │ ├── README.md
│ │ ├── assets
│ │ ├── Common_Btn_Normal_Y_S_Pt.webp
│ │ ├── Conquest_2nd_Eventlobby_GaugeBg.png
│ │ ├── Conquest_2nd_Eventlobby_GaugeFront.png
│ │ ├── Deco_GachaItemBg.webp
│ │ ├── Image_AngleBtn_Deco.png
│ │ ├── UITex_BGPoliLight_1.svg
│ │ ├── close.svg
│ │ ├── fast-forward.svg
│ │ ├── icon-hide.svg
│ │ ├── icon-show.svg
│ │ ├── menu.svg
│ │ ├── pan-arrow.svg
│ │ └── title-banner.svg
│ │ ├── components
│ │ ├── BaButton.vue
│ │ ├── BaChatLog
│ │ │ ├── BaChatLog.vue
│ │ │ └── BaChatMessage.vue
│ │ ├── BaDialog.vue
│ │ └── BaSelector.vue
│ │ ├── index.ts
│ │ ├── userInteract.ts
│ │ └── utils.ts
├── main.ts
├── stores
│ └── index.ts
├── types
│ ├── bgLayer.ts
│ ├── characterLayer.ts
│ ├── common.ts
│ ├── effectLayer.ts
│ ├── events.ts
│ ├── excels.ts
│ ├── l2d.ts
│ ├── resources.ts
│ └── store.ts
└── utils.ts
├── package.json
├── pnpm-lock.yaml
├── src
├── App.vue
├── assets
│ ├── scss
│ │ └── index.scss
│ └── vue.svg
├── components
│ ├── ModifyEmotionOption.vue
│ └── TestEffect.vue
├── data
│ ├── LocalizeScenarioExcelTable.json
│ ├── prologue1.1.json
│ └── yuuka.json
├── main.ts
├── utils.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | indent_style = space
4 | indent_size = 2
5 | end_of_line = lf
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "parser": "vue-eslint-parser",
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/eslint-recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:vue/vue3-essential",
12 | "@vue/eslint-config-typescript",
13 | "prettier"
14 | ],
15 | "plugins": ["vue", "import", "sort-exports", "@typescript-eslint"],
16 | "rules": {
17 | "indent": ["error", 2, { "SwitchCase": 1 }],
18 | "eqeqeq": ["error", "always"],
19 | "max-len": ["error", { "code": 120 }],
20 | "linebreak-style": ["error", "unix"],
21 | "quotes": ["error", "double"],
22 | "semi": ["warn", "always"],
23 | "space-before-function-paren": ["warn", {
24 | "anonymous": "never",
25 | "named": "never",
26 | "asyncArrow": "always"
27 | }],
28 | "comma-dangle": [
29 | "error",
30 | {
31 | "arrays": "always-multiline",
32 | "objects": "always-multiline",
33 | "imports": "always-multiline",
34 | "exports": "always-multiline",
35 | "functions": "ignore"
36 | }
37 | ],
38 | "sort-exports/sort-exports": ["error", { "sortDir": "asc" }],
39 | "sort-imports": [
40 | "error",
41 | {
42 | "ignoreCase": false,
43 | "ignoreDeclarationSort": true,
44 | "ignoreMemberSort": false,
45 | "memberSyntaxSortOrder": ["all", "single", "multiple", "none"]
46 | }
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 |
7 | # Declare files that will always have CRLF line endings on checkout.
8 | *.vue text eol=lf
9 | *.ts text eol=lf
10 | *.js text eol=lf
11 | *.scss text eol=lf
12 |
13 | # Denote all files that are truly binary and should not be modified.
14 | *.png binary
15 | *.jpg binary
16 | *.webp binary
17 | *.svg binary
18 | *.woff binary
19 | *.woff2 binary
20 | package.json !binary
21 | .prettierrc.json !binary
--------------------------------------------------------------------------------
/.github/issue-checker.yml:
--------------------------------------------------------------------------------
1 | # For labels
2 | labels:
3 | - name: bug
4 | mode: add
5 | content: bug
6 | regexes: '[Bb]ug'
7 | - name: enhancement
8 | mode: add
9 | content: enhancement
10 | regexes: '[Ee]nhancement|[Ff]eature [Rr]equest|功能请求|新功能|(能|可以?|行)不(行|能|可以)'
11 | - name: UI
12 | mode: add
13 | content: UI
14 | regexes: '[Uu][Ii]|界面'
15 | - name: momotalk
16 | mode: add
17 | content: 'module: momotalk'
18 | regexes: '([Mm][Oo]){2}\s{0,}[Tt][Aa][Ll][Kk]'
19 | - name: story
20 | mode: add
21 | content: 'module: story'
22 | regexes: '([Ss][Tt][Oo][Rr][Yy])|故事|剧情'
23 | - name: safari
24 | mode: add
25 | content: safari
26 | regexes: '[Ss][Aa][Ff][Aa][Rr][Ii]'
27 | - name: chrome
28 | mode: add
29 | content: chrome
30 | regexes: '[Cc][Hh][Rr][Oo][Mm][Ee]|谷歌'
31 | - name: firefox
32 | mode: add
33 | content: firefox
34 | regexes: '[Ff][Ii][Rr][Ee][Ff][Oo][Xx]|火狐'
35 | - name: duplicate
36 | mode: add
37 | content: duplicate
38 | regexes: '[Dd]uplicate of #\d+|[和与跟]\s?#\d+\s?是?重复'
39 | - name: wontfix
40 | mode: add
41 | content: wontfix
42 | regexes: '没有(这个)?(想法|计划)'
43 | - name: remove-question-tag
44 | content:
45 | regexes: '(?
6 | 过时内容
7 |
8 | # 剧情播放器组件仓库
9 |
10 | 本仓库是剧情播放器组件仓库, 用于播放碧蓝档案游戏剧情.
11 |
12 | 在进入全屏后, 如果宽高比小于 16/9, 则维持宽高比, 若大于, 则锁定为 16/9.
13 | 全屏时会自动检测是否横屏并自动旋转.
14 |
15 | # 使用
16 |
17 | ```html
18 |
22 | ```
23 |
24 | # props
25 |
26 | ## story
27 |
28 | type: `StoryRawUnit[]`
29 |
30 | 剧情原始数据数组.
31 |
32 | ## dataUrl
33 |
34 | type: `string`
35 |
36 | 资源服务器地址, 用于获取立绘语音等游戏资源. 各资源的具体路径请参照`lib/utils.ts`中的`getResourcesUrl`.
37 |
38 | ## width
39 |
40 | type: `number`
41 |
42 | 播放器宽度, 单位是 px, 可变.
43 |
44 | ## height
45 |
46 | 播放器高度, 单位是 px, 可变. 注意请不要设置偏离 16/9 太多的宽高比, 可能导致播放器表现变差.
47 |
48 | ## storySummary
49 |
50 | type:
51 |
52 | ```ts
53 | export interface StorySummary {
54 | /**
55 | * 章节名
56 | */
57 | chapterName: string;
58 | /**
59 | * 简介
60 | */
61 | summary: string;
62 | }
63 | ```
64 |
65 | ## language
66 |
67 | type: `'Cn'|'Jp'|'En'|'Tw'`
68 |
69 | 语言选项
70 |
71 | ## startFullScreen
72 |
73 | type: `boolean`
74 |
75 | 是否立即全屏, 用于移动端.
76 |
77 | ## useMp3
78 |
79 | type: `boolean`
80 |
81 | 使用 mp3 代替 ogg 格式音频, 用于解决 safari 浏览器的音频解码问题.
82 |
83 | ## useSuperSampling
84 |
85 | type: `boolean`
86 |
87 | 是否使用超分素材, 目前该功能尚未实现, 选项无实际效果.
88 |
89 | # event
90 |
91 | ## end
92 |
93 | 播放结束时发送
94 |
95 | ## error
96 |
97 | 发生错误时发送(播放错误或资源加载错误)
98 |
99 | # 贡献说明
100 |
101 | 请参照[贡献指南](./docs/contribute.md)
102 |
103 |
104 |
--------------------------------------------------------------------------------
/docs/contribute.md:
--------------------------------------------------------------------------------
1 | # 概括
2 | 首先非常欢迎, 也非常感谢您愿意为这个兴趣使然的项目添砖加瓦.
3 |
4 | 为方便与项目管理, 请开启新分支进行代码贡献.
5 |
6 | 剧情播放器采用了pixi.js+gsap+vue的技术栈, 编程语言主要为ts. 在贡献代码之前推荐先阅读这些框架与语言的官方文档确保有一定的基础.
7 |
8 | 本剧情播放器采用了事件总线的设计模式, 并将功能模块化, 抽象为一个个层方便协助开发与后续维护.
9 |
10 | 整体架构:
11 | 
12 | # 各层简易说明
13 | ## 本体
14 | 本体主要用于更改当前剧情信息.
15 |
16 | 同时, 本体还负责发出事件, 它是各层协同工作的桥梁.
17 | ## store
18 | store充当一个统一的数据交换中心, 获取资源和与其他层资源交互都在这里进行.
19 | ## 特效层
20 |
21 | 特效层用于播放除人物相关特效外的特效
22 | ## 人物层
23 |
24 | 人物层负责处理人物的显示, 人物特效, 人物动作.
25 |
26 | ## 背景层
27 |
28 | 背景层负责背景. 它在改变的同时会更新背景实例信息.
29 |
30 | ## l2d层
31 | l2d层负责l2d的显示
32 | ## UI层
33 |
34 | UI层负责UI的相关功能
35 | ## 文字层
36 |
37 | 文字层负责有对话框文字, 无对话框文字, 选项的显示.
38 |
39 | 文字层同时需要更新历史剧情信息与选项选择信息.
40 |
41 |
42 | # 各层代码贡献说明
43 | 请阅读[接口与事件文档](./layers.md), 在其中获取各层需要处理的事件与数据并实现.
44 |
45 | 目前尚未完成的层:
46 | - 声音层
47 | - 文字层(半成品)
48 | - L2D层(未知)
49 | - 特效层
50 | - UI层
51 |
52 | 对于内容庞大, 持续更新的层, 请根据该层实现者的文档进行代码贡献.
53 |
54 | [人物层](./layers/characterLayer.md)
55 |
56 | [特效层](./layers/effectLayer.md)
--------------------------------------------------------------------------------
/docs/img/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/docs/img/player.png
--------------------------------------------------------------------------------
/docs/img/text-position.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/docs/img/text-position.png
--------------------------------------------------------------------------------
/docs/layers.md:
--------------------------------------------------------------------------------
1 | # 通用
2 | 每层需提供一个init函数用于注册event handler(如果是组件类型则需自行使用eventBus注册).
3 | ## 资源访问
4 | 通过`@/stores`的`usePlayerStore`获取资源工具类, 注意请获取整个对象而不是某个属性, 不然可能会导致数据过时.
5 | 可访问getter:
6 | `app`: pixi-Application实例
7 | ## 事件管理
8 | 通过`eventBus.ts`导入`eventBus`.
9 |
10 | ## 接收事件
11 | `hide`: 清除当前内容
12 |
13 | # 人物层
14 | 人物层负责处理人物的显示, 人物特效, 人物动作.
15 | ## 接收事件
16 | `showCharacter`
17 | 展示人物
18 | 参数:
19 | ```ts
20 | interface ShowCharacter{
21 | characters: Character[],
22 | characterEffect: CharacterEffect[],
23 | }
24 | interface Character{
25 | position: number,
26 | CharacterName: number,
27 | face: number,
28 | highlight: boolean
29 | }
30 |
31 | interface CharacterEffect{
32 | target: number //注意指的是position
33 | type:'emotion'|'action'|'signal'|'fx'
34 | effect: string,
35 | async: boolean
36 | }
37 | ```
38 | 具体的人物特效请参考[剧情播放器人物特效索引表](https://github.com/ba-archive/blue-archive-story-viewer/issues/32)
39 | ## 发出事件
40 | `characterDone`: 人物各种处理已完成
41 | `playEmotionAudio`: 要求播放人物情绪特效语音
42 | ## 可使用的getter
43 | `characterSpineData`: 根据CharacterName获取spineData
44 | `emotionResources`: 获取人物情绪图片url, 返回一个string数组, 图片的排列按从底部到顶部, 从左到右排列.
45 | ## 需要处理的state
46 | `currentCharacterList: CharacterInstance[]`: 当前显示的人物与其其其
47 |
48 | # 背景层
49 | 背景层负责背景的显示
50 | ## 接收事件
51 | `showBg`: 展示背景, 带一个string参数表示背景图片url
52 |
53 | ## 需要处理的state
54 | `bgInstance`: 背景实例, 通过`setBgInstance`访问
55 |
56 | # 声音层
57 | 声音层负责背景音乐, 效果音, 语音等的播放
58 | ## 接受事件
59 | `playAudio`: 播放bgm, sound, 或voiceJP
60 |
61 | 参数:
62 | ```ts
63 | export interface PlayAudio {
64 | bgm?: {
65 | url?: string
66 | bgmArgs: BGMExcelTableItem
67 | }
68 | soundUrl?: string
69 | voiceJPUrl?: string
70 | }
71 | ```
72 | `playEmotionAudio`: 播放人物情绪动作特效音, 参数是一个string代表人物的情绪动作
73 |
74 | ## 可使用getter
75 | `otherSoundUrl`: 获取其他声音资源url
76 |
77 | `emotionSoundUrl`: 获取emotion特效对应的特效音
78 |
79 | # UI层
80 | UI层负责UI的相关功能
81 | ## 发出事件
82 | `skip`: 跳过剧情
83 |
84 | `auto`: 启动自动模式
85 |
86 | `stopAuto`: 自动模式停止
87 |
88 | `hideDialog`: 隐藏对话框
89 |
90 | `playOtherSound`: 参数: `select`. 播放选择时的特效音
91 | ## 接受事件
92 | `hidemenu`: 隐藏ui
93 |
94 | `showmenu`: 显示ui
95 |
96 | `option`: 显示选项
97 | 参数: ShowOption[]
98 | ```ts
99 | interface Option {
100 | SelectionGroup: number,
101 | text: string
102 | }
103 | ```
104 |
105 | ## 可使用getter
106 | `logText`: 已播放剧情语句
107 | ```ts
108 | export interface LogText {
109 | /**
110 | * user: 用户选项
111 | * character: 人物对话, 有头像
112 | * none: 无所属对话, 此时name不存在
113 | */
114 | type: 'user' | 'character' | 'none'
115 | text: string
116 | /**
117 | * 人物名
118 | */
119 | name?: string
120 | /**
121 | * 头像地址
122 | */
123 | avatarUrl?: string
124 | }
125 | ```
126 | `storySummary`: 剧情梗概
127 | ```ts
128 | export interface StorySummary {
129 | /**
130 | * 章节名
131 | */
132 | chapterName: string,
133 | /**
134 | * 简介
135 | */
136 | summary: string
137 | }
138 | ```
139 | # 文字层
140 | 文字层剧情里没有交互的内容的显示(图片, 文字, 视频等)
141 | ## 发出事件
142 | `titleDone`: 标题显示完成
143 |
144 | `stDone`: st文字显示完成
145 | ## 接收事件
146 | `showTitle`: 显示标题, 接受一个string参数作为标题.
147 |
148 | 示例: [体香1](https://www.bilibili.com/video/BV1qY411f72B?t=10.4)
149 |
150 | `showPlace`: 显示地点, 接受一个string参数作为地点
151 |
152 | 示例: [体香1](https://www.bilibili.com/video/BV1qY411f72B?t=14.0)
153 |
154 | `showText`: 显示普通对话框文字
155 | 参数
156 | ```ts
157 | export interface ShowText {
158 | text: Text[]
159 | speaker?: Speaker
160 | }
161 | interface Text {
162 | content: string
163 | effect: TextEffect[]
164 | waitTime?: number
165 | }
166 | interface TextEffect {
167 | name: 'color'|'fontsize'|'ruby',
168 | value: string[],
169 | textIndex: number
170 | }
171 | ```
172 |
173 | `st`: 显示无对话框文字
174 | 参数:
175 | ```ts
176 | interface StText{
177 | text: Text[]
178 | stArgs:[number[],'serial'|'instant',number]
179 | }
180 |
181 | ```
182 | stArgs:
183 | 第一个参数代表位置, 一般由两个数组成
184 | 位置示意图:
185 | 
186 | 注意100单位长度的位置应大于等于文字默认高度
187 |
188 | 第二个参数代表效果, serial打字机效果, instant立即全部显示, smooth表示fade in
189 |
190 | 第三个参数目前尚不明确
191 |
192 | 参数例子:[[-1200,-530],'serial',60](体香1 L2d第一句)
193 |
194 | `clearSt`: 清除无对话框文字
195 |
196 |
197 | `hideDialog`: 隐藏对话框
198 |
199 | `popupImage`: 显示弹出的图片, 参数是图片url
200 |
201 | `popupVideo`: 显示弹出的视频, 参数是视频地址
202 |
203 | `toBeContinue`: 显示未完待续
204 |
205 | `nextEpisode`: 显示下一章节
206 | type
207 | ```js
208 | {
209 | title: string
210 | text: string
211 | }
212 | ```
213 | ## 发出事件
214 | `next`: 进入下一语句
215 |
216 | `select`: 选择后加入下一剧情语句, 需要带一个number类型的参数
217 |
218 | ## 需要处理的state
219 | `logText`: 已播放剧情语句, 通过`setLogText`进行修改.
220 | # 特效层
221 | 特效层用于播放除人物相关特效外的特效
222 | ## 接收事件
223 | `playEffect`: 播放特效
224 | ## 发出事件
225 | `effectDone`: 特效播放完成时发出的事件
226 | # L2D层
227 | L2D层用于播放L2D
228 | ## live2D 动画
229 | 关于查看 spine 中有的动画, 参考 https://www.bilibili.com/read/cv18073492
230 | ## 发出事件
231 | `animationDone`: 当前l2d动画播放完成
232 | ## 接收事件
233 | `playL2D`: 加载L2D, 播放l2d初始动画
234 |
235 | `changeAnimation`: 更换动画, 接受一个string参数作为动画名
236 |
237 | `endL2D`: 停止L2D
238 | ## 可使用getter
239 | `l2dSpineData`: 获取l2d的spine数据
240 |
--------------------------------------------------------------------------------
/docs/layers/characterLayer.md:
--------------------------------------------------------------------------------
1 | 本层提供一个可视化工具方便实现.
2 |
3 | 本层功能分支请采用`feat/characterLayer/**`的格式命名.
4 |
5 | 本层的特效主要分为三个部分`emotion`, `action`, `fx`.
6 |
7 | 贡献流程:
8 | 1. 请先在[剧情播放器特效](https://docs.qq.com/sheet/DQ3doZ0NoUFZ6V0VJ?tab=BB08J2)文档中寻找未实现的特效, 并在实现者加上自己的github id.
9 | 2. 确保自己实现的特效已经在`lib/types/characterLayer.ts`的`EffectsWord`中存在定义, 若不存在请在相应的`Word`部分添加(如`EmotionWord`)
10 | 3. 在同个文件下寻找到自己实现的特效部分的参数定义(如`ActionOptions`), 填入参数定义.其中请注意`emotion`的参数定义为`BasicEmotionOptions`
11 | 4. 请在`lib/layer/characterLayer/options/`文件夹下寻找自己实习部分的参数文件(如actionOptions.ts), 填入参数和参数的解释.
12 | 5. 在同个文件夹下找到想要功能的实现文件(如`emotionPlayer.ts`), 然后在该文件夹export的对象中添加仿照其他功能实现函数添加属性并添加代码.
13 | 6. 实现可视化工具调整参数到满意, 提交功能分支并开启pull request等待审查合并.
--------------------------------------------------------------------------------
/docs/layers/effectLayer.md:
--------------------------------------------------------------------------------
1 | 本层中需要协力完成的部分是`BGEffect`
2 |
3 | 本层提供一个简易的可视化工具方便开发.
4 |
5 | # 基本流程
6 | 1. 在[bgEffect特效表](https://docs.qq.com/sheet/DQ1pFSmdHbFRNd3Fu?tab=BB08J2)找到未完成的特效并在实现者中填入自己的github id.
7 | 2. 根据[图像资源获取](#图片资源获取)获取图像资源
8 | 3. (可选) 中`@/types/effectLayer.ts`的`BGEffectHandlerOptions`中填入参数定义, 例子:
9 | ```js
10 | let BGEffectHandlerOptions={
11 | 'BG_Test':{
12 | testOptions1: number
13 | }
14 | }
15 | ```
16 | 4. 在`@/layers/effectLayer/effectFunctions/`文件夹中添加自己的实现并设置为默认导出, 需要命名为实现的特效名, 例子:
17 | ```js
18 | //BG_test.ts
19 | import { usePlayerStore } from "@/stores"
20 | import { BGEffectHandlerFunction } from "@/types/effectLayer"
21 | import { Emitter, EmitterConfigV2, upgradeConfig } from "@pixi/particle-emitter"
22 | import { emitterConfigs, emitterContainer, emitterStarter } from "../emitterUtils"
23 |
24 | const handler: BGEffectHandlerFunction<'BG_Test'> = async function (resources, setting, options) {
25 | ...
26 | return removeFunction
27 | }
28 |
29 | export default handler
30 | //请符合BGEffectHandlerFunction规范
31 | //注意需要返回一个特效移除函数
32 | ```
33 |
34 | 5. 功能测试无异常后在[bgEffect特效表](https://docs.qq.com/sheet/DQ1pFSmdHbFRNd3Fu?tab=BB08J2)中自己github id后加入`(已完成)`
35 |
36 | # 特效文档完善
37 | [bgEffect特效表](https://docs.qq.com/sheet/DQ1pFSmdHbFRNd3Fu?tab=BB08J2)中可能没有效果示例位置, 可按如下方法获取并填入完善:
38 |
39 | 1. 在[BGEffect table](https://github.com/aizawey479/ba-data/blob/jp/Excel/ScenarioBGEffectExcelTable.json)中搜索得到当前effect的id(也可在可视化工具中获取)
40 | 2. 在[主线剧情](https://github.com/aizawey479/ba-data/blob/jp/Excel/ScenarioScriptMain2ExcelTable.json)中搜索得到effect出现位置(地址main后数字可替换)
41 | 3. 在[威威的视频](https://www.bilibili.com/list/7045822?sid=1061322&desc=1&oid=765681436&bvid=BV1Zr4y1v7ZT)中找到找到对应出现位置, 复制视频详细位置信息填入表中
42 |
43 | # 图片资源获取
44 | 请在[特效资源后端资源地址](https://yuuka.diyigemt.com/files/ba-all-data/effectTexture/)(账号密码群中自行获取)中根据特效名寻找素材. 如找不到或原素材使用困难请自行上网寻找素材.
45 |
46 | 找到后请填入`@/stores/index.ts`中的`bgEffectImgTable`, 例子:
47 | ```js
48 | let bgEffectImgTable: BGEffectImgTable = {
49 | 'BG_ScrollT_0.5': ['img1.png','img2.png'],
50 | }
51 | ```
52 |
53 | # utils
54 | ## emitter
55 | 提供常量
56 | - emitterContainr 放置emitter的container
57 |
58 | 提供函数
59 | - emitterStarter 开启emitter并返回终止函数
60 |
61 | 设置路径
62 |
63 | 请将emitter设置的json放入`@/layers/effectLayer/emitterConfigs/`, 然后使用`emitterConfigs(filename)`获取config.
64 | ## resources
65 | 提供函数
66 | - sprite2TransParent 将黑色背景转为透明
67 | - loadSpriteSheet 自动分割图片并生成spritesheet
68 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | player
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/lib/eventBus.ts:
--------------------------------------------------------------------------------
1 | import mitt from "mitt";
2 | import { Events } from "@/types/events";
3 |
4 | const eventBus = mitt();
5 | export default eventBus;
6 |
--------------------------------------------------------------------------------
/lib/layers/bgLayer/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 初始化背景层, 订阅player的剧情信息.
3 | */
4 | import { Sprite, LoaderResource, Application } from "pixi.js";
5 | import gsap from "gsap";
6 |
7 | import { BgLayer } from "@/types/bgLayer";
8 | import { Dict } from "@/types/common";
9 | import { usePlayerStore } from "@/stores";
10 | import eventBus from "@/eventBus";
11 |
12 | export function bgInit() {
13 | return BgLayerInstance.init();
14 | }
15 |
16 | const BgLayerInstance: BgLayer = {
17 | /**
18 | * 注册/销毁实例
19 | */
20 | init() {
21 | this.initEvent();
22 | },
23 | dispose() {
24 | this.disposeEvent();
25 | },
26 |
27 | /**
28 | * 注册/销毁事件监听
29 | */
30 | initEvent() {
31 | this.handleShowBg = this.handleShowBg.bind(this);
32 |
33 | eventBus.on("showBg", this.handleShowBg);
34 | eventBus.on("resize", this.handleResize);
35 | },
36 | disposeEvent() {
37 | eventBus.off("showBg", this.handleShowBg);
38 | },
39 |
40 | /**
41 | * 事件监听处理函数
42 | */
43 | handleShowBg({ url, overlap }) {
44 | const {
45 | app: { loader },
46 | } = usePlayerStore();
47 |
48 | loader.load((loader, resources) => {
49 | const instance = this.getBgSpriteFromResource(resources, url);
50 |
51 | if (instance) {
52 | if (overlap) {
53 | this.loadBgOverlap(instance, overlap);
54 | } else {
55 | this.loadBg(instance);
56 | }
57 | }
58 | });
59 | },
60 |
61 | handleResize() {
62 | const { bgInstance, app } = usePlayerStore();
63 | if (bgInstance) {
64 | const { x, y, scale } = calcBackgroundImageSize(bgInstance, app);
65 | bgInstance.position.set(x, y);
66 | bgInstance.scale.set(scale);
67 | }
68 | },
69 |
70 | /**
71 | * 方法
72 | */
73 | getBgSpriteFromResource(resources: Dict, name: string) {
74 | const { app } = usePlayerStore();
75 | let sprite: Sprite | null = null;
76 |
77 | if (!resources[name]) {
78 | console.error(`can't find resource: ${name}`);
79 | return;
80 | }
81 |
82 | sprite = new Sprite(resources[name].texture);
83 |
84 | const { x, y, scale } = calcBackgroundImageSize(sprite, app);
85 | sprite.position.set(x, y);
86 | sprite.scale.set(scale);
87 | return sprite;
88 | },
89 | loadBg(instance: Sprite) {
90 | const { app, bgInstance: oldInstance, setBgInstance } = usePlayerStore();
91 |
92 | instance.zIndex = -100; // 背景层应该在特效, 人物层之下
93 | app.stage.addChild(instance);
94 | setBgInstance(instance);
95 |
96 | oldInstance && app.stage.removeChild(oldInstance);
97 | },
98 | async loadBgOverlap(instance: Sprite, overlap: number) {
99 | const { app, bgInstance: oldInstance, setBgInstance } = usePlayerStore();
100 | let tl = gsap.timeline();
101 | instance.zIndex = -99;
102 |
103 | app.stage.addChild(instance);
104 | setBgInstance(instance);
105 |
106 | await tl.fromTo(
107 | instance,
108 | { alpha: 0 },
109 | { alpha: 1, duration: overlap / 1000 }
110 | );
111 | eventBus.emit("bgOverLapDone");
112 |
113 | oldInstance && app.stage.removeChild(oldInstance);
114 | },
115 | };
116 |
117 | const StandardWith = 1902;
118 | const StandardWithPadding = 64;
119 | /**
120 | * 计算图片 cover 样式尺寸 - utils
121 | */
122 | export function calcBackgroundImageSize(background: Sprite, app: Application) {
123 | // 计算规则
124 | // 1.优先满足纵向宽度
125 | // 2.带上padding, 大小为1920px: 64px
126 | // **不能用stage的height和width** 他们可以超出视口
127 | const viewportWidth = app.screen.width;
128 | const viewportHeight = app.screen.height;
129 | const rawWidth = background.width / background.scale.x;
130 | const rawHeight = background.height / background.scale.y;
131 | const padding = (rawWidth / StandardWith) * StandardWithPadding;
132 | const finalWidth = viewportWidth + padding * 2;
133 | const scale = finalWidth / rawWidth;
134 | const finalHeight = rawHeight * scale;
135 | const x = -((finalWidth - viewportWidth) / 2);
136 | const y = -((finalHeight - viewportHeight) / 2);
137 |
138 | return { x, y, scale };
139 | }
140 |
--------------------------------------------------------------------------------
/lib/layers/characterLayer/fxPlayer.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import {
3 | CharacterEffectInstance,
4 | CharacterFXPlayer,
5 | PositionOffset,
6 | } from "@/types/characterLayer";
7 | import { Sprite } from "pixi.js";
8 | import fxOptions from "./options/fxOptions";
9 | import gsap from "gsap";
10 | import { getStandardWidth } from ".";
11 | import { AdjustmentFilter } from "@pixi/filter-adjustment";
12 |
13 | const CharacterFXPlayerInstance: CharacterFXPlayer = {
14 | init() {
15 | return;
16 | },
17 | dispose(): void {},
18 | getHandlerFunction(type) {
19 | return Reflect.get(this, type);
20 | },
21 | processEffect(type, instance) {
22 | const fn = this.getHandlerFunction(type);
23 | if (!fn) {
24 | return Promise.reject("该effect不存在或未实现");
25 | }
26 | const { fxImages, app } = usePlayerStore();
27 | let fxImageSprites: Sprite[] = [];
28 | let currentFxImgs = fxImages(type);
29 | if (!currentFxImgs) {
30 | Promise.reject(`fx中${type}对应的图像资源不存在`);
31 | }
32 | for (let imageResource of currentFxImgs!) {
33 | let tempSprite = Sprite.from(imageResource);
34 | tempSprite.visible = false;
35 | instance.instance.addChild(tempSprite);
36 | fxImageSprites.push(tempSprite);
37 | }
38 | return fn(instance, fxOptions[type], fxImageSprites) as Promise;
39 | },
40 | shot(instance, options, sprites) {
41 | let scale = (options.scale * getStandardWidth()) / sprites[0].width;
42 |
43 | let tl = gsap.timeline();
44 | for (let [index, sequence] of options.shotSequence.entries()) {
45 | let img = Sprite.from(sprites[sequence.startImg].texture);
46 | img.scale.set(scale * sequence.scale);
47 | img.angle = sequence.angle;
48 | img.zIndex = 10;
49 | let adjustmentFilter = new AdjustmentFilter({
50 | brightness: 3,
51 | alpha: 0.5,
52 | });
53 | img.filters = [adjustmentFilter];
54 | img.visible = false;
55 | instance.instance.addChild(img);
56 | setPos(instance, img, sequence.pos);
57 | tl.to(
58 | img,
59 | {
60 | duration: options.shotDuration,
61 | onComplete() {
62 | if (sequence.endRed) {
63 | adjustmentFilter.green = 0.3;
64 | adjustmentFilter.blue = 0;
65 | } else {
66 | img.texture = sprites[sequence.endImg!].texture;
67 | }
68 | setTimeout(() => (img.visible = false), 10);
69 | },
70 | onStart() {
71 | img.visible = true;
72 | },
73 | },
74 | index * 0.07
75 | );
76 | }
77 |
78 | return timelinePromise(tl, sprites);
79 | },
80 | };
81 |
82 | /**
83 | * 设置图片相对于人物位置
84 | * @param instance
85 | * @param img
86 | * @param pos
87 | * @returns
88 | */
89 | function setPos(
90 | instance: CharacterEffectInstance,
91 | img: Sprite,
92 | pos: PositionOffset
93 | ) {
94 | let standardWidth = getStandardWidth();
95 | let finalPos = {
96 | x: standardWidth * pos.x,
97 | y: standardWidth * pos.y,
98 | };
99 | img.position = finalPos;
100 |
101 | return pos;
102 | }
103 |
104 | /**
105 | * timeline执行后生成一个promise并自动回收sprite
106 | * @param timeLine 执行的timeline
107 | * @param destroyImgs 要回收的sprite对象数组
108 | * @returns 生成的promise
109 | */
110 | function timelinePromise(
111 | timeLine: gsap.core.Timeline,
112 | destroyImgs: Sprite[],
113 | callback?: () => any
114 | ) {
115 | return new Promise((resolve, reject) => {
116 | timeLine
117 | .then(() => {
118 | resolve();
119 | for (let img of destroyImgs) {
120 | img.destroy();
121 | }
122 | })
123 | .catch(reason => reject(reason));
124 | });
125 | }
126 |
127 | export default CharacterFXPlayerInstance;
128 |
--------------------------------------------------------------------------------
/lib/layers/characterLayer/options/actionOptions.ts:
--------------------------------------------------------------------------------
1 | import { ActionOptions, OptionDescriptions } from "@/types/characterLayer";
2 |
3 | /**
4 | * 立绘移动速度
5 | */
6 | export const moveSpeed = 2.4;
7 |
8 | export let actionDescriptions: OptionDescriptions["action"] = {
9 | a: {},
10 | d: {
11 | duration: "消失动画的时间",
12 | },
13 | dl: {
14 | speed: "移动速度",
15 | },
16 | dr: {
17 | speed: "移动速度",
18 | },
19 | ar: {
20 | speed: "移动速度",
21 | },
22 | al: {
23 | speed: "移动速度",
24 | },
25 | hophop: {
26 | yOffset: "跳动的高度",
27 | duration: "跳动一次的时间",
28 | },
29 | greeting: {
30 | yOffset: "移动的高度",
31 | duration: "移动所花的时间",
32 | },
33 | shake: {
34 | shakeAnimation: "晃动动画的参数, 包括起始点终结点, 时长, 重复次数",
35 | },
36 | m1: {},
37 | m2: {},
38 | m3: {},
39 | m4: {},
40 | m5: {},
41 | stiff: {
42 | shakeAnimation: "晃动动画的参数, 包括起始点终结点, 时长, 重复次数",
43 | },
44 | closeup: {
45 | scale: "相对于原来大小的缩放比例",
46 | },
47 | jump: {
48 | yOffset: "跳动的高度",
49 | duration: "跳动一次的时间",
50 | },
51 | falldownR: {
52 | rightAngle: "向右旋转的角度",
53 | leftAngle: "向左旋转的角度",
54 | firstRotateDuration: "第一个向右旋转的时间",
55 | falldownDuration: "向下移动的时间",
56 | anchor: "旋转原点",
57 | xOffset: "在x轴方向移动的距离",
58 | leftRotationPercent: "左转动画在下降动画中的时间比例",
59 | },
60 | falldownL: {
61 | rightAngle: "向右旋转的角度",
62 | leftAngle: "向左旋转的角度",
63 | firstRotateDuration: "第一个向右旋转的时间",
64 | falldownDuration: "向下移动的时间",
65 | anchor: "旋转原点",
66 | xOffset: "在x轴方向移动的距离",
67 | leftRotationPercent: "左转动画在下降动画中的时间比例",
68 | },
69 | hide: {},
70 | };
71 |
72 | let actionOptions: ActionOptions = {
73 | a: {},
74 | d: {
75 | duration: 0.6,
76 | },
77 | dl: {
78 | speed: moveSpeed,
79 | },
80 | dr: {
81 | speed: moveSpeed,
82 | },
83 | ar: {
84 | speed: moveSpeed,
85 | },
86 | al: {
87 | speed: moveSpeed,
88 | },
89 | hophop: {
90 | yOffset: 0.05,
91 | duration: 0.25,
92 | },
93 | greeting: {
94 | yOffset: -0.04,
95 | duration: 0.4,
96 | },
97 | shake: {
98 | shakeAnimation: {
99 | from: -0.03,
100 | to: 0.02,
101 | duration: 0.08,
102 | repeat: 2,
103 | },
104 | },
105 | m1: {},
106 | m2: {},
107 | m3: {},
108 | m4: {},
109 | m5: {},
110 | stiff: {
111 | shakeAnimation: {
112 | from: -0.01,
113 | to: 0.01,
114 | duration: 0.08,
115 | repeat: 4,
116 | },
117 | },
118 | closeup: {
119 | scale: 1.5,
120 | },
121 | jump: {
122 | yOffset: 0.03,
123 | duration: 0.25,
124 | },
125 | falldownR: {
126 | rightAngle: 10,
127 | anchor: {
128 | x: 0.4,
129 | y: 0.5,
130 | },
131 | leftAngle: -3,
132 | firstRotateDuration: 0.4,
133 | falldownDuration: 0.55,
134 | xOffset: 0.13,
135 | leftRotationPercent: 0.2,
136 | },
137 | falldownL: {
138 | rightAngle: 3,
139 | leftAngle: -10,
140 | anchor: {
141 | x: 0.4,
142 | y: 0.5,
143 | },
144 | firstRotateDuration: 0.4,
145 | falldownDuration: 0.55,
146 | xOffset: -0.13,
147 | leftRotationPercent: 0.2,
148 | },
149 | hide: {},
150 | };
151 |
152 | export default actionOptions;
153 |
--------------------------------------------------------------------------------
/lib/layers/characterLayer/options/fxOptions.ts:
--------------------------------------------------------------------------------
1 | import { FXOptions, OptionDescriptions } from "@/types/characterLayer";
2 |
3 | export let fxOptionsDescriptions: OptionDescriptions["fx"] = {
4 | shot: {
5 | scale: "图片缩放大小",
6 | shotDuration: "每次射击显示效果的时长",
7 | shotSequence: "每次击中效果的设置",
8 | },
9 | };
10 |
11 | let fxOptions: FXOptions = {
12 | shot: {
13 | scale: 0.2,
14 | shotDuration: 0.1,
15 | shotSequence: [
16 | {
17 | startImg: 1,
18 | endRed: true,
19 | pos: {
20 | x: -0.3,
21 | y: -1.6,
22 | },
23 | angle: -20,
24 | scale: 1,
25 | },
26 | {
27 | startImg: 2,
28 | endRed: true,
29 | pos: {
30 | x: 0.2,
31 | y: -1.7,
32 | },
33 | angle: 10,
34 | scale: 1,
35 | },
36 | {
37 | startImg: 0,
38 | endRed: true,
39 | pos: {
40 | x: -0.6,
41 | y: -2.1,
42 | },
43 | angle: -30,
44 | scale: 1.2,
45 | },
46 | {
47 | startImg: 1,
48 | endRed: false,
49 | endImg: 2,
50 | pos: {
51 | x: 0.7,
52 | y: -2.4,
53 | },
54 | angle: 80,
55 | scale: 1.2,
56 | },
57 | ],
58 | },
59 | };
60 |
61 | export default fxOptions;
62 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/bgEffectHandlers.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import {
3 | BGEffectHandlerFunction,
4 | BGEffectHandlerOptions,
5 | CurrentBGEffect,
6 | } from "@/types/effectLayer";
7 | import { BGEffectExcelTableItem, BGEffectType } from "@/types/excels";
8 | import { Sprite } from "pixi.js";
9 |
10 | const effectFunctionsRaw = import.meta.glob<{
11 | default: BGEffectHandlerFunction;
12 | }>("./effectFunctions/*", { eager: true });
13 | function getEffectFunctions(functionName: string) {
14 | const effectFunction = Reflect.get(
15 | effectFunctionsRaw,
16 | `./effectFunctions/${functionName}.ts`
17 | );
18 | if (!effectFunction) {
19 | return undefined;
20 | }
21 | return effectFunction.default;
22 | }
23 |
24 | /**
25 | * 当前播放的BGEffect
26 | */
27 | let currentBGEffect: CurrentBGEffect;
28 |
29 | /**
30 | * 播放对应的BGEffect
31 | * @param bgEffectItem
32 | * @returns
33 | */
34 | export async function playBGEffect(bgEffectItem: BGEffectExcelTableItem) {
35 | console.log(bgEffectHandlers);
36 | const effect = bgEffectItem.Effect;
37 | //此特效正在播放, 无需处理, 先移除保证开发便利
38 | // if (effect === currentBGEffect?.effect) {
39 | // return
40 | // }
41 | await removeBGEffect();
42 | let resources = usePlayerStore().bgEffectImgMap.get(effect);
43 | if (resources) {
44 | let imgs: Sprite[] = [];
45 | for (let resource of resources) {
46 | imgs.push(Sprite.from(resource));
47 | }
48 | let handler = bgEffectHandlers[effect];
49 | let removeFunction: any;
50 | try {
51 | removeFunction = await Reflect.apply(handler, undefined, [
52 | imgs,
53 | bgEffectItem,
54 | bgEffectHandlerOptions[effect],
55 | ]);
56 | } catch (e) {
57 | console.error(`执行 ${effect} 时发生错误`, e);
58 | }
59 | currentBGEffect = {
60 | effect,
61 | removeFunction,
62 | resources: imgs,
63 | };
64 | }
65 | }
66 |
67 | /**
68 | * 移除当前的BGEffect
69 | */
70 | export async function removeBGEffect() {
71 | if (currentBGEffect) {
72 | await currentBGEffect.removeFunction();
73 | for (let resource of currentBGEffect.resources) {
74 | resource.destroy();
75 | }
76 | currentBGEffect = undefined;
77 | }
78 | }
79 |
80 | const bgEffects = [
81 | "BG_ScrollT_0.5",
82 | "BG_Filter_Red",
83 | "BG_Wave_F",
84 | "BG_Flash",
85 | "BG_UnderFire_R",
86 | "BG_Love_L",
87 | "BG_ScrollB_0.5",
88 | "BG_Rain_L",
89 | "BG_UnderFire",
90 | "BG_WaveShort_F",
91 | "BG_SandStorm_L",
92 | "",
93 | "BG_ScrollT_1.5",
94 | "BG_Shining_L",
95 | "BG_ScrollB_1.0",
96 | "BG_Love_L_BGOff",
97 | "BG_Dust_L",
98 | "BG_ScrollL_0.5",
99 | "BG_ScrollL_1.0",
100 | "BG_Ash_Black",
101 | "BG_Mist_L",
102 | "BG_Flash_Sound",
103 | "BG_ScrollL_1.5",
104 | "BG_FocusLine",
105 | "BG_ScrollR_1.5",
106 | "BG_Shining_L_BGOff",
107 | "BG_ScrollT_1.0",
108 | "BG_ScrollB_1.5",
109 | "BG_Filter_Red_BG",
110 | "BG_Ash_Red",
111 | "BG_Fireworks_L_BGOff_02",
112 | "BG_ScrollR_0.5",
113 | "BG_Snow_L",
114 | "BG_Fireworks_L_BGOff_01",
115 | "BG_ScrollR_1.0",
116 | ];
117 | export let bgEffectHandlers: Record<
118 | string,
119 | BGEffectHandlerFunction
120 | > = {};
121 | for (const effect of bgEffects) {
122 | const handler = getEffectFunctions(effect);
123 | if (handler) {
124 | Reflect.set(bgEffectHandlers, effect, handler);
125 | } else {
126 | Reflect.set(bgEffectHandlers, effect, async () => {
127 | throw new Error("未找到该bgEffect实现");
128 | });
129 | }
130 | }
131 |
132 | /**
133 | * 处理函数的对应参数
134 | */
135 | export const bgEffectHandlerOptions: BGEffectHandlerOptions = {
136 | BG_FocusLine: {},
137 | "": {},
138 | "BG_ScrollT_0.5": {},
139 | BG_Filter_Red: {},
140 | BG_Wave_F: {},
141 | BG_Flash: {},
142 | BG_UnderFire_R: {},
143 | BG_Love_L: {},
144 | "BG_ScrollB_0.5": {},
145 | BG_Rain_L: {
146 | frequency: 0.05,
147 | },
148 | BG_UnderFire: {},
149 | BG_WaveShort_F: {},
150 | BG_SandStorm_L: {},
151 | "BG_ScrollT_1.5": {},
152 | BG_Shining_L: {},
153 | "BG_ScrollB_1.0": {},
154 | BG_Love_L_BGOff: {},
155 | BG_Dust_L: {},
156 | "BG_ScrollL_0.5": {},
157 | "BG_ScrollL_1.0": {},
158 | BG_Ash_Black: {},
159 | BG_Mist_L: {},
160 | BG_Flash_Sound: {},
161 | "BG_ScrollL_1.5": {},
162 | "BG_ScrollR_1.5": {},
163 | BG_Shining_L_BGOff: {},
164 | "BG_ScrollT_1.0": {},
165 | "BG_ScrollB_1.5": {},
166 | BG_Filter_Red_BG: {},
167 | BG_Ash_Red: {},
168 | BG_Fireworks_L_BGOff_02: {},
169 | "BG_ScrollR_0.5": {},
170 | BG_Snow_L: {},
171 | BG_Fireworks_L_BGOff_01: {},
172 | "BG_ScrollR_1.0": {},
173 | };
174 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_Dust_L.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import { EmitterConfigV3, Emitter } from "@pixi/particle-emitter";
3 | import { Container, Sprite, TilingSprite } from "pixi.js";
4 | import {
5 | emitterConfigs,
6 | emitterContainer,
7 | emitterStarter,
8 | } from "../emitterUtils";
9 | import {
10 | getEmitterType,
11 | loadSpriteSheet,
12 | sprite2TransParent,
13 | } from "../resourcesUtils";
14 |
15 | export default async function BG_Dust_L(resources: Sprite[]) {
16 | // 原理是三个平铺图片不断移动, 加上火光粒子效果
17 | const { app } = usePlayerStore();
18 | const appWidth = app.view.width;
19 | const appHeight = app.view.height;
20 | let smokeAnimationsName = "dust_smoke";
21 |
22 | let smokeSpritesheet = await loadSpriteSheet(
23 | resources[0],
24 | { x: 1, y: 2 },
25 | smokeAnimationsName
26 | );
27 | const smokeTexture = Reflect.get(
28 | smokeSpritesheet.animations,
29 | smokeAnimationsName
30 | )[0];
31 | const smokeTextureTilingL = new TilingSprite(smokeTexture);
32 | const smokeTextureTilingR = new TilingSprite(smokeTexture);
33 | const smokeTextureTilingR1 = new TilingSprite(smokeTexture);
34 | // 算出一个当前渲染中最长的长度
35 | const smokeWidth = Math.sqrt(appWidth * appWidth + appHeight * appHeight);
36 | // 高度应该是当前分切图片的高度
37 | const smokeHeight = smokeTexture.height;
38 | const scale = (appHeight / smokeHeight) * 0.6;
39 | [smokeTextureTilingL, smokeTextureTilingR, smokeTextureTilingR1].forEach(
40 | i => {
41 | // 避免 tiling 产生的像素
42 | i.clampMargin = 1.5;
43 | i.rotation = 0.55;
44 | i.tint = 0x4c413f;
45 | i.width = smokeWidth;
46 | i.height = smokeHeight;
47 | i.scale.set(scale);
48 | app.stage.addChild(i);
49 | }
50 | );
51 | smokeTextureTilingL.x = -(appWidth * 0.01);
52 | smokeTextureTilingL.y = appHeight - smokeHeight * scale;
53 | // 放大, 避免下方出现空隙
54 | smokeTextureTilingR.rotation = -0.35;
55 | smokeTextureTilingR.scale.set(1.2 * scale);
56 | smokeTextureTilingR.x = appWidth / 2 - appWidth * 0.08;
57 | smokeTextureTilingR.y = appHeight - appHeight * 0.08;
58 | // 角度高一点, 错乱一点, 避免和 R 一致, R1 是后边的图, 并且R1要在人物层之后
59 | smokeTextureTilingR1.rotation = -0.75;
60 | smokeTextureTilingR1.zIndex = -1;
61 | smokeTextureTilingR1.x = appWidth / 2 - appWidth * 0.05;
62 | smokeTextureTilingR1.y = appHeight - appHeight * 0.02;
63 | let smokeRemover = emitterStarter({
64 | update: () => {
65 | // 向左
66 | smokeTextureTilingL.tilePosition.x -= 1;
67 | smokeTextureTilingR.tilePosition.x += 1;
68 | smokeTextureTilingR1.tilePosition.x += 1;
69 | },
70 | destroy: () => {
71 | [smokeTextureTilingL, smokeTextureTilingR, smokeTextureTilingR1].forEach(
72 | i => {
73 | app.stage.removeChild(i);
74 | }
75 | );
76 | },
77 | } as any);
78 | // 火光粒子特效
79 | let fireContainer = new Container();
80 | emitterContainer.addChild(fireContainer);
81 | fireContainer.zIndex = 100;
82 | const transParentSprite = resources[1];
83 | let fireConfig: EmitterConfigV3 = {
84 | ...(emitterConfigs("dust_fire") as EmitterConfigV3),
85 | };
86 | fireConfig.pos = {
87 | x: 30,
88 | y: appHeight - 20,
89 | };
90 | let fireAnimationsName = "dust_fire";
91 | let fireSpritesheet = await loadSpriteSheet(
92 | transParentSprite,
93 | { x: 1, y: 3 },
94 | fireAnimationsName
95 | );
96 | const fireTextures = Reflect.get(
97 | fireSpritesheet.animations,
98 | fireAnimationsName
99 | );
100 | // 塞入随机 texture 中
101 | fireConfig.behaviors[2].config.textures.push(...fireTextures);
102 | const baseRatio = (0.05 * appWidth) / fireTextures[0].width;
103 | const scaleConfig = getEmitterType(fireConfig, "scale").config;
104 | scaleConfig.scale.list[0].value = baseRatio;
105 | scaleConfig.scale.list[1].value = baseRatio * 0.8;
106 | scaleConfig.scale.list[2].value = baseRatio;
107 | const speedConfig = getEmitterType(fireConfig, "moveSpeedStatic").config;
108 | speedConfig.min = appHeight * 0.4;
109 | speedConfig.max = appHeight * 0.65;
110 | let fireEmitter = new Emitter(fireContainer, fireConfig);
111 | setTimeout(() => {
112 | fireEmitter.maxParticles = 15;
113 | }, 1500);
114 | let fireRemover = emitterStarter(fireEmitter);
115 | return async () => {
116 | await smokeRemover();
117 | await fireRemover();
118 | };
119 | }
120 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_Flash_Sound.ts:
--------------------------------------------------------------------------------
1 | import { Sprite, AnimatedSprite } from "pixi.js";
2 | import { loadSpriteSheet } from "../resourcesUtils";
3 | import { usePlayerStore } from "@/stores";
4 | import { gsap } from "gsap";
5 | import { AdvancedBloomFilter } from "@pixi/filter-advanced-bloom";
6 | import eventBus from "@/eventBus";
7 |
8 | export default async function flash_sound(resources: Sprite[]) {
9 | const animationName = "lightning";
10 | const lightningLineSpriteSheet = await loadSpriteSheet(
11 | resources[0],
12 | { x: 1, y: 4 },
13 | animationName
14 | );
15 | const lightningAnimation = new AnimatedSprite(
16 | lightningLineSpriteSheet.animations[animationName]
17 | );
18 | lightningAnimation.animationSpeed = 0.3;
19 |
20 | const app = usePlayerStore().app;
21 | const scale = app.screen.width / lightningAnimation.width;
22 | const scaleYSmall = scale * 0.2;
23 | const scaleYLarge = scale * 0.4;
24 | lightningAnimation.scale.set(scale, scaleYSmall);
25 | lightningAnimation.x = app.screen.width;
26 | lightningAnimation.y = (app.screen.height * 5) / 12;
27 | lightningAnimation.anchor.set(0, 0.5);
28 | const bloomFilter = new AdvancedBloomFilter({ brightness: 1, blur: 4 });
29 | lightningAnimation.filters = [bloomFilter];
30 | app.stage.addChild(lightningAnimation);
31 |
32 | eventBus.emit("playBgEffectSound", "BG_Flash_Sound");
33 | const tl = gsap.timeline();
34 | await tl
35 | .to(lightningAnimation, {
36 | pixi: { x: 0, scaleY: scaleYLarge },
37 | duration: 7 / 60,
38 | })
39 | .to(lightningAnimation, {
40 | onStart: () => {
41 | lightningAnimation.visible = false;
42 | },
43 | duration: 9 / 60,
44 | })
45 | .to(lightningAnimation, {
46 | onStart: () => {
47 | lightningAnimation.visible = true;
48 | lightningAnimation.play();
49 | },
50 | pixi: { scaleY: scaleYSmall },
51 | duration: 41 / 60,
52 | });
53 |
54 | lightningAnimation.destroy();
55 |
56 | return async () => {};
57 | }
58 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_FocusLine.ts:
--------------------------------------------------------------------------------
1 | import eventBus from "@/eventBus";
2 | import { usePlayerStore } from "@/stores";
3 | import { Emitter, EmitterConfigV3, Particle } from "@pixi/particle-emitter";
4 | import { BehaviorOrder } from "@pixi/particle-emitter/lib/behaviors";
5 | import { Container, Sprite } from "pixi.js";
6 | import { emitterConfigs, emitterStarter } from "../emitterUtils";
7 | import { getEmitterType, sprite2TransParent } from "../resourcesUtils";
8 |
9 | export default async function BG_FocusLine(resources: Sprite[]) {
10 | // 原理是线条 emitter
11 | eventBus.emit("playBgEffectSound", "BG_FocusLine");
12 | const { app } = usePlayerStore();
13 | const appWidth = app.view.width;
14 | const appHeight = app.view.height;
15 | let emitterContainer = new Container();
16 | app.stage.addChild(emitterContainer);
17 | emitterContainer.zIndex = -1;
18 | const centerPoint = [appWidth / 2, appHeight / 2].map(i => parseInt(i + ""));
19 | class FocusLine {
20 | public static type = "focusLine";
21 | public order = 5; // 代表延迟执行, 可能是 emitter 包的问题, 引入定义报错
22 | /** 第一个点是两条线段的连接点 */
23 | getAngle = (pointArr: { x: number; y: number }[]) => {
24 | const [p1, p2, p3] = pointArr;
25 | const x1 = p2.x - p1.x;
26 | const x2 = p3.x - p1.x;
27 | const y1 = p2.y - p1.y;
28 | const y2 = p3.y - p1.y;
29 | const dot = x1 * x2 + y1 * y2;
30 | const det = x1 * y2 - y1 * x2;
31 | const angle = (Math.atan2(det, dot) / Math.PI) * 180;
32 | return (angle + 360) % 360;
33 | };
34 | initParticles(first: Particle): void {
35 | let next = first;
36 | while (next) {
37 | const { x, y } = next;
38 | const angle = this.getAngle([
39 | { x, y },
40 | { x: 1, y },
41 | { x: centerPoint[0], y: centerPoint[1] },
42 | ]);
43 | let transAngle = 0; // 最左边不用转
44 | if (x > 0 && y === 0) {
45 | transAngle = 180;
46 | }
47 | if (x > 0 && y === appHeight) {
48 | transAngle = 180;
49 | }
50 | if (x === appWidth && y > 0) {
51 | transAngle = 180;
52 | }
53 | next.angle = angle - transAngle;
54 | next.width =
55 | Math.random() * (appWidth * (0.24 - 0.037)) + appWidth * 0.037; // 0.037 - 0.24 之间
56 | next = next.next;
57 | }
58 | }
59 | }
60 | Emitter.registerBehavior(FocusLine);
61 | let emitterConfig: EmitterConfigV3 = {
62 | ...(emitterConfigs("focusline") as EmitterConfigV3),
63 | };
64 | const sprite = sprite2TransParent(resources[0]);
65 | getEmitterType(emitterConfig, "textureRandom").config.textures.push(
66 | sprite.texture
67 | );
68 | const shapeData = getEmitterType(emitterConfig, "spawnShape").config.data[0];
69 | shapeData[1].y = appHeight;
70 | shapeData[2].x = appWidth;
71 | shapeData[2].y = appHeight;
72 | shapeData[3].x = appWidth;
73 | const ringEmitter = new Emitter(emitterContainer, emitterConfig);
74 | const ringRemover = emitterStarter(ringEmitter);
75 | return async () => {
76 | await ringRemover();
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_Love_L.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import { Emitter, EmitterConfigV3 } from "@pixi/particle-emitter";
3 | import { Container, filters, Rectangle, Sprite, Texture } from "pixi.js";
4 | import { emitterConfigs, emitterStarter } from "../emitterUtils";
5 | import { getEmitterType, sprite2TransParent } from "../resourcesUtils";
6 |
7 | export default async function BG_Love_L(resources: Sprite[]) {
8 | // 原理是波纹, 粉红爱心, 渐变背景
9 | const { app } = usePlayerStore();
10 | const appWidth = app.view.width;
11 | const appHeight = app.view.height;
12 | const alphaFilter = new filters.AlphaFilter(0.5);
13 | const backSprite = resources[2];
14 | backSprite.tint = 0xffc0cb;
15 | backSprite.width = appWidth;
16 | backSprite.height = appHeight;
17 | backSprite.filters = [alphaFilter];
18 | backSprite.zIndex = -1;
19 | app.stage.addChild(backSprite);
20 | // 心心特效
21 | let emitterContainer = new Container();
22 | app.stage.addChild(emitterContainer);
23 | emitterContainer.zIndex = -1;
24 | let heartConfig: EmitterConfigV3 = {
25 | ...(emitterConfigs("love_heart") as EmitterConfigV3),
26 | };
27 | heartConfig.pos = {
28 | x: appWidth / 2,
29 | y: appHeight / 2,
30 | };
31 | // 塞入 texture 中
32 | getEmitterType(heartConfig, "textureRandom").config.textures.push(
33 | resources[0].texture
34 | );
35 | const heartTextureWidth = resources[0].texture.width;
36 | const heartBaseRatio = (0.074 * appWidth) / heartTextureWidth; // 0.074 量出来的, 此时定为emiter时会达到的最大值
37 | const scaleConfig = getEmitterType(heartConfig, "scale").config;
38 | scaleConfig.scale.list[0].value = heartBaseRatio * 0.8;
39 | scaleConfig.scale.list[1].value = heartBaseRatio;
40 | scaleConfig.scale.list[2].value = heartBaseRatio * 0.95;
41 | const curEmitter = new Emitter(emitterContainer, heartConfig);
42 | const heartRemover = emitterStarter(curEmitter);
43 | let ringConfig: EmitterConfigV3 = {
44 | ...(emitterConfigs("love_ring") as EmitterConfigV3),
45 | };
46 | const ringSprite = sprite2TransParent(resources[1]);
47 | getEmitterType(ringConfig, "textureRandom").config.textures.push(
48 | ringSprite.texture
49 | );
50 | getEmitterType(ringConfig, "spawnShape").config.data.w = appWidth;
51 | getEmitterType(ringConfig, "spawnShape").config.data.h = appHeight;
52 | const ringTextureWidth = resources[1].texture.width;
53 | const ringBaseRatio = (0.28 * appWidth) / ringTextureWidth;
54 | const ringScaleConfig = getEmitterType(ringConfig, "scale").config;
55 | ringScaleConfig.scale.list[0].value = ringBaseRatio * 0.9;
56 | ringScaleConfig.scale.list[1].value = ringBaseRatio;
57 | const ringEmitter = new Emitter(emitterContainer, ringConfig);
58 | const ringRemover = emitterStarter(ringEmitter);
59 | return async () => {
60 | await heartRemover();
61 | await ringRemover();
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_Love_L_BGOff.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import { Emitter, EmitterConfigV3 } from "@pixi/particle-emitter";
3 | import { Container, filters, Rectangle, Sprite, Texture } from "pixi.js";
4 | import { emitterConfigs, emitterStarter } from "../emitterUtils";
5 | import { getEmitterType, sprite2TransParent } from "../resourcesUtils";
6 |
7 | export default async function BG_Love_L_BGOff(resources: Sprite[]) {
8 | // 原理是波纹, 粉红爱心, 渐变背景
9 | const { app } = usePlayerStore();
10 | const appWidth = app.view.width;
11 | const appHeight = app.view.height;
12 | const backSprite = resources[2];
13 | backSprite.x = -0.2 * appWidth;
14 | backSprite.y = -0.2 * appHeight;
15 | backSprite.width = appWidth * 1.4;
16 | backSprite.height = appHeight * 1.4;
17 | backSprite.zIndex = -1;
18 | app.stage.addChild(backSprite);
19 | // 心心特效
20 | let emitterContainer = new Container();
21 | app.stage.addChild(emitterContainer);
22 | emitterContainer.zIndex = -1;
23 | let heartConfig: EmitterConfigV3 = {
24 | ...(emitterConfigs("love_heart") as EmitterConfigV3),
25 | };
26 | heartConfig.pos = {
27 | x: appWidth / 2,
28 | y: appHeight / 2,
29 | };
30 | // 塞入 texture 中
31 | getEmitterType(heartConfig, "textureRandom").config.textures.push(
32 | resources[0].texture
33 | );
34 | const heartTextureWidth = resources[0].texture.width;
35 | const heartBaseRatio = (0.074 * appWidth) / heartTextureWidth; // 0.074 量出来的, 此时定为emiter时会达到的最大值
36 | const scaleConfig = getEmitterType(heartConfig, "scale").config;
37 | scaleConfig.scale.list[0].value = heartBaseRatio * 0.8;
38 | scaleConfig.scale.list[1].value = heartBaseRatio;
39 | scaleConfig.scale.list[2].value = heartBaseRatio * 0.95;
40 | const curEmitter = new Emitter(emitterContainer, heartConfig);
41 | const heartRemover = emitterStarter(curEmitter);
42 | let ringConfig: EmitterConfigV3 = {
43 | ...(emitterConfigs("love_ring") as EmitterConfigV3),
44 | };
45 | const ringSprite = sprite2TransParent(resources[1]);
46 | getEmitterType(ringConfig, "textureRandom").config.textures.push(
47 | ringSprite.texture
48 | );
49 | getEmitterType(ringConfig, "spawnShape").config.data.w = appWidth;
50 | getEmitterType(ringConfig, "spawnShape").config.data.h = appHeight;
51 | getEmitterType(ringConfig, "colorStatic").config.color = "#ffe7d8";
52 | const ringTextureWidth = resources[1].texture.width;
53 | const ringBaseRatio = (0.28 * appWidth) / ringTextureWidth;
54 | const ringScaleConfig = getEmitterType(ringConfig, "scale").config;
55 | ringScaleConfig.scale.list[0].value = ringBaseRatio * 0.9;
56 | ringScaleConfig.scale.list[1].value = ringBaseRatio;
57 | const ringEmitter = new Emitter(emitterContainer, ringConfig);
58 | const ringRemover = emitterStarter(ringEmitter);
59 | return async () => {
60 | await heartRemover();
61 | await ringRemover();
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_Rain_L.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import { BGEffectHandlerFunction } from "@/types/effectLayer";
3 | import {
4 | Emitter,
5 | EmitterConfigV2,
6 | upgradeConfig,
7 | } from "@pixi/particle-emitter";
8 | import {
9 | emitterConfigs,
10 | emitterContainer,
11 | emitterStarter,
12 | } from "../emitterUtils";
13 |
14 | const handler: BGEffectHandlerFunction<"BG_Rain_L"> = async function (
15 | resources,
16 | setting,
17 | options
18 | ) {
19 | let newRainConfig: EmitterConfigV2 = { ...emitterConfigs("rain") };
20 | const app = usePlayerStore().app;
21 | newRainConfig.spawnRect!.w = app.view.width;
22 | newRainConfig.spawnRect!.h = app.view.height;
23 | newRainConfig.frequency = options.frequency;
24 | let emitter = new Emitter(
25 | emitterContainer,
26 | upgradeConfig(newRainConfig, [resources[0].texture])
27 | );
28 | return emitterStarter(emitter);
29 | };
30 |
31 | export default handler;
32 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_SandStorm_L.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import { filters, Sprite, TilingSprite } from "pixi.js";
3 | import { emitterStarter } from "../emitterUtils";
4 | import { loadSpriteSheet } from "../resourcesUtils";
5 |
6 | export default async function BG_SandStorm_L(resources: Sprite[]) {
7 | // 原理是两个平铺图片不断移动
8 | const { app } = usePlayerStore();
9 | const appWidth = app.view.width;
10 | const appHeight = app.view.height;
11 | let animationsName = "sandStorm";
12 | let spritesheet = await loadSpriteSheet(
13 | resources[0],
14 | { x: 1, y: 4 },
15 | animationsName
16 | );
17 | const texture = Reflect.get(spritesheet.animations, animationsName)[3];
18 | const TextureTilingBack = new TilingSprite(texture);
19 | const TextureTilingFront = new TilingSprite(texture);
20 | // 算出一个当前渲染中最长的长度
21 | const width = Math.sqrt(appWidth * appWidth + appHeight * appHeight);
22 | const height = texture.height;
23 | const scale = appHeight / height;
24 | const blurFilter = new filters.BlurFilter();
25 | [TextureTilingBack, TextureTilingFront].forEach(i => {
26 | // 避免 tiling 产生的像素
27 | i.clampMargin = 1.5;
28 | i.tint = 0xb98c56;
29 | i.width = width;
30 | i.height = height;
31 | i.filters = [blurFilter];
32 | i.scale.set(scale);
33 | app.stage.addChild(i);
34 | });
35 | TextureTilingBack.y = appHeight * 0.4;
36 | TextureTilingBack.zIndex = -1;
37 | TextureTilingFront.tilePosition.x = appWidth * 0.05; // 错开一点
38 | TextureTilingFront.y = appHeight * 0.75;
39 | let Remover = emitterStarter({
40 | update: () => {
41 | TextureTilingBack.tilePosition.x += 1.3;
42 | TextureTilingFront.tilePosition.x += 1.3;
43 | },
44 | destroy: () => {
45 | app.stage.removeChild(TextureTilingBack);
46 | app.stage.removeChild(TextureTilingFront);
47 | },
48 | } as any);
49 | return async () => {
50 | await Remover();
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_Shining_L.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import { Emitter, EmitterConfigV3 } from "@pixi/particle-emitter";
3 | import { Container, filters, Rectangle, Sprite, Texture } from "pixi.js";
4 | import { emitterConfigs, emitterStarter } from "../emitterUtils";
5 | import { getEmitterType, sprite2TransParent } from "../resourcesUtils";
6 |
7 | export default async function BG_Shining_L(resources: Sprite[]) {
8 | // 原理是波纹
9 | const { app } = usePlayerStore();
10 | const appWidth = app.view.width;
11 | const appHeight = app.view.height;
12 | // 底图
13 | const backSprite = resources[2];
14 | backSprite.x = -0.2 * appWidth;
15 | backSprite.y = -0.2 * appHeight;
16 | backSprite.width = appWidth * 1.4;
17 | backSprite.height = appHeight * 1.4;
18 | backSprite.zIndex = -1;
19 | // 紫色覆盖
20 | const alphaFilter = new filters.AlphaFilter(0.8);
21 | const backPurpleSprite = resources[3];
22 | backPurpleSprite.tint = 0xd59ffb;
23 | backPurpleSprite.width = appWidth;
24 | backPurpleSprite.height = appHeight;
25 | backPurpleSprite.filters = [alphaFilter];
26 | backPurpleSprite.zIndex = -1;
27 | app.stage.addChild(backSprite);
28 | app.stage.addChild(backPurpleSprite);
29 | // 波纹特效
30 | let emitterContainer = new Container();
31 | app.stage.addChild(emitterContainer);
32 | emitterContainer.zIndex = -1;
33 | let ringConfig: EmitterConfigV3 = {
34 | ...(emitterConfigs("love_ring") as EmitterConfigV3),
35 | };
36 | const ringSprite = sprite2TransParent(resources[0]);
37 | getEmitterType(ringConfig, "textureRandom").config.textures.push(
38 | ringSprite.texture
39 | );
40 | getEmitterType(ringConfig, "colorStatic").config.color = "#ffffff";
41 | getEmitterType(ringConfig, "spawnShape").config.data.w = appWidth;
42 | getEmitterType(ringConfig, "spawnShape").config.data.h = appHeight;
43 | const ringTextureWidth = resources[0].texture.width;
44 | const ringBaseRatio = (0.4 * appWidth) / ringTextureWidth;
45 | const ringScaleConfig = getEmitterType(ringConfig, "scale").config;
46 | ringScaleConfig.scale.list[0].value = ringBaseRatio * 0.9;
47 | ringScaleConfig.scale.list[1].value = ringBaseRatio;
48 | const ringEmitter = new Emitter(emitterContainer, ringConfig);
49 | const ringRemover = emitterStarter(ringEmitter);
50 | const flareSprite = sprite2TransParent(resources[1]);
51 | const flareConfig: EmitterConfigV3 = {
52 | ...(emitterConfigs("shining_flare") as EmitterConfigV3),
53 | };
54 | getEmitterType(flareConfig, "textureRandom").config.textures.push(
55 | flareSprite.texture
56 | );
57 | getEmitterType(flareConfig, "spawnShape").config.data.w = appWidth;
58 | getEmitterType(flareConfig, "spawnShape").config.data.h = appHeight;
59 | const flareEmitter = new Emitter(emitterContainer, flareConfig);
60 | const flareRemover = emitterStarter(flareEmitter);
61 | return async () => {
62 | await ringRemover();
63 | await flareRemover();
64 | app.stage.removeChild(backSprite);
65 | app.stage.removeChild(backPurpleSprite);
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_Shining_L_BGOff.ts:
--------------------------------------------------------------------------------
1 | import { usePlayerStore } from "@/stores";
2 | import { Emitter, EmitterConfigV3 } from "@pixi/particle-emitter";
3 | import { Container, filters, Rectangle, Sprite, Texture } from "pixi.js";
4 | import { emitterConfigs, emitterStarter } from "../emitterUtils";
5 | import { getEmitterType, sprite2TransParent } from "../resourcesUtils";
6 |
7 | export default async function BG_Shining_L_BGOff(resources: Sprite[]) {
8 | // 原理是波纹
9 | const { app } = usePlayerStore();
10 | const appWidth = app.view.width;
11 | const appHeight = app.view.height;
12 | // 紫色覆盖
13 | const alphaFilter = new filters.AlphaFilter(0.4);
14 | const backPurpleSprite = resources[2];
15 | backPurpleSprite.tint = 0xd59ffb;
16 | backPurpleSprite.width = appWidth;
17 | backPurpleSprite.height = appHeight;
18 | backPurpleSprite.filters = [alphaFilter];
19 | backPurpleSprite.zIndex = -1;
20 | app.stage.addChild(backPurpleSprite);
21 | // 波纹特效
22 | let emitterContainer = new Container();
23 | app.stage.addChild(emitterContainer);
24 | let ringConfig: EmitterConfigV3 = {
25 | ...(emitterConfigs("shining_ring") as EmitterConfigV3),
26 | };
27 | const ringSprite = sprite2TransParent(resources[0]);
28 | getEmitterType(ringConfig, "textureRandom").config.textures.push(
29 | ringSprite.texture
30 | );
31 | getEmitterType(ringConfig, "spawnShape").config.data.w = appWidth;
32 | getEmitterType(ringConfig, "spawnShape").config.data.h = appHeight;
33 | const ringTextureWidth = resources[0].texture.width;
34 | const ringBaseRatio = (0.7 * appWidth) / ringTextureWidth;
35 | const ringScaleConfig = getEmitterType(ringConfig, "scale").config;
36 | ringScaleConfig.scale.list[0].value = ringBaseRatio * 0.9;
37 | ringScaleConfig.scale.list[1].value = ringBaseRatio;
38 | // 闪光
39 | const flareSprite = sprite2TransParent(resources[1]);
40 | const flareConfig: EmitterConfigV3 = {
41 | ...(emitterConfigs("shining_flare") as EmitterConfigV3),
42 | };
43 | getEmitterType(flareConfig, "textureRandom").config.textures.push(
44 | flareSprite.texture
45 | );
46 | getEmitterType(flareConfig, "spawnShape").config.data.w = appWidth;
47 | getEmitterType(flareConfig, "spawnShape").config.data.h = appHeight;
48 | const flareEmitter = new Emitter(emitterContainer, flareConfig);
49 | const flareRemover = emitterStarter(flareEmitter);
50 | const ringEmitter = new Emitter(emitterContainer, ringConfig);
51 | const ringRemover = emitterStarter(ringEmitter);
52 | return async () => {
53 | await ringRemover();
54 | await flareRemover();
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/effectFunctions/BG_UnderFire.ts:
--------------------------------------------------------------------------------
1 | import eventBus from "@/eventBus";
2 | import { usePlayerStore } from "@/stores";
3 | import { BGEffectHandlerFunction } from "@/types/effectLayer";
4 | import { Emitter, EmitterConfigV3 } from "@pixi/particle-emitter";
5 | import { Container } from "pixi.js";
6 | import {
7 | emitterConfigs,
8 | emitterContainer,
9 | emitterStarter,
10 | } from "../emitterUtils";
11 | import { loadSpriteSheet } from "../resourcesUtils";
12 |
13 | const handler: BGEffectHandlerFunction<"BG_UnderFire"> = async function (
14 | resources,
15 | setting,
16 | options
17 | ) {
18 | let { height: appHeight, width: appWidth } = usePlayerStore().app.screen;
19 | let ininX = (appWidth * 7) / 8;
20 | let ininY = (appHeight * 7) / 8;
21 | eventBus.emit("playOtherSounds", "bg_underfire");
22 |
23 | //烟雾效果, 通过spritesheet实现烟雾散开
24 | let smokeContainer = new Container();
25 | emitterContainer.addChild(smokeContainer);
26 | smokeContainer.zIndex = 1;
27 | let smokeConifg: EmitterConfigV3 = {
28 | ...(emitterConfigs("smoke") as EmitterConfigV3),
29 | };
30 | smokeConifg.pos = {
31 | x: ininX,
32 | y: ininY,
33 | };
34 | let smokeAnimationsName = "smoke";
35 | let smokeSpritesheet = await loadSpriteSheet(
36 | resources[0],
37 | { x: 3, y: 3 },
38 | smokeAnimationsName
39 | );
40 | smokeConifg.behaviors[0].config.anim.textures = Reflect.get(
41 | smokeSpritesheet.animations,
42 | smokeAnimationsName
43 | );
44 | let smokeImageHeight = resources[0].height / 3;
45 | //根据高度算缩放比例
46 | let smokeScale = ((2 / 5) * appHeight) / smokeImageHeight;
47 | Reflect.set(smokeConifg.behaviors[1].config, "min", smokeScale);
48 | Reflect.set(smokeConifg.behaviors[1].config, "max", smokeScale + 0.5);
49 | let smokeMoveSpeed = (1 / 2) * appHeight;
50 | Reflect.set(smokeConifg.behaviors[2].config, "min", smokeMoveSpeed);
51 | Reflect.set(smokeConifg.behaviors[2].config, "max", smokeMoveSpeed + 10);
52 | let smokeEmitter = new Emitter(smokeContainer, smokeConifg);
53 | let smokeRemover = emitterStarter(smokeEmitter);
54 |
55 | //火焰效果, emitter随机从三个素材中选一个发出
56 | let fireContainer = new Container();
57 | emitterContainer.addChild(fireContainer);
58 | fireContainer.zIndex = 2;
59 | let fireConfig: EmitterConfigV3 = {
60 | ...(emitterConfigs("fire") as EmitterConfigV3),
61 | };
62 | fireConfig.pos = {
63 | x: ininX,
64 | y: ininY,
65 | };
66 | let fireImgs = resources.slice(1, 4);
67 | let fireScale = ((1 / 3) * appHeight) / fireImgs[0].height;
68 | Reflect.set(fireConfig.behaviors[0].config.scale, "list", [
69 | {
70 | value: fireScale,
71 | time: 0,
72 | },
73 | {
74 | value: fireScale - 0.1,
75 | time: 1,
76 | },
77 | ]);
78 | for (let i = 0; i < 3; ++i) {
79 | //textureRandom behaviors
80 | fireConfig.behaviors[2].config.textures.push(fireImgs[i].texture);
81 | }
82 | let fireEmitter = new Emitter(fireContainer, fireConfig);
83 | let fireRemover = emitterStarter(fireEmitter);
84 |
85 | let firelineContainer = new Container();
86 | emitterContainer.addChild(firelineContainer);
87 | firelineContainer.zIndex = 0;
88 | let firelineConfig: EmitterConfigV3 = {
89 | ...(emitterConfigs("fireline") as EmitterConfigV3),
90 | };
91 | let firelineImage = resources[4];
92 | firelineConfig.behaviors[0].config.texture = firelineImage.texture;
93 | firelineConfig.pos = {
94 | x: ininX,
95 | y: ininY,
96 | };
97 | let firelineScale = ((1 / 16) * appHeight) / firelineImage.height;
98 | Reflect.set(firelineConfig.behaviors[1].config, "min", firelineScale - 0.2);
99 | Reflect.set(firelineConfig.behaviors[1].config, "max", firelineScale);
100 | let firelineMoveSpeed = 2 * appHeight;
101 | Reflect.set(
102 | firelineConfig.behaviors[2].config,
103 | "min",
104 | firelineMoveSpeed * 0.95
105 | );
106 | Reflect.set(firelineConfig.behaviors[2].config, "max", firelineMoveSpeed);
107 | let fireLineEmitter = new Emitter(firelineContainer, firelineConfig);
108 | let firelineRemover = emitterStarter(fireLineEmitter);
109 |
110 | let posX = smokeEmitter.spawnPos.x;
111 | let posY = smokeEmitter.spawnPos.y;
112 |
113 | //原点向左移动, 移出屏幕后停止
114 | await new Promise(resolve => {
115 | let underfirePlay = setInterval(async () => {
116 | if (posX <= -ininX) {
117 | clearInterval(underfirePlay);
118 | await smokeRemover();
119 | await fireRemover();
120 | await firelineRemover();
121 | resolve();
122 | return;
123 | }
124 | posX -= usePlayerStore().app.screen.width / 5;
125 | smokeEmitter.updateSpawnPos(posX, posY);
126 | fireEmitter.updateSpawnPos(posX, posY);
127 | fireLineEmitter.updateSpawnPos(posX, posY);
128 | }, 100);
129 | });
130 |
131 | return () => Promise.resolve();
132 | };
133 |
134 | export default handler;
135 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/dust_fire.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 4,
4 | "max": 8
5 | },
6 | "frequency": 0.1,
7 | "maxParticles": 10,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "scale",
16 | "config": {
17 | "scale": {
18 | "list": [
19 | {
20 | "value": 0.3,
21 | "time": 0
22 | },
23 | {
24 | "value": 0.15,
25 | "time": 0.5
26 | },
27 | {
28 | "value": 0.3,
29 | "time": 1
30 | }
31 | ]
32 | },
33 | "minMult": 0.5
34 | }
35 | },
36 | {
37 | "type": "rotation",
38 | "config": {
39 | "accel": 0,
40 | "minSpeed": 0,
41 | "maxSpeed": 0,
42 | "minStart": 275,
43 | "maxStart": 353
44 | }
45 | },
46 | {
47 | "type": "textureRandom",
48 | "config": {
49 | "textures": []
50 | }
51 | },
52 | {
53 | "type": "spawnShape",
54 | "config": {
55 | "type": "rect",
56 | "data": {
57 | "x": 0,
58 | "y": 0,
59 | "w": 260,
60 | "h": 0
61 | }
62 | }
63 | },
64 | {
65 | "type": "moveSpeedStatic",
66 | "config": {
67 | "min": 300,
68 | "max": 350
69 | }
70 | },
71 | {
72 | "type": "alpha",
73 | "config": {
74 | "alpha": {
75 | "list": [
76 | { "value": 1, "time": 0 },
77 | { "value": 1, "time": 0.9 },
78 | { "value": 0, "time": 1 }
79 | ]
80 | }
81 | }
82 | }
83 | ]
84 | }
85 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/fire.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 0.15,
4 | "max": 0.2
5 | },
6 | "frequency": 0.07,
7 | "maxParticles": 1000,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "scale",
16 | "config": {
17 | "scale": {
18 | "list": []
19 | },
20 | "minMult": 0.5
21 | }
22 | },
23 | {
24 | "type": "rotation",
25 | "config": {
26 | "accel": 0,
27 | "minSpeed": 0,
28 | "maxSpeed": 0,
29 | "minStart": -45,
30 | "maxStart": 45
31 | }
32 | },
33 | {
34 | "type": "textureRandom",
35 | "config": {
36 | "textures": []
37 | }
38 | },
39 | {
40 | "type": "spawnShape",
41 | "config": {
42 | "type": "rect",
43 | "data": {
44 | "x": 0,
45 | "y": 0,
46 | "w": 60,
47 | "h": 0
48 | }
49 | }
50 | },
51 | {
52 | "type": "colorStatic",
53 | "config": {
54 | "color": "#fa8057"
55 | }
56 | }
57 | ]
58 | }
59 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/fireline.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 1,
4 | "max": 1.5
5 | },
6 | "frequency": 0.1,
7 | "maxParticles": 1000,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "textureSingle",
16 | "config": {
17 | "texture": null
18 | }
19 | },
20 | {
21 | "type": "scaleStatic",
22 | "config": {
23 | "min": 0.4,
24 | "max": 0.6
25 | }
26 | },
27 | {
28 | "type": "moveSpeedStatic",
29 | "config": {
30 | "min": 800,
31 | "max": 850
32 | }
33 | },
34 | {
35 | "type": "rotation",
36 | "config": {
37 | "accel": 0,
38 | "minSpeed": 0,
39 | "maxSpeed": 0,
40 | "minStart": 230,
41 | "maxStart": 300
42 | }
43 | },
44 | {
45 | "type": "spawnShape",
46 | "config": {
47 | "type": "rect",
48 | "data": {
49 | "x": 0,
50 | "y": 0,
51 | "w": 60,
52 | "h": 0
53 | }
54 | }
55 | }
56 | ]
57 | }
58 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/focusline.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 0.1,
4 | "max": 0.2
5 | },
6 | "frequency": 0.005,
7 | "maxParticles": 90,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "textureRandom",
16 | "config": {
17 | "textures": []
18 | }
19 | },
20 | {
21 | "type": "spawnShape",
22 | "config": {
23 | "type": "polygonalChain",
24 | "data": [
25 | [
26 | { "x": 0, "y": 0 },
27 | { "x": 0, "y": 1000 },
28 | { "x": 1000, "y": 1000 },
29 | { "x": 1000, "y": 0 },
30 | { "x": 0, "y": 0 }
31 | ]
32 | ]
33 | }
34 | },
35 | {
36 | "type": "colorStatic",
37 | "config": {
38 | "color": "#000000"
39 | }
40 | },
41 | {
42 | "type": "focusLine"
43 | }
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/love_heart.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 2,
4 | "max": 3
5 | },
6 | "frequency": 0.5,
7 | "maxParticles": 10,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "noRotation",
16 | "config": {
17 | "rotation": 0
18 | }
19 | },
20 | {
21 | "type": "scale",
22 | "config": {
23 | "scale": {
24 | "list": [
25 | {
26 | "value": 1,
27 | "time": 0
28 | },
29 | {
30 | "value": 0.8,
31 | "time": 0.5
32 | },
33 | {
34 | "value": 1,
35 | "time": 1
36 | }
37 | ]
38 | },
39 | "minMult": 0.5
40 | }
41 | },
42 | {
43 | "type": "rotation",
44 | "config": {
45 | "accel": 0,
46 | "minSpeed": 0,
47 | "maxSpeed": 0,
48 | "minStart": 180,
49 | "maxStart": 360
50 | }
51 | },
52 | {
53 | "type": "textureRandom",
54 | "config": {
55 | "textures": []
56 | }
57 | },
58 | {
59 | "type": "moveSpeedStatic",
60 | "config": {
61 | "min": 150,
62 | "max": 200
63 | }
64 | },
65 | {
66 | "type": "colorStatic",
67 | "config": {
68 | "color": "#fea399"
69 | }
70 | },
71 | {
72 | "type": "alpha",
73 | "config": {
74 | "alpha": {
75 | "list": [
76 | { "value": 1, "time": 0 },
77 | { "value": 0.3, "time": 0.5 },
78 | { "value": 1, "time": 0.9 },
79 | { "value": 0, "time": 1 }
80 | ]
81 | }
82 | }
83 | }
84 | ]
85 | }
86 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/love_ring.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 4,
4 | "max": 5
5 | },
6 | "frequency": 0.65,
7 | "maxParticles": 4,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "scale",
16 | "config": {
17 | "scale": {
18 | "list": [
19 | {
20 | "value": 1,
21 | "time": 0
22 | },
23 | {
24 | "value": 1.2,
25 | "time": 1
26 | }
27 | ]
28 | },
29 | "minMult": 0.5
30 | }
31 | },
32 | {
33 | "type": "textureRandom",
34 | "config": {
35 | "textures": []
36 | }
37 | },
38 | {
39 | "type": "moveSpeedStatic",
40 | "config": {
41 | "min": 0,
42 | "max": 0
43 | }
44 | },
45 | {
46 | "type": "alpha",
47 | "config": {
48 | "alpha": {
49 | "list": [
50 | { "value": 0, "time": 0 },
51 | { "value": 1, "time": 0.1 },
52 | { "value": 1, "time": 0.9 },
53 | { "value": 0, "time": 1 }
54 | ]
55 | }
56 | }
57 | },
58 | {
59 | "type": "spawnShape",
60 | "config": {
61 | "type": "rect",
62 | "data": {
63 | "x": 0,
64 | "y": 0,
65 | "w": 0,
66 | "h": 0
67 | }
68 | }
69 | },
70 | {
71 | "type": "colorStatic",
72 | "config": {
73 | "color": "#ffc0cb"
74 | }
75 | }
76 | ]
77 | }
78 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/rain.json:
--------------------------------------------------------------------------------
1 | {
2 | "alpha": {
3 | "start": 0.5,
4 | "end": 0.5
5 | },
6 | "scale": {
7 | "start": 1,
8 | "end": 1,
9 | "minimumScaleMultiplier": 1
10 | },
11 | "color": {
12 | "start": "#ffffff",
13 | "end": "#ffffff"
14 | },
15 | "speed": {
16 | "start": 1300,
17 | "end": 3000,
18 | "minimumSpeedMultiplier": 1
19 | },
20 | "acceleration": {
21 | "x": 0,
22 | "y": 0
23 | },
24 | "maxSpeed": 0,
25 | "startRotation": {
26 | "min": 90,
27 | "max": 90
28 | },
29 | "noRotation": false,
30 | "rotationSpeed": {
31 | "min": 0,
32 | "max": 0
33 | },
34 | "lifetime": {
35 | "min": 2,
36 | "max": 2
37 | },
38 | "blendMode": "normal",
39 | "frequency": 0.04,
40 | "emitterLifetime": -1,
41 | "maxParticles": 1000,
42 | "pos": {
43 | "x": 0,
44 | "y": 0
45 | },
46 | "addAtBack": false,
47 | "spawnType": "rect",
48 | "spawnRect": {
49 | "x": 0,
50 | "y": 0,
51 | "w": 900,
52 | "h": 20
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/shining_flare.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 1,
4 | "max": 1.5
5 | },
6 | "frequency": 0.2,
7 | "maxParticles": 8,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "scale",
16 | "config": {
17 | "scale": {
18 | "list": [
19 | {
20 | "value": 0.1,
21 | "time": 0
22 | },
23 | {
24 | "value": 1,
25 | "time": 0.5
26 | },
27 | {
28 | "value": 0,
29 | "time": 1
30 | }
31 | ]
32 | },
33 | "minMult": 0.5
34 | }
35 | },
36 | {
37 | "type": "textureRandom",
38 | "config": {
39 | "textures": []
40 | }
41 | },
42 | {
43 | "type": "spawnShape",
44 | "config": {
45 | "type": "rect",
46 | "data": {
47 | "x": 0,
48 | "y": 0,
49 | "w": 0,
50 | "h": 0
51 | }
52 | }
53 | },
54 | {
55 | "type": "colorStatic",
56 | "config": {
57 | "color": "#ffffff"
58 | }
59 | },
60 | {
61 | "type": "rotationStatic",
62 | "config": {
63 | "min": 45,
64 | "max": 45
65 | }
66 | }
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/shining_ring.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 4,
4 | "max": 5
5 | },
6 | "frequency": 1.5,
7 | "maxParticles": 3,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "scale",
16 | "config": {
17 | "scale": {
18 | "list": [
19 | {
20 | "value": 1,
21 | "time": 0
22 | },
23 | {
24 | "value": 1.2,
25 | "time": 1
26 | }
27 | ]
28 | },
29 | "minMult": 0.5
30 | }
31 | },
32 | {
33 | "type": "textureRandom",
34 | "config": {
35 | "textures": []
36 | }
37 | },
38 | {
39 | "type": "alpha",
40 | "config": {
41 | "alpha": {
42 | "list": [
43 | { "value": 0, "time": 0 },
44 | { "value": 0.7, "time": 0.1 },
45 | { "value": 1, "time": 0.4 },
46 | { "value": 0, "time": 1 }
47 | ]
48 | }
49 | }
50 | },
51 | {
52 | "type": "spawnShape",
53 | "config": {
54 | "type": "rect",
55 | "data": {
56 | "x": 0,
57 | "y": 0,
58 | "w": 0,
59 | "h": 0
60 | }
61 | }
62 | },
63 | {
64 | "type": "colorStatic",
65 | "config": {
66 | "color": "#ffffff"
67 | }
68 | }
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterConfigs/smoke.json:
--------------------------------------------------------------------------------
1 | {
2 | "lifetime": {
3 | "min": 0.2,
4 | "max": 0.3
5 | },
6 | "frequency": 0.08,
7 | "maxParticles": 1000,
8 | "addAtBack": false,
9 | "pos": {
10 | "x": 0,
11 | "y": 0
12 | },
13 | "behaviors": [
14 | {
15 | "type": "animatedSingle",
16 | "config": {
17 | "anim": {
18 | "framerate": 25,
19 | "textures": []
20 | }
21 | }
22 | },
23 | {
24 | "type": "scaleStatic",
25 | "config": {
26 | "min": 2.5,
27 | "max": 3
28 | }
29 | },
30 | {
31 | "type": "moveSpeedStatic",
32 | "config": {
33 | "min": 300,
34 | "max": 310
35 | }
36 | },
37 | {
38 | "type": "color",
39 | "config": {
40 | "color": {
41 | "list": [
42 | {
43 | "value": "#9A9388",
44 | "time": 0
45 | },
46 | {
47 | "value": "#9A9388",
48 | "time": 0.5
49 | },
50 | {
51 | "value": "#9A9388",
52 | "time": 1
53 | }
54 | ]
55 | }
56 | }
57 | },
58 | {
59 | "type": "rotation",
60 | "config": {
61 | "accel": 0,
62 | "minSpeed": 0,
63 | "maxSpeed": 50,
64 | "minStart": 260,
65 | "maxStart": 280
66 | }
67 | },
68 | {
69 | "type": "spawnShape",
70 | "config": {
71 | "type": "rect",
72 | "data": {
73 | "x": 0,
74 | "y": 0,
75 | "w": 60,
76 | "h": 0
77 | }
78 | }
79 | }
80 | ]
81 | }
82 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/emitterUtils.ts:
--------------------------------------------------------------------------------
1 | import { EffectRemoveFunction } from "@/types/effectLayer";
2 | import {
3 | Emitter,
4 | EmitterConfigV2,
5 | EmitterConfigV3,
6 | } from "@pixi/particle-emitter";
7 | import { Container } from "pixi.js";
8 |
9 | /**
10 | * 给emitter用的container
11 | */
12 | export const emitterContainer = new Container();
13 | emitterContainer.zIndex = 15;
14 | emitterContainer.sortableChildren = true;
15 |
16 | const emitterConfigsRaw = import.meta.glob(
17 | "./emitterConfigs/*.json",
18 | { eager: true }
19 | );
20 | /**
21 | * 获取emitter config
22 | * @param filename 文件名, 不需要加.json后缀
23 | * @returns
24 | */
25 | export function emitterConfigs(filename: string) {
26 | let config = Reflect.get(
27 | emitterConfigsRaw,
28 | `./emitterConfigs/${filename}.json`
29 | );
30 | if (!config) {
31 | throw new Error("emitter参数获取失败, 文件名错误或文件不存在");
32 | }
33 | return config;
34 | }
35 |
36 | /**
37 | * emitter工具函数, 会自动启动emitter并返回一个终止函数
38 | * @param emitter
39 | * @param stopCallback 终止函数中调用的函数
40 | * @returns 终止函数, 功能是停止当前emitter并回收
41 | */
42 | export function emitterStarter(
43 | emitter: Emitter,
44 | stopCallback?: () => void
45 | ): EffectRemoveFunction {
46 | let elapsed = Date.now();
47 | let stopFlag = false;
48 | // Update function every frame
49 | let update = function () {
50 | if (stopFlag) {
51 | return;
52 | }
53 | // Update the next frame
54 | requestAnimationFrame(update);
55 |
56 | var now = Date.now();
57 | // The emitter requires the elapsed
58 | // number of seconds since the last update
59 | emitter.update((now - elapsed) * 0.001);
60 | elapsed = now;
61 | };
62 |
63 | let stop = async function () {
64 | stopFlag = true;
65 | emitter.emit = false;
66 | emitter.destroy();
67 | if (stopCallback) {
68 | stopCallback();
69 | }
70 | };
71 |
72 | // Start emitting
73 | emitter.emit = true;
74 |
75 | // Start the update
76 | update();
77 |
78 | return stop;
79 | }
80 |
--------------------------------------------------------------------------------
/lib/layers/effectLayer/resourcesUtils.ts:
--------------------------------------------------------------------------------
1 | import { EmitterConfigV3 } from "@pixi/particle-emitter";
2 | import { BaseTexture, ImageResource, Sprite, Spritesheet } from "pixi.js";
3 | /**
4 | * 把黑白png图片的黑色转为透明度
5 | * https://blog.csdn.net/jdk137/article/details/106216318
6 | * @param img Sprite
7 | * @returns
8 | */
9 | export function sprite2TransParent(img: Sprite) {
10 | // 这里的 source 代表着dom中的image节点, 所以图片加载类型不是通过url的话就不行
11 | const texture = img.texture.baseTexture as BaseTexture;
12 | const { realWidth, realHeight } = img.texture.baseTexture;
13 | let imgSource = texture.resource.source;
14 | let canvas = document.createElement("canvas");
15 | // document.body.appendChild(canvas)
16 | canvas.setAttribute("width", realWidth + "");
17 | canvas.setAttribute("height", realHeight + "");
18 | let ctx = canvas.getContext("2d")!;
19 | ctx.drawImage(imgSource, 0, 0);
20 |
21 | let pixel = ctx.getImageData(0, 0, realWidth, realHeight);
22 | let data = pixel.data;
23 | for (var i = 0; i < data.length; i += 4) {
24 | data[i + 3] = Math.round((data[i] + data[i + 1] + data[i + 2]) / 3);
25 | data[i] = 255;
26 | data[i + 1] = 255;
27 | data[i + 2] = 255;
28 | }
29 | ctx.putImageData(pixel, 0, 0);
30 | const dataUrl = ctx.canvas.toDataURL("image/png");
31 | // download(dataUrl)
32 | const newSprite = Sprite.from(dataUrl);
33 | return newSprite;
34 | }
35 | function download(b64: string) {
36 | var a = document.createElement("a");
37 | a.href = b64;
38 | a.download = "result.png"; //设定下载名称
39 | var evt = document.createEvent("MouseEvents");
40 | evt.initEvent("click", true, true);
41 | a.dispatchEvent(evt);
42 | }
43 |
44 | /**
45 | * 根据给定的信息, 加载spriteSheet
46 | * @param img spriteSheet原图片Sprite
47 | * @param quantity x, y方向上小图片的个数
48 | * @param animationsName 该图片组成的动画的名字, 用于访问资源
49 | */
50 | export async function loadSpriteSheet(
51 | img: Sprite,
52 | quantity: { x: number; y: number },
53 | animationsName: string
54 | ): Promise {
55 | // Create object to store sprite sheet data
56 | let atlasData = {
57 | frames: {},
58 | meta: {
59 | scale: "1",
60 | },
61 | animations: {} as Record,
62 | };
63 | Reflect.set(atlasData.animations, animationsName, []);
64 |
65 | img.scale.set(1);
66 | let xNum = quantity.x;
67 | let yNum = quantity.y;
68 | let width = img.width / xNum;
69 | let height = img.height / yNum;
70 | for (let i = 0; i < xNum * yNum; ++i) {
71 | Reflect.set(atlasData.frames, `${animationsName}${i}`, {
72 | frame: {
73 | x: width * (i % xNum),
74 | y: height * Math.trunc(i / xNum),
75 | w: width,
76 | h: height,
77 | },
78 | sourceSize: { w: width, h: height },
79 | spriteSourceSize: { x: 0, y: 0, w: width, h: height },
80 | });
81 | atlasData.animations[animationsName].push(`${animationsName}${i}`);
82 | }
83 |
84 | // Create the SpriteSheet from data and image
85 | const spritesheet = new Spritesheet(img.texture, atlasData);
86 |
87 | // Generate all the Textures asynchronously
88 | await spritesheet.parse();
89 |
90 | return spritesheet;
91 | }
92 | /**
93 | * 获取 emitter config behaviors 中的配置
94 | */
95 | export function getEmitterType(config: EmitterConfigV3, type: string) {
96 | return config.behaviors.find(i => {
97 | return i.type === type;
98 | })!;
99 | }
100 |
--------------------------------------------------------------------------------
/lib/layers/l2dLayer/l2dConfig.ts:
--------------------------------------------------------------------------------
1 | import { IL2dConfig } from "@/types/l2d";
2 |
3 | export const l2dConfig: IL2dConfig = {
4 | CH0184_home: {
5 | name: "CH0184_home",
6 | playQue: [
7 | {
8 | name: "CH0184_home",
9 | animation: "Start_Idle_01",
10 | fade: true,
11 | },
12 | {
13 | name: "CH0184_home/CH0184_00/CH0184_00",
14 | animation: "Start_Idle_01",
15 | fade: true,
16 | },
17 | {
18 | name: "CH0184_home",
19 | animation: "Start_Idle_02",
20 | },
21 | ],
22 | otherSpine: ["CH0184_home/CH0184_00/CH0184_00"],
23 | },
24 | CH0198_home: {
25 | name: "CH0198_home",
26 | playQue: [
27 | {
28 | name: "CH0198_home",
29 | animation: "Start_Idle_01",
30 | fadeTime: 4.8,
31 | fade: true,
32 | },
33 | {
34 | name: "CH0198_home",
35 | animation: "Idle_01",
36 | },
37 | ],
38 | spineSettings: {
39 | CH0198_home: {
40 | scale: 1.5,
41 | },
42 | },
43 | },
44 | CH0087_home: {
45 | name: "CH0087_home",
46 | playQue: [
47 | {
48 | name: "CH0087_home",
49 | animation: "Start_Idle_01",
50 | fadeTime: 4.8,
51 | fade: true,
52 | },
53 | {
54 | name: "CH0087_home",
55 | animation: "Idle_01",
56 | },
57 | ],
58 | spineSettings: {
59 | CH0087_home: {
60 | scale: 1.3,
61 | },
62 | },
63 | },
64 | CH0107_home: {
65 | name: "CH0107_home",
66 | playQue: [
67 | {
68 | name: "CH0107_home",
69 | animation: "Start_Idle_01",
70 | fadeTime: 4.2,
71 | secondFadeTime: 8.2,
72 | fade: true,
73 | },
74 | {
75 | name: "CH0107_home",
76 | animation: "Idle_01",
77 | },
78 | ],
79 | spineSettings: {
80 | CH0107_home: {
81 | scale: 1.3,
82 | },
83 | },
84 | },
85 | CH0086_home: {
86 | name: "CH0087_home",
87 | playQue: [
88 | {
89 | name: "CH0087_home",
90 | animation: "Start_Idle_01",
91 | fadeTime: 4.3,
92 | fade: true,
93 | },
94 | {
95 | name: "CH0087_home",
96 | animation: "Idle_01",
97 | },
98 | ],
99 | spineSettings: {
100 | CH0087_home: {
101 | scale: 2,
102 | },
103 | },
104 | },
105 | CH0200_home: {
106 | name: "CH0200_home",
107 | playQue: [
108 | {
109 | name: "CH0200_home",
110 | animation: "Start_Idle_01",
111 | fadeTime: 4.3,
112 | fade: true,
113 | },
114 | {
115 | name: "CH0200_home",
116 | animation: "Idle_01",
117 | },
118 | ],
119 | },
120 | CH0203_home: {
121 | name: "CH0203_home",
122 | playQue: [
123 | {
124 | name: "CH0203_home",
125 | animation: "Start_Idle_01",
126 | fadeTime: 5.2,
127 | fade: true,
128 | sounds: [{ fileName: "maidYuzuCat", time: 0 }],
129 | },
130 | {
131 | name: "CH0203_home",
132 | animation: "Idle_01",
133 | },
134 | ],
135 | },
136 | CH0211_home: {
137 | name: "CH0211_home",
138 | playQue: [
139 | {
140 | name: "CH0211_home",
141 | animation: "Start_Idle_01",
142 | fadeTime: 5.1,
143 | fade: true,
144 | },
145 | {
146 | name: "CH0211_home",
147 | animation: "Idle_01",
148 | },
149 | ],
150 | },
151 | CH0092_home: {
152 | name: "CH0092_home",
153 | playQue: [
154 | {
155 | name: "CH0092_home",
156 | animation: "Start_Idle_01",
157 | fadeTime: 5.1,
158 | secondFadeTime: 14.7,
159 | fade: true,
160 | },
161 | {
162 | name: "CH0092_home",
163 | animation: "Idle_01",
164 | },
165 | ],
166 | spineSettings: {
167 | CH0092_home: {
168 | scale: 1.7,
169 | },
170 | },
171 | },
172 | Nonomi_home: {
173 | name: "Nonomi_home",
174 | playQue: [
175 | {
176 | name: "Nonomi_home",
177 | animation: "Start_Idle_01",
178 | fadeTime: 3.8,
179 | fade: true,
180 | },
181 | {
182 | name: "Nonomi_home",
183 | animation: "Idle_01",
184 | },
185 | ],
186 | },
187 | };
188 |
--------------------------------------------------------------------------------
/lib/layers/soundLayer/index.ts:
--------------------------------------------------------------------------------
1 | import eventBus from "@/eventBus";
2 | import { PlayAudio } from "@/types/events";
3 | import { Sound } from "@pixi/sound";
4 | import { usePlayerStore } from "@/stores";
5 |
6 | const audioMap = new Map();
7 | /**
8 | * 获取url对于的Sound对象, 缓存不存在则新建
9 | * @param url
10 | */
11 | function getAudio(url: string): Sound {
12 | const audio = audioMap.get(url);
13 | if (audio) {
14 | return audio;
15 | } else {
16 | const newAudio = Sound.from({
17 | url,
18 | autoPlay: false,
19 | });
20 | audioMap.set(url, newAudio);
21 | return newAudio;
22 | }
23 | }
24 |
25 | /**
26 | * 预加载与解析声音资源
27 | * @param audioUrls 声音地址数组
28 | */
29 | export async function preloadSound(audioUrls: string[]) {
30 | const audioLoadPromises: Promise[] = [];
31 | for (const audioUrl of audioUrls) {
32 | audioLoadPromises.push(
33 | new Promise(resolve => {
34 | audioMap.set(
35 | audioUrl,
36 | Sound.from({
37 | url: audioUrl,
38 | preload: true,
39 | autoPlay: false,
40 | loaded(err, resource) {
41 | eventBus.emit("oneResourceLoaded", {
42 | type: err ? "fail" : "success",
43 | resourceName: audioUrl,
44 | });
45 | resolve();
46 | },
47 | })
48 | );
49 | })
50 | );
51 | }
52 | await Promise.all(audioLoadPromises);
53 | }
54 |
55 | /**
56 | * 初始化声音层, 订阅player的剧情信息.
57 | */
58 | export function soundInit() {
59 | let bgm: Sound | undefined = undefined;
60 | let sfx: Sound | undefined = undefined;
61 | let voice: Sound | undefined = undefined;
62 | let emotionSound: Sound | undefined = undefined;
63 |
64 | /**
65 | * 声音层的全局设置, 包括BGM音量, 效果音量和语音音量
66 | */
67 | let soundSettings = new (class SoundSettings {
68 | BGMvolume = 0.3;
69 | SFXvolume = 1;
70 | Voicevolume = 1;
71 | })();
72 |
73 | /**
74 | * @description 播放声音
75 | * @param playAudioInfo
76 | */
77 | function playAudio(playAudioInfo: PlayAudio) {
78 | if (playAudioInfo.bgm) {
79 | // 替换BGM
80 | if (bgm) {
81 | bgm.stop();
82 | } // 如果有正在播放的BGM则停止当前播放, 替换为下一个BGM
83 | bgm = getAudio(playAudioInfo.bgm.url);
84 | bgm.volume = soundSettings.BGMvolume;
85 | bgm.play({
86 | // 第一次是非loop播放, 播放到LoopStartTime为止
87 | loop: false,
88 | start: 0,
89 | end: playAudioInfo.bgm?.bgmArgs.LoopEndTime,
90 | complete: function () {
91 | // 第一次播放结束后进入loop
92 | bgm?.play({
93 | loop: true,
94 | start: playAudioInfo.bgm?.bgmArgs.LoopStartTime,
95 | end: playAudioInfo.bgm?.bgmArgs.LoopEndTime,
96 | });
97 | },
98 | }); // 这样写真的好吗...
99 | }
100 |
101 | if (playAudioInfo.soundUrl) {
102 | if (sfx) {
103 | sfx.stop();
104 | }
105 | sfx = getAudio(playAudioInfo.soundUrl);
106 | sfx.volume = soundSettings.SFXvolume;
107 | sfx.play({
108 | complete: () => {
109 | console.log("Finish Playing Sound!");
110 | },
111 | });
112 | }
113 |
114 | if (playAudioInfo.voiceJPUrl) {
115 | if (voice) {
116 | voice.stop();
117 | }
118 | voice = getAudio(playAudioInfo.voiceJPUrl);
119 | voice.volume = soundSettings.Voicevolume;
120 | voice.play({
121 | complete: () => {
122 | eventBus.emit("playVoiceJPDone", playAudioInfo.voiceJPUrl || "");
123 | },
124 | });
125 | }
126 | }
127 |
128 | // 当想要播放VoiceJP的时候, 可以直接
129 | // eventBus.emit('playAudio', {voiceJPUrl: url})
130 | // 这样就可以了x
131 |
132 | eventBus.on("playAudio", (playAudioInfo: PlayAudio) => {
133 | console.log(
134 | `Get playAudioInfo: ${
135 | playAudioInfo.soundUrl ||
136 | playAudioInfo.voiceJPUrl ||
137 | playAudioInfo.bgm?.url
138 | }`
139 | );
140 | playAudio(playAudioInfo);
141 | });
142 |
143 | eventBus.on("playEmotionAudio", (emotype: string) => {
144 | if (emotionSound) {
145 | emotionSound.stop();
146 | }
147 | emotionSound = getAudio(usePlayerStore().emotionSoundUrl(emotype));
148 | emotionSound.play({
149 | volume: soundSettings.SFXvolume,
150 | });
151 | });
152 |
153 | eventBus.on("playOtherSounds", sound => {
154 | console.log("Play Select Sound!");
155 | playAudio({ soundUrl: usePlayerStore().otherSoundUrl(sound) });
156 | });
157 |
158 | eventBus.on("playBgEffectSound", bgEffect => {
159 | playAudio({ soundUrl: usePlayerStore().bgEffectSoundUrl(bgEffect) });
160 | });
161 |
162 | eventBus.on("dispose", () => soundDispose());
163 | eventBus.on("stop", () => soundDispose());
164 | eventBus.on("continue", () => bgm?.play());
165 | eventBus.on("playAudioWithConfig", ({ url, config }) => {
166 | getAudio(url).play(config);
167 | });
168 | }
169 |
170 | export function soundDispose() {
171 | for (const sound of audioMap.values()) {
172 | sound.stop();
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/lib/layers/textLayer/assets/text-next.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/lib/layers/textLayer/assets/text-next.webp
--------------------------------------------------------------------------------
/lib/layers/textLayer/assets/title_border__lower_left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/textLayer/assets/title_border__lower_right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/textLayer/assets/title_border__upper_left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/textLayer/assets/title_border__upper_right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/textLayer/index.d.ts:
--------------------------------------------------------------------------------
1 | import Typed from "typed.js";
2 | declare module "typed.js" {
3 | declare interface TypedExtend extends Typed {
4 | strings: string[]; // 要打印的内容
5 | pause: {
6 | // 暂停时保存的回复参数
7 | curStrPos: number; // 暂停前字符串指针位置
8 | curString: string; // 暂停前打印的字符串
9 | status: boolean; // 是否在暂停
10 | typewrite: boolean; // 是否是在打印, 如果为false表示暂停前在删除
11 | };
12 | strPos: number; // 开始位置, 已废弃
13 | options: TypedOptions; // 初始化参数
14 | startDelay: number; // 开始时延, 已废弃
15 | typingComplete: boolean; // 是否打印完成, 如果为true会忽略start()方法
16 | timeout: Nodejs.Timeout; // 打字机内部循环handler
17 | isSt: boolean; // 是否在显示特效字, 为true会在clearSt事件前将拿到的st特效append上去
18 | }
19 | declare interface TypedOptions {
20 | /**
21 | * strings to be typed
22 | */
23 | strings?: string[];
24 | /**
25 | * ID or instance of HTML element of element containing string children
26 | */
27 | stringsElement?: string | Element;
28 | /**
29 | * type speed in milliseconds
30 | */
31 | typeSpeed?: number;
32 | /**
33 | * time before typing starts in milliseconds
34 | */
35 | startDelay?: number;
36 | /**
37 | * backspacing speed in milliseconds
38 | */
39 | backSpeed?: number;
40 | /**
41 | * only backspace what doesn't match the previous string
42 | */
43 | smartBackspace?: boolean;
44 | /**
45 | * shuffle the strings
46 | */
47 | shuffle?: boolean;
48 | /**
49 | * time before backspacing in milliseconds
50 | */
51 | backDelay?: number;
52 | /**
53 | * Fade out instead of backspace
54 | */
55 | fadeOut?: boolean;
56 | /**
57 | * css class for fade animation
58 | */
59 | fadeOutClass?: string;
60 | /**
61 | * Fade out delay in milliseconds
62 | */
63 | fadeOutDelay?: number;
64 | /**
65 | * loop strings
66 | */
67 | loop?: boolean;
68 | /**
69 | * amount of loops
70 | */
71 | loopCount?: number;
72 | /**
73 | * show cursor
74 | */
75 | showCursor?: boolean;
76 | /**
77 | * character for cursor
78 | */
79 | cursorChar?: string;
80 | /**
81 | * insert CSS for cursor and fadeOut into HTML
82 | */
83 | autoInsertCss?: boolean;
84 | /**
85 | * attribute for typing Ex: input placeholder, value, or just HTML text
86 | */
87 | attr?: string;
88 | /**
89 | * bind to focus and blur if el is text input
90 | */
91 | bindInputFocusEvents?: boolean;
92 | /**
93 | * 'html' or 'null' for plaintext
94 | */
95 | contentType?: string;
96 | /**
97 | * All typing is complete
98 | */
99 | onComplete?(self: Typed): void;
100 | /**
101 | * Before each string is typed
102 | */
103 | preStringTyped?(arrayPos: number, self: Typed): void;
104 | /**
105 | * After each string is typed
106 | */
107 | onStringTyped?(arrayPos: number, self: Typed): void;
108 | /**
109 | * During looping, after last string is typed
110 | */
111 | onLastStringBackspaced?(self: Typed): void;
112 | /**
113 | * Typing has been stopped
114 | */
115 | onTypingPaused?(arrayPos: number, self: Typed): void;
116 | /**
117 | * Typing has been started after being stopped
118 | */
119 | onTypingResumed?(arrayPos: number, self: Typed): void;
120 | /**
121 | * After reset
122 | */
123 | onReset?(self: Typed): void;
124 | /**
125 | * After stop
126 | */
127 | onStop?(arrayPos: number, self: Typed): void;
128 | /**
129 | * After start
130 | */
131 | onStart?(arrayPos: number, self: Typed): void;
132 | /**
133 | * After destroy
134 | */
135 | onDestroy?(self: Typed): void;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/lib/layers/textLayer/responsive-video-background.d.ts:
--------------------------------------------------------------------------------
1 | declare module "vue-responsive-video-background-player";
2 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/README.md:
--------------------------------------------------------------------------------
1 | # UILayer (UI 层)
2 |
3 | ## TODOs
4 |
5 | - onUnmounted unregister event
6 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/Common_Btn_Normal_Y_S_Pt.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/lib/layers/uiLayer/assets/Common_Btn_Normal_Y_S_Pt.webp
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/Conquest_2nd_Eventlobby_GaugeBg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/lib/layers/uiLayer/assets/Conquest_2nd_Eventlobby_GaugeBg.png
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/Conquest_2nd_Eventlobby_GaugeFront.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/lib/layers/uiLayer/assets/Conquest_2nd_Eventlobby_GaugeFront.png
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/Deco_GachaItemBg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/lib/layers/uiLayer/assets/Deco_GachaItemBg.webp
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/Image_AngleBtn_Deco.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ba-archive/ba-story-player/5efd761d8f48f4be4510d2e0b1a0e585a21d779a/lib/layers/uiLayer/assets/Image_AngleBtn_Deco.png
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/UITex_BGPoliLight_1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/fast-forward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/icon-hide.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/icon-show.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/menu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/pan-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/assets/title-banner.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/components/BaButton.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
39 |
40 |
41 |
98 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/components/BaChatLog/BaChatLog.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
41 |
42 |
43 |
83 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/components/BaChatLog/BaChatMessage.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
33 |
34 |
52 |
53 |
57 |
{{ chatMessage?.name }}
58 |
{{ chatMessage?.text }}
59 |
60 |
61 |
62 |
63 |
213 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/components/BaDialog.vue:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
54 |
59 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
173 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/components/BaSelector.vue:
--------------------------------------------------------------------------------
1 |
120 |
121 |
122 |
127 |
132 |
133 |
134 |
154 |
155 |
156 |
157 |
158 |
159 |
235 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/index.ts:
--------------------------------------------------------------------------------
1 | export function uiInit() {}
2 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/userInteract.ts:
--------------------------------------------------------------------------------
1 | import eventBus from "@/eventBus";
2 | import { eventEmitter, storyHandler } from "@/index";
3 | import { usePlayerStore } from "@/stores";
4 | const keyStatus = {} as any;
5 |
6 | // 进行下一步的定时器
7 | let nexting: any;
8 | const keyEvent = (e: KeyboardEvent) => {
9 | keyStatus[e.key] = true;
10 | if (Object.values(keyStatus).filter(i => i).length >= 2) {
11 | return;
12 | }
13 | // 显示历史 log 不允许操作
14 | if (eventEmitter.isStoryLogShow || !isPlayerFocus()) {
15 | return;
16 | }
17 | switch (e.key) {
18 | case "Enter":
19 | case " ":
20 | if (!nexting) {
21 | interactNext();
22 | nexting = setInterval(() => {
23 | interactNext();
24 | }, 1000);
25 | }
26 | break;
27 | case "ArrowUp":
28 | eventBus.emit("showStoryLog", true);
29 | break;
30 | case "Control":
31 | // 限流
32 | storyHandler.isSkip = true;
33 | if (!nexting) {
34 | interactNext();
35 | nexting = setInterval(() => {
36 | eventBus.emit("skipping");
37 | interactNext();
38 | }, 200);
39 | }
40 | }
41 | };
42 | const keyUpEvent = (e: KeyboardEvent) => {
43 | keyStatus[e.key] = false;
44 | clearInterval(nexting);
45 | nexting = undefined;
46 | switch (e.key) {
47 | case "Control":
48 | storyHandler.isSkip = false;
49 | }
50 | };
51 | let isScrollBottom = false;
52 | const wheelEvent = (e: WheelEvent & { [key: string]: any }) => {
53 | const uiScrollElem = document.querySelector(".ba-chat-content");
54 | const delta = e.wheelDelta ? e.wheelDelta : -e.detail;
55 | if (delta > 0) {
56 | isScrollBottom = false;
57 | }
58 | if (
59 | delta < 0 &&
60 | (uiScrollElem?.scrollTop || 0) + (uiScrollElem?.clientHeight || 0) >=
61 | (uiScrollElem?.scrollHeight || 0) - 6
62 | ) {
63 | // 避免滑到底部就立刻关了, 再滚一次才生效
64 | if (isScrollBottom) {
65 | eventBus.emit("showStoryLog", false);
66 | isScrollBottom = false;
67 | return;
68 | }
69 | isScrollBottom = true;
70 | }
71 | if (eventEmitter.isStoryLogShow || !isPlayerFocus()) {
72 | return;
73 | }
74 | if (delta < 0) {
75 | interactNext();
76 | } else {
77 | eventBus.emit("showStoryLog", true);
78 | }
79 | };
80 |
81 | eventBus.on("loaded", () => {
82 | document.addEventListener("keydown", keyEvent);
83 | document.addEventListener("keyup", keyUpEvent);
84 | document
85 | .querySelector("#player")
86 | ?.addEventListener("wheel", wheelEvent as any);
87 | });
88 | eventBus.on("dispose", () => {
89 | document.removeEventListener("keydown", keyEvent);
90 | document.removeEventListener("keyup", keyUpEvent);
91 | document
92 | .querySelector("#player")
93 | ?.addEventListener("wheel", wheelEvent as any);
94 | });
95 |
96 | function getLastDataFromIndex(index: number) {
97 | const allStory = usePlayerStore().allStoryUnit;
98 | const recentStory = allStory.slice(0, index + 1).reverse();
99 | const lastCharacterIdx = recentStory.findIndex(currentStoryUnit => {
100 | return currentStoryUnit.characters?.length;
101 | });
102 | const lastCharacter = recentStory[lastCharacterIdx];
103 | const characters = lastCharacter?.characters || [];
104 | if (lastCharacter) {
105 | // 拼装人物层展示情况
106 | recentStory.slice(lastCharacterIdx + 1).some(story => {
107 | if (story.characters?.length) {
108 | const filterSamePosition = story.characters.filter(character => {
109 | return !characters.find(j => j.position === character.position);
110 | });
111 | filterSamePosition.forEach(character => {
112 | character.highlight = false;
113 | character.effects = [];
114 | });
115 | characters.push(...filterSamePosition);
116 | }
117 | return story.hide === "all";
118 | });
119 | }
120 | const lastBg = recentStory.find(currentStoryUnit => {
121 | return currentStoryUnit.bg;
122 | });
123 | const lastBgm = recentStory.find(currentStoryUnit => {
124 | return currentStoryUnit.audio?.bgm;
125 | });
126 | return { lastBg, lastBgm, lastCharacter };
127 | }
128 |
129 | export const changeStoryIndex = (index?: number) => {
130 | index = parseInt(index + "");
131 | if (typeof index !== "number") return;
132 | index -= 1;
133 | if (index < 0) index = 0;
134 | eventBus.emit("removeEffect");
135 | const { lastCharacter, lastBg, lastBgm } = getLastDataFromIndex(index);
136 | const {
137 | lastCharacter: curCharacter,
138 | lastBg: curBg,
139 | lastBgm: curBgm,
140 | } = getLastDataFromIndex(storyHandler.currentStoryIndex);
141 | const isSameCharacter =
142 | JSON.stringify(lastCharacter?.characters?.map(i => i.CharacterName)) ===
143 | JSON.stringify(curCharacter?.characters?.map(i => i.CharacterName));
144 | const isSameBgm =
145 | JSON.stringify(lastBgm?.audio?.bgm) === JSON.stringify(curBgm?.audio?.bgm);
146 | // 如果和跳转前相同就不去除了, 避免闪动
147 | if (!isSameCharacter) {
148 | eventBus.emit("hideCharacter");
149 | }
150 | setTimeout(() => {
151 | // 在 hideCharacter 后触发
152 | eventEmitter.showCharacter(lastCharacter);
153 | }, 4);
154 | !isSameBgm && eventEmitter.playAudio(lastBgm);
155 | eventEmitter.showBg(lastBg);
156 | storyHandler.currentStoryIndex = index;
157 | eventBus.emit("next");
158 | };
159 |
160 | function interactNext() {
161 | const currentStoryUnit = storyHandler.currentStoryUnit;
162 | if (
163 | currentStoryUnit?.textAbout?.options ||
164 | currentStoryUnit?.textAbout?.titleInfo ||
165 | eventEmitter.l2dPlaying ||
166 | currentStoryUnit.l2d
167 | ) {
168 | console.log("不允许下一步", storyHandler.currentStoryUnit);
169 | return;
170 | }
171 | eventBus.emit("next");
172 | }
173 | function isPlayerFocus() {
174 | return document.activeElement?.className.includes("baui");
175 | }
176 | function focusPlayer() {
177 | // 选择后继续能跳过
178 | (document.querySelector(".baui") as any).focus();
179 | }
180 | eventBus.on("select", () => {
181 | focusPlayer();
182 | });
183 | eventBus.on("isStoryLogShow", e => {
184 | // 选择后继续能跳过
185 | !e && focusPlayer();
186 | });
187 |
--------------------------------------------------------------------------------
/lib/layers/uiLayer/utils.ts:
--------------------------------------------------------------------------------
1 | import gsap from "gsap";
2 |
3 | // 按钮激活动画
4 | function effectBtnMouseDown(duration = 0.15, scale = 0.94) {
5 | return (ev: Event) => {
6 | console.log("effectBtnMouseDown");
7 | gsap.to(ev.currentTarget, {
8 | duration: duration,
9 | scale: scale,
10 | ease: "power3.out",
11 | force3D: true,
12 | });
13 | };
14 | }
15 |
16 | // 按钮失活动画
17 | function effectBtnMouseUp(duration = 0.3, scale = 1) {
18 | return (ev: Event) => {
19 | console.log("effectBtnMouseUp");
20 | gsap.to(ev.currentTarget, {
21 | duration: duration,
22 | scale: scale,
23 | force3D: true,
24 | });
25 | };
26 | }
27 |
28 | /**
29 | * 按钮动画
30 | * @args 控制动画参数
31 | * args.durationDown: 按下去的动画时间
32 | * args.scaleDown: 按下按钮,按钮的 scale 变化量
33 | * args.durationUp: 松开按钮的动画时间
34 | * args.scaleUp: 松开按钮的 scale 变化量
35 | */
36 | function buttonAnimation(
37 | elem: {
38 | cssSelector?: string;
39 | elem?: Element;
40 | elems?: Element[];
41 | },
42 | args: {
43 | scaleDown: number;
44 | durationDown: number;
45 | scaleUp: number;
46 | durationUp: number;
47 | } = { durationDown: 0.15, scaleDown: 0.95, durationUp: 0.3, scaleUp: 1 }
48 | ) {
49 | let elems;
50 | if (elem.cssSelector) {
51 | elems = document.querySelectorAll(elem.cssSelector);
52 | } else if (elem.elem) {
53 | elems = [elem.elem];
54 | } else if (elem.elems) {
55 | elems = elem.elems;
56 | } else {
57 | return;
58 | }
59 | console.log("buttonAnimation: ", elem, elems);
60 | elems.forEach(elem => {
61 | elem.addEventListener(
62 | "mousedown",
63 | effectBtnMouseDown(args.durationUp, args.scaleDown)
64 | );
65 | elem.addEventListener(
66 | "touchstart",
67 | effectBtnMouseDown(args.durationUp, args.scaleDown)
68 | );
69 | elem.addEventListener(
70 | "mouseup",
71 | effectBtnMouseUp(args.durationUp, args.scaleUp)
72 | );
73 | elem.addEventListener(
74 | "touchend",
75 | effectBtnMouseUp(args.durationUp, args.scaleUp)
76 | );
77 | elem.addEventListener(
78 | "mouseleave",
79 | effectBtnMouseUp(args.durationUp, args.scaleUp)
80 | );
81 | });
82 | }
83 |
84 | export { buttonAnimation, effectBtnMouseDown, effectBtnMouseUp };
85 |
--------------------------------------------------------------------------------
/lib/main.ts:
--------------------------------------------------------------------------------
1 | import BaStoryPlayer, { PlayerProps } from "./BaStoryPlayer.vue";
2 |
3 | export default BaStoryPlayer;
4 | export type { PlayerProps };
5 |
--------------------------------------------------------------------------------
/lib/stores/index.ts:
--------------------------------------------------------------------------------
1 | import { BGEffectImgTable } from "@/types/effectLayer";
2 | import {
3 | Actions,
4 | GetterFunctions,
5 | Getters,
6 | LogText,
7 | PrivateStates,
8 | PublicStates,
9 | } from "@/types/store";
10 | import { getResourcesUrl } from "@/utils";
11 | import { ref } from "vue";
12 | import { storyHandler } from "..";
13 |
14 | // let characterNameTable = {
15 | // '유우카 체육복ND': 3715128518,
16 | // '???': 0,
17 | // '린': 2690570743,
18 | // '유우카': 4283125014,
19 | // '하스미': 3571276574,
20 | // '치나츠': 1867911819,
21 | // '스즈미': 1034441153,
22 | // '통신모모카': 3025942184
23 | // }
24 |
25 | let emotionResourcesTable = {
26 | Heart: ["Emoticon_Balloon_N.png", "Emoticon_Heart.png"],
27 | Respond: ["Emoticon_Action.png"],
28 | Music: ["Emoticon_Note.png"],
29 | Twinkle: ["Emoticon_Twinkle.png"],
30 | Upset: ["Emoticon_Balloon_N.png", "Emoticon_Anxiety.png"],
31 | Sweat: ["Emoticon_Sweat_1.png", "Emoticon_Sweat_2.png"],
32 | Dot: ["Emoticon_Balloon_N.png", "Emoticon_Idea.png"],
33 | Exclaim: ["Emoticon_ExclamationMark.png"],
34 | Surprise: ["Emoticon_Exclamation.png", "Emoticon_Question.png"],
35 | Question: ["Emoticon_QuestionMark.png"],
36 | Shy: ["Emoticon_Balloon_N.png", "Emoticon_Shy.png"],
37 | Angry: ["Emoticon_Aggro.png"],
38 | Chat: ["Emoticon_Chat.png"],
39 | Sad: ["Emoji_Sad.png"],
40 | Steam: ["Emoji_Steam.png"],
41 | Sigh: ["Emoji_Sigh.png"],
42 | Bulb: ["Emoticon_Balloon_N.png", "Emoji_Bulb_1.png", "Emoji_Bulb_2.png"],
43 | Tear: ["Emoji_Tear_1.png", "Emoji_Tear_2.png"],
44 | Zzz: ["Emoji_Zzz.png"],
45 | // TODO: Upset, Music, Think, Bulb, Sigh, Steam, Zzz, Tear
46 | };
47 |
48 | let fxImageTable = {
49 | shot: ["fire1.png", "fire2.png", "fire3.png"],
50 | };
51 |
52 | /**
53 | * 请在此处填入需要的图片资源的名称
54 | */
55 | let bgEffectImgTable: BGEffectImgTable = {
56 | "": [],
57 | "BG_ScrollT_0.5": [],
58 | BG_Filter_Red: [],
59 | BG_Wave_F: [],
60 | BG_Flash: [],
61 | BG_UnderFire_R: [],
62 | BG_Love_L: [
63 | "FX_TEX_Img_Heart_01.png",
64 | "FX_TEX_SCN_Ring_02.png",
65 | "Gacha/FX_TEX_GT_Circle_Blur_inv.png",
66 | ],
67 | "BG_ScrollB_0.5": [],
68 | BG_Rain_L: ["HardRain.png"],
69 | BG_UnderFire: [
70 | "FX_TEX_Smoke_17.png",
71 | "fire1.png",
72 | "fire2.png",
73 | "fire3.png",
74 | "HardRain.png",
75 | ],
76 | BG_WaveShort_F: [],
77 | BG_SandStorm_L: ["FX_TEX_Smoke_10a.png"],
78 | "BG_ScrollT_1.5": [],
79 | BG_Shining_L: [
80 | "FX_TEX_SCN_Ring_02.png",
81 | "FX_TEX_Flare_23.png",
82 | "FX_TEX_SCN_Circle_Love.png",
83 | "Gacha/FX_TEX_GT_Circle_Blur_inv.png",
84 | ],
85 | "BG_ScrollB_1.0": [],
86 | BG_Love_L_BGOff: [
87 | "FX_TEX_Img_Heart_01.png",
88 | "FX_TEX_SCN_Ring_02.png",
89 | "FX_TEX_SCN_Circle_Love.png",
90 | ],
91 | BG_Dust_L: ["FX_TEX_Smoke_Scroll_23.png", "dust_spark.png"],
92 | "BG_ScrollL_0.5": [],
93 | "BG_ScrollL_1.0": [],
94 | BG_Ash_Black: [],
95 | BG_Mist_L: [],
96 | BG_Flash_Sound: ["FX_TEX_Lightning_Line_16.png"],
97 | "BG_ScrollL_1.5": [],
98 | BG_FocusLine: ["FX_TEX_SCN_FocusLine5.png"],
99 | "BG_ScrollR_1.5": [],
100 | BG_Shining_L_BGOff: [
101 | "FX_TEX_SCN_Ring_02.png",
102 | "FX_TEX_Flare_23.png",
103 | "Gacha/FX_TEX_GT_Circle_Blur_inv.png",
104 | ],
105 | "BG_ScrollT_1.0": [],
106 | "BG_ScrollB_1.5": [],
107 | BG_Filter_Red_BG: [],
108 | BG_Ash_Red: [],
109 | BG_Fireworks_L_BGOff_02: [],
110 | "BG_ScrollR_0.5": [],
111 | BG_Snow_L: [],
112 | BG_Fireworks_L_BGOff_01: [],
113 | "BG_ScrollR_1.0": [],
114 | };
115 |
116 | let privateState: PrivateStates = {
117 | language: "Cn",
118 | userName: "",
119 | dataUrl: "",
120 | app: null,
121 | l2dSpineUrl: "",
122 | curL2dConfig: null,
123 | translator: "",
124 | storySummary: {
125 | chapterName: "",
126 | summary: "",
127 | },
128 |
129 | allStoryUnit: [],
130 |
131 | //文字层
132 | logText: ref([]),
133 |
134 | //背景层
135 | bgInstance: null,
136 |
137 | //资源管理
138 | BGNameExcelTable: new Map(),
139 | CharacterNameExcelTable: new Map(),
140 | BGMExcelTable: new Map(),
141 | BGEffectExcelTable: new Map(),
142 | TransitionExcelTable: new Map(),
143 | EmotionExcelTable: new Map(),
144 | emotionResourcesTable: new Map(Object.entries(emotionResourcesTable)),
145 | fxImageTable: new Map(Object.entries(fxImageTable)),
146 | bgEffectImgMap: new Map(Object.entries(bgEffectImgTable)),
147 | };
148 |
149 | let getterFunctions: GetterFunctions = {
150 | app() {
151 | if (privateState === null) {
152 | throw new Error("app实例不存在");
153 | }
154 | return privateState.app!;
155 | },
156 |
157 | characterSpineData: () => (CharacterName: number) => {
158 | return privateState.app?.loader.resources[String(CharacterName)].spineData;
159 | },
160 |
161 | /**
162 | * 获取情绪动画的图片url, 按从底部到顶部, 从左到右排列资源.
163 | */
164 | emotionResources: () => (emotionName: string) => {
165 | return privateState.emotionResourcesTable.get(emotionName);
166 | },
167 |
168 | /**
169 | * 获取情绪动画的图片url, 按从底部到顶部, 从左到右排列资源.
170 | */
171 | fxImages: () => (fxName: string) => {
172 | return privateState.fxImageTable.get(fxName);
173 | },
174 |
175 | /**
176 | * 获取emotion的对应声音资源的url, 传入的参数是emotion的名字
177 | */
178 | emotionSoundUrl: () => emotionName => {
179 | return getResourcesUrl(
180 | "emotionSound",
181 | `SFX_Emoticon_Motion_${emotionName}`
182 | );
183 | },
184 |
185 | otherSoundUrl: () => sound => {
186 | return getResourcesUrl("otherSound", sound);
187 | },
188 |
189 | bgEffectSoundUrl: () => bgeffect => {
190 | return getResourcesUrl("bgEffectSounds", bgeffect);
191 | },
192 |
193 | l2dSpineData() {
194 | const resource =
195 | privateState.app?.loader.resources[privateState.l2dSpineUrl];
196 | if (resource) return resource.spineData;
197 | },
198 | };
199 |
200 | let actions: Actions = {
201 | setBgInstance(instance) {
202 | privateState.bgInstance = instance;
203 | },
204 | updateLogText(newLog) {
205 | if ("SelectionGroup" in newLog) {
206 | privateState.logText.value.push({
207 | type: "user",
208 | text: newLog.text.map(it => it.content).join(""),
209 | index: newLog.index,
210 | name: privateState.userName,
211 | });
212 | } else {
213 | let text = "";
214 | for (let textPart of newLog.text) {
215 | text += textPart.content;
216 | }
217 | if (newLog.speaker) {
218 | privateState.logText.value.push({
219 | type: "character",
220 | text,
221 | avatarUrl: newLog.avatarUrl,
222 | name: newLog.speaker.name,
223 | index: newLog.index,
224 | });
225 | } else {
226 | privateState.logText.value.push({
227 | type: "none",
228 | text,
229 | index: newLog.index,
230 | });
231 | }
232 | }
233 | // 日志不会超过当前的故事进度, 并且不重复
234 | privateState.logText.value = privateState.logText.value.reduce(
235 | (acc, cur) => {
236 | if (cur.index && cur.index <= storyHandler.currentStoryIndex) {
237 | const find = acc.find(i => i.index === cur.index);
238 | if (!find) {
239 | acc.push(cur);
240 | }
241 | }
242 | return acc;
243 | },
244 | [] as LogText[]
245 | );
246 | },
247 | setL2DSpineUrl(url) {
248 | privateState.l2dSpineUrl = url;
249 | },
250 | setL2DConfig(val) {
251 | privateState.curL2dConfig = val;
252 | },
253 | setTranslator(translator: string) {
254 | privateState.translator = translator;
255 | },
256 | };
257 |
258 | let store = {
259 | currentCharacterMap: new Map(),
260 | ...actions,
261 | };
262 |
263 | for (let getter of Object.keys(getterFunctions) as Array<
264 | keyof GetterFunctions
265 | >) {
266 | Reflect.defineProperty(store, getter, {
267 | get: () => getterFunctions[getter](),
268 | });
269 | }
270 |
271 | for (let state of Object.keys(privateState) as Array) {
272 | if (!["app"].includes(state)) {
273 | Reflect.defineProperty(store, state, {
274 | get: () => privateState[state],
275 | });
276 | }
277 | }
278 |
279 | /**
280 | * 资源调用接口
281 | * @returns 资源调用工具对象
282 | */
283 | export let usePlayerStore = () => {
284 | return store as unknown as PublicStates &
285 | Getters &
286 | Readonly &
287 | Actions;
288 | };
289 |
290 | /**
291 | * 返回可修改的privateState, 仅本体在初始化时可调用
292 | */
293 | export let initPrivateState = () => privateState;
294 |
--------------------------------------------------------------------------------
/lib/types/bgLayer.ts:
--------------------------------------------------------------------------------
1 | import { LoaderResource, Sprite } from "pixi.js";
2 |
3 | import { Dict } from "@/types/common";
4 | import { BgParams } from "@/types/events";
5 |
6 | export interface BgLayer {
7 | /**
8 | * 初始化 BgLayer 实例函数
9 | */
10 | init(): void;
11 | /**
12 | * 销毁 BgLayer 实例函数
13 | */
14 | dispose(): void;
15 | /**
16 | * 初始化实例事件
17 | */
18 | initEvent(): void;
19 | /**
20 | * 销毁实例事件
21 | */
22 | disposeEvent(): void;
23 | /**
24 | * showBg 事件处理函数
25 | * @param params 背景图片参数
26 | */
27 | handleShowBg(params: BgParams): void;
28 | /**
29 | * 处理canvas尺寸变化
30 | */
31 | handleResize(): void;
32 | /**
33 | * 从 Loader Resource 获取背景 Sprite
34 | * @param resources loader resources
35 | * @param name 背景图片名
36 | */
37 | getBgSpriteFromResource(
38 | resources: Dict,
39 | name: string
40 | ): Sprite | undefined;
41 | /**
42 | * 直接加载背景
43 | */
44 | loadBg(instance: Sprite): void;
45 | /**
46 | * 基于 bgoverlap 特效加载背景
47 | */
48 | loadBgOverlap(instance: Sprite, overlap: number): void;
49 | }
50 |
--------------------------------------------------------------------------------
/lib/types/common.ts:
--------------------------------------------------------------------------------
1 | import type { Spine } from "pixi-spine";
2 | import {
3 | PlayAudio,
4 | PlayEffect,
5 | ShowOption,
6 | ShowText,
7 | ShowTitleOption,
8 | StArgs,
9 | } from "./events";
10 | import { TransitionTableItem } from "./excels";
11 | import { Language, StorySummary } from "./store";
12 | export interface Character {
13 | /**
14 | * 人物位置
15 | */
16 | position: number;
17 | /**
18 | * 人物CharacterName, 请通过它获取人物spinedata
19 | */
20 | CharacterName: number;
21 | /**
22 | * 人物spinedata的url
23 | */
24 | spineUrl: string;
25 | /**
26 | * 人物表情
27 | */
28 | face: string;
29 | /**
30 | * 人物是否高亮
31 | */
32 | highlight: boolean;
33 | /**
34 | * 人物是否是全息投影状态
35 | */
36 | signal: boolean;
37 | /**
38 | * 人物特效
39 | */
40 | effects: CharacterEffect[];
41 | }
42 | export interface CharacterEffect {
43 | type: CharacterEffectType;
44 | effect: string;
45 | async: boolean;
46 | arg?: string;
47 | }
48 | export type CharacterEffectType = "emotion" | "action" | "fx";
49 | export interface CharacterInstance {
50 | CharacterName: number;
51 | /**
52 | * 角色当前所在位置 1,2,3,4,5
53 | *
54 | * 会根据m1m2m3m4m5动态更新
55 | */
56 | position: number;
57 | /**
58 | * nx脚本里那个初始位置, 永远不会改变
59 | *
60 | * 配合CharacterName唯一确定一个spine
61 | */
62 | initPosition: number;
63 | /**
64 | * 当前人物表情
65 | *
66 | * 用来判断是否触发眨眼动画
67 | */
68 | currentFace: string;
69 | /**
70 | * 眨眼定时器的handler
71 | */
72 | winkObject?: WinkObject;
73 | instance: Spine;
74 | isShow: () => boolean;
75 | isOnStage: () => boolean;
76 | isHeightLight: () => boolean;
77 | }
78 | export type Dict = {
79 | [key: string]: T;
80 | };
81 | export type Effect =
82 | | {
83 | type: "wait";
84 | /**
85 | * 单位为ms
86 | */
87 | args: number;
88 | }
89 | | {
90 | type: "zmc";
91 | args: ZmcArgs;
92 | }
93 | | {
94 | type: "bgshake";
95 | };
96 | export interface Option {
97 | SelectionGroup: number;
98 | text: {
99 | TextJp: string;
100 | TextCn?: string;
101 | TextTw?: string;
102 | TextEn?: string;
103 | };
104 | }
105 | export type PlayerConfigs = PlayerProps & { height: number };
106 | export type PlayerProps = {
107 | story: TranslatedStoryUnit;
108 | dataUrl: string;
109 | width: number;
110 | height: number;
111 | language: Language;
112 | userName: string;
113 | storySummary: StorySummary;
114 | startFullScreen?: boolean;
115 | useMp3?: boolean;
116 | useSuperSampling?: "2" | "4" | "";
117 | /** 跳转至传入的 index */
118 | changeIndex?: number;
119 | };
120 | export interface Speaker {
121 | /**
122 | * 人物姓名
123 | */
124 | name: string;
125 | /**
126 | * 人物所属
127 | */
128 | nickName: string;
129 | }
130 | export interface StoryRawUnit {
131 | GroupId: number;
132 | SelectionGroup: number;
133 | BGMId: number;
134 | Sound: string;
135 | Transition: number;
136 | BGName: number;
137 | BGEffect: number;
138 | PopupFileName: string;
139 | ScriptKr: string;
140 | TextJp: string;
141 | TextCn?: string;
142 | TextTw?: string;
143 | TextEn?: string;
144 | VoiceJp: string;
145 | }
146 | export type StoryType =
147 | | "title"
148 | | "place"
149 | | "text"
150 | | "option"
151 | | "st"
152 | | "effectOnly"
153 | | "continue"
154 | | "nextEpisode";
155 | export interface StoryUnit {
156 | //rawUnit中的属性
157 | GroupId: number;
158 | SelectionGroup: number;
159 | PopupFileName: string;
160 | type: StoryType;
161 | audio?: PlayAudio;
162 | /**
163 | * 渐变
164 | */
165 | transition?: TransitionTableItem;
166 | /**
167 | * 背景图片
168 | */
169 | bg?: {
170 | url: string;
171 | /**
172 | * 以覆盖原来背景的方式显示, 值为渐变时间
173 | */
174 | overlap?: number;
175 | };
176 | l2d?: {
177 | spineUrl: string;
178 | animationName: string;
179 | };
180 | effect: PlayEffect;
181 | characters: Character[];
182 | /**
183 | * 文字相关的属性, 包括选项, 对话, st文字, 章节名, 人物名
184 | */
185 | textAbout: {
186 | options?: ShowOption[];
187 | /**
188 | * 地点, 标题, 章节名, 第一个地点下面的译者信息(如果有)
189 | */
190 | titleInfo?: ShowTitleOption;
191 | /**
192 | * 显示的对话文字
193 | */
194 | showText: ShowText;
195 | st?: {
196 | stArgs?: StArgs;
197 | clearSt?: boolean;
198 | middle?: boolean;
199 | };
200 | };
201 | fight?: number;
202 | hide?: "menu" | "all";
203 | show?: "menu";
204 | video?: {
205 | videoPath: string;
206 | soundPath: string;
207 | };
208 | }
209 | export interface Text {
210 | /**
211 | * 文本
212 | */
213 | content: string;
214 | /**
215 | * 显示文本前等待的时间
216 | */
217 | waitTime?: number;
218 | /**
219 | * 文字特效
220 | */
221 | effects: TextEffect[];
222 | }
223 | export interface TextEffect {
224 | name: TextEffectName;
225 | /**
226 | * 特效参数
227 | */
228 | value: string[];
229 | }
230 | /**
231 | * 文字特效类型,
232 | * `color`颜色
233 | * `fontsize` 字体大小
234 | * `ruby` 日文注音
235 | */
236 | export type TextEffectName = "color" | "fontsize" | "ruby";
237 | export type TranslatedStoryUnit = {
238 | GroupId: number;
239 | translator: string;
240 | content: StoryRawUnit[];
241 | };
242 | export interface WinkAnimationObject {
243 | _pause: boolean;
244 | start(): void;
245 | pause(): void;
246 | }
247 | export interface WinkObject {
248 | handler: number;
249 | animationObject?: WinkAnimationObject;
250 | }
251 | /**
252 | * zmc参数, 当duration为10时代表是move起始
253 | */
254 | export type ZmcArgs =
255 | | {
256 | type: "move";
257 | position: [number, number];
258 | size: number;
259 | duration: number;
260 | }
261 | | {
262 | type: "instant";
263 | position: [number, number];
264 | size: number;
265 | };
266 |
--------------------------------------------------------------------------------
/lib/types/effectLayer.ts:
--------------------------------------------------------------------------------
1 | import { Sprite } from "pixi.js";
2 | import { BGEffectExcelTableItem, BGEffectType } from "./excels";
3 |
4 | export type BGEffectImgTable = Record;
5 |
6 | export type EffectRemoveFunction = () => Promise;
7 |
8 | export type CurrentBGEffect =
9 | | {
10 | effect: BGEffectType;
11 | removeFunction: EffectRemoveFunction;
12 | resources: Sprite[];
13 | }
14 | | undefined;
15 |
16 | export interface BGEffectHandlerOptions {
17 | BG_FocusLine: {};
18 | "": {};
19 | "BG_ScrollT_0.5": {};
20 | BG_Filter_Red: {};
21 | BG_Wave_F: {};
22 | BG_Flash: {};
23 | BG_UnderFire_R: {};
24 | BG_Love_L: {};
25 | "BG_ScrollB_0.5": {};
26 | BG_Rain_L: {
27 | frequency: number;
28 | };
29 | BG_UnderFire: {};
30 | BG_WaveShort_F: {};
31 | BG_SandStorm_L: {};
32 | "BG_ScrollT_1.5": {};
33 | BG_Shining_L: {};
34 | "BG_ScrollB_1.0": {};
35 | BG_Love_L_BGOff: {};
36 | BG_Dust_L: {};
37 | "BG_ScrollL_0.5": {};
38 | "BG_ScrollL_1.0": {};
39 | BG_Ash_Black: {};
40 | BG_Mist_L: {};
41 | BG_Flash_Sound: {};
42 | "BG_ScrollL_1.5": {};
43 | "BG_ScrollR_1.5": {};
44 | BG_Shining_L_BGOff: {};
45 | "BG_ScrollT_1.0": {};
46 | "BG_ScrollB_1.5": {};
47 | BG_Filter_Red_BG: {};
48 | BG_Ash_Red: {};
49 | BG_Fireworks_L_BGOff_02: {};
50 | "BG_ScrollR_0.5": {};
51 | BG_Snow_L: {};
52 | BG_Fireworks_L_BGOff_01: {};
53 | "BG_ScrollR_1.0": {};
54 | }
55 |
56 | /**
57 | * BGEffect处理函数
58 | */
59 | export type BGEffectHandlerFunction = (
60 | resources: Sprite[],
61 | setting: BGEffectExcelTableItem,
62 | options: BGEffectHandlerOptions[type]
63 | ) => Promise;
64 |
65 | /**
66 | * 类型与处理函数的对应
67 | */
68 | export type BGEffectHandlers = {
69 | [key in BGEffectType]: BGEffectHandlerFunction;
70 | };
71 |
--------------------------------------------------------------------------------
/lib/types/events.ts:
--------------------------------------------------------------------------------
1 | import { Character, Effect, Speaker, Text } from "./common";
2 | import { PlayOptions } from "@pixi/sound";
3 | import {
4 | BGEffectExcelTableItem,
5 | BGEffectType,
6 | BGMExcelTableItem,
7 | TransitionTableItem,
8 | } from "./excels";
9 | import { OtherSounds } from "./resources";
10 |
11 | export type Events = {
12 | //通用
13 | /**
14 | * 清除当前内容
15 | */
16 | hide: undefined;
17 | /**
18 | * 参数是原来的宽度大小
19 | */
20 | resize: number;
21 | /**
22 | * 注销
23 | */
24 | dispose: undefined;
25 | /**
26 | * 暂停
27 | */
28 | stop: undefined;
29 | /**
30 | * 继续播放bgm
31 | */
32 | continue: undefined;
33 |
34 | //特效层
35 |
36 | /**
37 | * 播放特效
38 | */
39 | playEffect: PlayEffect;
40 | /**
41 | * 移除当前特效
42 | */
43 | removeEffect: undefined;
44 | effectDone: undefined;
45 | transitionIn: TransitionTableItem;
46 | transitionInDone: undefined;
47 | transitionOut: TransitionTableItem;
48 | transitionOutDone: undefined;
49 |
50 | //人物层
51 | /**
52 | * 展示人物
53 | */
54 | showCharacter: ShowCharacter;
55 | /**
56 | * 隐藏角色
57 | */
58 | hideCharacter: undefined;
59 | /**
60 | * 人物已处理完毕
61 | */
62 | characterDone: undefined;
63 | /**
64 | * l2d 动画播放状态, 当前动画是否播放完成
65 | */
66 | l2dAnimationDone: { done: boolean; animation: string };
67 |
68 | //背景层
69 | /**
70 | * 展示背景图片
71 | */
72 | showBg: BgParams;
73 | /**
74 | * bgOverLap已完成
75 | */
76 | bgOverLapDone: undefined;
77 |
78 | /**
79 | * 播放bgm, sound或voiceJP
80 | */
81 | playAudio: PlayAudio;
82 | /**
83 | * 播放人物情绪动作特效音
84 | */
85 | playEmotionAudio: string;
86 | /**
87 | * 播放选项选择特效音
88 | */
89 | playOtherSounds: OtherSounds;
90 | playBgEffectSound: BGEffectType;
91 | /**
92 | * 播放voiceJP结束提示
93 | */
94 | playVoiceJPDone: string;
95 | /**
96 | * 根据指定的设置播放sound
97 | */
98 | playAudioWithConfig: {
99 | url: string;
100 | config: PlayOptions;
101 | };
102 |
103 | //UI层
104 | /**
105 | * 跳过剧情
106 | */
107 | skip: undefined;
108 | /** 按contrl时跳过剧情 */
109 | skipping: undefined;
110 | /**
111 | * 自动模式
112 | */
113 | auto: undefined;
114 | /**
115 | * 停止自动模式
116 | */
117 | stopAuto: undefined;
118 | /**
119 | * 隐藏对话框
120 | */
121 | hideDialog: undefined;
122 | hidemenu: undefined;
123 | showmenu: undefined;
124 | // 显示历史
125 | showStoryLog: boolean;
126 | // 当前历史log是否显示
127 | isStoryLogShow: boolean;
128 |
129 | //文字层
130 | /**
131 | * 展示标题
132 | */
133 | showTitle: ShowTitleOption;
134 | /**
135 | * 标题展示完成
136 | */
137 | titleDone: undefined;
138 | /**
139 | * 展示地点
140 | */
141 | showPlace: string;
142 | /**
143 | * 展示在地点下方的译者信息
144 | */
145 | showPlaceTranslator: string;
146 | /**
147 | * 显示普通对话框文字
148 | */
149 | showText: ShowText;
150 | /**
151 | * 显示无对话框文字
152 | */
153 | st: StText;
154 | /**
155 | * 清除无对话框文字和对话框
156 | */
157 | clearSt: undefined;
158 | /**
159 | * st动画播放完成
160 | */
161 | stDone: undefined;
162 | /**
163 | * 对话框内容播放完成
164 | * **实际上st动画播放完成也会触发**
165 | */
166 | textDone: undefined;
167 | /**
168 | * 显示选项
169 | */
170 | option: ShowOption[];
171 | /**
172 | * 进入下一剧情语句
173 | */
174 | next: undefined;
175 | /**
176 | * 根据选项加入下一剧情语句
177 | */
178 | select: number;
179 | /**
180 | * 弹出图片, 参数是图片url
181 | */
182 | popupImage: string;
183 | /**
184 | * 弹出视频, 参数是视频url
185 | */
186 | popupVideo: string;
187 | /**
188 | * 隐藏popup
189 | */
190 | hidePopup: undefined;
191 |
192 | /**
193 | * 显示未完待续
194 | */
195 | toBeContinue: undefined;
196 | toBeContinueDone: undefined;
197 | /**
198 | * 显示下章节
199 | */
200 | nextEpisode: ShowTitleOption;
201 | nextEpisodeDone: undefined;
202 |
203 | //L2D层
204 | /**
205 | * 加载L2D
206 | */
207 | playL2D: undefined;
208 | /**
209 | * 更换动画
210 | */
211 | changeAnimation: string;
212 | /**
213 | * 结束l2d播放
214 | */
215 | endL2D: undefined;
216 |
217 | /**
218 | * 用户点击
219 | */
220 | click: undefined;
221 | /**
222 | * 开始播放加载动画
223 | * @param string 资源地址
224 | */
225 | startLoading: string;
226 | /**
227 | * 某个资源加载完成或失败
228 | */
229 | oneResourceLoaded: ResourceLoadState;
230 | /**
231 | * 所有资源加载完成
232 | */
233 | loaded: undefined;
234 | };
235 |
236 | export interface BgParams {
237 | /**
238 | * 背景图片 url
239 | */
240 | url: string;
241 | /**
242 | * 以覆盖原来背景的方式显示新背景, 值为渐变时间
243 | */
244 | overlap?: number;
245 | }
246 |
247 | export interface ShowCharacter {
248 | /**
249 | * 角色列表
250 | */
251 | characters: Character[];
252 | /**
253 | * 角色特效
254 | */
255 | // characterEffects: CharacterEffect[]
256 | }
257 |
258 | export interface PlayAudio {
259 | bgm?: {
260 | url: string;
261 | bgmArgs: BGMExcelTableItem;
262 | };
263 | soundUrl?: string;
264 | voiceJPUrl?: string;
265 | }
266 |
267 | export interface ShowText {
268 | /**
269 | * 文本
270 | */
271 | text: Text[];
272 | /**
273 | * 说话的人, 包括名字和所属
274 | */
275 | speaker?: Speaker;
276 | /**
277 | * 人物头像, 填logText时使用
278 | */
279 | avatarUrl?: string;
280 | /** storyUnit 位置 */
281 | index?: number;
282 | }
283 |
284 | /**
285 | * st特效参数, 第一个为位置, 第二个为显示效果
286 | */
287 | export type StArgs = [number[], "serial" | "instant" | "smooth", number];
288 |
289 | export interface StText {
290 | /**
291 | * 文本
292 | */
293 | text: Text[];
294 | /**
295 | * st的参数, 目前只需要注意第二个参数, serial打字机效果, instant立即全部显示.
296 | */
297 | stArgs: StArgs;
298 | middle: boolean;
299 | }
300 |
301 | export interface ShowOption {
302 | /**
303 | * 剧情原始结构SelectionGroup, 请作为next的参数
304 | */
305 | SelectionGroup: number;
306 | /**
307 | * 选项文本
308 | */
309 | text: Text[];
310 | /** 当前剧情进度 */
311 | index: number;
312 | }
313 |
314 | export interface PlayEffect {
315 | BGEffect?: BGEffectExcelTableItem;
316 | otherEffect: Effect[];
317 | }
318 |
319 | export interface ShowTitleOption {
320 | title: Text[];
321 | subtitle?: string;
322 | translator?: string;
323 | }
324 |
325 | export interface ResourceLoadState {
326 | type: "success" | "fail";
327 | resourceName: string;
328 | }
329 |
--------------------------------------------------------------------------------
/lib/types/excels.ts:
--------------------------------------------------------------------------------
1 | export interface BGNameExcelTableItem {
2 | Name: number;
3 | ProductionStep: string;
4 | BGFileName: string;
5 | BGType: "Spine" | "Image";
6 | AnimationRoot: string;
7 | AnimationName: string;
8 | SpineScale: number;
9 | SpineLocalPosX: number;
10 | SpineLocalPosY: number;
11 | }
12 |
13 | export interface CharacterNameExcelTableItem {
14 | CharacterName: number;
15 | ProductionStep: string;
16 | NameKR: string;
17 | NicknameKR: string;
18 | NameJP: string;
19 | NicknameJP: string;
20 | NameCN?: string;
21 | NicknameCN?: string;
22 | Shape: string;
23 | SpinePrefabName: string;
24 | SmallPortrait: string;
25 | }
26 |
27 | export interface BGMExcelTableItem {
28 | Id: number;
29 | ProductionStep: string;
30 | Path: string;
31 | Volume: number;
32 | LoopStartTime: number;
33 | LoopEndTime: number;
34 | LoopTranstionTime: number;
35 | LoopOffsetTime: number;
36 | }
37 |
38 | export type TransitionTypes = "bgoverlap" | "fade" | "fade_white";
39 |
40 | export interface TransitionTableItem {
41 | Name: number;
42 | TransitionOut: TransitionTypes;
43 | TransitionOutDuration: number;
44 | TransitionOutResource: null | string;
45 | TransitionIn: TransitionTypes;
46 | TransitionInDuration: number;
47 | TransitionInResource: null | string;
48 | }
49 |
50 | export type BGEffectType =
51 | | "BG_ScrollT_0.5"
52 | | "BG_Filter_Red"
53 | | "BG_Wave_F"
54 | | "BG_Flash"
55 | | "BG_UnderFire_R"
56 | | "BG_Love_L"
57 | | "BG_ScrollB_0.5"
58 | | "BG_Rain_L"
59 | | "BG_UnderFire"
60 | | "BG_WaveShort_F"
61 | | "BG_SandStorm_L"
62 | | ""
63 | | "BG_ScrollT_1.5"
64 | | "BG_Shining_L"
65 | | "BG_ScrollB_1.0"
66 | | "BG_Love_L_BGOff"
67 | | "BG_Dust_L"
68 | | "BG_ScrollL_0.5"
69 | | "BG_ScrollL_1.0"
70 | | "BG_Ash_Black"
71 | | "BG_Mist_L"
72 | | "BG_Flash_Sound"
73 | | "BG_ScrollL_1.5"
74 | | "BG_FocusLine"
75 | | "BG_ScrollR_1.5"
76 | | "BG_Shining_L_BGOff"
77 | | "BG_ScrollT_1.0"
78 | | "BG_ScrollB_1.5"
79 | | "BG_Filter_Red_BG"
80 | | "BG_Ash_Red"
81 | | "BG_Fireworks_L_BGOff_02"
82 | | "BG_ScrollR_0.5"
83 | | "BG_Snow_L"
84 | | "BG_Fireworks_L_BGOff_01"
85 | | "BG_ScrollR_1.0";
86 |
87 | export interface BGEffectExcelTableItem {
88 | Name: number;
89 | Effect: BGEffectType;
90 | Scroll: "None" | "Vertical" | "Horizontal";
91 | ScrollTime: number;
92 | ScrollFrom: number;
93 | ScrollTo: number;
94 | }
95 |
--------------------------------------------------------------------------------
/lib/types/l2d.ts:
--------------------------------------------------------------------------------
1 | export type IL2dPlayQue = {
2 | name: string;
3 | animation: string;
4 | fadeTime?: number;
5 | secondFadeTime?: number;
6 | sounds?: {
7 | fileName: string;
8 | time: number;
9 | /**
10 | * 声音大小, 默认为2
11 | */
12 | volume?: number;
13 | }[];
14 | /** 和后一个动画是否fade */
15 | fade?: boolean;
16 | };
17 | export type IL2dConfig = {
18 | [key: string]: {
19 | name: string;
20 | playQue: IL2dPlayQue[];
21 | spineSettings?: {
22 | [key: string]: {
23 | scale?: number; // 对单个 spine 文件进行设置
24 | };
25 | };
26 | /** 实际上是请求的路径 */
27 | otherSpine?: string[];
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/lib/types/resources.ts:
--------------------------------------------------------------------------------
1 | export type ResourcesTypes =
2 | | "emotionImg"
3 | | "emotionSound"
4 | | "fx"
5 | | "l2dSpine"
6 | | "l2dVoice"
7 | | "excel"
8 | | "bgm"
9 | | "sound"
10 | | "voiceJp"
11 | | "characterSpine"
12 | | "bg"
13 | | "otherSound"
14 | | "otherL2dSpine"
15 | | "bgEffectImgs"
16 | | "avatar"
17 | | "video"
18 | | "popupImage"
19 | | "bgEffectSounds";
20 |
21 | // select:按钮点击音效,back:返回音效
22 | export type OtherSounds = "select" | "bg_underfire" | "back";
23 | export type OtherSoundsUrls = {
24 | [key in OtherSounds]: string;
25 | };
26 |
--------------------------------------------------------------------------------
/lib/types/store.ts:
--------------------------------------------------------------------------------
1 | import { Application, Sprite } from "pixi.js";
2 | import { Ref } from "vue";
3 | import { CharacterInstance, StoryUnit } from "./common";
4 | import { ShowOption, ShowText } from "./events";
5 | import {
6 | BGEffectExcelTableItem,
7 | BGEffectType,
8 | BGMExcelTableItem,
9 | BGNameExcelTableItem,
10 | CharacterNameExcelTableItem,
11 | TransitionTableItem,
12 | } from "./excels";
13 | import { IL2dConfig } from "./l2d";
14 | import { OtherSounds } from "./resources";
15 |
16 | export type Language = "Cn" | "Jp" | "En" | "Tw";
17 |
18 | /**
19 | * 仅可通过函数修改的state
20 | */
21 | export interface PrivateStates {
22 | /**
23 | * pixi.js app实例
24 | */
25 | app: Application | null;
26 | /**
27 | * 后端资源前缀
28 | */
29 | dataUrl: string;
30 | /**
31 | * 用户名, 如xx老师
32 | */
33 | userName: string;
34 | language: Language;
35 | /**
36 | * 当前故事, 由一个个单元结合而成
37 | */
38 | allStoryUnit: StoryUnit[];
39 | /**
40 | * 用于查找l2d spinedata
41 | */
42 | l2dSpineUrl: string;
43 | /** 当前剧情下的 l2d 特殊播放配置 */
44 | curL2dConfig: null | IL2dConfig[keyof IL2dConfig];
45 |
46 | /**
47 | * 译者信息, 仅在无title且无place的情况下有值
48 | */
49 | translator: string;
50 |
51 | //背景层
52 | /**
53 | * 背景实例
54 | */
55 | bgInstance: null | Sprite;
56 |
57 | //文字层
58 | /**
59 | * 已经展示过的语句的集合, 用于ui层显示日志
60 | */
61 | logText: Ref;
62 | /**
63 | * 故事简要概述
64 | */
65 | storySummary: StorySummary;
66 |
67 | //资源管理
68 | /**
69 | * 根据BGName获取资源信息, 包括l2d和背景图片
70 | */
71 | BGNameExcelTable: Map;
72 | /**
73 | * 根据CharacterName获取角色name和nickName(名字与所属)
74 | */
75 | CharacterNameExcelTable: Map;
76 | /**
77 | * 获取BGEffect
78 | */
79 | BGEffectExcelTable: Map;
80 |
81 | /**
82 | * 根据bgm id获取bgm资源信息
83 | */
84 | BGMExcelTable: Map;
85 | /**
86 | * 根据transition标识获取transition相关信息
87 | */
88 | TransitionExcelTable: Map;
89 | /**
90 | * 根据emotionName获取对于英文名
91 | */
92 | EmotionExcelTable: Map;
93 | /**
94 | * 根据emotion名获取emotion图片信息
95 | */
96 | emotionResourcesTable: Map;
97 | fxImageTable: Map;
98 | bgEffectImgMap: Map;
99 | }
100 |
101 | /**
102 | * 可直接修改的state
103 | */
104 | export interface PublicStates {
105 | /**
106 | * 人物层用于保存所有已创建的spine数据的map
107 | *
108 | * 注意, CharacterName只能唯一确定一个spine对象, 但是不能确定一个显示在player上的spine
109 | *
110 | * 在存在量产杂鱼的情况下, 需要结合initPosition来确定
111 | */
112 | currentCharacterMap: Map;
113 | }
114 |
115 | export interface BasicGetters {
116 | app: Application;
117 |
118 | /**
119 | * 获取角色spineData
120 | */
121 | characterSpineData: (
122 | CharacterName: number
123 | ) => import("@pixi-spine/base").ISkeletonData | undefined;
124 | /**
125 | * 获取情绪图像资源
126 | * @param emotionName 情绪名
127 | * @returns 情绪资源图片url数组, 按从底而上, 从左到右排列
128 | */
129 | emotionResources: (emotionName: string) => string[] | undefined;
130 | /**
131 | * 获取fx特效图像资源
132 | * @param fxName
133 | * @returns 图像资源url数组
134 | */
135 | fxImages: (fxName: string) => string[] | undefined;
136 |
137 | emotionSoundUrl: (emotionName: string) => string;
138 | /**
139 | * 获取其他特效音url
140 | * @param type 特效音类型, 如select
141 | * @returns
142 | */
143 | otherSoundUrl: (type: OtherSounds) => string;
144 | bgEffectSoundUrl: (bgEffect: BGEffectType) => string;
145 | /**
146 | * 获取L2D资源
147 | */
148 | l2dSpineData: import("@pixi-spine/base").ISkeletonData | undefined;
149 | }
150 |
151 | export type GetterFunctions = {
152 | [Getter in keyof BasicGetters]: () => BasicGetters[Getter];
153 | };
154 | export type Getters = Readonly;
155 |
156 | export interface Actions {
157 | setBgInstance: (sprite: Sprite) => void;
158 | /**
159 | * 更新logText的值, 即已经显示过的文字和选项
160 | * @param newLog 新加入log的值, 可为对话或选项
161 | * @returns
162 | */
163 | updateLogText: (newLog: ShowText | ShowOption) => void;
164 | /**
165 | * 设置l2d的spine数据地址便于l2d层获取spinedata
166 | * @param url
167 | * @returns
168 | */
169 | setL2DSpineUrl: (url: string) => void;
170 | /**
171 | * 设置当前l2d特殊配置
172 | * @param val l2dConfig
173 | * @returns
174 | */
175 | setL2DConfig: (val: IL2dConfig[keyof IL2dConfig]) => void;
176 |
177 | /**
178 | * 设置译者信息, 仅在无title且无place的情况下调用
179 | * @param translator
180 | */
181 | setTranslator(translator: string): void;
182 | }
183 |
184 | export interface LogText {
185 | /**
186 | * user: 用户选项
187 | * character: 人物对话, 有头像
188 | * none: 无所属对话, 此时name不存在
189 | */
190 | type: "user" | "character" | "none";
191 | text: string;
192 | /**
193 | * 人物名
194 | */
195 | name?: string;
196 | /**
197 | * 头像地址
198 | */
199 | avatarUrl?: string;
200 | /** storyUnit 位置 */
201 | index?: number;
202 | }
203 |
204 | export interface StorySummary {
205 | /**
206 | * 章节名
207 | */
208 | chapterName: string;
209 | /**
210 | * 简介
211 | */
212 | summary: string;
213 | }
214 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { OtherSoundsUrls, ResourcesTypes } from "@/types/resources";
2 |
3 | let dataUrl = "";
4 | let otherSoundMap: OtherSoundsUrls;
5 | /**
6 | * ogg类型的音频是否用其他音频类型代替
7 | */
8 | let oggAudioType = "ogg";
9 | let superSampling = "";
10 |
11 | /**
12 | * 设置数据站点
13 | * @param url
14 | */
15 | export function setDataUrl(url: string): void {
16 | dataUrl = url;
17 | otherSoundMap = {
18 | select: `${dataUrl}/Audio/Sound/UI_Button_Touch.wav`,
19 | bg_underfire: `${dataUrl}/Audio/Sound/UI_FX_BG_UnderFire.wav`,
20 | back: `${dataUrl}/Audio/Sound/UI_Button_Back.wav`,
21 | };
22 | }
23 |
24 | /**
25 | * 设置ogg类型音频的替代音频类型
26 | */
27 | export function setOggAudioType(audioType: "mp3") {
28 | oggAudioType = audioType;
29 | }
30 |
31 | export function setSuperSampling(type: string) {
32 | superSampling = `-${type}x`;
33 | }
34 |
35 | /**
36 | * 获取其他特效音资源, 用于本体资源加载
37 | * @returns
38 | */
39 | export function getOtherSoundUrls(): string[] {
40 | return Object.values(otherSoundMap);
41 | }
42 |
43 | /**
44 | * 根据资源类型和参数获取资源地址, 可根据服务器实际情况修改
45 | * @param type
46 | * @param arg
47 | * @returns
48 | */
49 | export function getResourcesUrl(type: ResourcesTypes, arg: string): string {
50 | switch (type) {
51 | case "emotionImg":
52 | return `${dataUrl}/emotions/${arg}`;
53 | case "emotionSound":
54 | return `${dataUrl}/Audio/Sound/${arg}.wav`;
55 | case "fx":
56 | return `${dataUrl}/effectTexture/${arg}`;
57 | case "l2dVoice":
58 | //arg "sound/CH0184_MemorialLobby_1_1"
59 | const voiceDirectory = arg.replace(
60 | /.*\/([A-Z0-9]*)_MemorialLobby.*/i,
61 | "JP_$1"
62 | );
63 | const voiceFilename = arg.split("/").pop();
64 | return `${dataUrl}/Audio/VoiceJp/Character_voice/${voiceDirectory}/${voiceFilename}.${oggAudioType}`;
65 | case "l2dSpine":
66 | return `${dataUrl}/spine/${arg}/${arg}.skel`;
67 | case "otherL2dSpine":
68 | return `${dataUrl}/spine/${arg}.skel`;
69 | case "excel":
70 | return `${dataUrl}/data/${arg}`;
71 | case "bgm":
72 | return `${dataUrl}/${arg}.${oggAudioType}`;
73 | case "sound":
74 | return `${dataUrl}/Audio/Sound/${arg}.wav`;
75 | case "voiceJp":
76 | return `${dataUrl}/Audio/VoiceJp/${arg}.${oggAudioType}`;
77 | case "characterSpine":
78 | //arg UIs/03_Scenario/02_Character/CharacterSpine_hasumi
79 | let temp = String(arg).split("/");
80 | let id = temp.pop();
81 | id = id?.replace("CharacterSpine_", "");
82 | if (id?.endsWith("ND")) {
83 | id = id.slice(0, id.length - 2);
84 | }
85 | let filename = `${id}_spr`; //hasumi_spr
86 | if (superSampling) {
87 | return `${dataUrl}/spine/${filename}/${filename}${superSampling}/${filename}.skel`;
88 | }
89 | return `${dataUrl}/spine/${filename}/${filename}.skel`;
90 | case "bg":
91 | // UIs/03_Scenario/01_Background/BG_WinterRoad.jpg
92 | if (superSampling && /01_Background/.test(arg)) {
93 | const pathArr = arg.split("/");
94 | const lastFileName = pathArr.pop();
95 | const dir = pathArr.join("/");
96 | return `${dataUrl}/${dir}/01_Background${superSampling}/${lastFileName}.png`;
97 | }
98 | return `${dataUrl}/${arg}.jpg`;
99 | case "otherSound":
100 | return Reflect.get(otherSoundMap, arg) || "";
101 | case "bgEffectImgs":
102 | return `${dataUrl}/effectTexture/${arg}`;
103 | case "bgEffectSounds":
104 | return `${dataUrl}/Audio/Sound/UI_FX_${arg}.wav`;
105 | case "avatar":
106 | //arg: UIs/01_Common/01_Character/Student_Portrait_Hasumi
107 | return `${dataUrl}/${arg}.png`;
108 | case "popupImage":
109 | return `${dataUrl}/UIs/03_Scenario/04_ScenarioImage/${arg}.png`;
110 | default:
111 | return "";
112 | }
113 | }
114 |
115 | /**
116 | * 字面意思, 深拷贝json
117 | */
118 | export function deepCopyObject(object: T): T {
119 | return JSON.parse(JSON.stringify(object));
120 | }
121 |
122 | /*
123 | * wait in promise
124 | */
125 | export function wait(milliseconds: number) {
126 | return new Promise(resolve => setTimeout(resolve, milliseconds));
127 | }
128 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ba-story-player",
3 | "version": "0.8.0",
4 | "license": "GPL-3.0",
5 | "type": "module",
6 | "files": [
7 | "dist"
8 | ],
9 | "main": "./dist/ba-story-player.umd.cjs",
10 | "module": "./dist/ba-story-player.js",
11 | "types": "./dist/lib/main.d.ts",
12 | "repository": {
13 | "url": "https://github.com/ba-archive/ba-story-player"
14 | },
15 | "exports": {
16 | ".": {
17 | "import": "./dist/ba-story-player.js",
18 | "require": "./dist/ba-story-player.umd.js"
19 | },
20 | "./dist/style.css": "./dist/style.css"
21 | },
22 | "scripts": {
23 | "dev": "vite --host",
24 | "build": "vite build && vue-tsc --emitDeclarationOnly",
25 | "preview": "vite preview",
26 | "format": "prettier --config .prettierrc.json -uw ./lib/ ./src/",
27 | "check": "eslint --fix --config ./.eslintrc.json ./lib/ --ext .js,.ts,.vue && vue-tsc --noEmit",
28 | "prepare": "husky install"
29 | },
30 | "lint-staged": {
31 | "lib/**/*.{ts,js,vue}": [
32 | "prettier --config .prettierrc.json --write",
33 | "git add"
34 | ],
35 | "src/**/*.{ts,js,vue}": [
36 | "prettier --config .prettierrc.json --write",
37 | "git add"
38 | ]
39 | },
40 | "dependencies": {
41 | "@pixi-spine/base": "^3.1.0",
42 | "@pixi/filter-adjustment": "^4.0.0",
43 | "@pixi/filter-advanced-bloom": "^4.0.0",
44 | "@pixi/filter-color-overlay": "^4.0.0",
45 | "@pixi/filter-crt": "^4.0.0",
46 | "@pixi/filter-motion-blur": "^4.2.0",
47 | "@pixi/particle-emitter": "^5.0.8",
48 | "@pixi/sound": "<5.0.0",
49 | "@vueuse/core": "^9.13.0",
50 | "gsap": "^3.11.3",
51 | "mitt": "^3.0.0",
52 | "pixi-spine": "^3.1.0",
53 | "pixi.js": "^6.0.0",
54 | "typed.js": "^2.0.12",
55 | "vue-responsive-video-background-player": "^2.3.1",
56 | "xxhashjs": "^0.2.2"
57 | },
58 | "peerDependencies": {
59 | "axios": "^1.1.3",
60 | "vue": "^3.2.41"
61 | },
62 | "devDependencies": {
63 | "@types/node": "^18.11.9",
64 | "@types/xxhashjs": "^0.2.2",
65 | "@typescript-eslint/eslint-plugin": "^5.55.0",
66 | "@vitejs/plugin-vue": "^3.2.0",
67 | "@vue/eslint-config-typescript": "^11.0.2",
68 | "eslint": "^8.36.0",
69 | "eslint-config-prettier": "^8.7.0",
70 | "eslint-plugin-import": "^2.27.5",
71 | "eslint-plugin-sort-exports": "^0.8.0",
72 | "eslint-plugin-vue": "^9.9.0",
73 | "husky": "^8.0.3",
74 | "prettier": "^2.8.4",
75 | "rollup-plugin-node-externals": "^5.1.2",
76 | "sass": "^1.57.1",
77 | "staged": "^0.0.0",
78 | "typescript": "^4.6.4",
79 | "vite": "^3.2.3",
80 | "vue-tsc": "^1.0.9"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
115 |
116 |
117 |
118 |
119 |
131 |
132 |
133 |
143 |
144 |
149 |
150 |
151 |
152 |
155 |
156 |
157 |
158 |
159 |
160 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
183 |
--------------------------------------------------------------------------------
/src/assets/scss/index.scss:
--------------------------------------------------------------------------------
1 | $main-color: #3f88f2;
2 | $text-layer-z-index: 50;
3 |
4 | #player__background {
5 | position: relative;
6 | .transition-cover {
7 | position: absolute;
8 | width: 100%;
9 | height: 100%;
10 | left: 0;
11 | top: 0;
12 | background-position-y: 0;
13 | background-size: 120% 100%;
14 | background: linear-gradient(90deg, transparent, black 3%) no-repeat;
15 | z-index: 9999;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ModifyEmotionOption.vue:
--------------------------------------------------------------------------------
1 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
149 |
154 |
155 |
156 |
{{ option }}
157 |
changeOption(option, (event.target as HTMLInputElement).value)"
160 | />
161 |
162 |
163 |
{{ option }}
164 |
changeOption(option, Number((event.target as HTMLInputElement).value))"
169 | />
170 |
171 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/src/components/TestEffect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
22 |
25 |
26 |
31 |
32 |
33 |
38 |
39 |
42 |
54 |
55 |
56 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
217 |
218 |
224 |
--------------------------------------------------------------------------------
/src/data/LocalizeScenarioExcelTable.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Key": 1190451628,
4 | "Kr": "프롤로그",
5 | "Jp": "プロローグ",
6 | "Cn": "序章",
7 | "Th": "บทเริ่มต้น",
8 | "Tw": "序幕",
9 | "En": "Prologue"
10 | },
11 | {
12 | "Key": 1058709048,
13 | "Kr": "기묘한 내용의 꿈으로부터 깨어난 직후, 선생님은 총학생회의 간부인 나나가미 린으로부터 총학생회장이 사라졌다는 소식을 전해 듣는다. 총학생회장의 실종으로 인해 학원도시 키보토스는 혼란에 휩싸이게 되고, 이 혼란을 해결하기 위해 선생님은 학생회 임원들과 함께 샬레 동아리실로 향한다.",
14 | "Jp": "奇妙な夢から目覚めた[USERNAME]先生は、連邦生徒会の幹部である七神リンから、連邦生徒会長がいなくなったということを聞かされる。連邦生徒会長の失踪により、現在ここ学園都市キヴォトスは混乱に包まれているのだという。この混乱を解決するため、[USERNAME]先生は生徒会の役員たちと共にシャーレの部室へと向かった。",
15 | "Cn": "从奇怪的梦中醒来之后的[USERNAME]老师从联邦学生会的干部七神凛那里听到学生会长失踪的消息。由于学生会长失踪,学园城市基沃托斯陷入了混乱。为了解决这场混乱,老师和学生会的干部一同前往夏莱办公室。",
16 | "Th": "หลังจากตื่นขึ้นมาจากฝันประหลาด คุณครูก็ได้ฟังข่าวจากนานากามิ ริน\nที่เป็นสมาชิกบริหารองค์การนักเรียนเรื่องที่ประธานองค์การนักเรียนหายตัวไป\nเนื่องจากประธานองค์การหายตัวไป เมืองแห่งการศึกษาคิโวทอสจึงเกิด\nความโกลาหลขึ้น และเพื่อแก้ไขสิ่งที่เกิดขึ้น คุณครูกับสภานักเรียนจึง\nมุ่งหน้าไปยังห้องชมรมชาเล่ต์",
17 | "Tw": "從莫名其妙的夢中醒來之後,老師從總學生會的幹部七神凛那裡聽到總學生會長消失的消息。由於總學生會長失蹤,學園城市奇普托斯陷入混亂。為了解決這場混亂,老師和學生會的幹部一同前往夏萊社團室。",
18 | "En": "Sensei wakes from a bizarre dream and learns from Nanakami Rin that the General Student Council president is missing. The Academy City, Kivotos, is in chaos. Sensei heads to the Schale club room in an attempt to resolve the situation."
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import App from "./App.vue";
3 |
4 | const app = createApp(App);
5 | app.mount("#app");
6 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function resizeTextareas() {
2 | let textAreas = document.querySelectorAll("textarea");
3 | textAreas.forEach(value => {
4 | value.style.height = value.scrollHeight + "px";
5 | });
6 | }
7 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module "*.vue" {
4 | import type { DefineComponent } from "vue";
5 | const component: DefineComponent<{}, {}, any>;
6 | export default component;
7 | }
8 |
9 | declare interface Window {
10 | baStore?: any;
11 | baResource: any;
12 | baStory: any;
13 | }
14 | declare interface Document {
15 | webkitCancelFullScreen?: any;
16 | mozCancelFullScreen?: any;
17 | webkitFullscreenElement?: any;
18 | mozFullScreenElement?: any;
19 | fullscreenElement?: any;
20 | webkitFullscreenEnabled?: any;
21 | mozFullScreenEnabled?: any;
22 | }
23 |
24 | declare interface HTMLElement {
25 | webkitRequestFullScreen: any;
26 | mozRequestFullScreen?: any;
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "lib": [
13 | "ESNext",
14 | "DOM"
15 | ],
16 | "skipLibCheck": true,
17 | "outDir": "dist",
18 | "declaration": true,
19 | "baseUrl": ".",
20 | "paths": {
21 | "@/*": [
22 | "lib/*"
23 | ]
24 | }
25 | },
26 | "include": [
27 | "lib/**/*.ts",
28 | "lib/**/*.d.ts",
29 | "lib/**/*.tsx",
30 | "lib/**/*.vue",
31 | "src/vite-env.d.ts"
32 | ],
33 | "references": [
34 | {
35 | "path": "./tsconfig.node.json"
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import vue from "@vitejs/plugin-vue";
3 | import { resolve } from "path";
4 | import type { Plugin } from "vite";
5 | import { externals, ExternalsOptions } from "rollup-plugin-node-externals";
6 |
7 | function nodeExternals(options?: ExternalsOptions): Plugin {
8 | return {
9 | ...externals(options),
10 | name: "vite-plugin-node-externals",
11 | enforce: "pre", // https://cn.vitejs.dev/guide/api-plugin.html#plugin-ordering
12 | };
13 | }
14 |
15 | // https://vitejs.dev/config/
16 | export default defineConfig(({ mode }) => {
17 | return {
18 | resolve: {
19 | alias: {
20 | "@": resolve(__dirname, "./lib"),
21 | },
22 | },
23 | plugins: [nodeExternals(), vue()],
24 | esbuild: {
25 | drop: mode === "production" ? ["debugger"] : [],
26 | pure: mode === "production" ? ["console.log"] : [],
27 | },
28 | build: {
29 | lib: {
30 | entry: resolve(__dirname, "lib/main.ts"),
31 | name: "BaStoryPlayer",
32 | fileName: "ba-story-player",
33 | },
34 | },
35 | css: {
36 | preprocessorOptions: {
37 | scss: {
38 | additionalData: '@import "./src/assets/scss/index.scss";',
39 | },
40 | },
41 | },
42 | };
43 | });
44 |
--------------------------------------------------------------------------------