dsh-piggy 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +1348 -0
  2. package/LICENSE +32 -0
  3. package/README.md +322 -0
  4. package/THIRD-PARTY.md +31 -0
  5. package/assets/elder.svg +35 -0
  6. package/assets/pig-king-bathe.svg +38 -0
  7. package/assets/pig-king-eat.svg +45 -0
  8. package/assets/pig-king-pet.svg +45 -0
  9. package/assets/pig-king-play.svg +45 -0
  10. package/assets/pig-king-relaxed.svg +56 -0
  11. package/assets/pig-king-study.svg +45 -0
  12. package/assets/pig-king-trip.svg +45 -0
  13. package/assets/pig-king-work.svg +45 -0
  14. package/assets/pig-king.svg +56 -0
  15. package/assets/piglet.svg +30 -0
  16. package/client.js +3438 -0
  17. package/commands.js +252 -0
  18. package/cordis.patch.yml +15 -0
  19. package/core.js +11 -0
  20. package/data.js +10 -0
  21. package/index.js +68 -0
  22. package/package.json +72 -0
  23. package/packages/pet-core/package.json +21 -0
  24. package/packages/pet-core/src/core/activity.js +80 -0
  25. package/packages/pet-core/src/core/care.js +160 -0
  26. package/packages/pet-core/src/core/clock.js +122 -0
  27. package/packages/pet-core/src/core/constants.js +109 -0
  28. package/packages/pet-core/src/core/daily.js +204 -0
  29. package/packages/pet-core/src/core/diary.js +108 -0
  30. package/packages/pet-core/src/core/drops.js +82 -0
  31. package/packages/pet-core/src/core/effects.js +84 -0
  32. package/packages/pet-core/src/core/egg.js +120 -0
  33. package/packages/pet-core/src/core/evolution.js +90 -0
  34. package/packages/pet-core/src/core/growth.js +110 -0
  35. package/packages/pet-core/src/core/illness.js +235 -0
  36. package/packages/pet-core/src/core/interests.js +55 -0
  37. package/packages/pet-core/src/core/inventory.js +177 -0
  38. package/packages/pet-core/src/core/lines.js +157 -0
  39. package/packages/pet-core/src/core/migrate.js +226 -0
  40. package/packages/pet-core/src/core/profile.js +102 -0
  41. package/packages/pet-core/src/core/random.js +79 -0
  42. package/packages/pet-core/src/core/school.js +81 -0
  43. package/packages/pet-core/src/core/settlement.js +308 -0
  44. package/packages/pet-core/src/core/state.js +210 -0
  45. package/packages/pet-core/src/core/travel.js +58 -0
  46. package/packages/pet-core/src/core/upgrades.js +193 -0
  47. package/packages/pet-core/src/core/views.js +55 -0
  48. package/packages/pet-core/src/core/work.js +50 -0
  49. package/packages/pet-core/src/core.js +95 -0
  50. package/packages/pet-core/src/data/daily.js +121 -0
  51. package/packages/pet-core/src/data/drops.js +34 -0
  52. package/packages/pet-core/src/data/evolution.js +41 -0
  53. package/packages/pet-core/src/data/growth.js +72 -0
  54. package/packages/pet-core/src/data/illness.js +188 -0
  55. package/packages/pet-core/src/data/interests.js +39 -0
  56. package/packages/pet-core/src/data/jobs.js +150 -0
  57. package/packages/pet-core/src/data/life.js +88 -0
  58. package/packages/pet-core/src/data/lines.js +198 -0
  59. package/packages/pet-core/src/data/minutes.js +23 -0
  60. package/packages/pet-core/src/data/profile.js +60 -0
  61. package/packages/pet-core/src/data/school.js +99 -0
  62. package/packages/pet-core/src/data/shop.js +142 -0
  63. package/packages/pet-core/src/data/traits.js +42 -0
  64. package/packages/pet-core/src/data/travel.js +87 -0
  65. package/packages/pet-core/src/data.js +52 -0
  66. package/packages/pet-core/src/index.js +12 -0
  67. package/render.js +357 -0
  68. package/routes.js +201 -0
  69. package/scripts/build-client.mjs +100 -0
  70. package/scripts/typecheck.mjs +122 -0
  71. package/snapshot.js +368 -0
  72. package/src/client/art.js +29 -0
  73. package/src/client/constants.js +88 -0
  74. package/src/client/css-base.js +225 -0
  75. package/src/client/css-card.js +63 -0
  76. package/src/client/css-tabs.js +250 -0
  77. package/src/client/css-tiles.js +89 -0
  78. package/src/client/dom.js +33 -0
  79. package/src/client/effects.js +159 -0
  80. package/src/client/format.js +27 -0
  81. package/src/client/index.js +393 -0
  82. package/src/client/io.js +100 -0
  83. package/src/client/layout.js +103 -0
  84. package/src/client/normalize.js +352 -0
  85. package/src/client/panel.js +319 -0
  86. package/src/client/scene.js +113 -0
  87. package/src/client/storage.js +19 -0
  88. package/src/client/styles.js +13 -0
  89. package/src/client/tabs/bag.js +203 -0
  90. package/src/client/tabs/card.js +110 -0
  91. package/src/client/tabs/crown.js +59 -0
  92. package/src/client/tabs/dev.js +101 -0
  93. package/src/client/tabs/home.js +72 -0
  94. package/src/client/tabs/shop.js +78 -0
  95. package/src/client/tabs/status.js +223 -0
  96. package/src/client/tabs/study.js +128 -0
  97. package/src/client/tabs/travel.js +89 -0
  98. package/src/client/tabs/update.js +164 -0
  99. package/src/client/tabs/work.js +106 -0
  100. package/src/client/values.js +11 -0
  101. package/src/client/widgets.js +126 -0
  102. package/store/api.js +260 -0
  103. package/store/state-file.js +129 -0
  104. package/store.js +96 -0
