Skip to content

玄学放置 · 装备系统与任务单线程改造

两个改动叠加

这一阶段做了两件事:

  1. 任务从"多线程"改为"单线程":之前每个学科都能同时挂机,现在全局只能挂机一个任务,进度条移到顶部
  2. 新增装备 / 物品面板:右侧增加角色装备槽(头/颈/胸/腿/脚/主手/副手/饰品)+ 背包

这两个改动表面上没关系,但放一起做是因为它们共同重塑了游戏的节奏感:玩家的注意力从"同时管理 8 个学科"收敛到"挑一个任务挂机 + 摆弄装备",体验更专注,也更有目标感。

任务单线程的设计抉择

原来的 store 是这样:

js
// 旧版:每个学科都有自己的进度
list: [
  { id: 'math', currentTaskId: 'count', progressMs: 0, ... },
  { id: 'chinese', currentTaskId: 'recite', progressMs: 0, ... },
  // 8 个学科都在跑
]

改造后变成:

js
// 新版:全局只有一个 active 任务
{
  list: [...],                  // 学科基础属性(等级/经验/任务列表)
  currentId: 'math',            // 侧栏当前查看的学科(仅影响视图)
  activeSubjectId: null,        // 当前正在挂机的学科
  activeTaskId: null,           // 当前正在挂机的任务
  activeProgressMs: 0,          // 全局唯一的进度
  activeCompletedCount: 0
}

currentIdactive* 严格分离的好处是:切换侧栏只是查看,不会影响挂机。玩家可以一边挂着「数学 - 求一个不定积分」,一边切到玄学面板看任务详情,进度不会被打断。

tickProgress(dt) 也跟着大幅简化:

js
tickProgress(dt) {
  if (!this.isRunning) return
  const subject = this.activeSubject
  const task = this.activeTask
  this.activeProgressMs += dt
  while (this.activeProgressMs >= task.durationMs) {
    this.activeProgressMs -= task.durationMs
    subject.completedCount += 1
    this._grantExp(subject, task.expReward)
  }
}

只推进一个任务、给对应学科加经验,逻辑变直了。

顶部 GlobalTaskBar

进度条从学科面板里移出来,单独做一个常驻的全局组件 GlobalTaskBar。它显示:

  • 学科图标(带主题色发光)
  • 学科名 chip + 任务名
  • 进度条
  • 完成次数 + 单次收益
  • 「停止」按钮

未挂机时显示带呼吸灯的占位提示:「当前未挂机 — 在中央面板选择一个任务开始」。这个空状态不是装饰,是体验提示:玩家任何时候打开页面都能立即明白下一步动作。

学科面板的角色变化

SubjectPanel 不再嵌入"当前任务卡 + 进度条",因为进度已经由顶部的 GlobalTaskBar 接管了。它现在只做两件事:

  • 头部:学科信息(名称 / 等级 / 经验进度条)
  • 中部:任务列表卡片

每张任务卡的按钮文案会动态变化:

状态文案说明
未挂机开始挂机点击启动该任务
已挂机其他任务替换当前任务提示玩家会覆盖正在跑的任务
自己正在跑停止挂机红色按钮,配「挂机中」徽章

文案的微调能让玩家在任意状态下都清楚知道自己点这个按钮会发生什么,不需要二次确认弹窗。

装备系统设计

槽位选型

8 槽:

SLOTS = [head, neck, chest, legs, feet, mainHand, offHand, accessory]

不做戒指 1 / 戒指 2 / 项链 / 披风 这种细分,原因有两个:

  • 学科主题对装备分类的需求没那么细致(不是 RPG)
  • 槽位多了之后右侧面板的视觉密度会过高

布局

左右两列对称,中央放人物剪影:

[头部]   [人物]   [颈部]
[上衣]   [像素]   [主手]
[裤子]   [学生]   [副手]
[鞋子]            [饰品]

人物剪影也是 16×16 像素艺术(学生形象:圆头 / 校服 / 裤子 / 鞋),放大到 120px。这样所有视觉资产风格统一,没有混搭的违和感。

装备 / 卸下逻辑

装备和卸下用同一套底层操作 takeFromBag / pushToBag

