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.
- package/CHANGELOG.md +1348 -0
- package/LICENSE +32 -0
- package/README.md +322 -0
- package/THIRD-PARTY.md +31 -0
- package/assets/elder.svg +35 -0
- package/assets/pig-king-bathe.svg +38 -0
- package/assets/pig-king-eat.svg +45 -0
- package/assets/pig-king-pet.svg +45 -0
- package/assets/pig-king-play.svg +45 -0
- package/assets/pig-king-relaxed.svg +56 -0
- package/assets/pig-king-study.svg +45 -0
- package/assets/pig-king-trip.svg +45 -0
- package/assets/pig-king-work.svg +45 -0
- package/assets/pig-king.svg +56 -0
- package/assets/piglet.svg +30 -0
- package/client.js +3438 -0
- package/commands.js +252 -0
- package/cordis.patch.yml +15 -0
- package/core.js +11 -0
- package/data.js +10 -0
- package/index.js +68 -0
- package/package.json +72 -0
- package/packages/pet-core/package.json +21 -0
- package/packages/pet-core/src/core/activity.js +80 -0
- package/packages/pet-core/src/core/care.js +160 -0
- package/packages/pet-core/src/core/clock.js +122 -0
- package/packages/pet-core/src/core/constants.js +109 -0
- package/packages/pet-core/src/core/daily.js +204 -0
- package/packages/pet-core/src/core/diary.js +108 -0
- package/packages/pet-core/src/core/drops.js +82 -0
- package/packages/pet-core/src/core/effects.js +84 -0
- package/packages/pet-core/src/core/egg.js +120 -0
- package/packages/pet-core/src/core/evolution.js +90 -0
- package/packages/pet-core/src/core/growth.js +110 -0
- package/packages/pet-core/src/core/illness.js +235 -0
- package/packages/pet-core/src/core/interests.js +55 -0
- package/packages/pet-core/src/core/inventory.js +177 -0
- package/packages/pet-core/src/core/lines.js +157 -0
- package/packages/pet-core/src/core/migrate.js +226 -0
- package/packages/pet-core/src/core/profile.js +102 -0
- package/packages/pet-core/src/core/random.js +79 -0
- package/packages/pet-core/src/core/school.js +81 -0
- package/packages/pet-core/src/core/settlement.js +308 -0
- package/packages/pet-core/src/core/state.js +210 -0
- package/packages/pet-core/src/core/travel.js +58 -0
- package/packages/pet-core/src/core/upgrades.js +193 -0
- package/packages/pet-core/src/core/views.js +55 -0
- package/packages/pet-core/src/core/work.js +50 -0
- package/packages/pet-core/src/core.js +95 -0
- package/packages/pet-core/src/data/daily.js +121 -0
- package/packages/pet-core/src/data/drops.js +34 -0
- package/packages/pet-core/src/data/evolution.js +41 -0
- package/packages/pet-core/src/data/growth.js +72 -0
- package/packages/pet-core/src/data/illness.js +188 -0
- package/packages/pet-core/src/data/interests.js +39 -0
- package/packages/pet-core/src/data/jobs.js +150 -0
- package/packages/pet-core/src/data/life.js +88 -0
- package/packages/pet-core/src/data/lines.js +198 -0
- package/packages/pet-core/src/data/minutes.js +23 -0
- package/packages/pet-core/src/data/profile.js +60 -0
- package/packages/pet-core/src/data/school.js +99 -0
- package/packages/pet-core/src/data/shop.js +142 -0
- package/packages/pet-core/src/data/traits.js +42 -0
- package/packages/pet-core/src/data/travel.js +87 -0
- package/packages/pet-core/src/data.js +52 -0
- package/packages/pet-core/src/index.js +12 -0
- package/render.js +357 -0
- package/routes.js +201 -0
- package/scripts/build-client.mjs +100 -0
- package/scripts/typecheck.mjs +122 -0
- package/snapshot.js +368 -0
- package/src/client/art.js +29 -0
- package/src/client/constants.js +88 -0
- package/src/client/css-base.js +225 -0
- package/src/client/css-card.js +63 -0
- package/src/client/css-tabs.js +250 -0
- package/src/client/css-tiles.js +89 -0
- package/src/client/dom.js +33 -0
- package/src/client/effects.js +159 -0
- package/src/client/format.js +27 -0
- package/src/client/index.js +393 -0
- package/src/client/io.js +100 -0
- package/src/client/layout.js +103 -0
- package/src/client/normalize.js +352 -0
- package/src/client/panel.js +319 -0
- package/src/client/scene.js +113 -0
- package/src/client/storage.js +19 -0
- package/src/client/styles.js +13 -0
- package/src/client/tabs/bag.js +203 -0
- package/src/client/tabs/card.js +110 -0
- package/src/client/tabs/crown.js +59 -0
- package/src/client/tabs/dev.js +101 -0
- package/src/client/tabs/home.js +72 -0
- package/src/client/tabs/shop.js +78 -0
- package/src/client/tabs/status.js +223 -0
- package/src/client/tabs/study.js +128 -0
- package/src/client/tabs/travel.js +89 -0
- package/src/client/tabs/update.js +164 -0
- package/src/client/tabs/work.js +106 -0
- package/src/client/values.js +11 -0
- package/src/client/widgets.js +126 -0
- package/store/api.js +260 -0
- package/store/state-file.js +129 -0
- package/store.js +96 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 存档的逐级升级:每改一次存档结构,就在 UPGRADES 末尾加一级。
|
|
4
|
+
*
|
|
5
|
+
* migrate() 先按版本号把原始存档一级一级升到最新,再做字段清洗。
|
|
6
|
+
* 一级只做一件事、只认上一级的形状,这样老存档跨多个版本升级时,
|
|
7
|
+
* 每一步都和当初写它的时候一样确定。
|
|
8
|
+
*
|
|
9
|
+
* 加一级的步骤:
|
|
10
|
+
* 1. 在 UPGRADES 末尾追加 `{ to: N, why, up(raw) }`,`up` 返回新对象,不改入参
|
|
11
|
+
* 2. STATE_VERSION 改成 N(constants.js)
|
|
12
|
+
* 3. 补一条迁移测试,用升级前的真实形状做样本
|
|
13
|
+
*
|
|
14
|
+
* 纯函数领域逻辑:时间由 nowMs 传入,不读写文件、不碰 DOM(见 docs/CONVENTIONS.md)。
|
|
15
|
+
* @module dsh-piggy/core/upgrades
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} Upgrade
|
|
20
|
+
* @property {number} to - 升级后的版本号
|
|
21
|
+
* @property {string} why - 这一级改了什么,写给以后读代码的人
|
|
22
|
+
* @property {(raw: Record<string, unknown>, nowMs: number) => Record<string, unknown>} up
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { roll } from './random.js'
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The curves below are frozen copies of what each version used. An upgrade
|
|
29
|
+
* must keep meaning what it meant when it was written, even after the live
|
|
30
|
+
* tables in data/ change again.
|
|
31
|
+
*/
|
|
32
|
+
const v8LevelFloor = level => (level <= 1 ? 0 : 20 * level * (level - 1))
|
|
33
|
+
const v9LevelFloor = level => (level <= 1 ? 0 : 122 * level * level)
|
|
34
|
+
const V9_MAX_LEVEL = 60
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* v11: the 23 subjects of the old seven-stage ladder, folded into the nine
|
|
38
|
+
* QQ Pet subjects (B4 sheet §6). 礼仪 and 武术 had no old counterpart.
|
|
39
|
+
*/
|
|
40
|
+
const V11_SUBJECT_OF = Object.freeze({
|
|
41
|
+
literacy: 'chinese', chinese: 'chinese', english: 'chinese', history: 'chinese', philosophy: 'chinese',
|
|
42
|
+
mathematics: 'mathematics', science: 'mathematics', physics: 'mathematics', chemistry: 'mathematics',
|
|
43
|
+
biology: 'mathematics', it: 'mathematics', economics: 'mathematics', go: 'mathematics',
|
|
44
|
+
politics: 'politics', geography: 'politics',
|
|
45
|
+
sing: 'music', piano: 'music',
|
|
46
|
+
doodle: 'art', painting: 'art', art: 'art',
|
|
47
|
+
pe: 'pe', football: 'pe',
|
|
48
|
+
engineering: 'labour',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** v11: what each job of the old table paid, for a shift still in progress. */
|
|
52
|
+
const V11_OLD_JOB_PAY = Object.freeze({
|
|
53
|
+
odd: 30, dish: 70, site: 260, foreman: 700, street: 200,
|
|
54
|
+
tutor: 480, office: 900, manager: 2200, researcher: 4000,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/** v10 refund for each retired generic medicine: the new price of the same tier. */
|
|
58
|
+
const V10_REFUND = Object.freeze({ med1: 30, med2: 70, med3: 140, med4: 260 })
|
|
59
|
+
|
|
60
|
+
function levelOnCurve(xp, floorOf, cap) {
|
|
61
|
+
let level = 1
|
|
62
|
+
while (level < cap && xp >= floorOf(level + 1)) level += 1
|
|
63
|
+
return level
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Carry an old xp total onto the v9 growth curve: same level, same fraction of
|
|
68
|
+
* the way to the next one. Lv8 at 40% stays Lv8 at 40%.
|
|
69
|
+
*/
|
|
70
|
+
function growthFromV8Xp(xp) {
|
|
71
|
+
const value = Number.isFinite(xp) && xp > 0 ? xp : 0
|
|
72
|
+
const level = levelOnCurve(value, v8LevelFloor, 999)
|
|
73
|
+
if (level >= V9_MAX_LEVEL) return v9LevelFloor(V9_MAX_LEVEL)
|
|
74
|
+
const floor = v8LevelFloor(level)
|
|
75
|
+
const fraction = Math.min(1, (value - floor) / Math.max(1, v8LevelFloor(level + 1) - floor))
|
|
76
|
+
const nextFloor = v9LevelFloor(level + 1)
|
|
77
|
+
return Math.round(v9LevelFloor(level) + fraction * (nextFloor - v9LevelFloor(level)))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The v9 body for a level: 幼年 from Lv1, 青年 from Lv10, 成年 from Lv40. */
|
|
81
|
+
const v9StageFor = level => (level >= 40 ? 'middle' : level >= 10 ? 'young' : 'piglet')
|
|
82
|
+
|
|
83
|
+
/** @type {ReadonlyArray<Upgrade>} */
|
|
84
|
+
export const UPGRADES = Object.freeze([
|
|
85
|
+
Object.freeze({
|
|
86
|
+
to: 8,
|
|
87
|
+
why: 'hatched 不再由 xp 推断:没拆的纸盒也会吃到经验,重启后会被误当成已孵化',
|
|
88
|
+
up(raw) {
|
|
89
|
+
// Only saves from before the flag existed get the old inference; a save
|
|
90
|
+
// that recorded `hatched: false` meant it, whatever its xp says.
|
|
91
|
+
if (typeof raw.hatched === 'boolean') return { ...raw, version: 8 }
|
|
92
|
+
return { ...raw, version: 8, hatched: typeof raw.xp === 'number' && raw.xp > 0 }
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
Object.freeze({
|
|
96
|
+
to: 9,
|
|
97
|
+
why: 'B2 成长:xp 改为成长值、按新曲线折算(等级和进度不变);形态改由等级决定、去掉老年;补性别',
|
|
98
|
+
up(raw) {
|
|
99
|
+
const next = { ...raw, version: 9, xp: growthFromV8Xp(raw.xp) }
|
|
100
|
+
if (next.hatched === true && next.dead !== true) {
|
|
101
|
+
next.stage = v9StageFor(levelOnCurve(next.xp, v9LevelFloor, V9_MAX_LEVEL))
|
|
102
|
+
} else if (next.stage === 'elder') {
|
|
103
|
+
next.stage = 'middle'
|
|
104
|
+
}
|
|
105
|
+
if (next.hatched === true && next.sex !== 'boy' && next.sex !== 'girl') {
|
|
106
|
+
next.sex = roll(next) < 0.5 ? 'boy' : 'girl'
|
|
107
|
+
}
|
|
108
|
+
return next
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
Object.freeze({
|
|
112
|
+
to: 10,
|
|
113
|
+
why: 'B3 疾病:四种通用药下架、按新同级药价退金币;病的必病计时 riskMinutes 换成连续外出计数',
|
|
114
|
+
up(raw) {
|
|
115
|
+
const next = { ...raw, version: 10 }
|
|
116
|
+
const bag = raw.inventory !== null && typeof raw.inventory === 'object' ? { ...raw.inventory } : {}
|
|
117
|
+
let refund = 0
|
|
118
|
+
for (const [key, price] of Object.entries(V10_REFUND)) {
|
|
119
|
+
const count = Number.isFinite(bag[key]) ? Math.max(0, Math.floor(bag[key])) : 0
|
|
120
|
+
refund += count * price
|
|
121
|
+
delete bag[key]
|
|
122
|
+
}
|
|
123
|
+
next.inventory = bag
|
|
124
|
+
next.coins = (Number.isFinite(raw.coins) ? raw.coins : 0) + refund
|
|
125
|
+
delete next.riskMinutes
|
|
126
|
+
next.outingStreak = 0
|
|
127
|
+
next.restMinutes = 0
|
|
128
|
+
return next
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
Object.freeze({
|
|
132
|
+
to: 11,
|
|
133
|
+
why: 'B4 学习→职业:23 门课按对照表并进九门课的课时;旧职业表里在打的工按旧报酬结算',
|
|
134
|
+
up(raw) {
|
|
135
|
+
const next = { ...raw, version: 11 }
|
|
136
|
+
const lessons = raw.lessons !== null && typeof raw.lessons === 'object' ? { ...raw.lessons } : {}
|
|
137
|
+
const courses = raw.courses !== null && typeof raw.courses === 'object' ? raw.courses : {}
|
|
138
|
+
for (const [oldKey, count] of Object.entries(courses)) {
|
|
139
|
+
const into = V11_SUBJECT_OF[oldKey]
|
|
140
|
+
if (into === undefined || !Number.isFinite(count) || count <= 0) continue
|
|
141
|
+
lessons[into] = (lessons[into] ?? 0) + Math.floor(count)
|
|
142
|
+
}
|
|
143
|
+
next.lessons = lessons
|
|
144
|
+
delete next.courses
|
|
145
|
+
delete next.coursesByStage
|
|
146
|
+
delete next.lessonsByStage
|
|
147
|
+
// v3 saves kept a shift under `work: { job }`; migrate() lifts it later.
|
|
148
|
+
const legacyWork = raw.work !== null && typeof raw.work === 'object' ? { kind: 'work', key: raw.work.job, ...raw.work } : null
|
|
149
|
+
const activity = raw.activity ?? legacyWork
|
|
150
|
+
if (activity !== null && typeof activity === 'object') {
|
|
151
|
+
if (activity.kind === 'study') {
|
|
152
|
+
// Same lesson, new subject name; the stage is decided afresh.
|
|
153
|
+
const into = V11_SUBJECT_OF[activity.key] ?? activity.key
|
|
154
|
+
next.activity = { ...activity, key: into, stage: undefined }
|
|
155
|
+
} else if (activity.kind === 'work' && V11_OLD_JOB_PAY[activity.key] !== undefined) {
|
|
156
|
+
const minutes = Math.max(0, Math.round((activity.endsAt - activity.startedAt) / 60000))
|
|
157
|
+
next.activity = { ...activity, legacyCoins: V11_OLD_JOB_PAY[activity.key], minutes }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return next
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
Object.freeze({
|
|
164
|
+
to: 12,
|
|
165
|
+
why: '加冕形态单独存成 form(普通猪是 null);PR #2 早期版本存的 finalForm 改名过来',
|
|
166
|
+
up(raw) {
|
|
167
|
+
const next = { ...raw, version: 12 }
|
|
168
|
+
next.form = typeof raw.form === 'string' ? raw.form : typeof raw.finalForm === 'string' ? raw.finalForm : null
|
|
169
|
+
delete next.finalForm
|
|
170
|
+
return next
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
])
|
|
174
|
+
|
|
175
|
+
/** The oldest version the step table starts from; earlier saves are cleaned by migrate() alone. */
|
|
176
|
+
export const FIRST_UPGRADE_FROM = 7
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Run every upgrade the save has not had yet.
|
|
180
|
+
* @param {Record<string, unknown>} raw
|
|
181
|
+
* @param {number} nowMs
|
|
182
|
+
* @returns {Record<string, unknown>}
|
|
183
|
+
*/
|
|
184
|
+
export function applyUpgrades(raw, nowMs) {
|
|
185
|
+
const onDisk = typeof raw.version === 'number' && Number.isFinite(raw.version) ? raw.version : 0
|
|
186
|
+
// Saves older than the table were shaped by migrate()'s field cleaning; they
|
|
187
|
+
// enter the table as if they were the version it starts from.
|
|
188
|
+
let current = onDisk < FIRST_UPGRADE_FROM ? { ...raw, version: FIRST_UPGRADE_FROM } : raw
|
|
189
|
+
for (const upgrade of UPGRADES) {
|
|
190
|
+
if (/** @type {number} */ (current.version) < upgrade.to) current = upgrade.up(current, nowMs)
|
|
191
|
+
}
|
|
192
|
+
return current
|
|
193
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 派生视图(属性/心情/血条)。
|
|
4
|
+
*
|
|
5
|
+
* 纯函数领域逻辑:时间由 nowMs 传入,不读写文件、不碰 DOM(见 docs/CONVENTIONS.md)。
|
|
6
|
+
* @module dsh-piggy/core/views
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { GRAVE, JOBS, LIFE_STAGES, MAX, REVIVE_ITEM, SCHOOL_STAGES, SHOP, SLEEPY_AFTER_MINUTES, SOUL, SOUL_AFTER_DAYS, SUBJECTS, THRESHOLDS, TRAITS, TRAIT_ORDER, TRIPS } from '../data.js'
|
|
10
|
+
import { AWAY_MOODS, DIET } from './constants.js'
|
|
11
|
+
import { clamp, clamp100 } from './effects.js'
|
|
12
|
+
import { currentIllness, decay } from './settlement.js'
|
|
13
|
+
|
|
14
|
+
/** Trait totals, always including every trait. */
|
|
15
|
+
export function traitView(state) {
|
|
16
|
+
const out = {}
|
|
17
|
+
for (const key of TRAIT_ORDER) out[key] = state.traits?.[key] ?? 0
|
|
18
|
+
return out
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function mood(state, nowMs) {
|
|
22
|
+
decay(state, nowMs)
|
|
23
|
+
if (state.dead) return { key: 'dead', emoji: '💀', label: '已经走了' }
|
|
24
|
+
if (state.illness !== null) {
|
|
25
|
+
const ill = currentIllness(state)
|
|
26
|
+
return { key: 'sick', emoji: '🤒', label: ill === null ? '生病了' : `得了${ill.name}` }
|
|
27
|
+
}
|
|
28
|
+
if (state.activity !== null) {
|
|
29
|
+
const base = AWAY_MOODS[state.activity.kind] ?? AWAY_MOODS.work
|
|
30
|
+
return { ...base, emoji: state.activity.emoji || base.emoji }
|
|
31
|
+
}
|
|
32
|
+
if (state.satiety < THRESHOLDS.hungry) return { key: 'hungry', emoji: '🍎', label: '饿了' }
|
|
33
|
+
if (state.cleanliness < THRESHOLDS.dirty) return { key: 'dirty', emoji: '🫧', label: '该洗澡了' }
|
|
34
|
+
if (nowMs - state.lastActiveAt > SLEEPY_AFTER_MINUTES * 60000) return { key: 'sleepy', emoji: '💤', label: '睡着了' }
|
|
35
|
+
if (state.happiness >= 75) return { key: 'happy', emoji: '❤️', label: '很开心' }
|
|
36
|
+
if (state.happiness < THRESHOLDS.lonely) return { key: 'lonely', emoji: '🥺', label: '有点孤单' }
|
|
37
|
+
return { key: 'fine', emoji: '😊', label: '还不错' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const healthPercent = state => Math.round((clamp(state.health, 0, MAX.health) / MAX.health) * 100)
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Display helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export const formatWeight = weightG => `${(weightG / 1000).toFixed(1)} kg`
|
|
47
|
+
|
|
48
|
+
export function bar(value, width = 10) {
|
|
49
|
+
const filled = Math.round((clamp100(value) / 100) * width)
|
|
50
|
+
return `${'▓'.repeat(filled)}${'░'.repeat(width - filled)}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { JOBS, SHOP, MAX, THRESHOLDS, REVIVE_ITEM, SUBJECTS, SCHOOL_STAGES, TRIPS, TRAITS, TRAIT_ORDER }
|
|
54
|
+
export { LIFE_STAGES, GRAVE, SOUL, SOUL_AFTER_DAYS }
|
|
55
|
+
export { DIET }
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 打工。
|
|
4
|
+
*
|
|
5
|
+
* 纯函数领域逻辑:时间由 nowMs 传入,不读写文件、不碰 DOM(见 docs/CONVENTIONS.md)。
|
|
6
|
+
* @module dsh-piggy/core/work
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TRAITS, jobByKey, jobRequirement, traitBonus } from '../data.js'
|
|
10
|
+
import { begin } from './activity.js'
|
|
11
|
+
import { levelFor } from './clock.js'
|
|
12
|
+
import { TOO_WEAK_HEALTH } from './constants.js'
|
|
13
|
+
import { remember } from './effects.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a job's gate looks at: level, lessons per subject, 兴趣课 counts (B4).
|
|
17
|
+
* @param {object} state
|
|
18
|
+
*/
|
|
19
|
+
export function jobFacts(state) {
|
|
20
|
+
return { level: levelFor(state.xp), lessons: state.lessons ?? {}, interests: state.interests ?? {} }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function startWork(state, jobKey, nowMs) {
|
|
24
|
+
const job = jobByKey(jobKey)
|
|
25
|
+
if (job === null) return { ok: false, reason: 'unknown' }
|
|
26
|
+
if (state.hatched !== true) return { ok: false, reason: 'box' }
|
|
27
|
+
if (state.dead) return { ok: false, reason: 'dead' }
|
|
28
|
+
if (state.activity !== null) return { ok: false, reason: 'away' }
|
|
29
|
+
if (state.health <= TOO_WEAK_HEALTH) return { ok: false, reason: 'weak' }
|
|
30
|
+
// The gate is checked before the pig walks out: an unqualified job is refused
|
|
31
|
+
// with everything it is short on (level, lessons, certificate).
|
|
32
|
+
const gate = jobRequirement(job, jobFacts(state))
|
|
33
|
+
if (gate !== null && !gate.ok) {
|
|
34
|
+
return { ok: false, reason: 'underqualified', missing: gate.missing, job: job.key }
|
|
35
|
+
}
|
|
36
|
+
if (state.satiety < 15) return { ok: false, reason: 'hungry' }
|
|
37
|
+
// The pig's trait shortens the shift; the pay bonus is applied on the way out.
|
|
38
|
+
const points = state.traits?.[job.trait] ?? 0
|
|
39
|
+
const bonus = traitBonus(job.trait, points)
|
|
40
|
+
const minutes = Math.max(1, Math.round(job.minutes * bonus.minutes))
|
|
41
|
+
const result = begin(state, {
|
|
42
|
+
kind: 'work', key: job.key, label: job.label, emoji: job.emoji, minutes, cost: 0,
|
|
43
|
+
trait: job.trait ?? null,
|
|
44
|
+
}, nowMs)
|
|
45
|
+
if (result.ok) {
|
|
46
|
+
const saved = job.minutes - minutes
|
|
47
|
+
remember(state, `${job.emoji} 出门${job.label}去了${saved > 0 ? `(${TRAITS[job.trait].label} ${points},省了 ${saved} 分钟)` : ''}`, nowMs)
|
|
48
|
+
}
|
|
49
|
+
return result
|
|
50
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* dsh-piggy · core — the pure pig model.
|
|
4
|
+
*
|
|
5
|
+
* No IO, no ctx, no clock of its own: every function takes the current time as
|
|
6
|
+
* a parameter, so the whole game model is testable in isolation. Nothing here
|
|
7
|
+
* schedules a timer — shifts, courses, trips, illness progression and decay are
|
|
8
|
+
* all resolved lazily from timestamps, which is why the pig survives restarts
|
|
9
|
+
* and why the tests never have to wait.
|
|
10
|
+
*
|
|
11
|
+
* Three ways the pig changes:
|
|
12
|
+
* - `feed()` digests a *passive* harness event (a turn ended, a tool ran)
|
|
13
|
+
* - `act()` applies a *deliberate* care action (feed it, bathe it, play)
|
|
14
|
+
* - `buy()` / `useItem()` / `start*()` drive the game loop
|
|
15
|
+
*
|
|
16
|
+
* Time away from the desk is one concept, not three: work, study and travel all
|
|
17
|
+
* become a single `activity` record, so "is the pig home?" has exactly one
|
|
18
|
+
* answer and `decay()` has exactly one place to settle it.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-piggy/core
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
AWAY_MULTIPLIER,
|
|
25
|
+
CARE_KIND,
|
|
26
|
+
DEFAULT_TOY,
|
|
27
|
+
GRAVE,
|
|
28
|
+
LIFE_STAGES,
|
|
29
|
+
SOUL,
|
|
30
|
+
SOUL_AFTER_DAYS,
|
|
31
|
+
ILLNESS_CHAINS,
|
|
32
|
+
DAYS_PER_MONTH,
|
|
33
|
+
DEFAULT_TIME_SCALE,
|
|
34
|
+
LEVEL_TITLES,
|
|
35
|
+
illnessStageMs,
|
|
36
|
+
xpForLevel,
|
|
37
|
+
traitBonus,
|
|
38
|
+
SELF_HEAL_CHANCE,
|
|
39
|
+
SICK_AWAY_MULTIPLIER,
|
|
40
|
+
SICK_PAY_MULTIPLIER,
|
|
41
|
+
JOBS,
|
|
42
|
+
KIND_ORDER,
|
|
43
|
+
MAX,
|
|
44
|
+
REVIVE_ITEM,
|
|
45
|
+
SCHOOL_STAGES,
|
|
46
|
+
SHOP,
|
|
47
|
+
SLEEPY_AFTER_MINUTES,
|
|
48
|
+
SOUVENIR_RARITY,
|
|
49
|
+
STAGE_HEALTH,
|
|
50
|
+
SUBJECTS,
|
|
51
|
+
THRESHOLDS,
|
|
52
|
+
TRAITS,
|
|
53
|
+
TRAIT_ORDER,
|
|
54
|
+
TRIPS,
|
|
55
|
+
illnessAt,
|
|
56
|
+
interestByKey,
|
|
57
|
+
INTERESTS,
|
|
58
|
+
dressSlotByKey,
|
|
59
|
+
itemByKey,
|
|
60
|
+
jobByKey,
|
|
61
|
+
jobRequirement,
|
|
62
|
+
careItems,
|
|
63
|
+
nextIllness,
|
|
64
|
+
rarityByKey,
|
|
65
|
+
schoolStageByKey,
|
|
66
|
+
subjectByKey,
|
|
67
|
+
tripByKey,
|
|
68
|
+
} from './data.js'
|
|
69
|
+
|
|
70
|
+
export { ACTIONS, ACTION_ORDER, STATE_VERSION } from './core/constants.js'
|
|
71
|
+
export { drainPending } from './core/effects.js'
|
|
72
|
+
export { ageDays, ageMonths, dayKeyFor, daysToNextStage, hasSoul, levelFor, levelProgress, levelTitle, lifeStageFor, nextLifeStage } from './core/clock.js'
|
|
73
|
+
export { careFactor, grow, growFromRealWork, outingGrowth } from './core/growth.js'
|
|
74
|
+
export { hatch, hatchEgg, layEgg } from './core/egg.js'
|
|
75
|
+
export { adopt, ageFromNow, applyDevPatch, inherit, rename, reset, revive, setTimeScale } from './core/state.js'
|
|
76
|
+
export { migrate } from './core/migrate.js'
|
|
77
|
+
export { assignPersonality, ensureProfile, profileView, setCatchphrase, setMotto, zodiacFor } from './core/profile.js'
|
|
78
|
+
export { crown, formStageView, formsView } from './core/evolution.js'
|
|
79
|
+
export { chat, pickLine, replyToLine, say, setOwnerName, setQuiet } from './core/lines.js'
|
|
80
|
+
export { canSignIn, dailyView, emptyDaily, ensureDaily, giftsWaiting, grantReward, openGift, pickGift, recordOnline, signIn } from './core/daily.js'
|
|
81
|
+
export { composeDiary, diaryView, emptyDiary, ensureDiary, noteToday, writeDiaryIfNewDay } from './core/diary.js'
|
|
82
|
+
export { currentIllness, decay } from './core/settlement.js'
|
|
83
|
+
export { doctorFee, onsetRisks, seeDoctor } from './core/illness.js'
|
|
84
|
+
export { activitySecondsLeft, awayBlockedReason, callOffActivity, callOffWork, canStartActivity, canWork, workSecondsLeft } from './core/activity.js'
|
|
85
|
+
export { act, actionCooldownSeconds, actionReady, canFeed, careOptions, careView, feed, feedCooldownSeconds } from './core/care.js'
|
|
86
|
+
export { buy, canAfford, dressView, grantAll, inventoryView, takeOff, useItem, wearItem } from './core/inventory.js'
|
|
87
|
+
export { courseView, startStudy, studyView } from './core/school.js'
|
|
88
|
+
export { jobFacts } from './core/work.js'
|
|
89
|
+
export { interestView, startInterest } from './core/interests.js'
|
|
90
|
+
export { startWork } from './core/work.js'
|
|
91
|
+
export { sellSouvenir, startTrip } from './core/travel.js'
|
|
92
|
+
export { bar, formatWeight, healthPercent, mood, traitView } from './core/views.js'
|
|
93
|
+
export { JOBS, MAX, REVIVE_ITEM, SCHOOL_STAGES, SHOP, SUBJECTS, THRESHOLDS, TRAITS, TRAIT_ORDER, TRIPS } from './data.js'
|
|
94
|
+
export { GRAVE, LIFE_STAGES, MAX_LEVEL, SOUL, SOUL_AFTER_DAYS } from './data.js'
|
|
95
|
+
export { DIET } from './core/constants.js'
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 日常玩法:签到 12 天、在线礼包、宠物日记的数值与文案。
|
|
4
|
+
*
|
|
5
|
+
* 数值以 docs/tasks/numbers/B5-daily.md(用户 2026-10-01 确认)为准 —— 不要随手改,
|
|
6
|
+
* 觉得不合理就写进任务卡等人拍板。零逻辑、零 IO(见 docs/CONVENTIONS.md)。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-piggy/data/daily
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 一签的奖励:金币和/或物品。物品按 `key` 进背包,key 对不上货架也照发
|
|
13
|
+
* (B3 的药可能还没上架)。
|
|
14
|
+
* @typedef {object} DailyReward
|
|
15
|
+
* @property {number} coins
|
|
16
|
+
* @property {ReadonlyArray<{ key: string, count: number }>} items
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 12 天签到礼包,价值由低到高:第 8 天是还魂丹,第 12 天是豪华大餐。
|
|
21
|
+
* 领完第 12 天回到第 1 天;断签不清零。
|
|
22
|
+
* @type {ReadonlyArray<DailyReward>}
|
|
23
|
+
*/
|
|
24
|
+
export const SIGN_IN_REWARDS = Object.freeze([
|
|
25
|
+
Object.freeze({ coins: 0, items: Object.freeze([Object.freeze({ key: 'apple', count: 3 })]) }),
|
|
26
|
+
Object.freeze({ coins: 50, items: Object.freeze([]) }),
|
|
27
|
+
Object.freeze({ coins: 0, items: Object.freeze([
|
|
28
|
+
Object.freeze({ key: 'soap', count: 3 }),
|
|
29
|
+
Object.freeze({ key: 'bread', count: 2 }),
|
|
30
|
+
]) }),
|
|
31
|
+
Object.freeze({ coins: 100, items: Object.freeze([]) }),
|
|
32
|
+
Object.freeze({ coins: 0, items: Object.freeze([
|
|
33
|
+
Object.freeze({ key: 'plush', count: 1 }),
|
|
34
|
+
Object.freeze({ key: 'rice', count: 2 }),
|
|
35
|
+
]) }),
|
|
36
|
+
Object.freeze({ coins: 0, items: Object.freeze([
|
|
37
|
+
Object.freeze({ key: 'banlangen', count: 1 }),
|
|
38
|
+
Object.freeze({ key: 'xiaoshipian', count: 1 }),
|
|
39
|
+
Object.freeze({ key: 'pipa-syrup', count: 1 }),
|
|
40
|
+
]) }),
|
|
41
|
+
Object.freeze({ coins: 200, items: Object.freeze([]) }),
|
|
42
|
+
Object.freeze({ coins: 0, items: Object.freeze([Object.freeze({ key: 'soul', count: 1 })]) }),
|
|
43
|
+
Object.freeze({ coins: 0, items: Object.freeze([
|
|
44
|
+
Object.freeze({ key: 'bubble', count: 3 }),
|
|
45
|
+
Object.freeze({ key: 'skewer', count: 2 }),
|
|
46
|
+
]) }),
|
|
47
|
+
Object.freeze({ coins: 300, items: Object.freeze([]) }),
|
|
48
|
+
Object.freeze({ coins: 0, items: Object.freeze([Object.freeze({ key: 'baicaodan', count: 1 })]) }),
|
|
49
|
+
Object.freeze({ coins: 500, items: Object.freeze([
|
|
50
|
+
Object.freeze({ key: 'feast', count: 2 }),
|
|
51
|
+
Object.freeze({ key: 'deadsea', count: 2 }),
|
|
52
|
+
Object.freeze({ key: 'carousel', count: 1 }),
|
|
53
|
+
]) }),
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
/** 签到一轮多少天。 */
|
|
57
|
+
export const SIGN_IN_CYCLE = SIGN_IN_REWARDS.length
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 在线礼包:只算真实时间,不受调试页的时间倍率影响。
|
|
61
|
+
* 「在线」= 面板在轮询,两次轮询间隔不超过 `pollGapMaxMs`。
|
|
62
|
+
*/
|
|
63
|
+
export const ONLINE_GIFT = Object.freeze({
|
|
64
|
+
/** 每在线满这么久给一个。 */
|
|
65
|
+
perGiftMs: 60 * 60 * 1000,
|
|
66
|
+
/** 每天最多几个(06:00 刷新)。 */
|
|
67
|
+
perDay: 8,
|
|
68
|
+
/** 没领的最多攒几个,攒满就不再给。 */
|
|
69
|
+
unclaimedMax: 3,
|
|
70
|
+
/** 两次轮询间隔超过这个值,中间那段不算在线。 */
|
|
71
|
+
pollGapMaxMs: 30 * 1000,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 礼包内容概率表:每个礼包从下表抽一项。
|
|
76
|
+
* `kind` + `maxPrice` 从货架上现取(B3/B4 上架新物品后自动进池子),
|
|
77
|
+
* `keys` 是点名要的固定几样。
|
|
78
|
+
* @type {ReadonlyArray<{ chance: number, kind?: string, kinds?: ReadonlyArray<string>, maxPrice?: number,
|
|
79
|
+
* coins?: ReadonlyArray<number>, keys?: ReadonlyArray<string> }>}
|
|
80
|
+
*/
|
|
81
|
+
export const GIFT_TABLE = Object.freeze([
|
|
82
|
+
Object.freeze({ chance: 0.40, kind: 'food', maxPrice: 40 }),
|
|
83
|
+
Object.freeze({ chance: 0.25, kinds: Object.freeze(['bath', 'toy']), maxPrice: 60 }),
|
|
84
|
+
Object.freeze({ chance: 0.20, coins: Object.freeze([30, 80]) }),
|
|
85
|
+
Object.freeze({ chance: 0.10, kind: 'medicine', maxPrice: 30 }),
|
|
86
|
+
Object.freeze({ chance: 0.04, keys: Object.freeze(['feast', 'carousel', 'bubbles']) }),
|
|
87
|
+
Object.freeze({ chance: 0.01, keys: Object.freeze(['baicaodan']) }),
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
/** 日记最多留几篇,更早的删掉。 */
|
|
91
|
+
export const DIARY_MAX = 60
|
|
92
|
+
|
|
93
|
+
/** 一天里最多数几句话(模板拼出来的日记不会比它长)。 */
|
|
94
|
+
export const DIARY_MAX_SENTENCES = 5
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 日记模板:按当天记下的东西挑句子拼,最多 `DIARY_MAX_SENTENCES` 句。
|
|
98
|
+
*
|
|
99
|
+
* `[主人]` 会换成主人称呼(和 data/lines.js 用同一个占位符)。顺序就是优先级:
|
|
100
|
+
* 先说吃,最后说陪主人干活。句子里的数字来自当天的计数(见 DIARY_COUNT_LABELS)。
|
|
101
|
+
* @type {ReadonlyArray<{ key: string, said: (counts: Record<string, number>) => string }>}
|
|
102
|
+
*/
|
|
103
|
+
export const DIARY_LINES = Object.freeze([
|
|
104
|
+
Object.freeze({ key: 'feed', said: c => `今天吃了 ${c.feed} 顿,[主人]喂的,好饱。` }),
|
|
105
|
+
Object.freeze({ key: 'bathe', said: c => `洗了 ${c.bathe} 次澡,身上香香的。` }),
|
|
106
|
+
Object.freeze({ key: 'pet', said: c => `[主人]摸了我 ${c.pet} 次,我呼噜呼噜了一下午。` }),
|
|
107
|
+
Object.freeze({ key: 'work', said: c => `出门打工 ${c.work} 趟${c.coinsEarned > 0 ? `,赚了 ${c.coinsEarned} 金币!` : ',累是累了点。'}` }),
|
|
108
|
+
Object.freeze({ key: 'study', said: c => `上了 ${c.study} 节课,脑袋里又装进去一点东西。` }),
|
|
109
|
+
Object.freeze({ key: 'graduate', said: c => `今天毕业啦,一共念完 ${c.graduate} 个学段!` }),
|
|
110
|
+
Object.freeze({ key: 'trip', said: c => `出去旅行 ${c.trip} 次,带回来 ${c.souvenirs ?? 0} 件纪念品。` }),
|
|
111
|
+
Object.freeze({ key: 'illness', said: () => '生病了,难受得不想动……' }),
|
|
112
|
+
Object.freeze({ key: 'cure', said: c => `吃了 ${c.cure} 次药,终于好了。` }),
|
|
113
|
+
Object.freeze({ key: 'wrongMedicine', said: () => '吃错药了,肚子更难受了。' }),
|
|
114
|
+
Object.freeze({ key: 'levelUp', said: c => `升级 ${c.levelUp} 次,[主人]看见了吗?` }),
|
|
115
|
+
Object.freeze({ key: 'stage', said: () => '今天长大了,样子变了一点点。' }),
|
|
116
|
+
Object.freeze({ key: 'turn', said: c => `[主人]今天敲了 ${c.turn} 轮代码,我在旁边看着。` }),
|
|
117
|
+
Object.freeze({ key: 'tool', said: c => `${c.tool} 个工具跑完了,我也跟着长了点。` }),
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
/** 什么都没发生的那一天。 */
|
|
121
|
+
export const DIARY_EMPTY_LINE = '今天[主人]没来,我睡了一整天。'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 打工、上课回来带的东西 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
|
|
4
|
+
*
|
|
5
|
+
* 数字来自 docs/tasks/numbers/B4-study-jobs.md §4(用户 2026-10-01 确认)。
|
|
6
|
+
* 参考 QQ 宠物:打工回来 75% 带 1 件、25% 带 2 件;这里给得少一点,因为钱多。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-piggy/data/drops
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** 打工回来:带 2 件 / 带 1 件的概率(其余什么都不带)。 */
|
|
12
|
+
export const WORK_DROP = Object.freeze({ two: 0.15, one: 0.5 })
|
|
13
|
+
|
|
14
|
+
/** 上课回来:带 1 件的概率。 */
|
|
15
|
+
export const STUDY_DROP_CHANCE = 0.3
|
|
16
|
+
|
|
17
|
+
/** 毕业(某门课第 9 / 20 / 40 / 95 节):必得几件。 */
|
|
18
|
+
export const GRADUATION_DROP_COUNT = 3
|
|
19
|
+
|
|
20
|
+
/** 抽到哪一类:食物 / 日用品和玩具 / 1 级药。 */
|
|
21
|
+
export const DROP_SHELVES = Object.freeze([
|
|
22
|
+
Object.freeze({ kinds: Object.freeze(['food']), weight: 0.45 }),
|
|
23
|
+
Object.freeze({ kinds: Object.freeze(['bath', 'toy']), weight: 0.4 }),
|
|
24
|
+
Object.freeze({ kinds: Object.freeze(['medicine']), weight: 0.15, tier: 1 }),
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
/** 打工掉落单件价值上限 = 这班报酬 × 0.3。 */
|
|
28
|
+
export const WORK_DROP_VALUE_RATIO = 0.3
|
|
29
|
+
|
|
30
|
+
/** 上课掉落单件价值上限。 */
|
|
31
|
+
export const STUDY_DROP_MAX_PRICE = 40
|
|
32
|
+
|
|
33
|
+
/** 毕业礼从「高一档」货架抽:价格在这个区间的食物、日用品、玩具。 */
|
|
34
|
+
export const GRADUATION_DROP_PRICE = Object.freeze({ min: 30, max: 150 })
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 加冕:长成之后可以选的形态(猪猪王是第一种,立绘来自 PR #2,作者 1nuoiscute)。
|
|
4
|
+
*
|
|
5
|
+
* 等级成长(data/life.js)照旧;形态只是在某个阶段上换一身样子,不加收益。
|
|
6
|
+
* 以后加新形态:在 FORMS 里加一行,在 assets/ 放 `<art>.svg`(有动作立绘的再放
|
|
7
|
+
* `<art>-eat/bathe/play/pet/relaxed/work/study/trip.svg` 并写 actionArt: true)。
|
|
8
|
+
* @module dsh-piggy/data/evolution
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} PigForm
|
|
13
|
+
* @property {string} key 存档里 state.form 的值
|
|
14
|
+
* @property {string} label
|
|
15
|
+
* @property {string} emoji
|
|
16
|
+
* @property {string} art assets/ 里的立绘名
|
|
17
|
+
* @property {boolean} actionArt 有没有喂食、打工等各个动作的立绘
|
|
18
|
+
* @property {string} line
|
|
19
|
+
* @property {string} stage 到了哪个阶段(data/life.js 的 key)才能加冕
|
|
20
|
+
* @property {Readonly<Record<string, number>>} requires 三维各要多少,`jobs` 是本代打完几份工
|
|
21
|
+
* @property {readonly string[]} hides 这身样子盖住的装扮位置
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @type {readonly Readonly<PigForm>[]} */
|
|
25
|
+
export const FORMS = Object.freeze([
|
|
26
|
+
Object.freeze({
|
|
27
|
+
key: 'king', label: '猪猪王', emoji: '👑', art: 'pig-king', actionArt: true,
|
|
28
|
+
line: '阅历与本事都攒够了,戴上自己的王冠。',
|
|
29
|
+
stage: 'middle',
|
|
30
|
+
requires: Object.freeze({ intel: 20, charm: 20, strong: 20, jobs: 10 }),
|
|
31
|
+
hides: Object.freeze(['head', 'back']),
|
|
32
|
+
}),
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
/** The form `/pig crown` picks when none is named. */
|
|
36
|
+
export const DEFAULT_FORM = 'king'
|
|
37
|
+
|
|
38
|
+
/** @returns {Readonly<PigForm> | null} */
|
|
39
|
+
export function formByKey(key) {
|
|
40
|
+
return FORMS.find(form => form.key === key) ?? null
|
|
41
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 成长值与等级 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
|
|
4
|
+
*
|
|
5
|
+
* 数字全部来自 docs/tasks/numbers/B2-growth.md(用户 2026-10-01 确认)。
|
|
6
|
+
* 参考 QQ 宠物 GrowUp.js:成长值随时间自己涨,心情、健康、饿、脏会拖慢它 ——
|
|
7
|
+
* 照顾得好就长得快。去掉的是原版 400 级的肝和粉钻加速。
|
|
8
|
+
*
|
|
9
|
+
* 存档里的 `xp` 字段就是成长值(沿用旧字段名,免得全库改名)。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-piggy/data/growth
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 满级。满级后成长值照样累计,只是不再升级。 */
|
|
15
|
+
export const MAX_LEVEL = 60
|
|
16
|
+
|
|
17
|
+
/** 等级曲线系数:到达 L 级需要的累计成长值 = K × L²。 */
|
|
18
|
+
export const GROWTH_CURVE_K = 122
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Growth needed to *reach* a level: 122 × L². Lv10 ≈ 5 days, Lv40 ≈ 2.7
|
|
22
|
+
* months, Lv60 ≈ 6 months at full care and the base rate alone.
|
|
23
|
+
* @param {number} level
|
|
24
|
+
*/
|
|
25
|
+
export const xpForLevel = level => (level <= 1 ? 0 : GROWTH_CURVE_K * level * level)
|
|
26
|
+
|
|
27
|
+
/** 猪时间每小时的基础成长值(关着 DSH 也长)。 */
|
|
28
|
+
export const GROWTH_PER_HOUR = 100
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 心情档位对成长速度的系数,按从高到低找第一个满足 `min` 的档。
|
|
32
|
+
* 原版按 1000 分制扣 20/70/100/140/180(基础 260),这里折成百分比取整。
|
|
33
|
+
*/
|
|
34
|
+
export const MOOD_GROWTH_FACTORS = Object.freeze([
|
|
35
|
+
Object.freeze({ min: 70, factor: 1 }),
|
|
36
|
+
Object.freeze({ min: 50, factor: 0.9 }),
|
|
37
|
+
Object.freeze({ min: 30, factor: 0.7 }),
|
|
38
|
+
Object.freeze({ min: 0, factor: 0.5 }),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
/** 饿(饱食 < THRESHOLDS.hungry)或脏(清洁 < THRESHOLDS.dirty)时的系数,两者只算一次。 */
|
|
42
|
+
export const NEGLECT_GROWTH_FACTOR = 0.7
|
|
43
|
+
|
|
44
|
+
/** 生病时每少 1 格健康,成长速度少 15%。 */
|
|
45
|
+
export const SICK_GROWTH_PENALTY_PER_HEALTH = 0.15
|
|
46
|
+
|
|
47
|
+
/** 各项系数相乘后的下限:再惨也不会完全不长。 */
|
|
48
|
+
export const MIN_GROWTH_FACTOR = 0.1
|
|
49
|
+
|
|
50
|
+
/** 陪主人真实干活给的成长值(每轮对话 / 每次工具调用)。 */
|
|
51
|
+
export const DSH_GROWTH = Object.freeze({ turn: 3, tool: 1 })
|
|
52
|
+
|
|
53
|
+
/** 真实干活加成每天的上限(约等于多养 3 小时)。 */
|
|
54
|
+
export const DSH_GROWTH_DAILY_CAP = 300
|
|
55
|
+
|
|
56
|
+
/** 打工、旅行:每小时 +25 成长值(按活动时长折算)。 */
|
|
57
|
+
export const WORK_GROWTH_PER_HOUR = 25
|
|
58
|
+
|
|
59
|
+
/** 每上一节课 +40(兴趣课同)。 */
|
|
60
|
+
export const STUDY_GROWTH_PER_LESSON = 40
|
|
61
|
+
|
|
62
|
+
/** 某门课毕业(第 9/20/40/95 节)额外 +500(B4 接上)。 */
|
|
63
|
+
export const GRADUATION_GROWTH = 500
|
|
64
|
+
|
|
65
|
+
/** 一天从几点算起:06:00 以前还算前一天(签到、在线礼包、日记、每日上限共用)。 */
|
|
66
|
+
export const DAY_STARTS_AT_HOUR = 6
|
|
67
|
+
|
|
68
|
+
/** 性别:拆纸盒时 50/50 定下来。 */
|
|
69
|
+
export const SEXES = Object.freeze({
|
|
70
|
+
boy: Object.freeze({ key: 'boy', label: '男孩', symbol: '♂' }),
|
|
71
|
+
girl: Object.freeze({ key: 'girl', label: '女孩', symbol: '♀' }),
|
|
72
|
+
})
|