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,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 极小的 DOM 构造助手。
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-piggy/client/dom
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { num } from './values.js'
|
|
9
|
+
export function el(tag, className, text) {
|
|
10
|
+
var node = document.createElement(tag)
|
|
11
|
+
if (className) node.className = className
|
|
12
|
+
if (text !== undefined) node.textContent = text
|
|
13
|
+
return node
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function button(className, attrs, onClick) {
|
|
17
|
+
var node = el('button', className)
|
|
18
|
+
node.type = 'button'
|
|
19
|
+
for (var key in attrs) node.setAttribute(key, attrs[key])
|
|
20
|
+
node.addEventListener('click', function (event) {
|
|
21
|
+
event.stopPropagation()
|
|
22
|
+
onClick()
|
|
23
|
+
})
|
|
24
|
+
return node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function meter(value, variant) {
|
|
28
|
+
var wrap = el('div', 'dp-meter' + (variant ? ' ' + variant : ''))
|
|
29
|
+
var fill = document.createElement('i')
|
|
30
|
+
fill.style.width = Math.max(0, Math.min(100, num(value, 0))) + '%'
|
|
31
|
+
wrap.appendChild(fill)
|
|
32
|
+
return wrap
|
|
33
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 猪的即时反馈:动作动画、粒子、气泡台词与 toast。
|
|
4
|
+
*
|
|
5
|
+
* 只碰交给它的几个元素,不读全局状态 —— 「猪还在不在」通过 isStopped()
|
|
6
|
+
* 回调问外壳(见 docs/CONVENTIONS.md)。
|
|
7
|
+
* @module dsh-piggy/client/effects
|
|
8
|
+
*/
|
|
9
|
+
import { syncPigArt } from './art.js'
|
|
10
|
+
import { PET_LINES } from './constants.js'
|
|
11
|
+
import { el } from './dom.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{ scene: object, pig: object, pigArt: object, card: object, bubble: object, isStopped: () => boolean }} deps
|
|
15
|
+
* @returns {{ react: Function, burst: Function, flash: Function, showBubble: Function, showLine: Function, toast: Function, dispose: Function }}
|
|
16
|
+
*/
|
|
17
|
+
export function createEffects(deps) {
|
|
18
|
+
var scene = deps.scene
|
|
19
|
+
var pig = deps.pig
|
|
20
|
+
var card = deps.card
|
|
21
|
+
var bubble = deps.bubble
|
|
22
|
+
var isStopped = deps.isStopped
|
|
23
|
+
|
|
24
|
+
var reactTimer = null
|
|
25
|
+
var bubbleTimer = null
|
|
26
|
+
|
|
27
|
+
// ---- animation ----
|
|
28
|
+
function react(kind, ms) {
|
|
29
|
+
if (reactTimer !== null) window.clearTimeout(reactTimer)
|
|
30
|
+
// Re-assigning the same value does NOT restart a CSS animation, so
|
|
31
|
+
// clicking three times quickly only played it once. Dropping the
|
|
32
|
+
// attribute and forcing a reflow makes every click start from zero.
|
|
33
|
+
pig.removeAttribute('data-react')
|
|
34
|
+
void pig.offsetWidth
|
|
35
|
+
pig.setAttribute('data-react', kind)
|
|
36
|
+
syncPigArt(pig, deps.pigArt)
|
|
37
|
+
reactTimer = window.setTimeout(function () {
|
|
38
|
+
pig.removeAttribute('data-react')
|
|
39
|
+
syncPigArt(pig, deps.pigArt)
|
|
40
|
+
reactTimer = null
|
|
41
|
+
}, ms || 900)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function burst(emojis, count) {
|
|
45
|
+
for (var i = 0; i < (count || 1); i += 1) {
|
|
46
|
+
(function (index) {
|
|
47
|
+
window.setTimeout(function () {
|
|
48
|
+
if (isStopped()) return
|
|
49
|
+
var node = el('span', 'dp-fx', emojis[index % emojis.length])
|
|
50
|
+
node.style.setProperty('--dx', Math.round((Math.random() - 0.5) * 46) + 'px')
|
|
51
|
+
// Anchor to the pig, not the scene. The scene is panel-wide when
|
|
52
|
+
// open, so fixed coordinates put every particle off to one side.
|
|
53
|
+
var spot = headSpot()
|
|
54
|
+
node.style.left = (spot.x + Math.round((Math.random() - 0.5) * 22)) + 'px'
|
|
55
|
+
node.style.top = spot.y + 'px'
|
|
56
|
+
scene.appendChild(node)
|
|
57
|
+
window.setTimeout(function () { node.remove() }, 1200)
|
|
58
|
+
}, index * 110)
|
|
59
|
+
})(i)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Just above the pig's head, in scene coordinates. */
|
|
64
|
+
function headSpot() {
|
|
65
|
+
var fallback = { x: 24, y: 8 }
|
|
66
|
+
if (typeof pig.getBoundingClientRect !== 'function' || typeof scene.getBoundingClientRect !== 'function') return fallback
|
|
67
|
+
var p = pig.getBoundingClientRect()
|
|
68
|
+
var s = scene.getBoundingClientRect()
|
|
69
|
+
if (p.width === 0 && p.height === 0) return fallback
|
|
70
|
+
return { x: p.left - s.left + p.width / 2, y: p.top - s.top - 20 }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var REACTIONS = {
|
|
74
|
+
hatch: { kind: 'levelup', ms: 980, fx: ['🥚', '✨', '🐖', '🎉'], count: 4, say: '孵出来啦!' },
|
|
75
|
+
feed: { kind: 'feed', ms: 900, fx: ['🍎', '😋', '✨'], count: 3, say: '吃掉了!' },
|
|
76
|
+
bathe: { kind: 'bathe', ms: 1050, fx: ['🫧', '🫧', '💧', '✨'], count: 4, say: '洗干净啦~' },
|
|
77
|
+
play: { kind: 'play', ms: 900, fx: ['🎾', '⭐', '💨'], count: 3, say: '好开心!' },
|
|
78
|
+
pet: { kind: 'pet', ms: 420, fx: ['❤️'], count: 1, say: '好舒服…' },
|
|
79
|
+
work: { kind: 'away', ms: 900, fx: ['💼', '🧱', '🪙'], count: 3, say: '出门打工!' },
|
|
80
|
+
study: { kind: 'away', ms: 900, fx: ['📚', '✏️', '🧠'], count: 3, say: '上学去!' },
|
|
81
|
+
trip: { kind: 'away', ms: 900, fx: ['🧳', '🗺', '✨'], count: 3, say: '出发旅行!' },
|
|
82
|
+
calloff: { kind: 'refuse', ms: 520, fx: ['💨'], count: 1, say: '提前回来了…' },
|
|
83
|
+
buy: { kind: 'pet', ms: 620, fx: ['🪙', '🛒'], count: 2, say: '买到了!' },
|
|
84
|
+
use: { kind: 'pet', ms: 620, fx: ['✨'], count: 2, say: '用掉了。' },
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function flash(action) {
|
|
88
|
+
var spec = REACTIONS[action]
|
|
89
|
+
if (spec === undefined) return
|
|
90
|
+
react(spec.kind, spec.ms)
|
|
91
|
+
burst(spec.fx, spec.count)
|
|
92
|
+
// Patting is the one thing you do over and over, so it gets a pool of
|
|
93
|
+
// lines rather than the same four characters every time.
|
|
94
|
+
var lines = action === 'pet' ? PET_LINES : null
|
|
95
|
+
showBubble(lines === null ? spec.say : lines[Math.floor(Math.random() * lines.length)], 1600)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var bubbleTimer = null
|
|
99
|
+
function showBubble(text, ms) {
|
|
100
|
+
if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
|
|
101
|
+
bubble.textContent = text
|
|
102
|
+
bubble.hidden = false
|
|
103
|
+
bubbleTimer = window.setTimeout(function () {
|
|
104
|
+
bubble.hidden = true
|
|
105
|
+
bubbleTimer = null
|
|
106
|
+
}, ms || 2600)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A line the pig says, with optional reply buttons. Replying closes the bubble;
|
|
111
|
+
* a line with buttons stays up longer so there is time to answer it.
|
|
112
|
+
* @param {string} text
|
|
113
|
+
* @param {string[]} replies
|
|
114
|
+
* @param {(index: number) => void} onReply
|
|
115
|
+
*/
|
|
116
|
+
function showLine(text, replies, onReply) {
|
|
117
|
+
if (replies.length === 0) {
|
|
118
|
+
showBubble(text, 2600)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
|
|
122
|
+
bubble.textContent = text
|
|
123
|
+
var row = el('div', 'dp-bubble-replies', '')
|
|
124
|
+
replies.forEach(function (label, index) {
|
|
125
|
+
var answer = el('button', 'dp-reply', label)
|
|
126
|
+
answer.type = 'button'
|
|
127
|
+
answer.addEventListener('click', function (event) {
|
|
128
|
+
// The bubble sits over the pig; a reply must not also count as a pat.
|
|
129
|
+
event.stopPropagation()
|
|
130
|
+
bubble.hidden = true
|
|
131
|
+
if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
|
|
132
|
+
bubbleTimer = null
|
|
133
|
+
onReply(index)
|
|
134
|
+
})
|
|
135
|
+
row.appendChild(answer)
|
|
136
|
+
})
|
|
137
|
+
bubble.appendChild(row)
|
|
138
|
+
bubble.hidden = false
|
|
139
|
+
bubbleTimer = window.setTimeout(function () {
|
|
140
|
+
bubble.hidden = true
|
|
141
|
+
bubbleTimer = null
|
|
142
|
+
}, 6000)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function toast(text) {
|
|
146
|
+
var node = el('div', 'dp-toast', text)
|
|
147
|
+
card.insertBefore(node, card.firstChild)
|
|
148
|
+
window.setTimeout(function () { node.remove() }, 4800)
|
|
149
|
+
}
|
|
150
|
+
/** Drop the pending timers — the panel is going away, nothing should fire. */
|
|
151
|
+
function dispose() {
|
|
152
|
+
if (reactTimer !== null) window.clearTimeout(reactTimer)
|
|
153
|
+
if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
|
|
154
|
+
reactTimer = null
|
|
155
|
+
bubbleTimer = null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { react: react, burst: burst, flash: flash, showBubble: showBubble, showLine: showLine, toast: toast, dispose: dispose }
|
|
159
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 客户端小格式化:天数、分钟数、物品类别名。
|
|
4
|
+
*
|
|
5
|
+
* 纯函数,输入输出都是字符串(见 docs/CONVENTIONS.md)。
|
|
6
|
+
* @module dsh-piggy/client/format
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function formatDays(days) {
|
|
10
|
+
if (days >= 1) return Math.round(days) + ' 天'
|
|
11
|
+
const hours = days * 24
|
|
12
|
+
return hours >= 1 ? Math.round(hours) + ' 小时' : Math.max(1, Math.round(hours * 60)) + ' 分钟'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatMinutes(minutes) {
|
|
16
|
+
if (minutes < 60) return minutes + ' 分钟'
|
|
17
|
+
const hours = Math.floor(minutes / 60)
|
|
18
|
+
const rest = minutes % 60
|
|
19
|
+
return rest === 0 ? hours + ' 小时' : hours + ' 小时' + rest + ' 分'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function kindLabel(item) {
|
|
23
|
+
if (item.kind === 'medicine') return item.needed ? '对症!' : '药'
|
|
24
|
+
if (item.kind === 'revive') return '复活用'
|
|
25
|
+
if (item.kind === 'bath') return '洗浴'
|
|
26
|
+
return '食物'
|
|
27
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-piggy · client — the floating pig.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written browser bundle: DSH loads it through `window.__ModuleLoader__`,
|
|
5
|
+
* bundled by esbuild from the modules beside it: raw DOM and `fetch` only, no framework.
|
|
6
|
+
*
|
|
7
|
+
* Layout follows the original QQ 宠物: the pet sits at the top, and a cream icon
|
|
8
|
+
* bar sits directly beneath it. Six icons — 状态 · 学习 · 打工 · 商店 · 旅行 ·
|
|
9
|
+
* 背包 — switch what the area below the bar shows, so nothing ever needs typing.
|
|
10
|
+
*
|
|
11
|
+
* Every value the host sends goes through `normalize()` first. A host/client
|
|
12
|
+
* version mismatch must degrade to sane defaults and an explanation, never to a
|
|
13
|
+
* screen full of `undefined`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { renderBagTab } from './tabs/bag.js'
|
|
17
|
+
import { renderDevTab } from './tabs/dev.js'
|
|
18
|
+
import { renderShopTab } from './tabs/shop.js'
|
|
19
|
+
import { renderStatusTab } from './tabs/status.js'
|
|
20
|
+
import { renderStudyTab } from './tabs/study.js'
|
|
21
|
+
import { renderTravelTab } from './tabs/travel.js'
|
|
22
|
+
import { renderWorkTab } from './tabs/work.js'
|
|
23
|
+
import { createEffects } from './effects.js'
|
|
24
|
+
import { createIo } from './io.js'
|
|
25
|
+
import { createLayout } from './layout.js'
|
|
26
|
+
import { createPanel } from './panel.js'
|
|
27
|
+
import { createScene } from './scene.js'
|
|
28
|
+
import { CSS } from './styles.js'
|
|
29
|
+
import { ACT_URL, ART_URL, BOX_POKES_TO_OPEN, BOX_POKE_LINES, CARE_LABEL, DEV_KEY, DEV_TAB, KIND_ORDER, KIND_TITLE, MOUNTED, MODES, NO_ITEM_LINE, OPEN_KEY, PANEL_GAP, PANEL_MARGIN, PANEL_MIN_HEIGHT, PANEL_WIDTH, PET_LINES, PIG_PADDING_X, GREET_DELAY_MS, IDLE_CHAT_MINUTES, POLL_MS, POSITION_KEY, SCENE_RESERVE, STAGES, STATE_URL, TABS } from './constants.js'
|
|
30
|
+
import { button, el, meter } from './dom.js'
|
|
31
|
+
import { normalize } from './normalize.js'
|
|
32
|
+
import { readStore, writeStore } from './storage.js'
|
|
33
|
+
import { arr, num, obj, str } from './values.js'
|
|
34
|
+
|
|
35
|
+
/** @type {any} */ (window).__ModuleLoader__.load({
|
|
36
|
+
id: 'dsh-piggy',
|
|
37
|
+
factory: (require) => {
|
|
38
|
+
var module = { exports: {} }
|
|
39
|
+
var exports = module.exports
|
|
40
|
+
|
|
41
|
+
var devMode = false
|
|
42
|
+
|
|
43
|
+
function apply(ctx) {
|
|
44
|
+
// A client plugin that throws while activating can take the whole web boot
|
|
45
|
+
// down with it, so the pig never lets an exception escape.
|
|
46
|
+
try {
|
|
47
|
+
return mount()
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn('[dsh-piggy] 挂载失败,猪先退到一边', error)
|
|
50
|
+
return () => {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mount() {
|
|
55
|
+
if (document.querySelector('[' + MOUNTED + ']') !== null) {
|
|
56
|
+
console.warn('[dsh-piggy] 已存在实例,跳过重复挂载')
|
|
57
|
+
return () => {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Nunito + Noto Sans SC, per the design system's standalone recipe. The
|
|
61
|
+
// request is non-blocking (`display=swap`) and the token font stack falls
|
|
62
|
+
// back to system faces, so an offline or blocked load degrades quietly
|
|
63
|
+
// instead of breaking the panel.
|
|
64
|
+
var parts = createScene()
|
|
65
|
+
var { font, style, host, card, scene, hud, hudName, hudCoins, hudHealth, bubble, work, prop,
|
|
66
|
+
progressWrap, progressFill, pokeHint, dailyHint, soul, pigArt, pigEmoji, pig, dressSlots, bar, content } = parts
|
|
67
|
+
|
|
68
|
+
var savedPos = readStore(POSITION_KEY)
|
|
69
|
+
// The pig's position as the user set it, before any on-screen clamp.
|
|
70
|
+
var userRight = 18
|
|
71
|
+
var userBottom = 18
|
|
72
|
+
if (savedPos !== null) {
|
|
73
|
+
try {
|
|
74
|
+
var parsed = JSON.parse(savedPos)
|
|
75
|
+
if (parsed && typeof parsed.right === 'number') userRight = parsed.right
|
|
76
|
+
if (parsed && typeof parsed.bottom === 'number') userBottom = parsed.bottom
|
|
77
|
+
} catch (error) { /* ignore */ }
|
|
78
|
+
}
|
|
79
|
+
host.style.right = userRight + 'px'
|
|
80
|
+
host.style.bottom = userBottom + 'px'
|
|
81
|
+
|
|
82
|
+
// Keep the pig itself on screen — and nothing more. There used to be a
|
|
83
|
+
// composer-avoidance floor here that forced the widget above the input box:
|
|
84
|
+
// it existed because the wrapper swallowed clicks aimed at the send button.
|
|
85
|
+
// `pointer-events:none` solves that properly now, so the clamp only ever
|
|
86
|
+
// stopped the user from parking their pet where they wanted it.
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Place the panel so it is fully on screen, wherever the pig has been
|
|
90
|
+
* parked. The pig itself is never moved by this: the panel is absolutely
|
|
91
|
+
* positioned, so it takes no space in the wrapper's box.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
var icons = {}
|
|
95
|
+
|
|
96
|
+
/** The tabs on show right now: the normal six, plus 调试 when dev mode is on. */
|
|
97
|
+
|
|
98
|
+
/** Rebuild the icon bar. Called whenever dev mode flips. */
|
|
99
|
+
|
|
100
|
+
// ---- state ----
|
|
101
|
+
var view = normalize(null)
|
|
102
|
+
// B9: the panel opens on the home screen of app tiles.
|
|
103
|
+
var tab = 'home'
|
|
104
|
+
var stage = 'primary'
|
|
105
|
+
// 用户自己点过学段之后,轮询就不许再替他改(B1 的「默认学段」只在没选过时生效)。
|
|
106
|
+
var stagePicked = false
|
|
107
|
+
// B8: which category each tile tab is opened into (null = the top layer), and a picked tile inside it.
|
|
108
|
+
var drill = { study: null, shop: null, bag: null, work: null, pick: null }
|
|
109
|
+
// Which souvenir's story card is open in the travel tab, if any.
|
|
110
|
+
var souvenirPick = null
|
|
111
|
+
// Which care action's item picker is open, if any.
|
|
112
|
+
var picker = null
|
|
113
|
+
// The owner-name draft while it is being edited on the status tab (null = not editing).
|
|
114
|
+
var ownerEdit = null
|
|
115
|
+
// 猪的名字草稿(同上:编辑期间轮询不许重绘,否则输入框会丢焦点)。
|
|
116
|
+
var pigNameEdit = null
|
|
117
|
+
// 居民卡: { field: 'catchphrase'|'motto', draft } while one is being edited.
|
|
118
|
+
var cardEdit = null
|
|
119
|
+
|
|
120
|
+
/** The tabs get an explicit context instead of closing over the shell locals. */
|
|
121
|
+
var isOpen = readStore(OPEN_KEY) === 'true'
|
|
122
|
+
var lastStage = null
|
|
123
|
+
var lastPendingAt = 0
|
|
124
|
+
var lastPendingId = 0
|
|
125
|
+
var pollTimer = null
|
|
126
|
+
var fx = createEffects({
|
|
127
|
+
scene: scene, pig: pig, pigArt: pigArt, card: card, bubble: bubble,
|
|
128
|
+
isStopped: function () { return stopped },
|
|
129
|
+
})
|
|
130
|
+
var react = fx.react, burst = fx.burst, flash = fx.flash
|
|
131
|
+
var showBubble = fx.showBubble, showLine = fx.showLine, toast = fx.toast
|
|
132
|
+
var stopped = false
|
|
133
|
+
var busy = false
|
|
134
|
+
|
|
135
|
+
/** The shell hands the modules an explicit context instead of sharing a scope. */
|
|
136
|
+
var ctx = {
|
|
137
|
+
host: host,
|
|
138
|
+
card: card,
|
|
139
|
+
content: content,
|
|
140
|
+
scene: scene,
|
|
141
|
+
hud: hud,
|
|
142
|
+
hudName: hudName,
|
|
143
|
+
hudCoins: hudCoins,
|
|
144
|
+
hudHealth: hudHealth,
|
|
145
|
+
bubble: bubble,
|
|
146
|
+
work: work,
|
|
147
|
+
prop: prop,
|
|
148
|
+
progressWrap: progressWrap,
|
|
149
|
+
progressFill: progressFill,
|
|
150
|
+
pokeHint: pokeHint,
|
|
151
|
+
dailyHint: dailyHint,
|
|
152
|
+
soul: soul,
|
|
153
|
+
pigArt: pigArt,
|
|
154
|
+
pigEmoji: pigEmoji,
|
|
155
|
+
pig: pig,
|
|
156
|
+
dressSlots: dressSlots,
|
|
157
|
+
bar: bar,
|
|
158
|
+
icons: icons,
|
|
159
|
+
flash: flash,
|
|
160
|
+
react: react,
|
|
161
|
+
burst: burst,
|
|
162
|
+
showBubble: showBubble,
|
|
163
|
+
showLine: showLine,
|
|
164
|
+
toast: toast,
|
|
165
|
+
get view() { return view }, set view(next) { view = next },
|
|
166
|
+
get tab() { return tab }, set tab(next) { tab = next },
|
|
167
|
+
get stage() { return stage }, set stage(next) { stage = next },
|
|
168
|
+
get stagePicked() { return stagePicked }, set stagePicked(next) { stagePicked = next },
|
|
169
|
+
get drill() { return drill },
|
|
170
|
+
get picker() { return picker }, set picker(next) { picker = next },
|
|
171
|
+
get souvenirPick() { return souvenirPick }, set souvenirPick(next) { souvenirPick = next },
|
|
172
|
+
get ownerEdit() { return ownerEdit }, set ownerEdit(next) { ownerEdit = next },
|
|
173
|
+
get pigNameEdit() { return pigNameEdit }, set pigNameEdit(next) { pigNameEdit = next },
|
|
174
|
+
get cardEdit() { return cardEdit }, set cardEdit(next) { cardEdit = next },
|
|
175
|
+
get isOpen() { return isOpen }, set isOpen(next) { isOpen = next },
|
|
176
|
+
get lastStage() { return lastStage }, set lastStage(next) { lastStage = next },
|
|
177
|
+
get lastPendingAt() { return lastPendingAt }, set lastPendingAt(next) { lastPendingAt = next },
|
|
178
|
+
get lastPendingId() { return lastPendingId }, set lastPendingId(next) { lastPendingId = next },
|
|
179
|
+
get userRight() { return userRight }, set userRight(next) { userRight = next },
|
|
180
|
+
get userBottom() { return userBottom }, set userBottom(next) { userBottom = next },
|
|
181
|
+
get busy() { return busy }, set busy(next) { busy = next },
|
|
182
|
+
get stopped() { return stopped }, set stopped(next) { stopped = next },
|
|
183
|
+
get devMode() { return devMode },
|
|
184
|
+
}
|
|
185
|
+
var layout = createLayout(ctx)
|
|
186
|
+
var panel = createPanel(ctx)
|
|
187
|
+
ctx.select = panel.select
|
|
188
|
+
ctx.setOpen = panel.setOpen
|
|
189
|
+
ctx.fitPanel = layout.fitPanel
|
|
190
|
+
ctx.paintBar = layout.paintBar
|
|
191
|
+
ctx.buildIcon = layout.buildIcon
|
|
192
|
+
ctx.clampPig = layout.clampPig
|
|
193
|
+
var render = panel.render, renderContent = panel.renderContent
|
|
194
|
+
var setOpen = panel.setOpen, select = panel.select
|
|
195
|
+
var fitPanel = layout.fitPanel, clampPig = layout.clampPig
|
|
196
|
+
var paintBar = layout.paintBar, buildIcon = layout.buildIcon
|
|
197
|
+
for (var t = 0; t < TABS.length; t += 1) buildIcon(TABS[t])
|
|
198
|
+
// `ui` was built before these existed; point it at the real ones now.
|
|
199
|
+
var io = createIo(ctx)
|
|
200
|
+
var send = io.send, refresh = io.refresh
|
|
201
|
+
ctx.send = send
|
|
202
|
+
ctx.render = render
|
|
203
|
+
ctx.renderContent = renderContent
|
|
204
|
+
ctx.send = send
|
|
205
|
+
ctx.renderContent = renderContent
|
|
206
|
+
ctx.setOpen = setOpen
|
|
207
|
+
ctx.fitPanel = fitPanel
|
|
208
|
+
ctx.flash = flash
|
|
209
|
+
|
|
210
|
+
// 日常气泡(签到/礼包)的点击只在这里绑一次;它压在猪上面,事件不能冒泡给
|
|
211
|
+
// 拖动和摸摸。
|
|
212
|
+
dailyHint.addEventListener('pointerdown', function (event) { event.stopPropagation() })
|
|
213
|
+
dailyHint.addEventListener('click', function (event) {
|
|
214
|
+
event.stopPropagation()
|
|
215
|
+
var action = dailyHint.getAttribute('data-action')
|
|
216
|
+
if (action !== null && action !== '') send(action)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// ---- open / close ----
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Developer tab. Drives the pig into any state so a change can be looked at
|
|
223
|
+
* immediately instead of waiting days for it — and so the states that are
|
|
224
|
+
* hard to reach by playing (dying, the last illness stage, an elder pig)
|
|
225
|
+
* can be checked at all.
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
// ---- drag the pig; right-click it for the menu ----
|
|
229
|
+
var drag = null
|
|
230
|
+
scene.addEventListener('pointerdown', function (event) {
|
|
231
|
+
if (event.button !== 0) return
|
|
232
|
+
drag = {
|
|
233
|
+
x: event.clientX, y: event.clientY,
|
|
234
|
+
right: parseFloat(getComputedStyle(host).right) || 18,
|
|
235
|
+
bottom: parseFloat(getComputedStyle(host).bottom) || 18,
|
|
236
|
+
moved: false,
|
|
237
|
+
}
|
|
238
|
+
scene.setAttribute('data-dragging', 'true')
|
|
239
|
+
scene.setPointerCapture?.(event.pointerId)
|
|
240
|
+
})
|
|
241
|
+
scene.addEventListener('pointermove', function (event) {
|
|
242
|
+
if (drag === null) return
|
|
243
|
+
var dx = event.clientX - drag.x
|
|
244
|
+
var dy = event.clientY - drag.y
|
|
245
|
+
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) drag.moved = true
|
|
246
|
+
userRight = drag.right - dx
|
|
247
|
+
userBottom = drag.bottom - dy
|
|
248
|
+
clampPig()
|
|
249
|
+
// The panel rides along so it is never left behind off screen.
|
|
250
|
+
fitPanel()
|
|
251
|
+
})
|
|
252
|
+
function endDrag() {
|
|
253
|
+
if (drag === null) return false
|
|
254
|
+
var moved = drag.moved
|
|
255
|
+
drag = null
|
|
256
|
+
scene.removeAttribute('data-dragging')
|
|
257
|
+
clampPig()
|
|
258
|
+
writeStore(POSITION_KEY, JSON.stringify({ right: userRight, bottom: userBottom }))
|
|
259
|
+
fitPanel()
|
|
260
|
+
return moved
|
|
261
|
+
}
|
|
262
|
+
var boxPokes = 0
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Poke the box. Three pokes and the piglet comes out — the count is what
|
|
266
|
+
* makes it feel like something is in there rather than a button that
|
|
267
|
+
* happens to be cardboard-shaped.
|
|
268
|
+
*/
|
|
269
|
+
function pokeBox() {
|
|
270
|
+
boxPokes += 1
|
|
271
|
+
react('poke', 560)
|
|
272
|
+
if (boxPokes >= BOX_POKES_TO_OPEN) {
|
|
273
|
+
boxPokes = 0
|
|
274
|
+
host.removeAttribute('data-poke')
|
|
275
|
+
showBubble('哇——!', 1200)
|
|
276
|
+
burst(['✨', '🎉', '💨'], 6)
|
|
277
|
+
send('hatch')
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
host.setAttribute('data-poke', String(boxPokes))
|
|
281
|
+
burst(['💨'], 2)
|
|
282
|
+
showBubble(BOX_POKE_LINES[boxPokes - 1], 2200)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Left click is a pat on the head. The menu is on the context menu, so a
|
|
286
|
+
// stray click can no longer open or close the panel by accident.
|
|
287
|
+
scene.addEventListener('pointerup', function () {
|
|
288
|
+
if (endDrag()) return
|
|
289
|
+
// An unhatched save is a box, whether or not one exists yet.
|
|
290
|
+
if (view.hatched !== true) {
|
|
291
|
+
pokeBox()
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
if (!view.dead) flash('pet')
|
|
295
|
+
})
|
|
296
|
+
scene.addEventListener('pointercancel', function () { endDrag() })
|
|
297
|
+
scene.addEventListener('contextmenu', function (event) {
|
|
298
|
+
event.preventDefault()
|
|
299
|
+
setOpen(!isOpen)
|
|
300
|
+
if (isOpen && view.pig !== null) flash('pet')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// ---- life ----
|
|
304
|
+
clampPig()
|
|
305
|
+
setOpen(isOpen)
|
|
306
|
+
render(view)
|
|
307
|
+
refresh()
|
|
308
|
+
pollTimer = window.setInterval(refresh, POLL_MS)
|
|
309
|
+
|
|
310
|
+
// B6: the pig greets the owner once per page load (the host decides
|
|
311
|
+
// whether it has been away long enough), then speaks up now and then.
|
|
312
|
+
var chatTimer = null
|
|
313
|
+
function scheduleChat() {
|
|
314
|
+
var minutes = IDLE_CHAT_MINUTES.min + Math.random() * (IDLE_CHAT_MINUTES.max - IDLE_CHAT_MINUTES.min)
|
|
315
|
+
chatTimer = window.setTimeout(function () {
|
|
316
|
+
if (!stopped && !busy && view.pig !== null) send('chat', { reason: 'idle' })
|
|
317
|
+
scheduleChat()
|
|
318
|
+
}, minutes * 60000)
|
|
319
|
+
}
|
|
320
|
+
var greetTimer = window.setTimeout(function () {
|
|
321
|
+
if (!stopped && view.pig !== null) send('chat', { reason: 'enter' })
|
|
322
|
+
}, GREET_DELAY_MS)
|
|
323
|
+
scheduleChat()
|
|
324
|
+
// Optional call: minimal test environments stub a window without listeners.
|
|
325
|
+
// Optional call: minimal test environments stub a window without listeners.
|
|
326
|
+
function onResize() {
|
|
327
|
+
clampPig()
|
|
328
|
+
fitPanel()
|
|
329
|
+
}
|
|
330
|
+
window.addEventListener?.('resize', onResize)
|
|
331
|
+
|
|
332
|
+
// ---- developer mode: Ctrl+Shift+D ----
|
|
333
|
+
function setDevMode(on) {
|
|
334
|
+
devMode = on === true
|
|
335
|
+
writeStore(DEV_KEY, devMode ? '1' : '0')
|
|
336
|
+
host.setAttribute('data-dev', devMode ? 'true' : 'false')
|
|
337
|
+
paintBar()
|
|
338
|
+
if (devMode) {
|
|
339
|
+
setOpen(true)
|
|
340
|
+
select('dev')
|
|
341
|
+
showBubble('🔧 开发者模式已开', 2000)
|
|
342
|
+
} else {
|
|
343
|
+
if (tab === 'dev') select('status')
|
|
344
|
+
showBubble('开发者模式已关', 1600)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
devMode = readStore(DEV_KEY) === '1'
|
|
349
|
+
host.setAttribute('data-dev', devMode ? 'true' : 'false')
|
|
350
|
+
if (devMode) paintBar()
|
|
351
|
+
|
|
352
|
+
function onKeyDown(event) {
|
|
353
|
+
if (event.ctrlKey && event.shiftKey && (event.key === 'D' || event.key === 'd')) {
|
|
354
|
+
event.preventDefault()
|
|
355
|
+
setDevMode(!devMode)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
window.addEventListener?.('keydown', onKeyDown)
|
|
359
|
+
|
|
360
|
+
// Also reachable from the console, for when the panel is off screen.
|
|
361
|
+
try {
|
|
362
|
+
/** @type {any} */ (window).dshPigDev = {
|
|
363
|
+
on: function () { setDevMode(true) },
|
|
364
|
+
off: function () { setDevMode(false) },
|
|
365
|
+
toggle: function () { setDevMode(!devMode) },
|
|
366
|
+
}
|
|
367
|
+
} catch (error) { /* frozen window */ }
|
|
368
|
+
|
|
369
|
+
function dispose() {
|
|
370
|
+
stopped = true
|
|
371
|
+
window.removeEventListener?.('resize', onResize)
|
|
372
|
+
// #11: the dev shortcut and the console handle outlived the pig, so a
|
|
373
|
+
// reload could toggle a panel that had already been disposed.
|
|
374
|
+
window.removeEventListener?.('keydown', onKeyDown)
|
|
375
|
+
try { delete (/** @type {any} */ (window)).dshPigDev } catch (error) { /* frozen window */ }
|
|
376
|
+
if (pollTimer !== null) window.clearInterval(pollTimer)
|
|
377
|
+
if (chatTimer !== null) window.clearTimeout(chatTimer)
|
|
378
|
+
window.clearTimeout(greetTimer)
|
|
379
|
+
fx.dispose()
|
|
380
|
+
pollTimer = null
|
|
381
|
+
host.remove()
|
|
382
|
+
style.remove()
|
|
383
|
+
font.remove()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return dispose
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
exports.name = 'dsh-piggy'
|
|
390
|
+
exports.apply = apply
|
|
391
|
+
return module.exports
|
|
392
|
+
},
|
|
393
|
+
})
|