package/commands.js ADDED
@@ -0,0 +1,252 @@
1
+ // @ts-check
2
+ /**
3
+ * 斜杠命令与拒绝话术。
4
+ *
5
+ * 只读取状态与数值表,不算业务规则(见 docs/CONVENTIONS.md)。
6
+ * @module dsh-piggy/commands
7
+ */
8
+
9
+ import { ACTIONS, JOBS, MAX, REVIVE_ITEM, SCHOOL_STAGES, SHOP, SUBJECTS, TRAITS, TRAIT_ORDER, TRIPS, buy, feed, hatch } from './core.js'
10
+ import { FORMS, INTERESTS, formByKey, jobByKey, stageForNextLesson } from './data.js'
11
+ import { renderAbout, renderAction, renderBuy, renderHatch, renderNoPig, renderStatus, renderStudyReport, renderTooSoon, renderTripReport, renderUse, renderWeigh, renderWorkRefusal, renderWorkReport } from './render.js'
12
+
13
+ const str = value => (typeof value === 'string' ? value : '')
14
+
15
+ export function performAction(store, action) {
16
+ const state = store.freshen()
17
+ if (state === null) return { kind: 'error', text: renderNoPig('pig') }
18
+ const result = store.act(action)
19
+ if (!result.ok) {
20
+ if (result.reason === 'cooldown') return { kind: 'success', text: renderTooSoon(state, Date.now(), action) }
21
+ if (result.reason === 'away') return { kind: 'success', text: renderWorkRefusal(state, `${state.name} 正在外面,回来再说。`) }
22
+ if (result.reason === 'dead') return { kind: 'error', text: `${state.name} 已经走了…用 ${REVIVE_ITEM.label} 可以救回来。` }
23
+ if (result.reason === 'absent') return { kind: 'error', text: renderNoPig('pig') }
24
+ return { kind: 'error', text: `🐖 ${ACTIONS[action]?.label ?? action} 没做成。` }
25
+ }
26
+ return { kind: 'success', text: renderAction(store.freshen(), Date.now(), action, result.crossed) }
27
+ }
28
+
29
+ export function dispatch(store, commandName, rawInput) {
30
+ const trimmed = rawInput.trim()
31
+ const [sub, ...rest] = trimmed === '' ? [''] : trimmed.split(/\s+/)
32
+ const argument = rest.join(' ')
33
+ const state = store.freshen()
34
+ const nowMs = Date.now()
35
+ const verb = sub.toLowerCase()
36
+
37
+ switch (verb) {
38
+ case '':
39
+ case 'status': {
40
+ if (state === null) return { kind: 'success', text: renderNoPig(commandName) }
41
+ return { kind: 'success', text: renderStatus(state, nowMs) }
42
+ }
43
+ case 'hatch': {
44
+ if (store.hatch()) return { kind: 'success', text: renderHatch(store.state, nowMs) }
45
+ const existing = store.freshen()
46
+ return { kind: 'success', text: `这里已经住着 ${existing.name} 了 🐖\n\n${renderStatus(existing, nowMs)}` }
47
+ }
48
+ case 'feed':
49
+ case 'bathe':
50
+ case 'play':
51
+ case 'pet':
52
+ case 'mo':
53
+ return performAction(store, verb === 'mo' ? 'pet' : verb)
54
+
55
+ case 'work': {
56
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
57
+ const key = argument.trim() === '' ? 'odd' : argument.trim()
58
+ const job = JOBS.find(j => j.key === key || j.label === key)
59
+ if (job === undefined) return { kind: 'error', text: `没有「${key}」这份工作。/${commandName} work 看有哪些。` }
60
+ const result = store.startWork(job.key)
61
+ if (!result.ok) return { kind: 'success', text: renderWorkRefusal(state, refusalText(result, state)) }
62
+ return { kind: 'success', text: renderWorkReport(store.freshen(), Date.now(), job) }
63
+ }
64
+
65
+ case 'study': {
66
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
67
+ // B4: a subject's stage follows from its lesson count, so only the subject is named.
68
+ const [subjectArg = ''] = argument.trim().split(/\s+/)
69
+ const subject = SUBJECTS.find(s => s.key === subjectArg || s.label === subjectArg)
70
+ if (subject === undefined) {
71
+ return { kind: 'error', text: `用法:/${commandName} study <科目>\n科目:${SUBJECTS.map(s => s.label).join(' · ')}` }
72
+ }
73
+ const stage = stageForNextLesson(state.lessons?.[subject.key] ?? 0)
74
+ const result = store.startStudy(subject.key)
75
+ if (!result.ok) return { kind: 'success', text: renderWorkRefusal(state, refusalText(result, state)) }
76
+ return { kind: 'success', text: renderStudyReport(store.freshen(), Date.now(), subject, stage) }
77
+ }
78
+
79
+ case 'trip':
80
+ case 'travel': {
81
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
82
+ const key = argument.trim() === '' ? 'suburb' : argument.trim()
83
+ const trip = TRIPS.find(t => t.key === key || t.label === key)
84
+ if (trip === undefined) return { kind: 'error', text: `没有「${key}」这个目的地。/${commandName} trip 看有哪些。` }
85
+ const result = store.startTrip(trip.key)
86
+ if (!result.ok) return { kind: 'success', text: renderWorkRefusal(state, refusalText(result, state)) }
87
+ return { kind: 'success', text: renderTripReport(store.freshen(), Date.now(), trip) }
88
+ }
89
+
90
+ case 'calloff': {
91
+ const result = store.callOffActivity()
92
+ if (!result.ok) return { kind: 'success', text: `${state?.name ?? '猪'} 没在外面。` }
93
+ return {
94
+ kind: 'success',
95
+ text: result.refunded > 0
96
+ ? `${state.name} 提前回来了,退回 ${result.refunded} 金币。`
97
+ : `${state.name} 提前回来了,这趟白跑。`,
98
+ }
99
+ }
100
+
101
+ case 'shop': {
102
+ const lines = SHOP.map(item => ` ${item.emoji} ${item.label} ${item.price} 金币`).join('\n')
103
+ return { kind: 'success', text: `🛒 商店(你有 ${state?.coins ?? 0} 金币)\n${lines}\n\n买:/${commandName} buy <物品>` }
104
+ }
105
+ case 'buy': {
106
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
107
+ const item = SHOP.find(i => i.key === argument.trim() || i.label === argument.trim())
108
+ if (item === undefined) return { kind: 'error', text: `没有「${argument}」这样东西。/${commandName} shop 看货架。` }
109
+ return { kind: 'success', text: renderBuy(store.freshen(), store.buy(item.key), item) }
110
+ }
111
+ case 'use': {
112
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
113
+ const item = SHOP.find(i => i.key === argument.trim() || i.label === argument.trim())
114
+ if (item === undefined) return { kind: 'error', text: `没有「${argument}」这样东西。` }
115
+ return { kind: 'success', text: renderUse(store.freshen(), store.useItem(item.key), item) }
116
+ }
117
+ case 'interest': {
118
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
119
+ const interest = INTERESTS.find(i => i.key === argument.trim() || i.label === argument.trim())
120
+ if (interest === undefined) {
121
+ const names = INTERESTS.map(i => `${i.label}(${i.key})`).join(' · ')
122
+ return { kind: 'error', text: `没有「${argument}」这门兴趣课。可选:${names}` }
123
+ }
124
+ const result = store.startInterest(interest.key)
125
+ if (!result.ok) return { kind: 'error', text: refusalText(result, state) }
126
+ return { kind: 'success', text: `${state.name} 去上${interest.label}课了,${interest.minutes} 分钟后回来。` }
127
+ }
128
+ case 'sell': {
129
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
130
+ const wanted = argument.trim()
131
+ const owned = state.souvenirs.find(entry => entry.key === wanted || entry.label === wanted)
132
+ if (owned === undefined) return { kind: 'error', text: `收藏里没有「${wanted}」。` }
133
+ const result = store.sellSouvenir(owned.key)
134
+ if (!result.ok) return { kind: 'error', text: refusalText(result, state) }
135
+ return { kind: 'success', text: `卖掉了「${result.sold.label}」,得到 ${result.sold.price} 金币。` }
136
+ }
137
+ case 'wear': {
138
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
139
+ const [wanted, mode] = argument.trim().split(/\s+/)
140
+ const item = SHOP.find(i => i.kind === 'dress' && (i.key === wanted || i.label === wanted))
141
+ if (item === undefined) return { kind: 'error', text: `没有「${wanted}」这件家当。` }
142
+ const on = mode !== 'off' && mode !== '脱'
143
+ const result = store.wear(item.key, on)
144
+ if (!result.ok) return { kind: 'error', text: refusalText(result, state) }
145
+ return { kind: 'success', text: `${state.name} ${on ? '穿上' : '脱下'}了「${item.label}」。` }
146
+ }
147
+ case 'adopt': {
148
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
149
+ if (!store.adopt()) return { kind: 'error', text: `${state.name} 还在,不能领养新的 —— 只有墓碑之后才能。` }
150
+ return { kind: 'success', text: '门口放了一个新纸盒 📦 用它继续吧。' }
151
+ }
152
+ case 'reply': {
153
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
154
+ const open = state.dialogue?.open ?? null
155
+ if (open === null) return { kind: 'error', text: `${state.name} 现在没在说话。` }
156
+ const index = Number.parseInt(argument.trim(), 10)
157
+ if (!Number.isInteger(index) || index < 1) {
158
+ const labels = open.replies.map((reply, i) => `${i + 1}=${reply.label}`).join(' · ')
159
+ return { kind: 'error', text: `用法:/${commandName} reply <序号>(${labels})` }
160
+ }
161
+ const result = store.reply(open.id, index - 1)
162
+ if (!result.ok) return { kind: 'error', text: refusalText(result, state) }
163
+ return { kind: 'success', text: `你说「${result.reply}」,${state.name} 很开心。` }
164
+ }
165
+ case 'weigh': {
166
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
167
+ return { kind: 'success', text: renderWeigh(state, nowMs) }
168
+ }
169
+ case 'name': {
170
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
171
+ const cleaned = store.rename(argument)
172
+ if (cleaned === null) return { kind: 'error', text: `用法:/${commandName} name <名字>(16 字以内)` }
173
+ return { kind: 'success', text: `从今天起,它叫「${cleaned}」🐖` }
174
+ }
175
+ case 'crown': {
176
+ if (state === null) return { kind: 'error', text: renderNoPig(commandName) }
177
+ const wanted = argument === '' ? undefined : (FORMS.find(form => form.label === argument || form.key === argument)?.key ?? argument)
178
+ const result = store.crown(wanted)
179
+ if (result.ok) return { kind: 'success', text: `👑 ${state.name} 现在是${formByKey(result.form)?.label ?? ''}了。\n` + renderStatus(store.freshen(), nowMs) }
180
+ if (result.reason === 'coronation-ineligible') {
181
+ const rows = result.missing.map(row => `${row.label} ${row.have}/${row.need}`).join(' · ')
182
+ return { kind: 'error', text: `还差一点:${rows}` }
183
+ }
184
+ if (result.reason === 'unknown') return { kind: 'error', text: `没有这种形态。可选:${FORMS.map(form => form.label).join(' · ')}` }
185
+ return { kind: 'error', text: refusalText(result, state) }
186
+ }
187
+ case 'about':
188
+ case 'help':
189
+ return { kind: 'success', text: renderAbout(commandName) }
190
+ default:
191
+ return {
192
+ kind: 'error',
193
+ text: `不认识「${sub}」。可用:/${commandName} · hatch · adopt · feed · bathe · play · pet · study · interest · work · trip · calloff · shop · buy · use · sell · wear · crown · weigh · name · reply · about`,
194
+ }
195
+ }
196
+ }
197
+
198
+ function refusalText(result, state) {
199
+ switch (result.reason) {
200
+ case 'dead': return `${state.name} 已经走了…`
201
+ case 'away': return `${state.name} 已经在外面了。`
202
+ case 'sick': return `${state.name} 病着,不能出门 —— 先治好它。`
203
+ case 'hungry': return `${state.name} 太饿了,先喂点东西。`
204
+ case 'poor': return `钱不够,需要 ${result.price} 金币,你只有 ${state.coins}。`
205
+ case 'underqualified': {
206
+ const want = (result.missing ?? []).map(entry => `${entry.emoji} ${entry.label} ${entry.need}(现在 ${entry.have})`).join('、')
207
+ return `这份工作还轮不到它 —— 需要 ${want}。去「学习」上课就能涨。`
208
+ }
209
+ case 'wrong-stage':
210
+ return `${result.stage}没有「${result.subject}」这门课 —— 换个学段,或者换一门课。`
211
+ case 'locked': {
212
+ const need = result.need
213
+ return need === null || need === undefined
214
+ ? '这一级还没解锁 —— 先把上一级的课念完。'
215
+ : `要先念完${need.label}(${need.done}/${need.need})。`
216
+ }
217
+ case 'box': return '先把纸盒拆开。'
218
+ case 'not-owned': return `${state.name} 还没有这件东西。`
219
+ case 'owned': return `${state.name} 已经有这件了。`
220
+ case 'low-level': return `等级不够(要 Lv.${result.need},现在 Lv.${result.have})。`
221
+ case 'stale-line': return `${state.name} 已经在说下一句了。`
222
+ case 'unknown': return '没有这个选项。'
223
+ default: return '现在没法出门。'
224
+ }
225
+ }
226
+
227
+ export { jobByKey, MAX, SHOP, JOBS, SUBJECTS, SCHOOL_STAGES, TRIPS, TRAITS, TRAIT_ORDER }
228
+
229
+ /**
230
+ * Register the `/pig` fallback command. The GUI is the primary path; this is
231
+ * what still works when the panel cannot mount.
232
+ * @param {object} ctx - plugin context
233
+ * @param {object} store - the pig store
234
+ * @param {string} commandName - validated command name
235
+ */
236
+ export function registerSlashCommand(ctx, store, commandName) {
237
+ ctx.inject(['commands'], (commandCtx) => {
238
+ commandCtx.commands.register({
239
+ name: commandName,
240
+ description: 'your pig 🐖: status · study · work · shop · travel · bag',
241
+ input: { hint: '[hatch|feed|bathe|play|pet|study <科目>|work <job>|trip <目的地>|shop|buy|use|weigh|about]' },
242
+ handler: invocation => {
243
+ try {
244
+ return dispatch(store, commandName, String(invocation.rawInput ?? ''))
245
+ } catch (error) {
246
+ const detail = error instanceof Error ? error.message : String(error)
247
+ return { kind: 'error', text: `🐖 猪摔了一跤:${detail}` }
248
+ }
249
+ },
250
+ })
251
+ })
252
+ }
@@ -0,0 +1,15 @@
1
+ # dsh-piggy's layer: one row, no dependencies on other bundles.
2
+ #
3
+ # `name` must match package.json's `name` exactly. It was left as `dsh-pig`
4
+ # after the package was renamed, and the mismatch silently dropped this
5
+ # plugin's CLIENT half — the host routes kept working (the loader resolved the
6
+ # stale node_modules symlink) while the widget never mounted at all.
7
+ #
8
+ # config keys (both optional):
9
+ # command: the slash command name, default "pig". Change it if another
10
+ # plugin already registered /pig.
11
+ # statePath: absolute path of the save file, default $DSH_HOME/dsh-piggy/state.json.
12
+ - insert:
13
+ - id: dsh-piggy
14
+ name: dsh-piggy
15
+ config: {}
package/core.js ADDED
@@ -0,0 +1,11 @@
1
+ // @ts-check
2
+ /**
3
+ * dsh-piggy · core —— 插件侧的入口,只做再导出。
4
+ *
5
+ * 领域模型本体在 packages/pet-core(`@dsh-piggy/core`),DSH 插件和以后的独立版
6
+ * 共用同一份。这个文件留在根目录,是为了让 `../core.js` 这类既有导入一行都不用改。
7
+ *
8
+ * @module dsh-piggy/core
9
+ */
10
+
11
+ export * from './packages/pet-core/src/core.js'
package/data.js ADDED
@@ -0,0 +1,10 @@
1
+ // @ts-check
2
+ /**
3
+ * dsh-piggy · data —— 插件侧的入口,只做再导出。
4
+ *
5
+ * 数值表本体在 packages/pet-core(`@dsh-piggy/core`),见 core.js 的说明。
6
+ *
7
+ * @module dsh-piggy/data
8
+ */
9
+
10
+ export * from './packages/pet-core/src/data.js'
package/index.js ADDED
@@ -0,0 +1,68 @@
1
+ // @ts-check
2
+ /**
3
+ * dsh-piggy — a pig that lives in your DeepSeek Harness.
4
+ *
5
+ * The plugin registers no model-facing tool and injects no context, so the
6
+ * model never learns the pig exists and the pig costs zero tokens per request.
7
+ * Every listener body is wrapped so a pig bug can never veto or delay real work.
8
+ *
9
+ * Two ways the human interacts, both ending in the same core calls:
10
+ * - the floating pig's six icons → POST /dsh-piggy/act
11
+ * - typing `/pig feed` → the slash command
12
+ * The GUI is the primary path; the command is the fallback.
13
+ *
14
+ * This file only wires things together: the store, the harness listeners, the
15
+ * routes and the command. The shapes live in snapshot.js, the command in
16
+ * commands.js, the HTTP surface in routes.js (see docs/CONVENTIONS.md).
17
+ *
18
+ * @module dsh-piggy
19
+ */
20
+
21
+ import { dispatch, registerSlashCommand } from './commands.js'
22
+ import { registerRoutes } from './routes.js'
23
+ import { createStore, moveLegacySaveDir } from './store.js'
24
+
25
+ export const name = 'dsh-piggy'
26
+
27
+ /**
28
+ * No required services. The pig rides on emit-mode events, and it reaches the
29
+ * optional web seam through the safe `ctx.get()` accessor rather than a
30
+ * declared injection — reading an undeclared service as a property throws in
31
+ * cordis and would take the whole plugin down.
32
+ */
33
+ export const inject = []
34
+
35
+ /** Wrap a listener so a pig bug can never veto or delay real harness work. */
36
+ const contained = fn => (...args) => {
37
+ try { fn(...args) } catch { /* the pig absorbs its own mishaps */ }
38
+ }
39
+
40
+ /** The pig eats your real work: turns, tool calls and errors. */
41
+ function registerDietListeners(ctx, store) {
42
+ ctx.on('agent/inbox/claimed', contained(() => store.feed('message')))
43
+ ctx.on('agent/turn-stopping', contained(() => store.feed('turn')))
44
+ ctx.on('agent/error', contained(() => store.feed('agentError')))
45
+ ctx.on('tools/result', contained((exec, result) => {
46
+ store.feed(result?.isError === true ? 'toolError' : 'tool')
47
+ }))
48
+ }
49
+
50
+ export function apply(ctx, config = {}) {
51
+ const commandName = typeof config.command === 'string' && /^[a-z][a-z0-9-]{0,23}$/.test(config.command)
52
+ ? config.command
53
+ : 'pig'
54
+ const customPath = typeof config.statePath === 'string' && config.statePath.trim() !== ''
55
+ // The default save moved from $DSH_HOME/dsh-pig/ to dsh-piggy/; bring an old one along.
56
+ if (!customPath) moveLegacySaveDir()
57
+ const store = createStore(customPath ? config.statePath : undefined)
58
+
59
+ registerDietListeners(ctx, store)
60
+ registerRoutes(ctx, store)
61
+ registerSlashCommand(ctx, store, commandName)
62
+
63
+ ctx.effect(() => () => store.dispose())
64
+ }
65
+
66
+ export { dispatch, performAction } from './commands.js'
67
+ export { snapshot } from './snapshot.js'
68
+ export { JOBS, MAX, SCHOOL_STAGES, SHOP, SUBJECTS, TRAITS, TRAIT_ORDER, TRIPS, jobByKey } from './data.js'
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "dsh-piggy",
3
+ "version": "0.25.0",
4
+ "description": "🐖 一只住在 DeepSeek Harness 里的猪:吃你的真实工作长大,会学习、打工、旅行、生病、需要照顾",
5
+ "keywords": [
6
+ "dsh",
7
+ "deepseek-harness",
8
+ "plugin",
9
+ "pet",
10
+ "pig",
11
+ "tamagotchi",
12
+ "desktop-pet"
13
+ ],
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "main": "index.js",
17
+ "exports": {
18
+ ".": "./index.js",
19
+ "./client": "./client.js",
20
+ "./core": "./core.js",
21
+ "./pet-core": "./packages/pet-core/src/index.js",
22
+ "./cordis.patch.yml": "./cordis.patch.yml",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "index.js",
27
+ "snapshot.js",
28
+ "commands.js",
29
+ "routes.js",
30
+ "core.js",
31
+ "data.js",
32
+ "store.js",
33
+ "render.js",
34
+ "client.js",
35
+ "packages/pet-core",
36
+ "store",
37
+ "src",
38
+ "scripts",
39
+ "cordis.patch.yml",
40
+ "README.md",
41
+ "CHANGELOG.md",
42
+ "THIRD-PARTY.md",
43
+ "LICENSE",
44
+ "assets"
45
+ ],
46
+ "scripts": {
47
+ "build": "node scripts/build-client.mjs",
48
+ "typecheck": "node scripts/typecheck.mjs",
49
+ "test": "node --test \"test/**/*.test.js\" \"apps/desktop/test/*.test.js\"",
50
+ "preview": "echo \"open tools/preview.html in a browser\""
51
+ },
52
+ "dsh": {
53
+ "bundle": {
54
+ "patch": "./cordis.patch.yml"
55
+ },
56
+ "client": {
57
+ "platform": "web"
58
+ }
59
+ },
60
+ "engines": {
61
+ "node": ">=20"
62
+ },
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/CLICGGER-TYPES/dsh-piggy.git"
66
+ },
67
+ "homepage": "https://github.com/CLICGGER-TYPES/dsh-piggy#readme",
68
+ "bugs": {
69
+ "url": "https://github.com/CLICGGER-TYPES/dsh-piggy/issues"
70
+ },
71
+ "author": "clicgger"
72
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@dsh-piggy/core",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "dsh-piggy 的纯领域模型与数值表:零依赖、零 IO,时间与随机数由宿主传入",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./core": "./src/core.js",
12
+ "./data": "./src/data.js",
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20"
20
+ }
21
+ }
@@ -0,0 +1,80 @@
1
+ // @ts-check
2
+ /**
3
+ * 外出活动的开始与召回。
4
+ *
5
+ * 纯函数领域逻辑:时间由 nowMs 传入,不读写文件、不碰 DOM(见 docs/CONVENTIONS.md)。
6
+ * @module dsh-piggy/core/activity
7
+ */
8
+
9
+ import { TOO_WEAK_HEALTH } from './constants.js'
10
+ import { remember } from './effects.js'
11
+ import { decay } from './settlement.js'
12
+
13
+ /**
14
+ * Why the pig cannot head out right now, or null when it can.
15
+ *
16
+ * Illness used to block this outright, which deadlocked the whole game:
17
+ * sick → cannot work → no coins → cannot buy medicine → still sick, and the
18
+ * only way out was to wait to die. A pig that can still stand up can go and
19
+ * earn its own prescription; only one at death's door has to stay in bed.
20
+ */
21
+ export function awayBlockedReason(state) {
22
+ if (state === null) return 'absent'
23
+ if (state.hatched !== true) return 'box'
24
+ if (state.dead) return 'dead'
25
+ if (state.activity !== null) return 'away'
26
+ if (state.health <= TOO_WEAK_HEALTH) return 'weak'
27
+ return null
28
+ }
29
+
30
+ /** Whether the pig is free to go out. */
31
+ export function canStartActivity(state) {
32
+ return awayBlockedReason(state) === null
33
+ }
34
+
35
+ /** Back-compat alias. */
36
+ export const canWork = canStartActivity
37
+
38
+ /** Start the named activity. Returns `{ ok, activity }` or `{ ok:false, reason }`. */
39
+ export function begin(state, activity, nowMs) {
40
+ decay(state, nowMs)
41
+ const blocked = awayBlockedReason(state)
42
+ if (blocked !== null) return { ok: false, reason: blocked }
43
+ state.activity = { ...activity, startedAt: nowMs, endsAt: nowMs + activity.minutes * 60000 }
44
+ state.lastActiveAt = nowMs
45
+ return { ok: true, activity: state.activity }
46
+ }
47
+
48
+ /** Seconds left on the current activity (0 when idle). */
49
+ export function activitySecondsLeft(state, nowMs) {
50
+ if (state.activity === null) return 0
51
+ return Math.max(0, Math.ceil((state.activity.endsAt - nowMs) / 60000 * 60))
52
+ }
53
+
54
+ /** Back-compat alias. */
55
+ export const workSecondsLeft = activitySecondsLeft
56
+
57
+ /** Bring the pig home early. Work forfeits pay; study and trips are refunded. */
58
+ export function callOffActivity(state, nowMs) {
59
+ // Settle first: a shift/lesson/trip that already ended must pay out (and be
60
+ // announced) before anyone decides it was "called off". Without this the
61
+ // panel's 召回 button voided the wages of a finished shift.
62
+ decay(state, nowMs)
63
+ if (state.activity === null) return { ok: false, reason: 'idle' }
64
+ const activity = state.activity
65
+ state.activity = null
66
+ if (activity.kind !== 'work' && activity.cost > 0) {
67
+ state.coins += activity.cost
68
+ remember(state, `${activity.emoji} 从${activity.label}提前回来了,钱退回来了`, nowMs)
69
+ return { ok: true, refunded: activity.cost }
70
+ }
71
+ remember(state, `${activity.emoji} 从${activity.label}提前回来了,白跑一趟`, nowMs)
72
+ return { ok: true, refunded: 0 }
73
+ }
74
+
75
+ /** Back-compat alias. */
76
+ export const callOffWork = callOffActivity
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Shop and inventory
80
+ // ---------------------------------------------------------------------------