@rpg-engine/long-bow 0.8.13 → 0.8.15

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.
@@ -1,3 +1,6 @@
1
1
  declare const _default: import("@storybook/csf").ComponentAnnotations<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
2
2
  export default _default;
3
3
  export declare const Default: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
4
+ export declare const WithCostWarning: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
5
+ export declare const WithCustomCurrency: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
6
+ export declare const Closed: import("@storybook/csf").AnnotatedStoryFn<import("@storybook/react").ReactFramework, import("@storybook/react").Args>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.13",
3
+ "version": "0.8.15",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { ReactNode } from 'react';
2
2
  import styled from 'styled-components';
3
3
  import ModalPortal from './Abstractions/ModalPortal';
4
4
  import { Button, ButtonTypes } from './Button';
@@ -7,34 +7,46 @@ import { DraggableContainer } from './DraggableContainer';
7
7
  export interface IConfirmModalProps {
8
8
  onConfirm: () => void;
9
9
  onClose: () => void;
10
- message?: string;
10
+ message?: string | ReactNode;
11
11
  }
12
12
 
13
13
  export const ConfirmModal: React.FC<IConfirmModalProps> = ({
14
14
  onConfirm,
15
15
  onClose,
16
- message,
16
+ message = 'Are you sure?',
17
17
  }) => {
18
+ const handleConfirm = (e: React.MouseEvent) => {
19
+ e.preventDefault();
20
+ e.stopPropagation();
21
+ onConfirm();
22
+ };
23
+
24
+ const handleClose = (e: React.MouseEvent) => {
25
+ e.preventDefault();
26
+ e.stopPropagation();
27
+ onClose();
28
+ };
29
+
18
30
  return (
19
31
  <ModalPortal>
20
32
  <Background />
21
- <Container onPointerDown={onClose}>
33
+ <Container onClick={handleClose}>
22
34
  <DraggableContainer width="auto" dragDisabled>
23
- <Wrapper onPointerDown={e => e.stopPropagation()}>
24
- <p>{message ?? 'Are you sure?'}</p>
35
+ <Wrapper onClick={e => e.stopPropagation()}>
36
+ {typeof message === 'string' ? <p>{message}</p> : message}
25
37
 
26
38
  <ButtonsWrapper>
27
39
  <div className="cancel-button">
28
40
  <Button
29
41
  buttonType={ButtonTypes.RPGUIButton}
30
- onPointerDown={onClose}
42
+ onClick={handleClose}
31
43
  >
32
44
  No
33
45
  </Button>
34
46
  </div>
35
47
  <Button
36
48
  buttonType={ButtonTypes.RPGUIButton}
37
- onPointerDown={onConfirm}
49
+ onClick={handleConfirm}
38
50
  >
39
51
  Yes
40
52
  </Button>
@@ -1,75 +1,162 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import { HexColorPicker } from 'react-colorful';
3
3
  import styled from 'styled-components';
4
+ import { uiColors } from '../../../constants/uiColors';
4
5
  import { Button, ButtonTypes } from '../../Button';
6
+ import { ConfirmModal } from '../../ConfirmModal';
5
7
  import { DraggableContainer } from '../../DraggableContainer';
6
8
  import { RPGUIContainerTypes } from '../../RPGUI/RPGUIContainer';
7
9
 
8
- interface Props {
10
+ interface IColorSelectorProps {
9
11
  selectedColor: string;
10
12
  isOpen: boolean;
11
13
  onClose: () => void;
12
14
  onConfirm: (color: string) => void;
13
15
  onChange: (color: string) => void;
16
+ costWarning?: {
17
+ cost: number;
18
+ currency?: string;
19
+ };
14
20
  }
15
21
 
16
- export const ColorSelector: React.FC<Props> = ({
22
+ export const ColorSelector: React.FC<IColorSelectorProps> = ({
17
23
  selectedColor,
18
24
  isOpen,
19
25
  onClose,
20
26
  onConfirm,
21
27
  onChange,
28
+ costWarning,
22
29
  }) => {
23
30
  const [currentColor, setCurrentColor] = useState(selectedColor);
31
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
24
32
 
25
33
  useEffect(() => {
26
34
  if (isOpen) setCurrentColor(selectedColor);
27
35
  }, [isOpen, selectedColor]);
28
36
 
29
- const handleConfirm = () => {
37
+ const handleConfirm = (e: React.MouseEvent) => {
38
+ e.preventDefault();
39
+ if (costWarning) {
40
+ setShowConfirmModal(true);
41
+ } else {
42
+ onConfirm(currentColor);
43
+ onClose();
44
+ }
45
+ };
46
+
47
+ const handleConfirmCost = () => {
30
48
  onConfirm(currentColor);
49
+ setShowConfirmModal(false);
31
50
  onClose();
32
51
  };
33
52
 
53
+ const handleClose = () => {
54
+ setShowConfirmModal(false);
55
+ };
56
+
57
+ const renderConfirmMessage = () => (
58
+ <ConfirmContent>
59
+ <p>
60
+ Cost:
61
+ <CostDisplay>
62
+ {costWarning?.cost.toLocaleString()}
63
+ {costWarning?.currency || ' gold'}
64
+ </CostDisplay>
65
+ </p>
66
+ <p>Proceed with color change?</p>
67
+ </ConfirmContent>
68
+ );
69
+
34
70
  if (!isOpen) return null;
35
71
 
36
- return isOpen ? (
37
- <DraggableContainer
38
- type={RPGUIContainerTypes.Framed}
39
- cancelDrag=".react-colorful"
40
- width="20rem"
41
- onCloseButton={onClose}
42
- >
43
- <Container>
44
- <Header>Select Color</Header>
45
- <HexColorPicker
46
- color={currentColor}
47
- onChange={color => {
48
- setCurrentColor(color);
49
- onChange(color);
50
- }}
72
+ return (
73
+ <>
74
+ <DraggableContainer
75
+ type={RPGUIContainerTypes.Framed}
76
+ cancelDrag=".react-colorful"
77
+ width="25rem"
78
+ onCloseButton={onClose}
79
+ >
80
+ <Container>
81
+ <Header>Select Color</Header>
82
+ <ColorPickerWrapper>
83
+ <HexColorPicker
84
+ color={currentColor}
85
+ onChange={color => {
86
+ setCurrentColor(color);
87
+ onChange(color);
88
+ }}
89
+ />
90
+ </ColorPickerWrapper>
91
+ <ButtonContainer>
92
+ <Button
93
+ buttonType={ButtonTypes.RPGUIButton}
94
+ type="button"
95
+ onClick={handleConfirm}
96
+ >
97
+ Confirm
98
+ </Button>
99
+ </ButtonContainer>
100
+ </Container>
101
+ </DraggableContainer>
102
+
103
+ {showConfirmModal && costWarning && (
104
+ <ConfirmModal
105
+ message={renderConfirmMessage()}
106
+ onConfirm={handleConfirmCost}
107
+ onClose={handleClose}
51
108
  />
52
- <Button
53
- buttonType={ButtonTypes.RPGUIButton}
54
- type="button"
55
- onClick={handleConfirm}
56
- >
57
- Confirm
58
- </Button>
59
- </Container>
60
- </DraggableContainer>
61
- ) : null;
109
+ )}
110
+ </>
111
+ );
62
112
  };
63
113
 
64
114
  const Container = styled.div`
65
- padding: 2rem;
66
115
  text-align: center;
67
116
  background: inherit;
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 1.5rem;
120
+ align-items: center;
121
+ width: 100%;
122
+ max-width: 24rem;
123
+ margin: 0 auto;
68
124
  `;
69
125
 
70
126
  const Header = styled.h2`
71
- font-family: 'Press Start 2P', cursive;
72
127
  color: white;
73
128
  font-size: 1rem;
74
- margin-bottom: 1rem;
129
+ margin: 0;
130
+ width: 100%;
131
+ text-align: center;
132
+ `;
133
+
134
+ const ColorPickerWrapper = styled.div`
135
+ display: flex;
136
+ justify-content: center;
137
+ width: 100%;
138
+
139
+ .react-colorful {
140
+ width: 100%;
141
+ max-width: 200px;
142
+ }
143
+ `;
144
+
145
+ const ButtonContainer = styled.div`
146
+ display: flex;
147
+ justify-content: center;
148
+ width: 100%;
149
+ `;
150
+
151
+ const ConfirmContent = styled.div`
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 0.5rem;
155
+ text-align: center;
156
+ font-family: 'Press Start 2P', cursive;
157
+ font-size: 0.75rem;
158
+ `;
159
+
160
+ const CostDisplay = styled.span`
161
+ color: ${uiColors.yellow} !important;
75
162
  `;
@@ -1,6 +1,6 @@
1
1
  import { GRID_HEIGHT, GRID_WIDTH } from '@rpg-engine/shared';
2
2
  import React from 'react';
3
- import styled from 'styled-components';
3
+ import styled, { css } from 'styled-components';
4
4
  import { toUppercaseHexColor } from '../../utils/colorUtils';
5
5
 
6
6
  interface IProps {
@@ -53,6 +53,9 @@ export const SpriteFromAtlas: React.FC<IProps> = ({
53
53
  }
54
54
 
55
55
  const normalizedTintColor = toUppercaseHexColor(tintColor);
56
+ // For white tint, we'll use a very light gray instead
57
+ const effectiveTintColor =
58
+ normalizedTintColor === '#FFFFFF' ? '#EEEEEE' : normalizedTintColor;
56
59
 
57
60
  return (
58
61
  <Container
@@ -72,7 +75,7 @@ export const SpriteFromAtlas: React.FC<IProps> = ({
72
75
  style={imgStyle}
73
76
  centered={centered}
74
77
  borderRadius={borderRadius}
75
- tintColor={normalizedTintColor}
78
+ tintColor={effectiveTintColor}
76
79
  />
77
80
  </Container>
78
81
  );
@@ -115,23 +118,59 @@ const ImgSprite = styled.div<IImgSpriteProps>`
115
118
  width: ${props => props.frame.w}px;
116
119
  height: ${props => props.frame.h}px;
117
120
  background-image: url(${props => props.atlasIMG});
118
- background-position: -${props => props.frame.x}px -${props => props.frame.y}px;
121
+ background-position: -${props => props.frame.x}px -${props =>
122
+ props.frame.y}px;
119
123
  transform: scale(${props => props.scale});
120
124
  position: relative;
121
125
  top: ${props => (props.centered ? '0' : '8px')};
122
126
  left: ${props => (props.centered ? '0' : '8px')};
123
- filter: ${props => {
124
- const filters = [];
125
- if (props.grayScale) filters.push('grayscale(100%)');
126
- if (props.tintColor)
127
- filters.push(
128
- `brightness(0.8) contrast(1.2) sepia(100%) hue-rotate(${
129
- props.tintColor === '#FFD700' ? '40deg' : '210deg'
130
- }) saturate(400%)`
131
- );
132
- return filters.length ? filters.join(' ') : 'none';
133
- }};
134
127
  opacity: ${props => props.opacity};
135
128
  border-radius: ${props => props.borderRadius || '0'};
136
129
  overflow: hidden;
130
+
131
+ /* Apply grayscale if needed */
132
+ ${props =>
133
+ props.grayScale &&
134
+ css`
135
+ filter: grayscale(100%);
136
+ `}
137
+
138
+ /* Add tint overlay if tintColor is provided */
139
+ ${props =>
140
+ props.tintColor &&
141
+ css`
142
+ &::before {
143
+ content: '';
144
+ position: absolute;
145
+ top: 0;
146
+ left: 0;
147
+ width: ${props.frame.w}px;
148
+ height: ${props.frame.h}px;
149
+ background-color: ${props.tintColor};
150
+ mix-blend-mode: multiply;
151
+ opacity: 0.6;
152
+ pointer-events: none;
153
+ -webkit-mask-image: url(${props.atlasIMG});
154
+ -webkit-mask-position: -${props.frame.x}px -${props.frame.y}px;
155
+ mask-image: url(${props.atlasIMG});
156
+ mask-position: -${props.frame.x}px -${props.frame.y}px;
157
+ }
158
+
159
+ &::after {
160
+ content: '';
161
+ position: absolute;
162
+ top: 0;
163
+ left: 0;
164
+ width: ${props.frame.w}px;
165
+ height: ${props.frame.h}px;
166
+ background-color: ${props.tintColor};
167
+ mix-blend-mode: soft-light;
168
+ opacity: 0.6;
169
+ pointer-events: none;
170
+ -webkit-mask-image: url(${props.atlasIMG});
171
+ -webkit-mask-position: -${props.frame.x}px -${props.frame.y}px;
172
+ mask-image: url(${props.atlasIMG});
173
+ mask-position: -${props.frame.x}px -${props.frame.y}px;
174
+ }
175
+ `}
137
176
  `;
@@ -8,6 +8,44 @@ import {
8
8
  UserAccountTypes,
9
9
  } from '@rpg-engine/shared';
10
10
 
11
+ const createBagItem = (id: string, tintColor: string): IItem => ({
12
+ _id: id,
13
+ hasUseWith: false,
14
+ type: ItemType.Container,
15
+ subType: ItemSubType.Other,
16
+ textureAtlas: 'items',
17
+ allowedEquipSlotType: [ItemSlotType.Inventory],
18
+ isEquipable: false,
19
+ isStackable: true,
20
+ maxStackSize: 100,
21
+ isUsable: false,
22
+ isStorable: true,
23
+ isTwoHanded: false,
24
+ layer: 1,
25
+ isItemContainer: true,
26
+ isSolid: false,
27
+ key: 'bag',
28
+ texturePath: 'containers/bag.png',
29
+ textureKey: 'bag',
30
+ name: 'Bag',
31
+ generateContainerSlots: 10,
32
+ description:
33
+ 'You see a bag. It has made using leather and it has 10 total slots.',
34
+ attack: 7,
35
+ defense: 3,
36
+ weight: 13,
37
+ tiledId: 67,
38
+ x: 320,
39
+ y: 144,
40
+ scene: 'MainScene',
41
+ fullDescription:
42
+ 'You see a bag. It has made using leather and it has 10 total slots.',
43
+ createdAt: '2022-06-04T03:18:09.335Z',
44
+ updatedAt: '2022-06-04T18:16:49.056Z',
45
+ rarity: ItemRarities.Common,
46
+ tintColor,
47
+ });
48
+
11
49
  export const items: IItem[] = [
12
50
  {
13
51
  _id: '629acef1c7c8e8002ff60736',
@@ -286,44 +324,14 @@ export const items: IItem[] = [
286
324
  updatedAt: '2022-06-04T18:16:49.056Z',
287
325
  rarity: ItemRarities.Common,
288
326
  },
289
- {
290
- _id: '392acek4j7c8e8002ff60404',
291
- hasUseWith: false,
292
- type: ItemType.Container,
293
- subType: ItemSubType.Other,
294
- textureAtlas: 'items',
295
- allowedEquipSlotType: [ItemSlotType.Inventory],
296
- isEquipable: false,
297
- isStackable: true,
298
- maxStackSize: 100,
299
-
300
- isUsable: false,
301
- isStorable: true,
302
- isTwoHanded: false,
303
- layer: 1,
304
- isItemContainer: true,
305
- isSolid: false,
306
- key: 'bag',
307
- texturePath: 'containers/bag.png',
308
- textureKey: 'bag',
309
- name: 'Bag',
310
- generateContainerSlots: 10,
311
- description:
312
- 'You see a bag. It has made using leather and it has 10 total slots.',
313
- attack: 7,
314
- defense: 3,
315
- weight: 13,
316
- tiledId: 67,
317
- x: 320,
318
- y: 144,
319
- scene: 'MainScene',
320
- fullDescription:
321
- 'You see a bag. It has made using leather and it has 10 total slots.',
322
- createdAt: '2022-06-04T03:18:09.335Z',
323
- updatedAt: '2022-06-04T18:16:49.056Z',
324
- rarity: ItemRarities.Common,
325
- tintColor: '#FFD700',
326
- },
327
+ createBagItem('392acek4j7c8e8002ff60404', '#FF0000'), // red
328
+ createBagItem('392acek4j7c8e8002ff60405', '#0000FF'), // blue
329
+ createBagItem('392acek4j7c8e8002ff60406', '#00FF00'), // green
330
+ createBagItem('392acek4j7c8e8002ff60407', '#FFFF00'), // yellow
331
+ createBagItem('392acek4j7c8e8002ff60408', '#800080'), // purple
332
+ createBagItem('392acek4j7c8e8002ff60409', '#FFA500'), // orange
333
+ createBagItem('392acek4j7c8e8002ff60410', '#00FFFF'), // cyan
334
+ createBagItem('392acek4j7c8e8002ff60411', '#FFFFFF'), // white
327
335
  {
328
336
  _id: '392acek4j7c8e80d2fs60404',
329
337
  hasUseWith: false,
@@ -1,39 +1,36 @@
1
1
  import { Meta, Story } from '@storybook/react';
2
2
  import React, { useState } from 'react';
3
- import { ItemPropertySimpleHandler } from '../../../components/Item/Inventory/ItemPropertySimpleHandler';
3
+ import { ColorSelector } from '../../../components/Item/Inventory/ItemPropertyColorSelector';
4
+ import { RPGUIRoot } from '../../../components/RPGUI/RPGUIRoot';
4
5
 
5
6
  export default {
6
- title: 'UI/Dropdowns & Selectors/ItemPropertySimpleHandler',
7
- component: ItemPropertySimpleHandler,
7
+ title: 'UI/Dropdowns & Selectors/Item Property Color Selector',
8
+ component: ColorSelector,
8
9
  parameters: {
10
+ layout: 'centered',
9
11
  docs: {
10
12
  description: {
11
13
  component:
12
- 'A simple handler for selecting and confirming item properties, including colors.',
14
+ 'A color selector component for items with optional cost warning.',
13
15
  },
14
16
  },
15
17
  },
16
18
  argTypes: {
17
- isOpen: {
18
- control: { type: 'boolean' },
19
- description: 'Controls whether the color selector is open.',
20
- },
21
19
  selectedColor: {
22
20
  control: { type: 'color' },
23
21
  description: 'Initial selected color.',
24
22
  },
25
- onClose: {
26
- action: 'onClose',
27
- description: 'Callback when the selector is closed.',
28
- },
29
- onConfirm: {
30
- action: 'onConfirm',
31
- description: 'Callback when a color is confirmed.',
23
+ isOpen: {
24
+ control: { type: 'boolean' },
25
+ description: 'Controls whether the color selector is open.',
32
26
  },
33
- onChange: {
34
- action: 'onChange',
35
- description: 'Callback when a color is being selected.',
27
+ costWarning: {
28
+ control: 'object',
29
+ description: 'Optional warning about the cost of changing colors.',
36
30
  },
31
+ onClose: { action: 'closed' },
32
+ onConfirm: { action: 'confirmed' },
33
+ onChange: { action: 'changed' },
37
34
  },
38
35
  } as Meta;
39
36
 
@@ -60,13 +57,16 @@ const Template: Story = args => {
60
57
  };
61
58
 
62
59
  return (
63
- <ItemPropertySimpleHandler
64
- isOpen={isOpen}
65
- selectedColor={currentColor}
66
- onClose={handleClose}
67
- onConfirm={handleConfirm}
68
- onChange={handleChange}
69
- />
60
+ <RPGUIRoot>
61
+ <ColorSelector
62
+ isOpen={isOpen}
63
+ selectedColor={currentColor}
64
+ onClose={handleClose}
65
+ onConfirm={handleConfirm}
66
+ onChange={handleChange}
67
+ {...args}
68
+ />
69
+ </RPGUIRoot>
70
70
  );
71
71
  };
72
72
 
@@ -75,3 +75,29 @@ Default.args = {
75
75
  isOpen: true,
76
76
  selectedColor: '#ff0000',
77
77
  };
78
+
79
+ export const WithCostWarning = Template.bind({});
80
+ WithCostWarning.args = {
81
+ isOpen: true,
82
+ selectedColor: '#ff0000',
83
+ costWarning: {
84
+ cost: 10000,
85
+ currency: ' gold',
86
+ },
87
+ };
88
+
89
+ export const WithCustomCurrency = Template.bind({});
90
+ WithCustomCurrency.args = {
91
+ isOpen: true,
92
+ selectedColor: '#ff0000',
93
+ costWarning: {
94
+ cost: 5000,
95
+ currency: ' crystals',
96
+ },
97
+ };
98
+
99
+ export const Closed = Template.bind({});
100
+ Closed.args = {
101
+ isOpen: false,
102
+ selectedColor: '#ff0000',
103
+ };