@rpg-engine/long-bow 0.8.65 → 0.8.67
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/dist/components/Character/SkinSelectionGrid.d.ts +11 -0
- package/dist/components/Store/CartView.d.ts +1 -0
- package/dist/components/Store/MetadataCollector.d.ts +9 -0
- package/dist/components/Store/StoreCharacterSkinRow.d.ts +11 -0
- package/dist/components/Store/StoreItemRow.d.ts +1 -1
- package/dist/components/Store/hooks/useStoreCart.d.ts +6 -2
- package/dist/components/Store/hooks/useStoreMetadata.d.ts +15 -0
- package/dist/components/Store/sections/StoreItemsSection.d.ts +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/long-bow.cjs.development.js +1572 -193
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +1540 -165
- package/dist/long-bow.esm.js.map +1 -1
- package/dist/stories/Character/SkinSelectionGrid.stories.d.ts +1 -0
- package/dist/stories/Character/character/CharacterSkinSelectionModal.stories.d.ts +1 -5
- package/dist/stories/Features/store/MetadataCollector.stories.d.ts +1 -0
- package/package.json +2 -2
- package/src/components/Character/CharacterSkinSelectionModal.tsx +18 -71
- package/src/components/Character/SkinSelectionGrid.tsx +179 -0
- package/src/components/Store/CartView.tsx +66 -7
- package/src/components/Store/MetadataCollector.tsx +48 -0
- package/src/components/Store/Store.tsx +38 -5
- package/src/components/Store/StoreCharacterSkinRow.tsx +286 -0
- package/src/components/Store/StoreItemRow.tsx +1 -1
- package/src/components/Store/__test__/MetadataCollector.spec.tsx +228 -0
- package/src/components/Store/__test__/useStoreMetadata.spec.tsx +181 -0
- package/src/components/Store/hooks/useStoreCart.ts +89 -44
- package/src/components/Store/hooks/useStoreMetadata.ts +55 -0
- package/src/components/Store/sections/StoreItemsSection.tsx +30 -11
- package/src/index.tsx +6 -3
- package/src/stories/Character/SkinSelectionGrid.stories.tsx +106 -0
- package/src/stories/Character/character/CharacterSkinSelectionModal.stories.tsx +86 -25
- package/src/stories/Features/store/MetadataCollector.stories.tsx +94 -0
- package/src/stories/Features/store/Store.stories.tsx +100 -2
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ICartItem,
|
|
3
|
-
IItemPack,
|
|
2
|
+
ICartItem as IBaseCartItem,
|
|
4
3
|
IPurchase,
|
|
5
4
|
IPurchaseUnit,
|
|
6
5
|
IStoreItem,
|
|
7
|
-
|
|
6
|
+
MetadataType,
|
|
7
|
+
PurchaseType
|
|
8
8
|
} from '@rpg-engine/shared';
|
|
9
9
|
import { useEffect, useRef, useState } from 'react';
|
|
10
|
+
import { useStoreMetadata } from './useStoreMetadata';
|
|
11
|
+
|
|
12
|
+
// Extend the base cart item to include metadata
|
|
13
|
+
interface ICartItem extends IBaseCartItem {
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
interface IUseStoreCart {
|
|
12
18
|
cartItems: ICartItem[];
|
|
13
19
|
isCartOpen: boolean;
|
|
14
|
-
handleAddToCart: (item: IStoreItem, quantity: number) => void;
|
|
20
|
+
handleAddToCart: (item: IStoreItem, quantity: number, metadata?: Record<string, any>) => void;
|
|
15
21
|
handleRemoveFromCart: (itemKey: string) => void;
|
|
16
22
|
handlePurchase: (onPurchase: (purchase: IPurchase) => void) => void;
|
|
17
23
|
openCart: () => void;
|
|
18
24
|
closeCart: () => void;
|
|
19
25
|
getTotalItems: () => number;
|
|
20
26
|
getTotalPrice: () => number;
|
|
27
|
+
isCollectingMetadata: boolean;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export const useStoreCart = (): IUseStoreCart => {
|
|
@@ -31,25 +38,62 @@ export const useStoreCart = (): IUseStoreCart => {
|
|
|
31
38
|
};
|
|
32
39
|
}, []);
|
|
33
40
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
const { collectMetadata, isCollectingMetadata } = useStoreMetadata();
|
|
42
|
+
|
|
43
|
+
const handleAddToCart = async (item: IStoreItem, quantity: number, preselectedMetadata?: Record<string, any>) => {
|
|
44
|
+
// If metadata is already provided (from inline selection), use it directly
|
|
45
|
+
if (preselectedMetadata) {
|
|
46
|
+
setCartItems(prevItems => {
|
|
47
|
+
return [
|
|
48
|
+
...prevItems,
|
|
49
|
+
{
|
|
50
|
+
item,
|
|
51
|
+
quantity,
|
|
52
|
+
metadata: preselectedMetadata,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If item requires metadata but none was provided, collect it before adding to cart
|
|
60
|
+
if (item.metadataType && item.metadataType !== MetadataType.None) {
|
|
61
|
+
const metadata = await collectMetadata(item);
|
|
62
|
+
if (!metadata) return; // User cancelled
|
|
63
|
+
|
|
64
|
+
// Add item with metadata
|
|
65
|
+
setCartItems(prevItems => {
|
|
66
|
+
// Create new cart item with metadata
|
|
67
|
+
return [
|
|
68
|
+
...prevItems,
|
|
69
|
+
{
|
|
70
|
+
item,
|
|
71
|
+
quantity,
|
|
72
|
+
metadata,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
// Existing add to cart logic for items without metadata
|
|
78
|
+
setCartItems(prevItems => {
|
|
79
|
+
const existingItem = prevItems.find(
|
|
80
|
+
cartItem => cartItem.item.key === item.key
|
|
48
81
|
);
|
|
49
|
-
}
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
if (existingItem) {
|
|
84
|
+
return prevItems.map(cartItem =>
|
|
85
|
+
cartItem.item.key === item.key
|
|
86
|
+
? {
|
|
87
|
+
...cartItem,
|
|
88
|
+
quantity: Math.min(cartItem.quantity + quantity, 99),
|
|
89
|
+
}
|
|
90
|
+
: cartItem
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [...prevItems, { item, quantity }];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
53
97
|
};
|
|
54
98
|
|
|
55
99
|
const handleRemoveFromCart = (itemKey: string) => {
|
|
@@ -59,28 +103,13 @@ export const useStoreCart = (): IUseStoreCart => {
|
|
|
59
103
|
};
|
|
60
104
|
|
|
61
105
|
const handlePurchase = (onPurchase: (purchase: IPurchase) => void) => {
|
|
62
|
-
const purchaseUnits: IPurchaseUnit[] = cartItems.map(cartItem => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
purchaseKey: packItem.key,
|
|
71
|
-
qty: cartItem.quantity,
|
|
72
|
-
type: PurchaseType.Pack,
|
|
73
|
-
name: packItem.title,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
purchaseKey: cartItem.item.key,
|
|
79
|
-
qty: cartItem.quantity,
|
|
80
|
-
type: PurchaseType.Item,
|
|
81
|
-
name: cartItem.item.name,
|
|
82
|
-
};
|
|
83
|
-
});
|
|
106
|
+
const purchaseUnits: IPurchaseUnit[] = cartItems.map(cartItem => ({
|
|
107
|
+
purchaseKey: cartItem.item.key,
|
|
108
|
+
qty: cartItem.quantity,
|
|
109
|
+
type: getPurchaseType(cartItem.item),
|
|
110
|
+
name: cartItem.item.name,
|
|
111
|
+
metadata: cartItem.metadata || cartItem.item.metadataConfig, // Use collected metadata if available
|
|
112
|
+
}));
|
|
84
113
|
|
|
85
114
|
const purchase: IPurchase = {
|
|
86
115
|
_id: uuidv4(),
|
|
@@ -122,9 +151,25 @@ export const useStoreCart = (): IUseStoreCart => {
|
|
|
122
151
|
closeCart,
|
|
123
152
|
getTotalItems,
|
|
124
153
|
getTotalPrice,
|
|
154
|
+
isCollectingMetadata,
|
|
125
155
|
};
|
|
126
156
|
};
|
|
157
|
+
|
|
158
|
+
// Helper functions
|
|
159
|
+
function getPurchaseType(item: IStoreItem): PurchaseType {
|
|
160
|
+
// Check if the item comes from a pack based on naming convention or other property
|
|
161
|
+
if (item.key.startsWith('pack_')) {
|
|
162
|
+
return PurchaseType.Pack;
|
|
163
|
+
} else {
|
|
164
|
+
return PurchaseType.Item;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
127
168
|
function uuidv4(): string {
|
|
128
|
-
|
|
169
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
170
|
+
const r = (Math.random() * 16) | 0,
|
|
171
|
+
v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
172
|
+
return v.toString(16);
|
|
173
|
+
});
|
|
129
174
|
}
|
|
130
175
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { IStoreItem, MetadataType } from "@rpg-engine/shared";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
interface IUseStoreMetadata {
|
|
5
|
+
collectMetadata: (item: IStoreItem) => Promise<Record<string, any> | null>;
|
|
6
|
+
isCollectingMetadata: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useStoreMetadata = (): IUseStoreMetadata => {
|
|
10
|
+
const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
|
|
11
|
+
|
|
12
|
+
const collectMetadata = async (item: IStoreItem): Promise<Record<string, any> | null> => {
|
|
13
|
+
if (!item.metadataType || item.metadataType === MetadataType.None) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setIsCollectingMetadata(true);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// This is a promise-based approach that will be resolved when the MetadataCollector
|
|
21
|
+
// component calls the onCollect or onCancel callbacks
|
|
22
|
+
return await new Promise<Record<string, any> | null>((resolve) => {
|
|
23
|
+
// We'll store the resolver functions in a global context
|
|
24
|
+
// that will be accessible to the MetadataCollector component
|
|
25
|
+
window.__metadataResolvers = {
|
|
26
|
+
resolve: (metadata: Record<string, any> | null) => {
|
|
27
|
+
resolve(metadata);
|
|
28
|
+
},
|
|
29
|
+
item,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
} finally {
|
|
33
|
+
setIsCollectingMetadata(false);
|
|
34
|
+
// Clean up the resolvers
|
|
35
|
+
if (window.__metadataResolvers) {
|
|
36
|
+
delete window.__metadataResolvers;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
collectMetadata,
|
|
43
|
+
isCollectingMetadata,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Add TypeScript declaration for the global object
|
|
48
|
+
declare global {
|
|
49
|
+
interface Window {
|
|
50
|
+
__metadataResolvers?: {
|
|
51
|
+
resolve: (metadata: Record<string, any> | null) => void;
|
|
52
|
+
item: IStoreItem;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { IStoreItem, UserAccountTypes } from '@rpg-engine/shared';
|
|
1
|
+
import { IStoreItem, MetadataType, UserAccountTypes } from '@rpg-engine/shared';
|
|
2
2
|
import React, { useState } from 'react';
|
|
3
3
|
import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
|
|
4
|
+
import { StoreCharacterSkinRow } from '../StoreCharacterSkinRow';
|
|
4
5
|
import { StoreItemRow } from '../StoreItemRow';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
interface IStoreItemsSectionProps {
|
|
8
9
|
items: IStoreItem[];
|
|
9
|
-
onAddToCart: (item: IStoreItem, quantity: number) => void;
|
|
10
|
+
onAddToCart: (item: IStoreItem, quantity: number, metadata?: Record<string, any>) => void;
|
|
10
11
|
atlasJSON: Record<string, any>;
|
|
11
12
|
atlasIMG: string;
|
|
12
13
|
userAccountType?: UserAccountTypes;
|
|
@@ -25,15 +26,33 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
|
|
|
25
26
|
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
26
27
|
);
|
|
27
28
|
|
|
28
|
-
const renderStoreItem = (item: IStoreItem) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
const renderStoreItem = (item: IStoreItem) => {
|
|
30
|
+
// Use the specialized character skin row for items with character skin metadata
|
|
31
|
+
if (item.metadataType === MetadataType.CharacterSkin) {
|
|
32
|
+
return (
|
|
33
|
+
<StoreCharacterSkinRow
|
|
34
|
+
key={item._id}
|
|
35
|
+
item={item}
|
|
36
|
+
atlasJSON={atlasJSON}
|
|
37
|
+
atlasIMG={atlasIMG}
|
|
38
|
+
onAddToCart={onAddToCart}
|
|
39
|
+
userAccountType={userAccountType || UserAccountTypes.Free}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Use the standard item row for all other items
|
|
45
|
+
return (
|
|
46
|
+
<StoreItemRow
|
|
47
|
+
key={item._id}
|
|
48
|
+
item={item}
|
|
49
|
+
atlasJSON={atlasJSON}
|
|
50
|
+
atlasIMG={atlasIMG}
|
|
51
|
+
onAddToCart={onAddToCart}
|
|
52
|
+
userAccountType={userAccountType || UserAccountTypes.Free}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
37
56
|
|
|
38
57
|
return (
|
|
39
58
|
<ScrollableContent
|
package/src/index.tsx
CHANGED
|
@@ -42,6 +42,7 @@ export * from './components/NPCDialog/QuestionDialog/QuestionDialog';
|
|
|
42
42
|
export * from './components/PartySystem';
|
|
43
43
|
export * from './components/ProgressBar';
|
|
44
44
|
export * from './components/PropertySelect/PropertySelect';
|
|
45
|
+
export * from './components/QuantitySelector/QuantitySelectorModal';
|
|
45
46
|
export * from './components/Quests/QuestInfo/QuestInfo';
|
|
46
47
|
export * from './components/Quests/QuestList';
|
|
47
48
|
export * from './components/RadioButton';
|
|
@@ -55,6 +56,10 @@ export * from './components/SkillsContainer';
|
|
|
55
56
|
export * from './components/SocialModal/SocialModal';
|
|
56
57
|
export * from './components/Spellbook/Spellbook';
|
|
57
58
|
export * from './components/Stepper';
|
|
59
|
+
export * from './components/Store/CartView';
|
|
60
|
+
export * from './components/Store/hooks/useStoreCart';
|
|
61
|
+
export * from './components/Store/MetadataCollector';
|
|
62
|
+
export * from './components/Store/Store';
|
|
58
63
|
export * from './components/Table/Table';
|
|
59
64
|
export * from './components/TextArea';
|
|
60
65
|
export * from './components/TimeWidget/TimeWidget';
|
|
@@ -63,7 +68,5 @@ export * from './components/TradingMenu/TradingMenu';
|
|
|
63
68
|
export * from './components/Truncate';
|
|
64
69
|
export * from './components/Tutorial/TutorialStepper';
|
|
65
70
|
export * from './components/typography/DynamicText';
|
|
66
|
-
|
|
67
|
-
export * from './components/QuantitySelector/QuantitySelectorModal';
|
|
68
|
-
|
|
69
71
|
export { useEventListener } from './hooks/useEventListener';
|
|
72
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { SkinSelectionGrid } from '../../components/Character/SkinSelectionGrid';
|
|
4
|
+
import { RPGUIRoot } from '../../components/RPGUI/RPGUIRoot';
|
|
5
|
+
import entitiesAtlasJSON from '../../mocks/atlas/entities/entities.json';
|
|
6
|
+
import entitiesAtlasIMG from '../../mocks/atlas/entities/entities.png';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SkinSelectionGrid stories showcasing the carousel-style character skin selection component
|
|
10
|
+
*/
|
|
11
|
+
const meta = {
|
|
12
|
+
title: 'Character/SkinSelectionGrid',
|
|
13
|
+
component: SkinSelectionGrid,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'centered',
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'The SkinSelectionGrid component provides a carousel-style selection interface ' +
|
|
20
|
+
'for character skins. Users can navigate through available skins using left and right arrows.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
decorators: [
|
|
25
|
+
Story => (
|
|
26
|
+
<RPGUIRoot>
|
|
27
|
+
<div style={{ width: '400px', background: '#333', padding: '20px', borderRadius: '5px' }}>
|
|
28
|
+
<Story />
|
|
29
|
+
</div>
|
|
30
|
+
</RPGUIRoot>
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
} satisfies Meta<typeof SkinSelectionGrid>;
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof SkinSelectionGrid>;
|
|
37
|
+
|
|
38
|
+
// Sample character skins
|
|
39
|
+
const availableCharacters = [
|
|
40
|
+
{
|
|
41
|
+
id: 'black-knight',
|
|
42
|
+
name: 'Black Knight',
|
|
43
|
+
textureKey: 'black-knight',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'dragon-knight',
|
|
47
|
+
name: 'Dragon Knight',
|
|
48
|
+
textureKey: 'dragon-knight',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'senior-knight-1',
|
|
52
|
+
name: 'Senior Knight',
|
|
53
|
+
textureKey: 'senior-knight-1',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'character-1',
|
|
57
|
+
name: 'Character 1',
|
|
58
|
+
textureKey: 'character-1',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'character-2',
|
|
62
|
+
name: 'Character 2',
|
|
63
|
+
textureKey: 'character-2',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'character-3',
|
|
67
|
+
name: 'Character 3',
|
|
68
|
+
textureKey: 'character-3',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'character-4',
|
|
72
|
+
name: 'Character 4',
|
|
73
|
+
textureKey: 'character-4',
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default story showing the carousel-style skin selection with multiple character options
|
|
79
|
+
* Users can navigate through the skins using left and right arrows
|
|
80
|
+
*/
|
|
81
|
+
export const Default: Story = {
|
|
82
|
+
render: () => (
|
|
83
|
+
<SkinSelectionGrid
|
|
84
|
+
availableCharacters={availableCharacters}
|
|
85
|
+
initialSelectedSkin="black-knight"
|
|
86
|
+
onChange={(skinKey) => console.log('Selected skin:', skinKey)}
|
|
87
|
+
atlasJSON={entitiesAtlasJSON}
|
|
88
|
+
atlasIMG={entitiesAtlasIMG}
|
|
89
|
+
/>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Story showing the skin selection with only a few options
|
|
95
|
+
*/
|
|
96
|
+
export const FewOptions: Story = {
|
|
97
|
+
render: () => (
|
|
98
|
+
<SkinSelectionGrid
|
|
99
|
+
availableCharacters={availableCharacters.slice(0, 3)}
|
|
100
|
+
initialSelectedSkin="dragon-knight"
|
|
101
|
+
onChange={(skinKey) => console.log('Selected skin:', skinKey)}
|
|
102
|
+
atlasJSON={entitiesAtlasJSON}
|
|
103
|
+
atlasIMG={entitiesAtlasIMG}
|
|
104
|
+
/>
|
|
105
|
+
),
|
|
106
|
+
};
|
|
@@ -1,52 +1,113 @@
|
|
|
1
|
-
import { Meta,
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { RPGUIRoot } from '../../..';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
ICharacterSkinSelectionModalProps,
|
|
5
|
+
CharacterSkinSelectionModal
|
|
7
6
|
} from '../../../components/Character/CharacterSkinSelectionModal';
|
|
8
7
|
import atlasJSON from '../../../mocks/atlas/entities/entities.json';
|
|
9
8
|
import atlasIMG from '../../../mocks/atlas/entities/entities.png';
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Character skin selection modal with carousel-style navigation.
|
|
12
|
+
* This allows users to browse through available character skins using arrow buttons.
|
|
13
|
+
*/
|
|
14
|
+
const meta = {
|
|
12
15
|
title: 'Character/Character/Skin Selection',
|
|
13
16
|
component: CharacterSkinSelectionModal,
|
|
14
|
-
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: 'centered',
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component:
|
|
22
|
+
'Character Skin Selection modal with carousel-style navigation. ' +
|
|
23
|
+
'Users can browse through available skins using left and right arrows.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
decorators: [
|
|
28
|
+
Story => (
|
|
29
|
+
<RPGUIRoot>
|
|
30
|
+
<div style={{ background: '#333', padding: '10px', borderRadius: '5px' }}>
|
|
31
|
+
<Story />
|
|
32
|
+
</div>
|
|
33
|
+
</RPGUIRoot>
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
} satisfies Meta<typeof CharacterSkinSelectionModal>;
|
|
15
37
|
|
|
16
38
|
export default meta;
|
|
39
|
+
type Story = StoryObj<typeof CharacterSkinSelectionModal>;
|
|
17
40
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
) => (
|
|
21
|
-
<RPGUIRoot>
|
|
22
|
-
<CharacterSkinSelectionModal {...args} />
|
|
23
|
-
</RPGUIRoot>
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export const KnightSkins = Template.bind({});
|
|
27
|
-
|
|
28
|
-
// Example of different knight skins
|
|
29
|
-
const knightCharacters = [
|
|
41
|
+
// Example of different character skins
|
|
42
|
+
const characterSkins = [
|
|
30
43
|
{
|
|
44
|
+
id: 'black-knight',
|
|
31
45
|
name: 'Black Knight',
|
|
32
46
|
textureKey: 'black-knight',
|
|
33
47
|
},
|
|
34
48
|
{
|
|
49
|
+
id: 'dragon-knight',
|
|
35
50
|
name: 'Dragon Knight',
|
|
36
51
|
textureKey: 'dragon-knight',
|
|
37
52
|
},
|
|
38
53
|
{
|
|
54
|
+
id: 'senior-knight-1',
|
|
39
55
|
name: 'Senior Knight',
|
|
40
56
|
textureKey: 'senior-knight-1',
|
|
41
57
|
},
|
|
58
|
+
{
|
|
59
|
+
id: 'character-1',
|
|
60
|
+
name: 'Character 1',
|
|
61
|
+
textureKey: 'character-1',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'character-2',
|
|
65
|
+
name: 'Character 2',
|
|
66
|
+
textureKey: 'character-2',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'character-3',
|
|
70
|
+
name: 'Character 3',
|
|
71
|
+
textureKey: 'character-3',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'character-4',
|
|
75
|
+
name: 'Character 4',
|
|
76
|
+
textureKey: 'character-4',
|
|
77
|
+
},
|
|
42
78
|
];
|
|
43
79
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Default story showing the character skin selection modal with multiple options.
|
|
82
|
+
* Users can navigate through the available skins using the left and right arrows.
|
|
83
|
+
*/
|
|
84
|
+
export const Default: Story = {
|
|
85
|
+
render: () => (
|
|
86
|
+
<CharacterSkinSelectionModal
|
|
87
|
+
isOpen={true}
|
|
88
|
+
onClose={() => console.log('Modal closed')}
|
|
89
|
+
onConfirm={(textureKey: string) => console.log('Selected skin:', textureKey)}
|
|
90
|
+
availableCharacters={characterSkins}
|
|
91
|
+
atlasJSON={atlasJSON}
|
|
92
|
+
atlasIMG={atlasIMG}
|
|
93
|
+
initialSelectedSkin="black-knight"
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Story showing the skin selection modal with just a few options.
|
|
100
|
+
*/
|
|
101
|
+
export const FewOptions: Story = {
|
|
102
|
+
render: () => (
|
|
103
|
+
<CharacterSkinSelectionModal
|
|
104
|
+
isOpen={true}
|
|
105
|
+
onClose={() => console.log('Modal closed')}
|
|
106
|
+
onConfirm={(textureKey: string) => console.log('Selected skin:', textureKey)}
|
|
107
|
+
availableCharacters={characterSkins.slice(0, 3)}
|
|
108
|
+
atlasJSON={atlasJSON}
|
|
109
|
+
atlasIMG={atlasIMG}
|
|
110
|
+
initialSelectedSkin="dragon-knight"
|
|
111
|
+
/>
|
|
112
|
+
),
|
|
52
113
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { MetadataType } from '@rpg-engine/shared';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { RPGUIRoot } from '../../../components/RPGUI/RPGUIRoot';
|
|
5
|
+
import { MetadataCollector } from '../../../components/Store/MetadataCollector';
|
|
6
|
+
import entitiesAtlasJSON from '../../../mocks/atlas/entities/entities.json';
|
|
7
|
+
import entitiesAtlasIMG from '../../../mocks/atlas/entities/entities.png';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MetadataCollector stories showcasing different use cases for collecting metadata
|
|
11
|
+
* during the purchase flow. This component is used when a purchase requires additional
|
|
12
|
+
* user input before completing the transaction.
|
|
13
|
+
*/
|
|
14
|
+
const meta = {
|
|
15
|
+
title: 'Features/Store/MetadataCollector',
|
|
16
|
+
component: MetadataCollector,
|
|
17
|
+
parameters: {
|
|
18
|
+
layout: 'centered',
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component:
|
|
22
|
+
'The MetadataCollector component handles collecting additional information from ' +
|
|
23
|
+
'users during the purchase flow when needed. For example, when purchasing a ' +
|
|
24
|
+
'character skin, the user needs to select which skin they want to use.',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
decorators: [
|
|
29
|
+
Story => (
|
|
30
|
+
<RPGUIRoot>
|
|
31
|
+
<Story />
|
|
32
|
+
</RPGUIRoot>
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
} satisfies Meta<typeof MetadataCollector>;
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof MetadataCollector>;
|
|
39
|
+
|
|
40
|
+
// Sample character skins
|
|
41
|
+
const availableCharacters = [
|
|
42
|
+
{
|
|
43
|
+
id: 'black-knight',
|
|
44
|
+
name: 'Black Knight',
|
|
45
|
+
textureKey: 'black-knight',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'dragon-knight',
|
|
49
|
+
name: 'Dragon Knight',
|
|
50
|
+
textureKey: 'dragon-knight',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'senior-knight-1',
|
|
54
|
+
name: 'Senior Knight',
|
|
55
|
+
textureKey: 'senior-knight-1',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Story showing the Character Skin selection interface that appears
|
|
61
|
+
* when a user purchases a character skin item that requires selecting
|
|
62
|
+
* which skin to apply.
|
|
63
|
+
*/
|
|
64
|
+
export const CharacterSkin: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<MetadataCollector
|
|
67
|
+
metadataType={MetadataType.CharacterSkin}
|
|
68
|
+
config={{
|
|
69
|
+
availableCharacters,
|
|
70
|
+
atlasJSON: entitiesAtlasJSON,
|
|
71
|
+
atlasIMG: entitiesAtlasIMG,
|
|
72
|
+
initialSelectedSkin: 'black-knight',
|
|
73
|
+
}}
|
|
74
|
+
onCollect={(metadata: Record<string, any>) => console.log('Metadata collected:', metadata)}
|
|
75
|
+
onCancel={() => console.log('Metadata collection cancelled')}
|
|
76
|
+
/>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Story showing the fallback for unhandled metadata types.
|
|
82
|
+
* This demonstrates the behavior when a metadata type is requested
|
|
83
|
+
* that hasn't been implemented yet.
|
|
84
|
+
*/
|
|
85
|
+
export const Unhandled: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<MetadataCollector
|
|
88
|
+
metadataType={'unhandled-type' as MetadataType}
|
|
89
|
+
config={{}}
|
|
90
|
+
onCollect={() => console.log('Metadata collected')}
|
|
91
|
+
onCancel={() => console.log('Metadata collection cancelled')}
|
|
92
|
+
/>
|
|
93
|
+
),
|
|
94
|
+
};
|