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
@@ -0,0 +1,23 @@
1
+ // @ts-check
2
+ /**
3
+ * 时间常量 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
4
+ * @module dsh-piggy/data/minutes
5
+ */
6
+
7
+ export const MINUTES = Object.freeze({
8
+ quarter: 15,
9
+ half: 30,
10
+ hour: 60,
11
+ ninety: 90,
12
+ twoHours: 120,
13
+ threeHours: 180,
14
+ fourHours: 240,
15
+ sixHours: 360,
16
+ eightHours: 480,
17
+ halfDay: 720,
18
+ day: 1440,
19
+ })
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Illness — three chains of four stages, straight from the reverse engineering.
23
+ // ---------------------------------------------------------------------------
@@ -0,0 +1,60 @@
1
+ // @ts-check
2
+ /**
3
+ * 居民卡:性格、口头禅、签名、星座 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
4
+ *
5
+ * B9(用户 2026-10-01):照动森居民卡。性格在拆纸盒时随机一种,带一个默认口头禅和签名;
6
+ * 口头禅和签名主人都能改,猪说话时会不时带上口头禅。
7
+ * @module dsh-piggy/data/profile
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} Personality
12
+ * @property {string} key
13
+ * @property {string} label
14
+ * @property {string} emoji
15
+ * @property {string} catchphrase - 默认口头禅
16
+ * @property {string} motto - 默认签名
17
+ */
18
+
19
+ /** @type {ReadonlyArray<Personality>} */
20
+ export const PERSONALITIES = Object.freeze([
21
+ Object.freeze({ key: 'peppy', label: '元气', emoji: '🌟', catchphrase: '嘿嘿', motto: '今天也要元气满满!' }),
22
+ Object.freeze({ key: 'lazy', label: '悠闲', emoji: '😴', catchphrase: '呼噜', motto: '能躺着,绝不坐着。' }),
23
+ Object.freeze({ key: 'cranky', label: '暴躁', emoji: '😤', catchphrase: '哼', motto: '别惹我,除非你带了吃的。' }),
24
+ Object.freeze({ key: 'sisterly', label: '大姐姐', emoji: '💁', catchphrase: '懂吗', motto: '有事找我,我罩着你。' }),
25
+ Object.freeze({ key: 'snooty', label: '自恋', emoji: '💅', catchphrase: '啧啧', motto: '我是这片最可爱的猪。' }),
26
+ Object.freeze({ key: 'normal', label: '普通', emoji: '🙂', catchphrase: '嗯嗯', motto: '平平淡淡才是真。' }),
27
+ ])
28
+
29
+ export const personalityByKey = key => PERSONALITIES.find(entry => entry.key === key) ?? null
30
+
31
+ /** 口头禅最多几个字。 */
32
+ export const CATCHPHRASE_MAX = 6
33
+
34
+ /** 签名最多几个字。 */
35
+ export const MOTTO_MAX = 24
36
+
37
+ /** 猪说一句话时,句末带上口头禅的概率。 */
38
+ export const CATCHPHRASE_CHANCE = 0.4
39
+
40
+ /** 这些场景不带口头禅:生病、吃错药、走了的时候不该卖萌。 */
41
+ export const SERIOUS_SCENES = Object.freeze(['death', 'sick', 'wrongMedicine'])
42
+
43
+ /**
44
+ * 星座:每个星座的**起始**月日(含),按日历顺序;1 月 1 日到 1 月 19 日落在最后一行之后,
45
+ * 即摩羯座(从上一年 12 月 22 日开始)。
46
+ */
47
+ export const ZODIAC = Object.freeze([
48
+ Object.freeze({ month: 1, day: 20, label: '水瓶座', emoji: '♒' }),
49
+ Object.freeze({ month: 2, day: 19, label: '双鱼座', emoji: '♓' }),
50
+ Object.freeze({ month: 3, day: 21, label: '白羊座', emoji: '♈' }),
51
+ Object.freeze({ month: 4, day: 20, label: '金牛座', emoji: '♉' }),
52
+ Object.freeze({ month: 5, day: 21, label: '双子座', emoji: '♊' }),
53
+ Object.freeze({ month: 6, day: 22, label: '巨蟹座', emoji: '♋' }),
54
+ Object.freeze({ month: 7, day: 23, label: '狮子座', emoji: '♌' }),
55
+ Object.freeze({ month: 8, day: 23, label: '处女座', emoji: '♍' }),
56
+ Object.freeze({ month: 9, day: 23, label: '天秤座', emoji: '♎' }),
57
+ Object.freeze({ month: 10, day: 24, label: '天蝎座', emoji: '♏' }),
58
+ Object.freeze({ month: 11, day: 23, label: '射手座', emoji: '♐' }),
59
+ Object.freeze({ month: 12, day: 22, label: '摩羯座', emoji: '♑' }),
60
+ ])
@@ -0,0 +1,99 @@
1
+ // @ts-check
2
+ /**
3
+ * 九门课与课时学段 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
4
+ *
5
+ * 数字全部来自 docs/tasks/numbers/B4-study-jobs.md(用户 2026-10-01 确认)。
6
+ * 照 QQ 宠物怀旧服 GrowUp.js:九门课各算各的课时,一门课上到第 9 / 20 / 40 / 95 节
7
+ * 就算这门课小学 / 中学 / 大学 / 研究生毕业,不需要九门一起升。
8
+ *
9
+ * @module dsh-piggy/data/school
10
+ */
11
+
12
+ /**
13
+ * @typedef {object} Subject
14
+ * @property {string} key
15
+ * @property {string} label
16
+ * @property {string} emoji
17
+ * @property {'intel'|'charm'|'strong'} trait - 主要涨的属性
18
+ * @property {'intel'|'charm'|'strong'|null} secondary - 顺带涨的属性
19
+ */
20
+
21
+ /**
22
+ * @param {string} key
23
+ * @param {string} label
24
+ * @param {string} emoji
25
+ * @param {'intel'|'charm'|'strong'} trait
26
+ * @param {'intel'|'charm'|'strong'|null} [secondary]
27
+ * @returns {Subject}
28
+ */
29
+ const subject = (key, label, emoji, trait, secondary = null) => Object.freeze({ key, label, emoji, trait, secondary })
30
+
31
+ /** @type {ReadonlyArray<Subject>} */
32
+ export const SUBJECTS = Object.freeze([
33
+ subject('chinese', '语文', '📖', 'intel', 'charm'),
34
+ subject('mathematics', '数学', '🔢', 'intel'),
35
+ subject('politics', '政治', '⚖️', 'intel', 'strong'),
36
+ subject('music', '音乐', '🎵', 'charm'),
37
+ subject('art', '艺术', '🎨', 'charm', 'intel'),
38
+ subject('manners', '礼仪', '🎩', 'charm'),
39
+ subject('pe', '体育', '🏃', 'strong', 'charm'),
40
+ subject('labour', '劳技', '🔧', 'strong', 'intel'),
41
+ subject('wushu', '武术', '🥋', 'strong'),
42
+ ])
43
+
44
+ export const subjectByKey = key => SUBJECTS.find(entry => entry.key === key) ?? null
45
+
46
+ /**
47
+ * @typedef {object} SchoolStage
48
+ * @property {string} key
49
+ * @property {string} label
50
+ * @property {string} emoji
51
+ * @property {number} upTo - 这门课上到第几节还算这个学段(含);最后一档是 Infinity
52
+ * @property {number} minutes - 每节时长
53
+ * @property {number} tuition - 每节学费
54
+ * @property {number} gain - 主属性 +
55
+ * @property {number} secondaryGain - 顺带属性 +
56
+ * @property {number} satiety - 每节消耗饱食
57
+ * @property {number} happiness - 每节消耗心情
58
+ */
59
+
60
+ /**
61
+ * Where a subject is, by how many lessons of it the pig has taken. Lesson N
62
+ * belongs to the first stage whose `upTo` is ≥ N; finishing the lesson numbered
63
+ * exactly `upTo` is that stage's graduation.
64
+ * @type {ReadonlyArray<SchoolStage>}
65
+ */
66
+ export const SCHOOL_STAGES = Object.freeze([
67
+ Object.freeze({ key: 'primary', label: '小学', emoji: '📚', upTo: 9, minutes: 20, tuition: 10, gain: 1, secondaryGain: 0, satiety: -5, happiness: -1 }),
68
+ Object.freeze({ key: 'middle', label: '中学', emoji: '🏫', upTo: 20, minutes: 30, tuition: 25, gain: 2, secondaryGain: 1, satiety: -7, happiness: -2 }),
69
+ Object.freeze({ key: 'college', label: '大学', emoji: '🏛', upTo: 40, minutes: 45, tuition: 60, gain: 3, secondaryGain: 1, satiety: -10, happiness: -3 }),
70
+ Object.freeze({ key: 'graduate', label: '研究生', emoji: '🔬', upTo: 95, minutes: 60, tuition: 120, gain: 4, secondaryGain: 2, satiety: -12, happiness: -4 }),
71
+ Object.freeze({ key: 'beyond', label: '学无止境', emoji: '🌌', upTo: Infinity, minutes: 60, tuition: 150, gain: 5, secondaryGain: 2, satiety: -12, happiness: -4 }),
72
+ ])
73
+
74
+ /** Lesson counts that end a stage: finishing one of these is a graduation. */
75
+ export const GRADUATION_LESSONS = Object.freeze(SCHOOL_STAGES.filter(stage => Number.isFinite(stage.upTo)).map(stage => stage.upTo))
76
+
77
+ export const schoolStageByKey = key => SCHOOL_STAGES.find(stage => stage.key === key) ?? null
78
+
79
+ /**
80
+ * The stage the *next* lesson of a subject belongs to, given how many have been
81
+ * taken. 0 taken → lesson 1 → 小学; 9 taken → lesson 10 → 中学.
82
+ * @param {number} taken
83
+ * @returns {SchoolStage}
84
+ */
85
+ export function stageForNextLesson(taken) {
86
+ const lesson = Math.max(0, Math.floor(Number.isFinite(taken) ? taken : 0)) + 1
87
+ return SCHOOL_STAGES.find(stage => lesson <= stage.upTo) ?? SCHOOL_STAGES[SCHOOL_STAGES.length - 1]
88
+ }
89
+
90
+ /**
91
+ * The stage a subject has *finished*, or null before its first graduation.
92
+ * @param {number} taken
93
+ * @returns {SchoolStage|null}
94
+ */
95
+ export function graduatedStage(taken) {
96
+ let done = null
97
+ for (const stage of SCHOOL_STAGES) if (Number.isFinite(stage.upTo) && taken >= stage.upTo) done = stage
98
+ return done
99
+ }
@@ -0,0 +1,142 @@
1
+ // @ts-check
2
+ /**
3
+ * 商店、装扮与照护物品 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
4
+ * @module dsh-piggy/data/shop
5
+ */
6
+
7
+ import { MEDICINES, REVIVE_ITEM } from './illness.js'
8
+
9
+ /** @type {ShopItem} */
10
+ export const DEFAULT_TOY = Object.freeze({
11
+ key: 'ball', label: '小皮球', emoji: '🎾', price: 0, kind: 'toy',
12
+ happiness: 12, satiety: -3, default: true,
13
+ })
14
+
15
+ /**
16
+ * One shelf entry. Everything past `kind` is optional: food carries satiety,
17
+ * medicine carries a tier (and 百草丹 `cureAll`), 装扮 carries a slot and a level gate.
18
+ *
19
+ * @typedef {object} ShopItem
20
+ * @property {string} key
21
+ * @property {string} label
22
+ * @property {string} emoji
23
+ * @property {number} price
24
+ * @property {string} kind
25
+ * @property {number} [satiety]
26
+ * @property {number} [happiness]
27
+ * @property {number} [cleanliness]
28
+ * @property {number} [health]
29
+ * @property {number} [level]
30
+ * @property {boolean} [default]
31
+ * @property {boolean} [needed]
32
+ * @property {string} [slot]
33
+ * @property {number} [tier]
34
+ * @property {boolean} [cureAll]
35
+ * @property {string} [blurb]
36
+ */
37
+ /**
38
+ * 装扮的等级门槛按 B2 的 60 级曲线重排(2026-10-01):Lv10 约 5 天、Lv20 约 20 天、
39
+ * Lv40 约 2.7 个月、Lv50 约 4 个月 —— 翅膀是养了小半年的猪才穿得上的。
40
+ * @type {ReadonlyArray<ShopItem>}
41
+ */
42
+ export const SHOP = Object.freeze([
43
+ // --- food ---------------------------------------------------------------
44
+ Object.freeze({ key: 'apple', label: '苹果', emoji: '🍎', price: 6, kind: 'food', satiety: 22, happiness: 3 }),
45
+ Object.freeze({ key: 'bread', label: '面包', emoji: '🍞', price: 10, kind: 'food', satiety: 32, happiness: 4 }),
46
+ Object.freeze({ key: 'bone', label: '肉骨头', emoji: '🍖', price: 15, kind: 'food', satiety: 45, happiness: 8, cleanliness: -4 }),
47
+ Object.freeze({ key: 'rice', label: '蛋炒饭', emoji: '🍚', price: 24, kind: 'food', satiety: 58, happiness: 10 }),
48
+ Object.freeze({ key: 'cake', label: '奶油蛋糕', emoji: '🎂', price: 40, kind: 'food', satiety: 80, happiness: 18, cleanliness: -8 }),
49
+ Object.freeze({ key: 'noodle', label: '大碗拉面', emoji: '🍜', price: 66, kind: 'food', satiety: 100, happiness: 26, cleanliness: -6 }),
50
+ Object.freeze({ key: 'fish', label: '小鱼干', emoji: '🐟', price: 12, kind: 'food', satiety: 30, happiness: 7 }),
51
+ Object.freeze({ key: 'pumpkin', label: '南瓜粥', emoji: '🎃', price: 34, kind: 'food', satiety: 62, happiness: 13 }),
52
+ Object.freeze({ key: 'skewer', label: '烤肉串', emoji: '🍢', price: 58, kind: 'food', satiety: 76, happiness: 21, cleanliness: -7 }),
53
+ Object.freeze({ key: 'feast', label: '豪华大餐', emoji: '🍱', price: 130, kind: 'food', satiety: 100, happiness: 34, cleanliness: -10 }),
54
+ // --- bath ---------------------------------------------------------------
55
+ Object.freeze({ key: 'soap', label: '香皂', emoji: '🧼', price: 6, kind: 'bath', cleanliness: 35, happiness: 2 }),
56
+ Object.freeze({ key: 'shower', label: '冲个澡', emoji: '🚿', price: 10, kind: 'bath', cleanliness: 50, happiness: 3 }),
57
+ Object.freeze({ key: 'shampoo', label: '沐浴露', emoji: '🧴', price: 14, kind: 'bath', cleanliness: 65, happiness: 6 }),
58
+ Object.freeze({ key: 'bubble', label: '泡泡浴', emoji: '🛁', price: 28, kind: 'bath', cleanliness: 100, happiness: 14 }),
59
+ Object.freeze({ key: 'sauna', label: '泡温泉', emoji: '🧖', price: 58, kind: 'bath', cleanliness: 100, happiness: 24, satiety: -8 }),
60
+ Object.freeze({ key: 'candle', label: '香薰', emoji: '🕯', price: 22, kind: 'bath', cleanliness: 58, happiness: 13 }),
61
+ Object.freeze({ key: 'milkbath', label: '牛奶浴', emoji: '🥛', price: 44, kind: 'bath', cleanliness: 85, happiness: 19 }),
62
+ Object.freeze({ key: 'deadsea', label: '死海泥', emoji: '🫧', price: 78, kind: 'bath', cleanliness: 100, happiness: 27, satiety: -6 }),
63
+ // --- toys ---------------------------------------------------------------
64
+ Object.freeze({ key: 'yoyo', label: '悠悠球', emoji: '🪀', price: 30, kind: 'toy', happiness: 22, satiety: -4 }),
65
+ Object.freeze({ key: 'blocks', label: '积木', emoji: '🎲', price: 45, kind: 'toy', happiness: 30, satiety: -5 }),
66
+ Object.freeze({ key: 'plush', label: '布偶', emoji: '🧸', price: 75, kind: 'toy', happiness: 42, satiety: -7 }),
67
+ Object.freeze({ key: 'scooter', label: '滑板车', emoji: '🛴', price: 120, kind: 'toy', happiness: 58, satiety: -10, cleanliness: -6 }),
68
+ Object.freeze({ key: 'carousel', label: '旋转木马', emoji: '🎠', price: 260, kind: 'toy', happiness: 80, satiety: -12, cleanliness: -8 }),
69
+ Object.freeze({ key: 'balloon', label: '气球', emoji: '🎈', price: 22, kind: 'toy', happiness: 24, satiety: -3 }),
70
+ Object.freeze({ key: 'puzzle', label: '拼图', emoji: '🧩', price: 60, kind: 'toy', happiness: 34, satiety: -5 }),
71
+ Object.freeze({ key: 'kite', label: '风筝', emoji: '🪁', price: 95, kind: 'toy', happiness: 48, satiety: -7 }),
72
+ Object.freeze({ key: 'rccar', label: '遥控车', emoji: '🏎', price: 180, kind: 'toy', happiness: 62, satiety: -9, cleanliness: -6 }),
73
+ Object.freeze({ key: 'bubbles', label: '泡泡机', emoji: '🫧', price: 220, kind: 'toy', happiness: 74, satiety: -10, cleanliness: -8 }),
74
+ // --- dress (家当) --------------------------------------------------------
75
+ // Not consumables: buy once, own forever, wear them. Each one needs a level,
76
+ // which is what ties 装扮 to the level axis instead of to the wallet.
77
+ // `slot` is a fixed anchor on the pig (see DRESS_SLOTS); one item per slot,
78
+ // so the artwork can be swapped in later without touching the logic.
79
+ Object.freeze({ key: 'scarf', label: '红围巾', emoji: '🧣', price: 80, kind: 'dress', level: 1, slot: 'neck', blurb: '脖子上暖乎乎的' }),
80
+ Object.freeze({ key: 'strawhat', label: '草帽', emoji: '👒', price: 150, kind: 'dress', level: 3, slot: 'head', blurb: '遮阳,也遮心虚' }),
81
+ Object.freeze({ key: 'sunglasses', label: '墨镜', emoji: '🕶', price: 260, kind: 'dress', level: 5, slot: 'face', blurb: '谁也不知道它在想什么' }),
82
+ Object.freeze({ key: 'overalls', label: '背带裤', emoji: '👖', price: 420, kind: 'dress', level: 8, slot: 'body', blurb: '干体力活穿的' }),
83
+ Object.freeze({ key: 'bowtie', label: '领结', emoji: '🎀', price: 600, kind: 'dress', level: 10, slot: 'neck', blurb: '上班用' }),
84
+ Object.freeze({ key: 'rainboots', label: '雨靴', emoji: '🥾', price: 900, kind: 'dress', level: 13, slot: 'feet', blurb: '踩水坑专用' }),
85
+ Object.freeze({ key: 'cape', label: '披风', emoji: '🦸', price: 1300, kind: 'dress', level: 16, slot: 'back', blurb: '风一吹就飘起来' }),
86
+ Object.freeze({ key: 'flowercrown', label: '花环', emoji: '💐', price: 1800, kind: 'dress', level: 20, slot: 'head', blurb: '春天做的' }),
87
+ Object.freeze({ key: 'tophat', label: '礼帽', emoji: '🎩', price: 2600, kind: 'dress', level: 25, slot: 'head', blurb: '正式场合' }),
88
+ Object.freeze({ key: 'necklace', label: '项链', emoji: '📿', price: 3600, kind: 'dress', level: 30, slot: 'neck', blurb: '据说是祖传的' }),
89
+ Object.freeze({ key: 'crown', label: '王冠', emoji: '👑', price: 5200, kind: 'dress', level: 40, slot: 'head', blurb: '自己给自己加冕' }),
90
+ Object.freeze({ key: 'wings', label: '翅膀', emoji: '🪽', price: 8000, kind: 'dress', level: 50, slot: 'back', blurb: '能不能飞,谁也没见它飞过' }),
91
+ // --- medicine: the twenty stage cures and 百草丹 (data/illness.js) ------
92
+ ...MEDICINES,
93
+ // --- revive -------------------------------------------------------------
94
+ REVIVE_ITEM,
95
+ ])
96
+
97
+ /** Shop shelves, in the order the panel shows them. */
98
+ export const KIND_ORDER = Object.freeze(['food', 'bath', 'toy', 'dress', 'medicine', 'revive'])
99
+
100
+ export const KIND_LABEL = Object.freeze({
101
+ food: '食物',
102
+ bath: '洗浴',
103
+ toy: '玩具',
104
+ dress: '装扮',
105
+ medicine: '药品',
106
+ revive: '复活',
107
+ })
108
+
109
+ /** Which care action spends which shelf. */
110
+ export const CARE_KIND = Object.freeze({ feed: 'food', bathe: 'bath', play: 'toy' })
111
+
112
+ /**
113
+ * 装扮点位 —— 猪身上固定的几个锚点。
114
+ *
115
+ * 每个点位同时只挂一件;以后换成真正的立绘时,只需要改 client.js 里
116
+ * `.dp-slot[data-slot="…"]` 的偏移,逻辑和存档都不用动。
117
+ */
118
+ export const DRESS_SLOTS = Object.freeze([
119
+ Object.freeze({ key: 'head', label: '头' }),
120
+ Object.freeze({ key: 'face', label: '脸' }),
121
+ Object.freeze({ key: 'neck', label: '脖子' }),
122
+ Object.freeze({ key: 'body', label: '身子' }),
123
+ Object.freeze({ key: 'back', label: '背后' }),
124
+ Object.freeze({ key: 'feet', label: '脚' }),
125
+ ])
126
+
127
+ export const dressSlotByKey = key => DRESS_SLOTS.find(entry => entry.key === key) ?? null
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Lookups
131
+ // ---------------------------------------------------------------------------
132
+
133
+ export const itemByKey = key => SHOP.find(item => item.key === key) ?? null
134
+
135
+ export const itemsOfKind = kind => SHOP.filter(item => item.kind === kind)
136
+
137
+ /** Every item a care action will accept: the shelf, plus the free default toy. */
138
+ export function careItems(kind) {
139
+ const bought = itemsOfKind(kind)
140
+ return kind === 'toy' ? [DEFAULT_TOY, ...bought] : bought
141
+ }
142
+
@@ -0,0 +1,42 @@
1
+ // @ts-check
2
+ /**
3
+ * 三维属性 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
4
+ * @module dsh-piggy/data/traits
5
+ */
6
+
7
+ /**
8
+ * Extra pay per trait point, as a fraction: 150 points doubles the wage.
9
+ *
10
+ * Was 1/15 when a lesson gave one point. Since B4 a subject taken to 大学 is
11
+ * worth ~90 points of its trait, and 1/15 hit the ×3 cap in the first month
12
+ * (on top of halved shifts, about 6× the pay on the job sheet). Cut to a tenth
13
+ * on 2026-10-01 at the owner's request.
14
+ */
15
+ export const TRAIT_PAY_PER_POINT = 1 / 150
16
+
17
+ /** ...but a pig that studied everything still only triples the wage, or the
18
+ * late game has no shape left. */
19
+ export const TRAIT_PAY_CAP = 3
20
+
21
+ /** Shorter shift per trait point. 0 since 2026-10-01: traits pay more, shifts stay as long as the job table says. */
22
+ export const TRAIT_SPEED_PER_POINT = 0
23
+
24
+ export const TRAIT_SPEED_CAP = 0.5
25
+
26
+ /** What one trait point buys on a given job. */
27
+ export function traitBonus(traitKey, points) {
28
+ const n = Number.isFinite(points) ? Math.max(0, points) : 0
29
+ return {
30
+ pay: Math.min(TRAIT_PAY_CAP, 1 + n * TRAIT_PAY_PER_POINT),
31
+ minutes: Math.max(1 - TRAIT_SPEED_CAP, 1 - n * TRAIT_SPEED_PER_POINT),
32
+ }
33
+ }
34
+
35
+ /** The three traits QQ Pet tracks alongside growth. */
36
+ export const TRAITS = Object.freeze({
37
+ intel: Object.freeze({ key: 'intel', label: '智力', emoji: '🧠' }),
38
+ charm: Object.freeze({ key: 'charm', label: '魅力', emoji: '✨' }),
39
+ strong: Object.freeze({ key: 'strong', label: '武力', emoji: '💪' }),
40
+ })
41
+
42
+ export const TRAIT_ORDER = Object.freeze(['intel', 'charm', 'strong'])
@@ -0,0 +1,87 @@
1
+ // @ts-check
2
+ /**
3
+ * 旅行与纪念品 —— 静态数值表(零逻辑、零 IO,见 docs/CONVENTIONS.md)。
4
+ * @module dsh-piggy/data/travel
5
+ */
6
+
7
+ import { MINUTES } from './minutes.js'
8
+
9
+ /**
10
+ * Rarity tiers for souvenirs: bragging rights, and a price tag.
11
+ *
12
+ * Deterministic — a souvenir's tier is a property of the souvenir, not a dice
13
+ * roll, so the whole collection stays testable and "the far trips are worth
14
+ * more" is a fact about the tables rather than a probability.
15
+ */
16
+ export const SOUVENIR_RARITY = Object.freeze({
17
+ common: Object.freeze({ key: 'common', label: '普通', emoji: '⚪', price: 60 }),
18
+ rare: Object.freeze({ key: 'rare', label: '稀有', emoji: '🔵', price: 320 }),
19
+ legend: Object.freeze({ key: 'legend', label: '传说', emoji: '🟡', price: 1600 }),
20
+ })
21
+
22
+ export const rarityByKey = key => SOUVENIR_RARITY[key] ?? SOUVENIR_RARITY.common
23
+
24
+ const souvenir = (key, emoji, label, rarity, story) => Object.freeze({ key, emoji, label, rarity, story })
25
+
26
+ export const TRIPS = Object.freeze([
27
+ Object.freeze({ key: 'suburb', label: '郊游', emoji: '🧺', minutes: MINUTES.hour, cost: 60, happiness: 10, satiety: -8, souvenirs: Object.freeze([
28
+ souvenir('clover', '🍀', '四叶草', 'common', '在草堆里翻到一片四叶草,据说会带来好运。'),
29
+ souvenir('pinecone', '🌰', '松果', 'common', '捡了一颗松果,捏起来有点扎手。'),
30
+ souvenir('wildflower', '🌼', '野花', 'rare', '摘了一朵小野花,一路上都小心护着。'),
31
+ ]) }),
32
+ Object.freeze({ key: 'mountain', label: '名山大川', emoji: '🏔', minutes: MINUTES.threeHours, cost: 200, happiness: 16, satiety: -20, souvenirs: Object.freeze([
33
+ souvenir('cloudsea', '☁️', '云海照片', 'common', '爬到半山腰回头看,云在脚底下走。'),
34
+ souvenir('stone', '🪨', '山石', 'common', '从山涧里捡了块石头,摸起来凉凉的。'),
35
+ souvenir('bamboo', '🎍', '竹杖', 'rare', '在竹林里挑了根直溜的竹子当拐杖,拄着它上了山顶。'),
36
+ souvenir('echo', '🏔', '山谷回声', 'legend', '在山顶冲着对面喊了一嗓子,山谷把你的名字还了回来。'),
37
+ ]) }),
38
+ Object.freeze({ key: 'hotspring', label: '泡温泉', emoji: '♨️', minutes: MINUTES.sixHours, cost: 380, happiness: 20, satiety: -28, souvenirs: Object.freeze([
39
+ souvenir('towelmint', '🧺', '温泉毛巾', 'common', '印着当地小图案的毛巾,闻起来有硫磺味。'),
40
+ souvenir('springegg', '🥚', '温泉蛋', 'common', '在池边煮的蛋,蛋黄是半凝固的。'),
41
+ souvenir('omamori', '🧿', '平安符', 'rare', '泡完汤求了个平安符,据说能防着点倒霉事。'),
42
+ ]) }),
43
+ Object.freeze({ key: 'sea', label: '看海', emoji: '🌊', minutes: MINUTES.eightHours, cost: 620, happiness: 24, satiety: -42, souvenirs: Object.freeze([
44
+ souvenir('seasalt', '🧂', '海盐', 'common', '在礁石坑里刮了点海盐,咸得发苦。'),
45
+ souvenir('starfish', '⭐', '海星', 'common', '退潮时捡到一只海星,看完就放回水里了。'),
46
+ souvenir('shell', '🐚', '一枚海螺', 'rare', '在海边捡到一枚海螺,贴在耳朵上能听见浪声。'),
47
+ souvenir('driftbottle', '🍾', '漂流瓶', 'legend', '瓶子里有张潮湿的纸条,写着「你好,陌生人」。'),
48
+ ]) }),
49
+ Object.freeze({ key: 'oldtown', label: '古镇', emoji: '🏮', minutes: MINUTES.halfDay, cost: 1100, happiness: 30, satiety: -50, souvenirs: Object.freeze([
50
+ souvenir('woodcut', '🪵', '木刻', 'common', '巷口老师傅刻的小木牌,背面还有刀痕。'),
51
+ souvenir('lantern', '🏮', '小灯笼', 'rare', '买了一盏纸灯笼,晚上提着一路走回客栈。'),
52
+ souvenir('teapot', '🫖', '紫砂壶', 'legend', '在旧货摊上淘到的紫砂壶,壶底刻着一个看不清的年份。'),
53
+ ]) }),
54
+ Object.freeze({ key: 'abroad', label: '出国', emoji: '🌍', minutes: MINUTES.day, cost: 2000, happiness: 38, satiety: -80, souvenirs: Object.freeze([
55
+ souvenir('foreigncoin', '🪙', '外国硬币', 'common', '找零收到一枚硬币,上面的字一个都不认识。'),
56
+ souvenir('stamp', '📮', '异国邮票', 'rare', '在旧书摊上买了张盖过戳的邮票。'),
57
+ souvenir('postcard', '💌', '手写明信片', 'rare', '给自己寄了一张明信片,上面写着「我很好,就是有点想你」。'),
58
+ souvenir('snowglobe', '🔮', '水晶球', 'legend', '晃一晃,陌生的城市就会下雪。'),
59
+ ]) }),
60
+ Object.freeze({ key: 'aurora', label: '看极光', emoji: '🌌', minutes: MINUTES.day, cost: 3600, happiness: 44, satiety: -90, souvenirs: Object.freeze([
61
+ souvenir('polestone', '⛰', '极地石', 'common', '从冻土上抠下来一块石头,冰得刺手。'),
62
+ souvenir('compass', '🧭', '老罗盘', 'rare', '指针一直抖,最后还是能指回住的地方。'),
63
+ souvenir('icecrystal', '❄️', '冰晶', 'rare', '接住了一片雪花,在掌心化成一小滴水。'),
64
+ souvenir('auroraphoto', '🌌', '极光照片', 'legend', '守了三个晚上才等到的那一片绿光,快门按下去的时候手在抖。'),
65
+ ]) }),
66
+ ])
67
+
68
+ /** Every souvenir on the map, flattened — the collection is global. */
69
+ export const ALL_SOUVENIRS = Object.freeze(TRIPS.flatMap(trip => trip.souvenirs.map(entry => Object.freeze({ ...entry, from: trip.key, fromLabel: trip.label }))))
70
+
71
+ export const souvenirByKey = key => ALL_SOUVENIRS.find(entry => entry.key === key) ?? null
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Items — one shop, four shelves.
75
+ //
76
+ // food feeds the pig
77
+ // bath washes it
78
+ // toy plays with it
79
+ // medicine cures it (four tiers, one per illness stage)
80
+ // revive brings it back
81
+ //
82
+ // Buying is not the only way in: every pig owns one scruffy default toy for
83
+ // free, so `玩耍` always works even with an empty bag. That mirrors QQ Pet,
84
+ // where the pet can amuse itself without a bought plaything.
85
+ // ---------------------------------------------------------------------------
86
+
87
+ export const tripByKey = key => TRIPS.find(trip => trip.key === key) ?? null
@@ -0,0 +1,52 @@
1
+ // @ts-check
2
+ /**
3
+ * dsh-piggy · data — the static game tables.
4
+ *
5
+ * Numbers and names only, no behaviour. The illness chains, thresholds, the
6
+ * nine school subjects, the job/trip idea and the item categories are lifted
7
+ * from QQ 宠物 (怀旧服 v1.2.4) as documented by xuemian168/qqpet_automation's
8
+ * reverse engineering and the original asset tree:
9
+ *
10
+ * img_res/study/ xx-* (小学) · dx-* (大学) · yjs-* (研究生)
11
+ * art 美术 · chinese 语文 · labouring 劳动 · manner 礼仪 ·
12
+ * mathematics 数学 · music 音乐 · pe 体育 · politics 政治 ·
13
+ * wushu 武术
14
+ * img_res/work/ the job set
15
+ * img_res/food/ food art, one entry per dish
16
+ * img_res/commodity/ the sundries — bath things and toys
17
+ * models.py ActiveOption { work, study, trip, ill, die }
18
+ * PetInfo { growth, hunger, clean, health, mood, yb,
19
+ * intel 智力, charm 魅力, strong 武力 }
20
+ * StoreInventory { food, commodity, medicine, background }
21
+ *
22
+ * The last one is the reason care costs an item: the original keeps **food**,
23
+ * **commodity** and **medicine** as separate inventory categories, and its own
24
+ * heal path is "find the matching medicine in the bag → use it → recover".
25
+ * Feeding, bathing and playing work the same way here.
26
+ *
27
+ * Attribute scale: QQ Pet counts in the thousands and its hunger ceiling grows
28
+ * with level (3000 + 100 × min(level, 30)); this pig draws 0-100 bars instead,
29
+ * with the QQ Pet thresholds rescaled:
30
+ * hunger 720/3100 ≈ 23% → satiety < 25
31
+ * clean 1080/3100 ≈ 35% → cleanliness < 35
32
+ * mood 100/1000 = 10% → happiness < 35 (a little kinder)
33
+ * health 5 → 5, unchanged — 0 is still death.
34
+ *
35
+ * @module dsh-piggy/data
36
+ */
37
+
38
+ export * from './data/minutes.js'
39
+ export * from './data/traits.js'
40
+ export * from './data/illness.js'
41
+ export * from './data/life.js'
42
+ export * from './data/growth.js'
43
+ export * from './data/jobs.js'
44
+ export * from './data/school.js'
45
+ export * from './data/interests.js'
46
+ export * from './data/travel.js'
47
+ export * from './data/shop.js'
48
+ export * from './data/drops.js'
49
+ export * from './data/lines.js'
50
+ export * from './data/profile.js'
51
+ export * from './data/daily.js'
52
+ export * from './data/evolution.js'
@@ -0,0 +1,12 @@
1
+ // @ts-check
2
+ /**
3
+ * @dsh-piggy/core —— 宠物的纯领域模型与数值表。
4
+ *
5
+ * 零依赖、零 IO:不读文件、不发请求、不碰 DOM、不自己取时间或随机数。
6
+ * 宿主(DSH 插件、以后的独立版)负责存档、时钟和界面,把 nowMs 等外部输入传进来。
7
+ *
8
+ * @module @dsh-piggy/core
9
+ */
10
+
11
+ export * from './core.js'
12
+ export * from './data.js'