dev-react-microstore 4.0.0 → 5.0.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/README.md +101 -2
- package/dist/index.d.mts +85 -2
- package/dist/index.d.ts +85 -2
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/example/README.md +54 -0
- package/example/eslint.config.js +28 -0
- package/example/index.html +13 -0
- package/example/package-lock.json +3382 -0
- package/example/package.json +29 -0
- package/example/public/index.html +98 -0
- package/example/public/vite.svg +1 -0
- package/example/src/App.css +613 -0
- package/example/src/App.tsx +34 -0
- package/example/src/assets/react.svg +1 -0
- package/example/src/components/Counter.tsx +112 -0
- package/example/src/components/CustomCompare.tsx +466 -0
- package/example/src/components/Logs.tsx +28 -0
- package/example/src/components/Search.tsx +38 -0
- package/example/src/components/ThemeToggle.tsx +25 -0
- package/example/src/components/TodoList.tsx +63 -0
- package/example/src/components/UserManager.tsx +68 -0
- package/example/src/index.css +68 -0
- package/example/src/main.tsx +10 -0
- package/example/src/store.ts +223 -0
- package/example/src/vite-env.d.ts +1 -0
- package/example/tsconfig.app.json +26 -0
- package/example/tsconfig.json +7 -0
- package/example/tsconfig.node.json +25 -0
- package/example/vite.config.ts +7 -0
- package/package.json +10 -3
- package/src/index.ts +247 -12
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useStoreSelector } from '../../../src/index'
|
|
2
|
+
import store from '../store'
|
|
3
|
+
import { useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
export default function Counter() {
|
|
6
|
+
const { count } = useStoreSelector(store, ['count'])
|
|
7
|
+
|
|
8
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
9
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
10
|
+
const isLongPressRef = useRef(false)
|
|
11
|
+
|
|
12
|
+
const START_DELAY = 250
|
|
13
|
+
const REPEAT_DELAY = 50
|
|
14
|
+
|
|
15
|
+
const handleCountIncrement = () => {
|
|
16
|
+
const {count} = store.select(['count'])
|
|
17
|
+
store.set({ count: count + 1 })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const handleCountDecrement = () => {
|
|
21
|
+
const {count} = store.select(['count'])
|
|
22
|
+
store.set({ count: count - 1 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const handleCountReset = () => {
|
|
26
|
+
store.set({ count: 0 })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const startIncrement = () => {
|
|
30
|
+
isLongPressRef.current = false
|
|
31
|
+
timeoutRef.current = setTimeout(() => {
|
|
32
|
+
isLongPressRef.current = true
|
|
33
|
+
handleCountIncrement()
|
|
34
|
+
intervalRef.current = setInterval(() => {
|
|
35
|
+
handleCountIncrement()
|
|
36
|
+
}, REPEAT_DELAY)
|
|
37
|
+
}, START_DELAY)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const startDecrement = () => {
|
|
41
|
+
isLongPressRef.current = false
|
|
42
|
+
timeoutRef.current = setTimeout(() => {
|
|
43
|
+
isLongPressRef.current = true
|
|
44
|
+
handleCountDecrement()
|
|
45
|
+
intervalRef.current = setInterval(() => {
|
|
46
|
+
handleCountDecrement()
|
|
47
|
+
}, REPEAT_DELAY)
|
|
48
|
+
}, START_DELAY)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stopPress = () => {
|
|
52
|
+
if (timeoutRef.current) {
|
|
53
|
+
clearTimeout(timeoutRef.current)
|
|
54
|
+
timeoutRef.current = null
|
|
55
|
+
}
|
|
56
|
+
if (intervalRef.current) {
|
|
57
|
+
clearInterval(intervalRef.current)
|
|
58
|
+
intervalRef.current = null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleIncrementClick = () => {
|
|
63
|
+
if (!isLongPressRef.current) {
|
|
64
|
+
handleCountIncrement()
|
|
65
|
+
}
|
|
66
|
+
isLongPressRef.current = false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleDecrementClick = () => {
|
|
70
|
+
if (!isLongPressRef.current) {
|
|
71
|
+
handleCountDecrement()
|
|
72
|
+
}
|
|
73
|
+
isLongPressRef.current = false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<section className="section">
|
|
78
|
+
<h3>🔢 Counter (with validation middleware)</h3>
|
|
79
|
+
<div className="counter">
|
|
80
|
+
<span className="count-display">Count: {count}</span>
|
|
81
|
+
<div className="button-group">
|
|
82
|
+
<button
|
|
83
|
+
onMouseDown={startDecrement}
|
|
84
|
+
onMouseUp={stopPress}
|
|
85
|
+
onMouseLeave={stopPress}
|
|
86
|
+
onTouchStart={startDecrement}
|
|
87
|
+
onTouchEnd={stopPress}
|
|
88
|
+
onClick={handleDecrementClick}
|
|
89
|
+
>
|
|
90
|
+
-1
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
onMouseDown={startIncrement}
|
|
94
|
+
onMouseUp={stopPress}
|
|
95
|
+
onMouseLeave={stopPress}
|
|
96
|
+
onTouchStart={startIncrement}
|
|
97
|
+
onTouchEnd={stopPress}
|
|
98
|
+
onClick={handleIncrementClick}
|
|
99
|
+
>
|
|
100
|
+
+1
|
|
101
|
+
</button>
|
|
102
|
+
<button onClick={handleCountReset}>Reset</button>
|
|
103
|
+
</div>
|
|
104
|
+
<p className="help-text">
|
|
105
|
+
Negative values are validated and blocked by middleware
|
|
106
|
+
<br />
|
|
107
|
+
<strong>Hold buttons to rapidly increment/decrement!</strong>
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useStoreSelector } from '../../../src/index'
|
|
3
|
+
import store from '../store'
|
|
4
|
+
|
|
5
|
+
// Child component that only animates when player health changes
|
|
6
|
+
function PlayerHealthBar() {
|
|
7
|
+
const { gameState } = useStoreSelector(store, [
|
|
8
|
+
{
|
|
9
|
+
gameState: (prev, next) => {
|
|
10
|
+
// Only re-render if player health changes
|
|
11
|
+
return prev.player.health === next.player.health
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="health-bar" key={gameState.player.health}>
|
|
18
|
+
<div className="bar-label">Player Health</div>
|
|
19
|
+
<div className="bar-container">
|
|
20
|
+
<div
|
|
21
|
+
className="bar-fill health-fill animate-change"
|
|
22
|
+
style={{ width: `${gameState.player.health}%` }}
|
|
23
|
+
>
|
|
24
|
+
{gameState.player.health}/100
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Child component that only animates when player mana changes
|
|
32
|
+
function PlayerManaBar() {
|
|
33
|
+
const { gameState } = useStoreSelector(store, [
|
|
34
|
+
{
|
|
35
|
+
gameState: (prev, next) => {
|
|
36
|
+
// Only re-render if player mana changes
|
|
37
|
+
return prev.player.mana === next.player.mana
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="mana-bar" key={gameState.player.mana}>
|
|
44
|
+
<div className="bar-label">Player Mana</div>
|
|
45
|
+
<div className="bar-container">
|
|
46
|
+
<div
|
|
47
|
+
className="bar-fill mana-fill animate-change"
|
|
48
|
+
style={{ width: `${gameState.player.mana * 2}%` }}
|
|
49
|
+
>
|
|
50
|
+
{gameState.player.mana}/50
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Child component that only animates when enemy health changes
|
|
58
|
+
function EnemyHealthBar() {
|
|
59
|
+
const { gameState } = useStoreSelector(store, [
|
|
60
|
+
{
|
|
61
|
+
gameState: (prev, next) => {
|
|
62
|
+
// Only re-render if enemy health, name, or max health changes
|
|
63
|
+
return prev.enemy.health === next.enemy.health &&
|
|
64
|
+
prev.enemy.name === next.enemy.name
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
// Get max health based on enemy type
|
|
70
|
+
const getMaxHealth = (enemyName: string) => {
|
|
71
|
+
const enemies: Record<string, number> = {
|
|
72
|
+
'Goblin': 30,
|
|
73
|
+
'Orc': 50,
|
|
74
|
+
'Troll': 75,
|
|
75
|
+
'Dragon': 120,
|
|
76
|
+
'Wizard': 80,
|
|
77
|
+
'Demon Lord': 150
|
|
78
|
+
}
|
|
79
|
+
return enemies[enemyName] || 30
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const maxHealth = getMaxHealth(gameState.enemy.name)
|
|
83
|
+
const healthPercent = (gameState.enemy.health / maxHealth) * 100
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="enemy-health" key={`${gameState.enemy.name}-${gameState.enemy.health}`}>
|
|
87
|
+
<div className="bar-label">Enemy: {gameState.enemy.name}</div>
|
|
88
|
+
<div className="bar-container">
|
|
89
|
+
<div
|
|
90
|
+
className="bar-fill enemy-fill animate-change"
|
|
91
|
+
style={{ width: `${healthPercent}%` }}
|
|
92
|
+
>
|
|
93
|
+
{gameState.enemy.health}/{maxHealth}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Child component that only animates when UI notifications change
|
|
101
|
+
function NotificationPanel() {
|
|
102
|
+
const { gameState } = useStoreSelector(store, [
|
|
103
|
+
{
|
|
104
|
+
gameState: (prev, next) => {
|
|
105
|
+
// Only re-render if notifications change
|
|
106
|
+
return JSON.stringify(prev.ui.notifications) === JSON.stringify(next.ui.notifications)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="notifications" key={gameState.ui.notifications.length}>
|
|
113
|
+
<div className="notification-header">Combat Log</div>
|
|
114
|
+
<div className="notification-list animate-change">
|
|
115
|
+
{gameState.ui.notifications.length === 0 ? (
|
|
116
|
+
<div className="no-notifications">Ready for battle!</div>
|
|
117
|
+
) : (
|
|
118
|
+
gameState.ui.notifications.slice(-5).map((notification, index) => (
|
|
119
|
+
<div key={index} className="notification-item">
|
|
120
|
+
{notification}
|
|
121
|
+
</div>
|
|
122
|
+
))
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default function CustomCompare() {
|
|
130
|
+
const { gameState } = useStoreSelector(store, ['gameState'])
|
|
131
|
+
|
|
132
|
+
// Handle enemy AI turn automatically
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (gameState.turn === 'enemy' && !gameState.isProcessingTurn && gameState.player.health > 0) {
|
|
135
|
+
// Set processing flag to prevent multiple AI actions
|
|
136
|
+
store.set({
|
|
137
|
+
gameState: {
|
|
138
|
+
...gameState,
|
|
139
|
+
isProcessingTurn: true
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// AI action after 1 second delay
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
const currentState = store.get().gameState
|
|
146
|
+
|
|
147
|
+
if (currentState.player.health <= 0) return // Player already dead
|
|
148
|
+
|
|
149
|
+
// AI chooses action based on enemy type
|
|
150
|
+
const enemyActions = {
|
|
151
|
+
'Goblin': ['scratch', 'bite'],
|
|
152
|
+
'Orc': ['club', 'roar', 'charge'],
|
|
153
|
+
'Troll': ['smash', 'regenerate', 'throw_rock'],
|
|
154
|
+
'Dragon': ['fire_breath', 'claw', 'wing_attack'],
|
|
155
|
+
'Wizard': ['magic_missile', 'lightning', 'heal'],
|
|
156
|
+
'Demon Lord': ['dark_magic', 'soul_drain', 'summon']
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const availableActions = enemyActions[currentState.enemy.name as keyof typeof enemyActions] || ['attack']
|
|
160
|
+
const action = availableActions[Math.floor(Math.random() * availableActions.length)]
|
|
161
|
+
|
|
162
|
+
let damage = 0
|
|
163
|
+
let actionText = ''
|
|
164
|
+
let newEnemyHealth = currentState.enemy.health
|
|
165
|
+
|
|
166
|
+
switch (action) {
|
|
167
|
+
case 'scratch':
|
|
168
|
+
case 'bite':
|
|
169
|
+
case 'attack':
|
|
170
|
+
damage = 8
|
|
171
|
+
actionText = `${currentState.enemy.name} attacks for ${damage} damage!`
|
|
172
|
+
break
|
|
173
|
+
case 'club':
|
|
174
|
+
case 'smash':
|
|
175
|
+
case 'claw':
|
|
176
|
+
damage = 12
|
|
177
|
+
actionText = `${currentState.enemy.name} uses ${action} for ${damage} damage!`
|
|
178
|
+
break
|
|
179
|
+
case 'fire_breath':
|
|
180
|
+
case 'dark_magic':
|
|
181
|
+
damage = 18
|
|
182
|
+
actionText = `${currentState.enemy.name} casts ${action.replace('_', ' ')} for ${damage} damage!`
|
|
183
|
+
break
|
|
184
|
+
case 'regenerate':
|
|
185
|
+
case 'heal': {
|
|
186
|
+
const maxHealth = getMaxHealth(currentState.enemy.name)
|
|
187
|
+
const healAmount = 15
|
|
188
|
+
newEnemyHealth = Math.min(maxHealth, currentState.enemy.health + healAmount)
|
|
189
|
+
actionText = `${currentState.enemy.name} heals for ${newEnemyHealth - currentState.enemy.health} HP!`
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
damage = 10
|
|
194
|
+
actionText = `${currentState.enemy.name} uses ${action.replace('_', ' ')} for ${damage} damage!`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const newPlayerHealth = Math.max(0, currentState.player.health - damage)
|
|
198
|
+
const newNotifications = [...currentState.ui.notifications, actionText]
|
|
199
|
+
|
|
200
|
+
store.set({
|
|
201
|
+
gameState: {
|
|
202
|
+
...currentState,
|
|
203
|
+
player: { ...currentState.player, health: newPlayerHealth },
|
|
204
|
+
enemy: { ...currentState.enemy, health: newEnemyHealth },
|
|
205
|
+
ui: { ...currentState.ui, notifications: newNotifications },
|
|
206
|
+
turn: 'player',
|
|
207
|
+
isProcessingTurn: false
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}, 1000)
|
|
211
|
+
}
|
|
212
|
+
}, [gameState.turn, gameState.isProcessingTurn, gameState])
|
|
213
|
+
|
|
214
|
+
const getMaxHealth = (enemyName: string) => {
|
|
215
|
+
const enemies: Record<string, number> = {
|
|
216
|
+
'Goblin': 30,
|
|
217
|
+
'Orc': 50,
|
|
218
|
+
'Troll': 75,
|
|
219
|
+
'Dragon': 120,
|
|
220
|
+
'Wizard': 80,
|
|
221
|
+
'Demon Lord': 150
|
|
222
|
+
}
|
|
223
|
+
return enemies[enemyName] || 30
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const playerAction = (actionType: 'attack' | 'heal' | 'magic' | 'restore' | 'rest') => {
|
|
227
|
+
if (gameState.turn !== 'player' || gameState.isProcessingTurn) return
|
|
228
|
+
|
|
229
|
+
let newPlayerHealth = gameState.player.health
|
|
230
|
+
let newPlayerMana = gameState.player.mana
|
|
231
|
+
let newEnemyHealth = gameState.enemy.health
|
|
232
|
+
let actionText = ''
|
|
233
|
+
let enemyDefeated = false
|
|
234
|
+
|
|
235
|
+
switch (actionType) {
|
|
236
|
+
case 'attack': {
|
|
237
|
+
const damage = 15
|
|
238
|
+
newEnemyHealth = Math.max(0, gameState.enemy.health - damage)
|
|
239
|
+
actionText = `You attack ${gameState.enemy.name} for ${damage} damage!`
|
|
240
|
+
enemyDefeated = newEnemyHealth === 0
|
|
241
|
+
break
|
|
242
|
+
}
|
|
243
|
+
case 'heal': {
|
|
244
|
+
if (gameState.player.mana < 10) {
|
|
245
|
+
actionText = 'Not enough mana for healing!'
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
const healAmount = 25
|
|
249
|
+
newPlayerHealth = Math.min(100, gameState.player.health + healAmount)
|
|
250
|
+
newPlayerMana = gameState.player.mana - 10
|
|
251
|
+
actionText = `You heal for ${newPlayerHealth - gameState.player.health} HP! (-10 mana)`
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
case 'magic': {
|
|
255
|
+
if (gameState.player.mana < 10) {
|
|
256
|
+
actionText = 'Not enough mana!'
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
const magicDamage = 25
|
|
260
|
+
newEnemyHealth = Math.max(0, gameState.enemy.health - magicDamage)
|
|
261
|
+
newPlayerMana = gameState.player.mana - 10
|
|
262
|
+
actionText = `You cast fireball for ${magicDamage} damage!`
|
|
263
|
+
enemyDefeated = newEnemyHealth === 0
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
case 'restore': {
|
|
267
|
+
const restoreAmount = 20
|
|
268
|
+
newPlayerMana = Math.min(50, gameState.player.mana + restoreAmount)
|
|
269
|
+
actionText = `You meditate and restore ${newPlayerMana - gameState.player.mana} mana!`
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
case 'rest': {
|
|
273
|
+
const restoreAmount = 15
|
|
274
|
+
newPlayerHealth = Math.min(100, gameState.player.health + restoreAmount)
|
|
275
|
+
actionText = `You rest and restore ${newPlayerHealth - gameState.player.health} HP!`
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let finalNotifications = [...gameState.ui.notifications, actionText]
|
|
281
|
+
let nextTurn: 'player' | 'enemy' = 'enemy'
|
|
282
|
+
let newEnemy = gameState.enemy
|
|
283
|
+
|
|
284
|
+
if (enemyDefeated && gameState.enemy.isAlive) {
|
|
285
|
+
// Enemy defeated - check if it's the final boss
|
|
286
|
+
if (gameState.enemy.name === 'Demon Lord') {
|
|
287
|
+
// Player wins the game!
|
|
288
|
+
finalNotifications = ['Demon Lord defeated! You are victorious!']
|
|
289
|
+
newEnemy = { ...gameState.enemy, health: 0, isAlive: false }
|
|
290
|
+
nextTurn = 'player'
|
|
291
|
+
} else {
|
|
292
|
+
// Spawn next enemy
|
|
293
|
+
const enemies = [
|
|
294
|
+
{ name: 'Goblin', health: 30, damage: 5 },
|
|
295
|
+
{ name: 'Orc', health: 50, damage: 8 },
|
|
296
|
+
{ name: 'Troll', health: 75, damage: 12 },
|
|
297
|
+
{ name: 'Dragon', health: 120, damage: 20 },
|
|
298
|
+
{ name: 'Wizard', health: 80, damage: 25 },
|
|
299
|
+
{ name: 'Demon Lord', health: 150, damage: 30 }
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
const currentEnemyIndex = enemies.findIndex(e => e.name === gameState.enemy.name)
|
|
303
|
+
const nextEnemyIndex = Math.min(currentEnemyIndex + 1, enemies.length - 1)
|
|
304
|
+
const nextEnemy = enemies[nextEnemyIndex]
|
|
305
|
+
|
|
306
|
+
// Clear all logs and only keep the defeat message
|
|
307
|
+
finalNotifications = [`${gameState.enemy.name} defeated! ${nextEnemy.name} appears!`]
|
|
308
|
+
newEnemy = { ...nextEnemy, isAlive: true }
|
|
309
|
+
nextTurn = 'player' // Stay on player turn for new enemy
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Just update enemy health if not defeated
|
|
313
|
+
newEnemy = { ...gameState.enemy, health: newEnemyHealth }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
store.set({
|
|
317
|
+
gameState: {
|
|
318
|
+
...gameState,
|
|
319
|
+
player: { ...gameState.player, health: newPlayerHealth, mana: newPlayerMana },
|
|
320
|
+
enemy: newEnemy,
|
|
321
|
+
ui: { ...gameState.ui, notifications: finalNotifications },
|
|
322
|
+
turn: nextTurn,
|
|
323
|
+
isProcessingTurn: false
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const resetGame = () => {
|
|
329
|
+
store.set({
|
|
330
|
+
gameState: {
|
|
331
|
+
player: {
|
|
332
|
+
name: 'Player 1',
|
|
333
|
+
health: 100,
|
|
334
|
+
mana: 50,
|
|
335
|
+
level: 1,
|
|
336
|
+
experience: 0
|
|
337
|
+
},
|
|
338
|
+
enemy: {
|
|
339
|
+
name: 'Goblin',
|
|
340
|
+
health: 30,
|
|
341
|
+
damage: 5,
|
|
342
|
+
isAlive: true
|
|
343
|
+
},
|
|
344
|
+
ui: {
|
|
345
|
+
showInventory: false,
|
|
346
|
+
showMap: false,
|
|
347
|
+
notifications: []
|
|
348
|
+
},
|
|
349
|
+
turn: 'player',
|
|
350
|
+
isProcessingTurn: false
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const isPlayerDefeated = gameState.player.health <= 0
|
|
356
|
+
const isPlayerTurn = gameState.turn === 'player'
|
|
357
|
+
const canAct = isPlayerTurn && !gameState.isProcessingTurn && !isPlayerDefeated
|
|
358
|
+
const isGameWon = gameState.enemy.name === 'Demon Lord' && !gameState.enemy.isAlive
|
|
359
|
+
|
|
360
|
+
// Add defeat log when player is defeated
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
if (isPlayerDefeated && gameState.ui.notifications.length > 0 &&
|
|
363
|
+
!gameState.ui.notifications.some(n => n.includes('You have been defeated'))) {
|
|
364
|
+
store.set({
|
|
365
|
+
gameState: {
|
|
366
|
+
...gameState,
|
|
367
|
+
ui: {
|
|
368
|
+
...gameState.ui,
|
|
369
|
+
notifications: [...gameState.ui.notifications, 'You have been defeated!']
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
}, [isPlayerDefeated, gameState])
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<section className="section">
|
|
378
|
+
<h3>⚙️ Custom Compare Functions Demo</h3>
|
|
379
|
+
<p className="demo-description">
|
|
380
|
+
Each UI component below uses a custom compare function to only re-render when its specific data changes.
|
|
381
|
+
Watch the flash animations - they only trigger when that component's subscribed data updates!
|
|
382
|
+
</p>
|
|
383
|
+
|
|
384
|
+
{isPlayerDefeated ? (
|
|
385
|
+
<div className="game-over">
|
|
386
|
+
<div className="defeated-message">DEFEATED</div>
|
|
387
|
+
<button onClick={resetGame}>Reset Game</button>
|
|
388
|
+
</div>
|
|
389
|
+
) : isGameWon ? (
|
|
390
|
+
<div className="game-over">
|
|
391
|
+
<div className="defeated-message" style={{color: '#4CAF50'}}>VICTORY!</div>
|
|
392
|
+
<button onClick={resetGame}>Play Again</button>
|
|
393
|
+
</div>
|
|
394
|
+
) : (
|
|
395
|
+
<>
|
|
396
|
+
<div className="turn-indicator">
|
|
397
|
+
{gameState.isProcessingTurn ? (
|
|
398
|
+
<div className="processing-turn">Processing...</div>
|
|
399
|
+
) : (
|
|
400
|
+
<div className={`turn-display ${gameState.turn}`}>
|
|
401
|
+
{gameState.turn === 'player' ? '🗡️ Your Turn' : '👹 Enemy Turn'}
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div className="game-controls">
|
|
407
|
+
<button
|
|
408
|
+
onClick={() => playerAction('attack')}
|
|
409
|
+
disabled={!canAct}
|
|
410
|
+
>
|
|
411
|
+
⚔️ Attack (15 dmg)
|
|
412
|
+
</button>
|
|
413
|
+
<button
|
|
414
|
+
onClick={() => playerAction('heal')}
|
|
415
|
+
disabled={!canAct}
|
|
416
|
+
>
|
|
417
|
+
❤️ Heal (+25 HP, -10 mana)
|
|
418
|
+
</button>
|
|
419
|
+
<button
|
|
420
|
+
onClick={() => playerAction('magic')}
|
|
421
|
+
disabled={!canAct || gameState.player.mana < 10}
|
|
422
|
+
>
|
|
423
|
+
🔥 Fireball (25 dmg, -10 mana)
|
|
424
|
+
</button>
|
|
425
|
+
<button
|
|
426
|
+
onClick={() => playerAction('restore')}
|
|
427
|
+
disabled={!canAct}
|
|
428
|
+
>
|
|
429
|
+
🧘 Restore Mana (+20)
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
onClick={() => playerAction('rest')}
|
|
433
|
+
disabled={!canAct}
|
|
434
|
+
>
|
|
435
|
+
😴 Rest (+15 HP)
|
|
436
|
+
</button>
|
|
437
|
+
<button onClick={resetGame} className="small">Reset Game</button>
|
|
438
|
+
</div>
|
|
439
|
+
</>
|
|
440
|
+
)}
|
|
441
|
+
|
|
442
|
+
<div className="game-ui">
|
|
443
|
+
<div className="player-stats">
|
|
444
|
+
<PlayerHealthBar />
|
|
445
|
+
<PlayerManaBar />
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div className="enemy-stats">
|
|
449
|
+
<EnemyHealthBar />
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div className="ui-panel">
|
|
453
|
+
<NotificationPanel />
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<div className="help-text">
|
|
458
|
+
<strong>Fine-grained subscriptions:</strong> Each health/mana bar only re-renders when its specific data changes.
|
|
459
|
+
<br />
|
|
460
|
+
<strong>Performance optimization:</strong> PlayerHealthBar won't re-render when mana changes, EnemyHealthBar won't re-render when player stats change, etc.
|
|
461
|
+
<br />
|
|
462
|
+
The flash animations demonstrate exactly when each component updates - notice how they're independent!
|
|
463
|
+
</div>
|
|
464
|
+
</section>
|
|
465
|
+
)
|
|
466
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useStoreSelector } from '../../../src/index'
|
|
2
|
+
import store from '../store'
|
|
3
|
+
|
|
4
|
+
export default function Logs() {
|
|
5
|
+
const { logs } = useStoreSelector(store, ['logs'])
|
|
6
|
+
|
|
7
|
+
const handleClearLogs = () => {
|
|
8
|
+
store.set({ logs: [] })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<section className="section">
|
|
13
|
+
<h3>📜 Middleware Logs</h3>
|
|
14
|
+
<div className="logs-header">
|
|
15
|
+
<span>{logs.length} logs</span>
|
|
16
|
+
<button onClick={handleClearLogs} className="small">Clear</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div className="logs">
|
|
19
|
+
{logs.map(log => (
|
|
20
|
+
<div key={log.id} className={`log-entry ${log.type}`}>
|
|
21
|
+
<span className="timestamp">[{log.timestamp}]</span>
|
|
22
|
+
<pre className="message">{log.message}</pre>
|
|
23
|
+
</div>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useStoreSelector } from '../../../src/index'
|
|
2
|
+
import store, { updateSearchQuery } from '../store'
|
|
3
|
+
|
|
4
|
+
export default function Search() {
|
|
5
|
+
const { searchResults, isSearching } = useStoreSelector(store, ['searchResults', 'isSearching'])
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<section className="section">
|
|
9
|
+
<h3>🔍 Debounced Search</h3>
|
|
10
|
+
<div className="search-form">
|
|
11
|
+
<input
|
|
12
|
+
type="text"
|
|
13
|
+
placeholder="Search technologies..."
|
|
14
|
+
onChange={(e) => updateSearchQuery(e.target.value)}
|
|
15
|
+
/>
|
|
16
|
+
{isSearching && <span className="searching">Searching...</span>}
|
|
17
|
+
</div>
|
|
18
|
+
<div className="search-results">
|
|
19
|
+
{searchResults.length > 0 ? (
|
|
20
|
+
<div>
|
|
21
|
+
<p>{searchResults.length} results:</p>
|
|
22
|
+
<ul>
|
|
23
|
+
{searchResults.map((result: string) => (
|
|
24
|
+
<li key={result}>{result}</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
) : (
|
|
30
|
+
<p>Type to search technologies...</p>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
<p className="help-text">
|
|
34
|
+
Search is debounced by 300ms - watch the logs!
|
|
35
|
+
</p>
|
|
36
|
+
</section>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useStoreSelector } from '../../../src/index'
|
|
2
|
+
import store from '../store'
|
|
3
|
+
|
|
4
|
+
export default function ThemeToggle() {
|
|
5
|
+
const { theme } = useStoreSelector(store, ['theme'])
|
|
6
|
+
|
|
7
|
+
const handleThemeToggle = () => {
|
|
8
|
+
store.set({ theme: theme === 'light' ? 'dark' : 'light' })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<section className="section">
|
|
13
|
+
<h3>🎨 Theme (persisted)</h3>
|
|
14
|
+
<div className="theme-controls">
|
|
15
|
+
<span>Current theme: <strong>{theme}</strong></span>
|
|
16
|
+
<button onClick={handleThemeToggle}>
|
|
17
|
+
Switch to {theme === 'light' ? 'dark' : 'light'}
|
|
18
|
+
</button>
|
|
19
|
+
<p className="help-text">
|
|
20
|
+
Theme persists across page reloads!
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
</section>
|
|
24
|
+
)
|
|
25
|
+
}
|