js
equip(itemId) {
  const item = ITEMS_DB[itemId]
  const slotKey = item.slot

  const ok = takeFromBag(this.bag, itemId)  // 背包减 1
  if (!ok) return false

  const prev = this.equipped[slotKey]        // 旧装备
  this.equipped[slotKey] = itemId            // 装上新的
  if (prev) pushToBag(this.bag, prev)        // 旧装备退回背包
  return true
}

这种设计自然支持「直接换装」:如果槽位已有装备,旧装备会自动退回背包,玩家不需要先卸下再装。少一步操作就少一次心智负担。

内联子组件 SlotCell

装备槽是一个会复用 8 次的小组件,但又不复杂到值得开一个独立 .vue 文件。我用 render 函数模式内联在 EquipmentPanel.vue 里:

js
const SlotCell = defineComponent({
  name: 'SlotCell',
  props: { slotData: { type: Object, required: true } },
  emits: ['click'],
  setup(props, { emit }) {
    return () => {
      const s = props.slotData
      const rarity = s.item?.rarity || 'empty'
      return h('button', {
        class: ['slot-cell', `rarity-${rarity}`, { 'is-empty': !s.item }],
        title: s.item ? buildTooltip(s.item) : `${s.name}(空)`,
        onClick: () => emit('click', s)
      }, [
        s.item
          ? h(PixelIcon, { name: s.item.iconKey, size: 30 })
          : h('span', { class: 'slot-cell__placeholder' }, s.name)
      ])
    }
  }
})

这样既享受了组件化的好处(独立 props、独立样式、独立事件),又避免了"每多一个零碎组件就开一个文件"的目录污染。buildTooltip 由父级 setup 闭包提供,子组件无需重复实现。

Mock 装备:9 件基础道具

基础装备:
  铅笔(主手)   橡皮(副手)    画笔(主手)
  校服上衣(胸) 校服裤(腿)    运动鞋(脚)
  学士帽(头)   红领巾(颈)    眼镜(饰品)

每件装备包含:

  • id / name / slot / iconKey
  • rarity:5 级品质(普通 / 优秀 / 稀有 / 史诗 / 传说)
  • desc:一句吐槽风的物品描述
  • stats:属性加成 KV(本期仅展示用,不参与计算)

属性文案刻意带一点学生时代的吐槽感,例如:

  • 橡皮:「能擦掉绝大部分错误,但擦不掉过去。」
  • 校服上衣:「宽松的海军蓝校服,自带随机污渍。」
  • 眼镜:「看书四小时后必备品。」stats: { 暴击率: '+5%', 视力: '-1' }

这些文案对玩法没有数值影响,但能在玩家把鼠标悬停到物品上时给出一个会心一笑的瞬间——这是放置游戏维持长期黏性的隐形要素。

品质边框颜色

视觉上用边框颜色区分品质,沿用 RPG 游戏的通用语言:

css
.rarity-common    { border-color: rgba(156, 163, 175, 0.5); }
.rarity-uncommon  { border-color: rgba(74, 222, 128, 0.6); box-shadow: 0 0 6px rgba(74, 222, 128, 0.3); }
.rarity-rare      { border-color: rgba(96, 165, 250, 0.7); box-shadow: 0 0 8px rgba(96, 165, 250, 0.35); }
.rarity-epic      { border-color: rgba(167, 139, 250, 0.8); box-shadow: 0 0 8px rgba(167, 139, 250, 0.4); }
.rarity-legendary { border-color: rgba(251, 191, 36, 0.9); box-shadow: 0 0 10px rgba(251, 191, 36, 0.5); }

普通品质只有边框颜色,不发光;从优秀开始加 box-shadow 外发光,越高级越亮。这种渐进式视觉强化,玩家扫一眼就能定位"哪件最稀有"。

收获

这一阶段的几个关键决策:

  • 任务单线程让游戏体验更专注,也让 store 设计变得更直接
  • currentIdactive* 解耦让"切换查看"和"切换挂机"是两件独立的事
  • 装备/卸下共享底层操作支持自然的"直接换装"
  • 内联子组件适合短小但要复用的渲染单元,省去开新文件的成本

下一篇会做一些细节优化:右侧面板加 Tab 切换分离装备和道具、生成 favicon、把"语文"改成更通用的"语言"。