ps99-api 2.3.3 → 2.5.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/.github/workflows/release-on-main.yml +1 -2
- package/.idea/node-ps99-api.iml +1 -0
- package/.idea/runConfigurations/test_changing.xml +1 -1
- package/README.md +1 -1
- package/debug_currency.json +57 -0
- package/debug_goals.json +271 -0
- package/dist/ps99-api.d.ts +2 -0
- package/dist/ps99-api.js +4 -1
- package/dist/ps99-api.js.map +1 -1
- package/dist/request-client/axios.js +6 -1
- package/dist/request-client/axios.js.map +1 -1
- package/dist/responses/collection/achievement.d.ts +2 -0
- package/dist/responses/collection/guild-battle.d.ts +2 -4
- package/dist/responses/collection/index.d.ts +1 -0
- package/dist/responses/collection/index.js +15 -0
- package/dist/responses/collection/index.js.map +1 -1
- package/dist/responses/collection/rank.d.ts +1 -0
- package/dist/responses/collection/rarity.d.ts +1 -0
- package/dist/responses/collection/seed.d.ts +1 -0
- package/dist/responses/exists.d.ts +2 -0
- package/example-web/react/package-lock.json +1504 -1470
- package/example-web/react2/package-lock.json +3082 -2759
- package/example-web/react2/package.json +6 -1
- package/example-web/react2/public/assets/gold_variant_icon.png +0 -0
- package/example-web/react2/public/assets/hot_cocoa_egg.png +0 -0
- package/example-web/react2/public/index.html +34 -31
- package/example-web/react2/src/App.tsx +6 -9
- package/example-web/react2/src/assets/guild_placeholder.png +0 -0
- package/example-web/react2/src/components/AchievementsComponent.tsx +78 -30
- package/example-web/react2/src/components/BoostsComponent.tsx +18 -14
- package/example-web/react2/src/components/BoothsComponent.tsx +24 -22
- package/example-web/react2/src/components/BoxesComponent.tsx +46 -21
- package/example-web/react2/src/components/BuffsComponent.tsx +83 -13
- package/example-web/react2/src/components/CharmsComponent.tsx +47 -29
- package/example-web/react2/src/components/CollectionConfigIndex.tsx +398 -35
- package/example-web/react2/src/components/CollectionsIndex.tsx +132 -23
- package/example-web/react2/src/components/CollectionsLayout.tsx +50 -0
- package/example-web/react2/src/components/CurrencyComponent.tsx +59 -50
- package/example-web/react2/src/components/DynamicCollectionConfigData.tsx +178 -11
- package/example-web/react2/src/components/EggsComponent.tsx +77 -44
- package/example-web/react2/src/components/EnchantsComponent.tsx +84 -34
- package/example-web/react2/src/components/FishingRodsComponent.tsx +38 -31
- package/example-web/react2/src/components/Footer.tsx +75 -18
- package/example-web/react2/src/components/FruitsComponent.tsx +41 -25
- package/example-web/react2/src/components/GenericFetchComponent.tsx +40 -22
- package/example-web/react2/src/components/GuildBattlesComponent.tsx +93 -65
- package/example-web/react2/src/components/Header.tsx +5 -37
- package/example-web/react2/src/components/HomePage.tsx +16 -16
- package/example-web/react2/src/components/HoverboardsComponent.tsx +25 -37
- package/example-web/react2/src/components/ImageComponent.tsx +255 -45
- package/example-web/react2/src/components/ItemCard.tsx +240 -0
- package/example-web/react2/src/components/LootboxesComponent.tsx +24 -16
- package/example-web/react2/src/components/MasteryComponent.tsx +93 -37
- package/example-web/react2/src/components/MerchantsComponent.tsx +46 -28
- package/example-web/react2/src/components/MiscItemsComponent.tsx +28 -26
- package/example-web/react2/src/components/PetsComponent.tsx +115 -46
- package/example-web/react2/src/components/PotionsComponent.tsx +53 -36
- package/example-web/react2/src/components/RandomEventsComponent.tsx +39 -35
- package/example-web/react2/src/components/RanksComponent.tsx +187 -71
- package/example-web/react2/src/components/RarityComponent.tsx +124 -13
- package/example-web/react2/src/components/RebirthsComponent.tsx +37 -26
- package/example-web/react2/src/components/SecretRoomsComponent.tsx +7 -13
- package/example-web/react2/src/components/SeedsComponent.tsx +45 -32
- package/example-web/react2/src/components/ShovelsComponent.tsx +23 -18
- package/example-web/react2/src/components/Sidebar.tsx +105 -0
- package/example-web/react2/src/components/SprinklersComponent.tsx +27 -19
- package/example-web/react2/src/components/Tooltip.tsx +36 -0
- package/example-web/react2/src/components/UltimatesComponent.tsx +29 -24
- package/example-web/react2/src/components/UpgradesComponent.tsx +99 -55
- package/example-web/react2/src/components/WateringCansComponent.tsx +23 -21
- package/example-web/react2/src/components/WorldsComponent.tsx +27 -24
- package/example-web/react2/src/components/XPPotionsComponent.tsx +29 -19
- package/example-web/react2/src/components/ZoneFlagsComponent.tsx +27 -23
- package/example-web/react2/src/components/ZonesComponent.tsx +56 -73
- package/example-web/react2/src/constants/collectionIcons.ts +29 -0
- package/example-web/react2/src/context/CollectionDataContext.tsx +62 -0
- package/example-web/react2/src/hooks/useExpandableList.ts +38 -0
- package/example-web/react2/src/hooks/useItemResolution.ts +351 -0
- package/example-web/react2/src/index.css +257 -0
- package/example-web/react2/src/index.tsx +2 -1
- package/example-web/react2/temp_model.rbxm +0 -0
- package/example-web/react2/webpack.config.js +103 -47
- package/package.json +11 -11
- package/ranks.json +1 -0
- package/repro_collection_fetch.ts +33 -0
- package/repro_image_fetch.ts +50 -0
- package/src/__tests__/__snapshots__/ps99-api-changes.ts.snap +34841 -10439
- package/src/__tests__/__snapshots__/ps99-api-live.ts.snap +160667 -67217
- package/src/ps99-api.ts +9 -5
- package/src/request-client/axios.ts +6 -2
- package/src/responses/collection/achievement.ts +2 -0
- package/src/responses/collection/guild-battle.ts +2 -4
- package/src/responses/collection/index.ts +1 -0
- package/src/responses/collection/rank.ts +1 -0
- package/src/responses/collection/rarity.ts +1 -0
- package/src/responses/collection/seed.ts +1 -0
- package/src/responses/exists.ts +2 -0
- package/tsconfig.json +1 -1
- package/example-web/react2/public/service-worker.js +0 -63
|
@@ -1,46 +1,34 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { CollectionConfigData } from "ps99-api";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import ItemCard from "./ItemCard";
|
|
4
|
+
import { useItemResolution } from "../hooks/useItemResolution";
|
|
5
5
|
|
|
6
6
|
const HoverboardsComponent: React.FC<{
|
|
7
|
-
configData
|
|
7
|
+
configData: CollectionConfigData<"Hoverboards">;
|
|
8
8
|
}> = ({ configData }) => {
|
|
9
|
+
const { getRarityColor } = useItemResolution();
|
|
10
|
+
const rarityColor = configData.Rarity ? getRarityColor(configData.Rarity) : null;
|
|
11
|
+
|
|
9
12
|
return (
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{data.DefaultJumpSpeedBoost && (
|
|
30
|
-
<p>Default Jump Speed Boost: {data.DefaultJumpSpeedBoost}</p>
|
|
31
|
-
)}
|
|
32
|
-
{data.IdleVolumeSpeedScale && (
|
|
33
|
-
<p>Idle Volume Speed Scale: {data.IdleVolumeSpeedScale}</p>
|
|
34
|
-
)}
|
|
35
|
-
{data.IdlePitchScale && (
|
|
36
|
-
<p>Idle Pitch Scale: {data.IdlePitchScale}</p>
|
|
37
|
-
)}
|
|
38
|
-
{data.BlockcastScale && <p>Blockcast Scale: {data.BlockcastScale}</p>}
|
|
39
|
-
{data.SkateMode && <p>Skate Mode: Yes</p>}
|
|
40
|
-
{data.IdleVolume && <p>Idle Volume: {data.IdleVolume}</p>}
|
|
41
|
-
</div>
|
|
42
|
-
)}
|
|
43
|
-
/>
|
|
13
|
+
<div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
|
|
14
|
+
<ItemCard
|
|
15
|
+
id={configData.DisplayName}
|
|
16
|
+
amount={1}
|
|
17
|
+
label={configData.DisplayName}
|
|
18
|
+
itemData={{
|
|
19
|
+
icon: configData.Icon,
|
|
20
|
+
rarity: configData.Rarity,
|
|
21
|
+
name: configData.DisplayName
|
|
22
|
+
}}
|
|
23
|
+
rarityColor={rarityColor}
|
|
24
|
+
/>
|
|
25
|
+
<div style={{ marginTop: '10px', fontSize: '0.9em', color: '#666', textAlign: 'center' }}>
|
|
26
|
+
<p>{configData.Desc}</p>
|
|
27
|
+
{configData.ProductId && <p>ID: {configData.ProductId}</p>}
|
|
28
|
+
{configData.HoverHeight && <p>Height: {configData.HoverHeight}</p>}
|
|
29
|
+
<p>Speed: {configData.DefaultJumpSpeedBoost || 'Normal'}</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
44
32
|
);
|
|
45
33
|
};
|
|
46
34
|
|
|
@@ -1,62 +1,272 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
1
|
+
import React, { useEffect, useState, useRef } from "react";
|
|
2
2
|
import { PetSimulator99API } from "ps99-api";
|
|
3
|
+
import { get, set, del } from "idb-keyval";
|
|
3
4
|
|
|
4
5
|
interface ImageProps {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
src: string;
|
|
7
|
+
alt: string;
|
|
8
|
+
style?: React.CSSProperties;
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
// Global Queue State
|
|
9
12
|
const MAX_CONCURRENT_REQUESTS = 5;
|
|
10
|
-
const
|
|
13
|
+
const MIN_CONCURRENT_REQUESTS = 1;
|
|
14
|
+
let currentConcurrencyLimit = MAX_CONCURRENT_REQUESTS;
|
|
15
|
+
|
|
16
|
+
interface QueueItem {
|
|
17
|
+
src: string;
|
|
18
|
+
resolve: (blob: Blob) => void;
|
|
19
|
+
reject: (err: any) => void;
|
|
20
|
+
retries: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const requestQueue: QueueItem[] = [];
|
|
11
24
|
let activeRequests = 0;
|
|
25
|
+
let isPaused = false;
|
|
26
|
+
let pauseTimeout: NodeJS.Timeout | null = null;
|
|
27
|
+
let backoffDelay = 2000; // Start at 2s
|
|
28
|
+
let consecutiveErrors = 0;
|
|
29
|
+
|
|
30
|
+
// Map to dedupe requests: src -> Promise<Blob>
|
|
31
|
+
const pendingRequests = new Map<string, Promise<Blob>>();
|
|
12
32
|
|
|
13
33
|
const processQueue = () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (nextRequest) {
|
|
17
|
-
activeRequests++;
|
|
18
|
-
nextRequest();
|
|
34
|
+
if (isPaused || activeRequests >= currentConcurrencyLimit || requestQueue.length === 0) {
|
|
35
|
+
return;
|
|
19
36
|
}
|
|
20
|
-
|
|
37
|
+
|
|
38
|
+
const request = requestQueue.shift();
|
|
39
|
+
if (!request) return;
|
|
40
|
+
|
|
41
|
+
activeRequests++;
|
|
42
|
+
const { src, resolve, reject, retries } = request;
|
|
43
|
+
|
|
44
|
+
// Use empty string as baseUrl to force relative paths (which hit the webpack proxy)
|
|
45
|
+
// This is the "shim" layer making it seamless for the component
|
|
46
|
+
const api = new PetSimulator99API({ baseUrl: "" });
|
|
47
|
+
|
|
48
|
+
api.getImage(src)
|
|
49
|
+
.then((data: any) => {
|
|
50
|
+
const blob = new Blob([data], { type: "image/png" });
|
|
51
|
+
resolve(blob);
|
|
52
|
+
set(`img-cache-${src}`, blob).catch(err => console.error("Cache set failed", err));
|
|
53
|
+
|
|
54
|
+
// Success: Reset backoff and slowly ramp up concurrency
|
|
55
|
+
consecutiveErrors = 0;
|
|
56
|
+
backoffDelay = 2000;
|
|
57
|
+
if (currentConcurrencyLimit < MAX_CONCURRENT_REQUESTS) {
|
|
58
|
+
currentConcurrencyLimit++;
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.catch((error: any) => {
|
|
62
|
+
console.error(`[ImageComponent] Failed ${src}`, error);
|
|
63
|
+
|
|
64
|
+
// Check for 429 or other rate limit errors
|
|
65
|
+
const isRateLimit = error?.response?.status === 429 || error?.status === 429;
|
|
66
|
+
|
|
67
|
+
if (isRateLimit) {
|
|
68
|
+
consecutiveErrors++;
|
|
69
|
+
console.warn(`Rate limit hit (429). Backoff: ${backoffDelay}ms. Concurrency: ${currentConcurrencyLimit}`);
|
|
70
|
+
|
|
71
|
+
// Throttle concurrency
|
|
72
|
+
currentConcurrencyLimit = MIN_CONCURRENT_REQUESTS;
|
|
73
|
+
|
|
74
|
+
// Backoff logic
|
|
75
|
+
handleRateLimit();
|
|
76
|
+
|
|
77
|
+
// Retry if under limit
|
|
78
|
+
if (retries < 3) {
|
|
79
|
+
requestQueue.unshift({ ...request, retries: retries + 1 });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// If not rate limit, or max retries exceeded, reject
|
|
84
|
+
reject(error);
|
|
85
|
+
})
|
|
86
|
+
.finally(() => {
|
|
87
|
+
activeRequests--;
|
|
88
|
+
processQueue();
|
|
89
|
+
});
|
|
21
90
|
};
|
|
22
91
|
|
|
23
|
-
const
|
|
24
|
-
|
|
92
|
+
const handleRateLimit = () => {
|
|
93
|
+
if (isPaused) return;
|
|
94
|
+
isPaused = true;
|
|
25
95
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
new Blob([imageBlob], { type: "image/png" }),
|
|
33
|
-
);
|
|
34
|
-
setImageUrl(url);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error("Error fetching image:", error);
|
|
37
|
-
} finally {
|
|
38
|
-
activeRequests--;
|
|
96
|
+
if (pauseTimeout) clearTimeout(pauseTimeout);
|
|
97
|
+
|
|
98
|
+
pauseTimeout = setTimeout(() => {
|
|
99
|
+
isPaused = false;
|
|
100
|
+
// Increase backoff for next time (Exponential)
|
|
101
|
+
backoffDelay = Math.min(backoffDelay * 2, 60000); // Cap at 60s
|
|
39
102
|
processQueue();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
}, backoffDelay);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// Helper to fetch blob with deduping
|
|
108
|
+
const fetchImageBlob = (src: string): Promise<Blob> => {
|
|
109
|
+
if (pendingRequests.has(src)) {
|
|
110
|
+
return pendingRequests.get(src)!;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const promise = new Promise<Blob>((resolve, reject) => {
|
|
114
|
+
requestQueue.push({ src, resolve, reject, retries: 0 });
|
|
115
|
+
processQueue();
|
|
116
|
+
}).finally(() => {
|
|
117
|
+
pendingRequests.delete(src);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
pendingRequests.set(src, promise);
|
|
121
|
+
return promise;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const ImageComponent: React.FC<ImageProps> = ({ src, alt, style }) => {
|
|
125
|
+
// console.log(`[ImageComponent] Mounting for ${src}`); // Commented out to reduce noise, enable if needed
|
|
126
|
+
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
127
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
128
|
+
const [hasError, setHasError] = useState<boolean>(false);
|
|
129
|
+
const mountedRef = useRef(true);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
mountedRef.current = true;
|
|
133
|
+
// Reset state on src change
|
|
134
|
+
setImageUrl(null);
|
|
135
|
+
setIsLoading(true);
|
|
136
|
+
setHasError(false);
|
|
137
|
+
|
|
138
|
+
// Check for local assets or full URLs (non-api)
|
|
139
|
+
if (src.startsWith("/") || src.startsWith("http")) {
|
|
140
|
+
setImageUrl(src);
|
|
141
|
+
setIsLoading(false);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const load = async () => {
|
|
146
|
+
try {
|
|
147
|
+
// 1. Try Cache
|
|
148
|
+
try {
|
|
149
|
+
const cached = await get<Blob>(`img-cache-${src}`);
|
|
150
|
+
if (cached && cached instanceof Blob) {
|
|
151
|
+
if (mountedRef.current) {
|
|
152
|
+
const url = URL.createObjectURL(cached);
|
|
153
|
+
setImageUrl(url);
|
|
154
|
+
setIsLoading(false);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
} else if (cached) {
|
|
158
|
+
await del(`img-cache-${src}`);
|
|
159
|
+
}
|
|
160
|
+
} catch (idbErr) {
|
|
161
|
+
// console.error(`[ImageComponent] IDB Error for ${src}:`, idbErr);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!mountedRef.current) return;
|
|
165
|
+
|
|
166
|
+
// 2. Fetch Network
|
|
167
|
+
const blob = await fetchImageBlob(src);
|
|
168
|
+
if (mountedRef.current) {
|
|
169
|
+
if (blob instanceof Blob) {
|
|
170
|
+
const url = URL.createObjectURL(blob);
|
|
171
|
+
setImageUrl(url);
|
|
172
|
+
setIsLoading(false);
|
|
173
|
+
} else {
|
|
174
|
+
console.error("Fetched data is not a Blob:", blob);
|
|
175
|
+
setHasError(true);
|
|
176
|
+
setIsLoading(false);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (err: any) {
|
|
180
|
+
if (mountedRef.current) {
|
|
181
|
+
// console.error(`[ImageComponent] Load failed for ${src}:`, err);
|
|
182
|
+
setHasError(true);
|
|
183
|
+
setIsLoading(false);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
load();
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
mountedRef.current = false;
|
|
192
|
+
};
|
|
193
|
+
}, [src]);
|
|
194
|
+
|
|
195
|
+
if (hasError) {
|
|
196
|
+
return (
|
|
197
|
+
<div style={{
|
|
198
|
+
width: '100%',
|
|
199
|
+
height: '100%',
|
|
200
|
+
display: 'flex',
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
justifyContent: 'center',
|
|
203
|
+
backgroundColor: 'rgba(0,0,0,0.05)',
|
|
204
|
+
borderRadius: '8px',
|
|
205
|
+
color: '#aaa',
|
|
206
|
+
fontSize: '1.5rem',
|
|
207
|
+
fontWeight: 'bold',
|
|
208
|
+
...style // Preserve layout styles
|
|
209
|
+
}}>
|
|
210
|
+
?
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (imageUrl) {
|
|
216
|
+
return (
|
|
217
|
+
<div style={{
|
|
218
|
+
display: 'flex',
|
|
219
|
+
justifyContent: 'center',
|
|
220
|
+
alignItems: 'center',
|
|
221
|
+
width: '100%',
|
|
222
|
+
height: '100%',
|
|
223
|
+
position: 'relative',
|
|
224
|
+
animation: "fadeIn 0.3s ease-in-out", // Smooth fade-in
|
|
225
|
+
}}>
|
|
226
|
+
<style>{`
|
|
227
|
+
@keyframes fadeIn {
|
|
228
|
+
from { opacity: 0; transform: scale(0.95); }
|
|
229
|
+
to { opacity: 1; transform: scale(1); }
|
|
230
|
+
}
|
|
231
|
+
`}</style>
|
|
232
|
+
<img
|
|
233
|
+
src={imageUrl}
|
|
234
|
+
alt={alt}
|
|
235
|
+
style={{
|
|
236
|
+
maxWidth: "100%",
|
|
237
|
+
maxHeight: "100%",
|
|
238
|
+
objectFit: "contain",
|
|
239
|
+
borderRadius: "8px",
|
|
240
|
+
...style
|
|
241
|
+
}}
|
|
242
|
+
onError={() => {
|
|
243
|
+
setImageUrl(null);
|
|
244
|
+
setHasError(true);
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Loading State (Skeleton)
|
|
252
|
+
return (
|
|
253
|
+
<div style={{
|
|
254
|
+
width: '100%',
|
|
255
|
+
height: '100%',
|
|
256
|
+
backgroundColor: '#eee',
|
|
257
|
+
borderRadius: '8px',
|
|
258
|
+
animation: 'pulse 1.5s infinite ease-in-out',
|
|
259
|
+
...style
|
|
260
|
+
}}>
|
|
261
|
+
<style>{`
|
|
262
|
+
@keyframes pulse {
|
|
263
|
+
0% { opacity: 0.6; }
|
|
264
|
+
50% { opacity: 0.8; }
|
|
265
|
+
100% { opacity: 0.6; }
|
|
266
|
+
}
|
|
267
|
+
`}</style>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
60
270
|
};
|
|
61
271
|
|
|
62
272
|
export default ImageComponent;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import ImageComponent from "./ImageComponent";
|
|
3
|
+
|
|
4
|
+
interface ItemCardProps {
|
|
5
|
+
id: string;
|
|
6
|
+
amount: string | number;
|
|
7
|
+
label: string;
|
|
8
|
+
itemData?: any; // Pass full item data for customization
|
|
9
|
+
rarityColor?: string;
|
|
10
|
+
content?: React.ReactNode;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
// New props for visual variants
|
|
13
|
+
variant?: "Normal" | "Golden" | "Rainbow";
|
|
14
|
+
shiny?: boolean;
|
|
15
|
+
// Legacy/Other props
|
|
16
|
+
tn?: any;
|
|
17
|
+
weight?: any;
|
|
18
|
+
typeId?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ItemCard: React.FC<ItemCardProps> = ({
|
|
22
|
+
id,
|
|
23
|
+
amount,
|
|
24
|
+
label,
|
|
25
|
+
itemData,
|
|
26
|
+
rarityColor = "#e0e0e0", // Default gray
|
|
27
|
+
content,
|
|
28
|
+
children,
|
|
29
|
+
variant = "Normal",
|
|
30
|
+
shiny = false,
|
|
31
|
+
tn, // Destructure but ignore
|
|
32
|
+
weight, // Destructure but ignore
|
|
33
|
+
typeId // Destructure but ignore
|
|
34
|
+
}) => {
|
|
35
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
36
|
+
|
|
37
|
+
// If custom content is provided, render that (for list view etc)
|
|
38
|
+
if (content || children) {
|
|
39
|
+
return <>{content || children}</>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Determine visual styles based on variant
|
|
43
|
+
// Determine visual styles based on variant
|
|
44
|
+
const filters: string[] = [];
|
|
45
|
+
if (isHovered) {
|
|
46
|
+
filters.push("drop-shadow(0 5px 10px rgba(0,0,0,0.2))");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let cardBorder = "2px solid #e0e0e0";
|
|
50
|
+
let bgStyle = "#ffffff";
|
|
51
|
+
|
|
52
|
+
if (variant === "Golden") {
|
|
53
|
+
filters.push("sepia(100%) saturate(300%) hue-rotate(10deg)");
|
|
54
|
+
cardBorder = "2px solid #FFD700";
|
|
55
|
+
bgStyle = "#FFFDF0";
|
|
56
|
+
} else if (variant === "Rainbow") {
|
|
57
|
+
// Rainbow effect usually implies vibrant changing colors or just a rainbow gradient border
|
|
58
|
+
// For item image, maybe a slight hue rotate or saturation boost?
|
|
59
|
+
// Actual PS99 rainbow pets are distinct, but a filter approximation:
|
|
60
|
+
filters.push("saturate(200%) hue-rotate(30deg) contrast(120%)");
|
|
61
|
+
cardBorder = "2px solid #a29bfe"; // Placeholder for rainbow, usually gradient
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const imageFilter = filters.length > 0 ? filters.join(" ") : "none";
|
|
65
|
+
|
|
66
|
+
// Shiny Effect
|
|
67
|
+
// Shiny acts as an overlay or texture
|
|
68
|
+
|
|
69
|
+
// Clean White Square Style (Reference Match)
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
73
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
74
|
+
style={{
|
|
75
|
+
display: "flex",
|
|
76
|
+
flexDirection: "column",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
justifyContent: "space-between",
|
|
79
|
+
// border handled below
|
|
80
|
+
borderRadius: "16px",
|
|
81
|
+
backgroundColor: bgStyle,
|
|
82
|
+
boxShadow: isHovered
|
|
83
|
+
? "0 8px 20px rgba(0,0,0,0.1)"
|
|
84
|
+
: "0 2px 5px rgba(0,0,0,0.05)",
|
|
85
|
+
transition: "all 0.1s ease-out",
|
|
86
|
+
// transform: isHovered ? "translateY(-4px)" : "translateY(0)", // User requested no movement
|
|
87
|
+
cursor: "pointer",
|
|
88
|
+
width: "100%",
|
|
89
|
+
aspectRatio: "1 / 1",
|
|
90
|
+
position: "relative",
|
|
91
|
+
overflow: "hidden",
|
|
92
|
+
padding: "20px",
|
|
93
|
+
boxSizing: "border-box", // CRITICAL: Ensure padding is included in width to prevent overlap
|
|
94
|
+
// Rainbow Border Trick
|
|
95
|
+
background: variant === "Rainbow"
|
|
96
|
+
? "linear-gradient(#fff, #fff) padding-box, linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3) border-box"
|
|
97
|
+
: undefined,
|
|
98
|
+
border: variant === "Rainbow" ? "3px solid transparent" : cardBorder,
|
|
99
|
+
zIndex: isHovered ? 100 : 1, // Ensure it pops over neighbors on hover
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{/* Shiny Sparkles Overlay */}
|
|
103
|
+
{shiny && (
|
|
104
|
+
<div style={{
|
|
105
|
+
position: "absolute",
|
|
106
|
+
top: 0,
|
|
107
|
+
left: 0,
|
|
108
|
+
right: 0,
|
|
109
|
+
bottom: 0,
|
|
110
|
+
pointerEvents: "none",
|
|
111
|
+
zIndex: 10,
|
|
112
|
+
backgroundImage: "url('data:image/svg+xml;utf8,%3Csvg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cpath d=\"M10 0L12 8L20 10L12 12L10 20L8 12L0 10L8 8L10 0Z\" fill=\"%23FFD700\" opacity=\"0.4\"/%3E%3C/svg%3E')",
|
|
113
|
+
backgroundSize: "40px 40px",
|
|
114
|
+
opacity: 0.5,
|
|
115
|
+
}} />
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Rarity Indicator REMOVED based on user feedback */}
|
|
119
|
+
|
|
120
|
+
{/* Item Image */}
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
flex: 1,
|
|
124
|
+
width: "90%",
|
|
125
|
+
display: "flex",
|
|
126
|
+
justifyContent: "center",
|
|
127
|
+
alignItems: "center",
|
|
128
|
+
position: "relative",
|
|
129
|
+
marginTop: "10px",
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{(() => {
|
|
133
|
+
// Use a simple local resolver if hook isn't available or just use the passed data
|
|
134
|
+
// actually we can't easily use the hook inside a map or conditional without refactoring parent
|
|
135
|
+
// BUT, we can just use the same logic here or expect the parent to pass a resolved/normalized object.
|
|
136
|
+
// However, to be "Universal" and "Clean", let's replicate the safe check here or expect `itemData` to be rich.
|
|
137
|
+
|
|
138
|
+
const iconSrc =
|
|
139
|
+
itemData?.icon ||
|
|
140
|
+
itemData?.Icon ||
|
|
141
|
+
itemData?.thumbnail ||
|
|
142
|
+
itemData?.image ||
|
|
143
|
+
itemData?.texture ||
|
|
144
|
+
itemData?.orbImage ||
|
|
145
|
+
itemData?.titanicIcon ||
|
|
146
|
+
itemData?.petIcon ||
|
|
147
|
+
itemData?.eggIcon ||
|
|
148
|
+
itemData?.enchantIcon ||
|
|
149
|
+
itemData?.potionIcon ||
|
|
150
|
+
itemData?.fruitIcon ||
|
|
151
|
+
itemData?.toyIcon ||
|
|
152
|
+
itemData?.charmIcon ||
|
|
153
|
+
itemData?.boothIcon ||
|
|
154
|
+
itemData?.flagIcon ||
|
|
155
|
+
itemData?.keyIcon ||
|
|
156
|
+
itemData?.seedIcon ||
|
|
157
|
+
itemData?.bookIcon ||
|
|
158
|
+
itemData?.giftIcon ||
|
|
159
|
+
itemData?.currencyIcon ||
|
|
160
|
+
itemData?.miscIcon;
|
|
161
|
+
|
|
162
|
+
if (iconSrc) {
|
|
163
|
+
return (
|
|
164
|
+
<ImageComponent
|
|
165
|
+
src={iconSrc}
|
|
166
|
+
alt={label}
|
|
167
|
+
style={{
|
|
168
|
+
width: "100%",
|
|
169
|
+
height: "100%",
|
|
170
|
+
objectFit: "contain",
|
|
171
|
+
filter: imageFilter,
|
|
172
|
+
transition: "filter 0.2s ease",
|
|
173
|
+
transform: isHovered ? "scale(1.1)" : "scale(1)",
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
return <span style={{ fontSize: "3.5rem", opacity: 0.3 }}>?</span>;
|
|
179
|
+
}
|
|
180
|
+
})()}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Quantity Badge (Top Right) */}
|
|
184
|
+
{amount && amount !== 1 && amount !== "1" && (
|
|
185
|
+
<div
|
|
186
|
+
style={{
|
|
187
|
+
position: "absolute",
|
|
188
|
+
top: "12px",
|
|
189
|
+
right: "12px",
|
|
190
|
+
backgroundColor: "#333",
|
|
191
|
+
color: "#fff",
|
|
192
|
+
padding: "2px 8px",
|
|
193
|
+
borderRadius: "8px",
|
|
194
|
+
fontWeight: "800",
|
|
195
|
+
fontSize: "0.8rem",
|
|
196
|
+
zIndex: 5,
|
|
197
|
+
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
x{amount.toLocaleString()}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{/* Label (Hover Only Overlay) */}
|
|
205
|
+
<div
|
|
206
|
+
style={{
|
|
207
|
+
position: "absolute",
|
|
208
|
+
bottom: 0,
|
|
209
|
+
left: 0,
|
|
210
|
+
width: "100%",
|
|
211
|
+
padding: "8px 4px",
|
|
212
|
+
backgroundColor: "rgba(255, 255, 255, 0.95)",
|
|
213
|
+
textAlign: "center",
|
|
214
|
+
opacity: isHovered ? 1 : 0,
|
|
215
|
+
transform: isHovered ? "translateY(0)" : "translateY(100%)",
|
|
216
|
+
transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
217
|
+
zIndex: 20,
|
|
218
|
+
borderTop: "2px solid rgba(0,0,0,0.05)",
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<span
|
|
222
|
+
style={{
|
|
223
|
+
fontSize: "0.85rem",
|
|
224
|
+
fontWeight: "800",
|
|
225
|
+
color: "#2d3436",
|
|
226
|
+
lineHeight: "1.1",
|
|
227
|
+
display: "-webkit-box",
|
|
228
|
+
WebkitLineClamp: 2,
|
|
229
|
+
WebkitBoxOrient: "vertical",
|
|
230
|
+
overflow: "hidden",
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{label}
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export default ItemCard;
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { CollectionConfigData } from "ps99-api";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import ItemCard from "./ItemCard";
|
|
4
|
+
import { useItemResolution } from "../hooks/useItemResolution";
|
|
5
5
|
|
|
6
6
|
const LootboxesComponent: React.FC<{
|
|
7
|
-
configData
|
|
7
|
+
configData: CollectionConfigData<"Lootboxes">;
|
|
8
8
|
}> = ({ configData }) => {
|
|
9
|
+
const { getRarityColor } = useItemResolution();
|
|
10
|
+
const rarityColor = (configData as any).Rarity ? getRarityColor((configData as any).Rarity) : null;
|
|
11
|
+
|
|
9
12
|
return (
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
<div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
|
|
14
|
+
<ItemCard
|
|
15
|
+
id={configData.DisplayName}
|
|
16
|
+
amount={1}
|
|
17
|
+
label={configData.DisplayName}
|
|
18
|
+
itemData={{
|
|
19
|
+
icon: configData.Icon,
|
|
20
|
+
rarity: (configData as any).Rarity,
|
|
21
|
+
name: configData.DisplayName
|
|
22
|
+
}}
|
|
23
|
+
rarityColor={rarityColor}
|
|
24
|
+
typeId={(configData as any)._index}
|
|
25
|
+
/>
|
|
26
|
+
{/* Additional details provided by ItemCard, but we can add more if needed below */}
|
|
27
|
+
<div style={{ marginTop: '10px', fontSize: '0.9em', color: '#666', textAlign: 'center' }}>
|
|
28
|
+
<p>{configData.Desc}</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
23
31
|
);
|
|
24
32
|
};
|
|
25
33
|
|