mobile

Apotheca ⸸

重构!重构!

デンシの呪文
3.7k 8 mins ... ...

  什么不是昨天才刚开始做吗怎么今天就开始重构了。
  主要是昨天不小心一口气写了太多功能导致代码结构开始混乱了,老话说得好打孩子要趁早(?一开始就推倒重建总比变成屎山了再愚公移山一样一勺一勺地挑大份好……
  愚公:我要发起名誉权诉讼

ʚ ⸸ ɞ

模块设计

  写下这些玩意的时候惊觉这真到了用 notion 或者 dynalist 比较方便排版的时候了……
  理想的状态是根目录下只有:网页本体,main.css,main.js。
  CSS 只有 import 和字体行距之类的配置项(引入 scss 好了);
  JS 是一个用最简单直观的函数呈现的功能列表,默认只开启最基础的文本显示和跳转功能,立绘特效之类的附加功能算高级函数,默认注释掉,需要开启的时候把注释符号删掉或者在配置里决定……?
  等等所以不是自用吗搞这么傻瓜式干什么。
  算了万一我哪天失忆了呢(

data・资源注册文件

  一些迷思:一些只包含文件列表和调用 id 的配置文件是不是直接放对应目录底下比较方便管理?

  • config.json
    主配置文件,存放:
    游戏基本信息(标题,作者)
    剧本文件列表,起始节点
    角色配置文件位置
    背景、特效、BGM、语音配置文件位置(如果有的话,为了方便初始化这里应该填 null 而不是留空?)
    CSS 文件(动画和特效列表)位置
  • chara.json
    存放角色信息:姓 名 立绘位置
  • bg.json
    存放背景图列表和对应 id
  • weather.css
    我感觉这个就直接写份 CSS 用 class 控制吧
    可以做电闪雷鸣风雨大作之类的复合效果……(想了一下已经开始笑了
  • BGM.json 暂不考虑
  • sound.json 音效列表,暂不考虑
  • voice.json 语音列表,暂不考虑
    我寻思一个网页引擎放语音文件不如调用 edge 朗读接口……

chara

  存放角色立绘。
  在配置文件中注册 ./chara/charaId/main.xxx 作为路径登记,自动按照 emote 属性获取同目录下相应标题的同拓展名文件。虽然这个逻辑比较罕见但一般也不会出现同一个角色底下有一堆不同拓展名的资源吧?
  呃好像可以用静态素体 + 动态表情……住手啊花里胡哨的功能怎么越设计越多了
  那就把后缀名做成可选参数,检测到输入的是 img.xxx 自动获取后缀名,输入的是 img 就默认使用和注册路径相同的后缀名。

bg

  存放背景图。

fx

  比起给 classname 加前缀是不是直接统一用 data 属性比较好……
  哎算了两种选择器都写上也不碍事。

  • fx.css
    存放特效(CSS filter)
  • animate.css
    存放动画,类名应当以 anime 开头
  • weather.css
    存放天气,对这个文件还是移到这里来比较合适

???

  拆分代码功能


  预处理部分:

  • 【getStory.js】
    从 config 获取剧本文件列表
    移除空行与注释
    --- 拆分为基本数组
  • 【TextParser.js】
    剧本解析器
    负责将原始剧本语言解析成包含 type 的对象
    是不是可以和上面那个文件合并?
    这两个文件加起来只需要输出一个剧本节点对象就行了,或者其实创建数组和对象的操作可以并行
  • 【init.js】
    获取剧本
    从 config 获取资源列表并加载
    在控制台输出 log

  辅助函数部分:

  • 【createNode.js】
    创建基本文字 / 图片节点的辅助函数
    • createRuby(text, reading)
      创建 ruby 元素
    • createText(text, type, class)
      创建 b em del 等基础 html 元素
      type 默认为 span
      class 默认为空
    • createImg(src, class)
      创建img 并 设置 classname
      data 太复杂了而且可能要传多个参数,放到其它高级函数里
  • 【getData.js】
    存放获取数据的辅助函数
    • getCharaImg(charaId, emote)
      获取对应角色的对应表情图片,icon 的逻辑直接放在 emote 里
      然后调用 createImg(src, class)
      设置 data-chara="charaId" data-emote="emote",暂时没想到这俩 data 有什么用但是先埋着
    • getCharaName(charaId, type)
      根据对应 ID 和 config 中的角色配置文件查询角色的 name reading surName surNameReading nick 与 cjkName
      type: name 返回单独名的 span
      type: rubyName 返回带注音标记的名 ruby
      type: fullName 返回按 cjkName 属性确定格式的全名
      type: fullRubyNmae 返回注音全名
      type: nick 返回昵称
      如角色未配置则返回 ID span
    • getSourceUrl(sourceType, id)
      获取背景图 / bgm / 音效等无额外配置项的资源的链接
      返回一个 url 地址,额外的控制放到专属控制函数里去
  • 【uiControl.js】
    存放控制 UI 的辅助函数
    • uiVisible(show/hide, …elements)
      为多个元素移除或添加 class hide
    • uiClear(keep=false, …elements)
      为多个元素添加 class hide 并清空 html
      +keep=true 时仅移除所有不包含 data-keep 的子元素
      为实现记忆菜单设计的新功能,具体默认行为是 true 还是 false 没想好
    • uiFx(‘selector’, fxClass)
      用 querySlector 选中需要处理的元素
      移除可能存在的已有 fxClass
      添加新的 fxClass
    • uiAnimate(‘selector’, animeClass)
      用 querySlector 选中需要处理的元素
      添加新的 animeClass
      uiFx 只有是否移除已有 class 的区别所以理论上可以混用,但真混用起来不保证任何 unexpected error
    • uiCharaActive(???)
      高亮正在说话的角色的立绘
      为所有 data-chara 不等于当前说话人的立绘添加 class=hold
      具体传入什么参数没想好,而且命名和实际操作其实相反……
      但是把 .hold 压暗比较符合 css 的逻辑,也不能所有立绘出场自带 .hold 吧那有点蠢
  • 【textRender.js】
    控制文本渲染的辅助函数
    创建单独或是 addLine 的文本节点
    渲染富文本
    创建文字动画

  看了一下感觉逻辑有点混乱,尤其是姓名处理的方面,理论上获取数据和渲染 dom 应该拆分成两个函数……
  + 此外如果在之后要添加资源动态预加载功能的话,所有图片和资源的 src 理论上都应该直接写在结构化的数据里了,「获取 url」功能的函数在那个时候就应该被统一优化掉,写在一起到时候优化就非常麻烦。
  但是姓名显示这个玩意太特殊了,因为数据获取要获取一长串,渲染对象又要渲染一长串,但是数据和对象之间又是非常简单的一对一关系,分开来写多少有点脱裤子放屁。但是 getName 不会涉及图片渲染所以理论上不存在重复劳动的问题所以写在一起大概也 OK……?


  运行时部分,没想好要不要拆成多个文件。

  • 即时 debug 信息渲染
    已完成的功能,还没想好怎么命名,有点困了感觉脑子不太转了
  • 【playDialogueBox】
    渲染普通 AVG 样式的对话框
  • 【playTalkBubble】
    渲染聊天记录样式的特殊对话框
  • 立绘的出现,隐藏,激活
  • 背景切换

  其实没想好到底要不要拆分这两个对话框样式,虽然目前的代码已经写好了,但是如果再加上头像的话为了兼容排版 dom 格式就会变成这样:

  • wrapper
    • icon
    • wrapper
      • name
      • text

  这就实在有点恶心了

插播吐槽

  让 Gemini 给我查作业的时候发现我的函数命名和传参顺序都是 对象命令,而标准的 JS 规范则是相反的,虽然这里大概领悟到了什么面向方法编程和面向对象编程的风格差异……但导致我这么做的原因绝对是日文语序「これを〇〇する」和英文语序「do sth. to this」的区别吧……!

功能卫星

  用来存档一些还没想好怎么写也不知道怎么归类的功能:

选项状态存储

  这玩意我都不知道怎么命名……总之是对于需要重复出现的选项列表,需要为「已访问过的选项」设置特殊的样式或是移除,当然移除用 display: none 实现就等于特殊样式,没有区别。

  设计方案 1:使用不同的剧本语法,例:

这是普通选项:
	- A => nodeA
	- B => nodeB

这是需要重复出现的选项:
	* A1 => nodeA1
	* B1 => nodeB1
	* 结束对话

  在生成重复选项的列表时添加一个 data-keep="true",后续添加新选项的时候只移除不带这个 data 的子元素(得用 removeChild 吧不能直接清空了),直到选择了结束对话选项或者手动 # clear keep
  暂时看起来好像感觉不出有什么问题?已经不记得我昨天是怎么设计了反正这个方案应该比我之前设计的健壮很多,还可以在 keep 选项菜单的同时在前台继续显示和执行其它普通的选项。
  可能存在的隐患是:同时出现两个隐藏的 keep 菜单怎么办?这时候就需要 id 识别了。
  啊等等其实我应该直接在生成所有选项的时候就按照他们的节点和行号生成 id,不然跳转回菜单以后也没办法激活原本 keep 在后台的那个隐藏菜单。这样隐患也就一起消失了,可喜可贺可喜可贺!

  最后的执行逻辑:

  • 提取 renderChoice(text, target) 为辅助函数(可以同时在 # end 里复用)
    在生成选项 li 的时候就按 nodeId[n] 的格式指定 id
    对不起我突然想起来根据目前的数据结构整个选项列表是在同一行的,那直接生成 ul ID 就行了
  • 识别 - 开头的列表为普通列表
    创建 ul class="choice" id="nodeId[n] 后执行若干 renderChoice
  • 识别 * 开头的列表为 keep 列表
    创建列表后指定 data-keep="true"
  • 执行至 type:keep-option
    移除 optionBox 的 .hide
    判断 optionBox 中是否含有对应 ID 的元素
    • 有 => 移除它的 .hide
    • 无 => 正常创建 ul
  • 在点击选项后为此 ul 和 optionBox 添加 .hide

  同时之前的 uiClear(xxx, xxx, ...) 函数也要修改,要添加一个 keep 参数决定是直接清空还是只移除不包含 data-keep 的子元素。是的这个功能既然做了那就肯定不限定给选项用,具体哪里能用上没想好反正先埋着……
  然后是一个隐藏的问题是由于涉及到 dom ID 所以剧本的 nodeId 里不应该再出现 . 号,虽然改改代码可以显示但还是修改剧本规范一劳永逸。反正重构会放在新页面上之前的剧本不管怎么写的都无所谓了 xd

资源加载

  虽然现在还根本没开始引入立绘只是尝试在文本里添加了一张图片但是已经是肉眼可见的有感加载了,理想的状态当然还是全部加载完毕之后再开始游戏,但身为一个网页项目加载的时间如果太长还不如下载……
  目前设想的方案是在进行数据结构化的时候识别所有引用的图片(以及未来可能要用到的音乐音效之类的资源)生成一个列表添加到属性中,然后在初始化的时候只加载初始节点和初始节点包含的分支节点中需要用到的资源。
  嗯那就是还要收集所有的分支信息存储到对象里。虽然可以预见的结构化和初始化的代码会变得非常可怕但是为了客户端的流畅度这点牺牲还是可以接受的……
  不过实现起来好像也不是特别麻烦?只要在结构化数据的同时收集资源和跳转列表,然后新建一个加载函数获取本节点和跳转节点的资源列表并进行预加载就可以了。
  初始化的时候只对 start 节点执行。然后还应该加入「预加载深度」的可选配置项,默认只查找一次,如果加大深度的话就继续在跳转后的节点执行相同的任务获取更多节点的资源列表。
  看起来思路还挺清晰的但是好麻烦啊以及我手里压根没有那么多能用来加载的美术资源,设为优先级低(。

Live2D 交互

  立绘画完了吗就开始惦记 Live2D 的事了
  优先级设为极低,啥时候我能看懂 L2D api 文档了再考虑这玩意……


Copyright 2025. All rights reserved.

魔女の部屋