@rpg-engine/long-bow 0.1.6 → 0.1.7
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/package.json +4 -3
- package/src/NPCDialog/NPCDialog.tsx +106 -0
- package/src/NPCDialog/NPCDialogText.tsx +46 -0
- package/src/RPGUI/RPGUI.tsx +11 -0
- package/src/RPGUI/RPGUIContainer.tsx +28 -0
- package/src/index.tsx +4 -0
- package/src/libs/StringHelpers.ts +3 -0
- package/src/types/index.d.ts +1 -0
- package/src/typography/DynamicText.tsx +48 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpg-engine/long-bow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"main": "dist/index",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"src": "src"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"dist"
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|
|
18
19
|
"node": ">=10"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { RPGUI } from '../RPGUI/RPGUI';
|
|
4
|
+
import { RPGUIContainer } from '../RPGUI/RPGUIContainer';
|
|
5
|
+
import { NPCDialogText } from './NPCDialogText';
|
|
6
|
+
|
|
7
|
+
export enum NPCDialogType {
|
|
8
|
+
TextOnly = 'TextOnly',
|
|
9
|
+
TextAndThumbnail = 'TextAndThumbnail',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface INPCDialogProps {
|
|
13
|
+
text: string;
|
|
14
|
+
type: NPCDialogType;
|
|
15
|
+
npcKey: string;
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const NPCDialog: React.FC<INPCDialogProps> = ({
|
|
20
|
+
text,
|
|
21
|
+
type,
|
|
22
|
+
npcKey,
|
|
23
|
+
onClose,
|
|
24
|
+
}) => {
|
|
25
|
+
const [isOpen, setIsOpen] = useState<boolean>(true);
|
|
26
|
+
const [showGoNextIndicator, setShowGoNextIndicator] =
|
|
27
|
+
useState<boolean>(false);
|
|
28
|
+
|
|
29
|
+
return isOpen ? (
|
|
30
|
+
<RPGUI>
|
|
31
|
+
<RPGUIContainer type="framed-golden">
|
|
32
|
+
<Container>
|
|
33
|
+
<TextContainer
|
|
34
|
+
flex={type === NPCDialogType.TextAndThumbnail ? '70%' : '100%'}
|
|
35
|
+
>
|
|
36
|
+
<NPCDialogText
|
|
37
|
+
onStartStep={() => setShowGoNextIndicator(false)}
|
|
38
|
+
onEndStep={() => setShowGoNextIndicator(true)}
|
|
39
|
+
text={text}
|
|
40
|
+
onClose={() => {
|
|
41
|
+
if (onClose) {
|
|
42
|
+
onClose();
|
|
43
|
+
setIsOpen(false);
|
|
44
|
+
}
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
</TextContainer>
|
|
48
|
+
{type === NPCDialogType.TextAndThumbnail && (
|
|
49
|
+
<ThumbnailContainer>
|
|
50
|
+
<NPCThumbnail src={`/npcDialog/npcThumbnails/${npcKey}.png`} />
|
|
51
|
+
</ThumbnailContainer>
|
|
52
|
+
)}
|
|
53
|
+
</Container>
|
|
54
|
+
{showGoNextIndicator && (
|
|
55
|
+
<PressSpaceIndicator
|
|
56
|
+
right={type === NPCDialogType.TextOnly ? '1rem' : '10.5rem'}
|
|
57
|
+
src="/space.gif"
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</RPGUIContainer>
|
|
61
|
+
</RPGUI>
|
|
62
|
+
) : null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const Container = styled.div`
|
|
66
|
+
display: flex;
|
|
67
|
+
width: 100%;
|
|
68
|
+
height: 125px;
|
|
69
|
+
box-sizing: border-box;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
align-items: flex-start;
|
|
72
|
+
position: relative;
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
interface ITextContainerProps {
|
|
76
|
+
flex: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const TextContainer = styled.div<ITextContainerProps>`
|
|
80
|
+
flex: ${({ flex }) => flex} 0 0;
|
|
81
|
+
width: 355px;
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const ThumbnailContainer = styled.div`
|
|
85
|
+
flex: 30% 0 0;
|
|
86
|
+
display: flex;
|
|
87
|
+
justify-content: flex-end;
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const NPCThumbnail = styled.img`
|
|
91
|
+
image-rendering: pixelated;
|
|
92
|
+
height: 128px;
|
|
93
|
+
width: 128px;
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
interface IPressSpaceIndicatorProps {
|
|
97
|
+
right: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const PressSpaceIndicator = styled.img<IPressSpaceIndicatorProps>`
|
|
101
|
+
position: absolute;
|
|
102
|
+
right: ${({ right }) => right};
|
|
103
|
+
bottom: 1rem;
|
|
104
|
+
height: 20.7px;
|
|
105
|
+
image-rendering: -webkit-optimize-contrast;
|
|
106
|
+
`;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import useEventListener from '@use-it/event-listener';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { chunkString } from '../libs/StringHelpers';
|
|
5
|
+
import { DynamicText } from '../typography/DynamicText';
|
|
6
|
+
|
|
7
|
+
interface IProps {
|
|
8
|
+
text: string;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
onEndStep: () => void;
|
|
11
|
+
onStartStep: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NPCDialogText: React.FC<IProps> = ({
|
|
15
|
+
text,
|
|
16
|
+
onClose,
|
|
17
|
+
onEndStep,
|
|
18
|
+
onStartStep,
|
|
19
|
+
}) => {
|
|
20
|
+
const textChunks = chunkString(text, 85);
|
|
21
|
+
|
|
22
|
+
const [chunkIndex, setChunkIndex] = useState<number>(0);
|
|
23
|
+
|
|
24
|
+
useEventListener('keydown', (event: KeyboardEvent) => {
|
|
25
|
+
if (event.code === 'Space') {
|
|
26
|
+
if (textChunks?.[chunkIndex + 1]) {
|
|
27
|
+
setChunkIndex((prev) => prev + 1);
|
|
28
|
+
} else {
|
|
29
|
+
// if there's no more text chunks, close the dialog
|
|
30
|
+
onClose();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Container>
|
|
37
|
+
<DynamicText
|
|
38
|
+
text={textChunks?.[chunkIndex]!}
|
|
39
|
+
onFinish={onEndStep}
|
|
40
|
+
onStart={onStartStep}
|
|
41
|
+
/>
|
|
42
|
+
</Container>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const Container = styled.div``;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import 'rpgui/rpgui.css';
|
|
3
|
+
import 'rpgui/rpgui.js';
|
|
4
|
+
|
|
5
|
+
interface IProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const RPGUI: React.FC<IProps> = ({ children }) => {
|
|
10
|
+
return <div className="rpgui-content">{children}</div>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
interface IProps {
|
|
5
|
+
type: 'framed' | 'framed-golden' | 'framed-golden-2' | 'framed-grey';
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
width?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const RPGUIContainer: React.FC<IProps> = ({
|
|
11
|
+
children,
|
|
12
|
+
type,
|
|
13
|
+
width = '50%',
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<Container width={width} className={`rpgui-container ${type}`}>
|
|
17
|
+
{children}
|
|
18
|
+
</Container>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface IContainerProps {
|
|
23
|
+
width: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const Container = styled.div<IContainerProps>`
|
|
27
|
+
max-width: ${({ width }) => width};
|
|
28
|
+
`;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.gif';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
interface IProps {
|
|
5
|
+
text: string;
|
|
6
|
+
onFinish?: () => void;
|
|
7
|
+
onStart?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const DynamicText: React.FC<IProps> = ({ text, onFinish, onStart }) => {
|
|
11
|
+
const [textState, setTextState] = useState<string>('');
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let i = 0;
|
|
15
|
+
const interval = setInterval(() => {
|
|
16
|
+
// on every interval, show one more character
|
|
17
|
+
|
|
18
|
+
if (i === 0) {
|
|
19
|
+
if (onStart) {
|
|
20
|
+
onStart();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (i < text.length) {
|
|
25
|
+
setTextState(text.substring(0, i + 1));
|
|
26
|
+
i++;
|
|
27
|
+
} else {
|
|
28
|
+
clearInterval(interval);
|
|
29
|
+
if (onFinish) {
|
|
30
|
+
onFinish();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, 50);
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
clearInterval(interval);
|
|
37
|
+
};
|
|
38
|
+
}, [text]);
|
|
39
|
+
|
|
40
|
+
return <TextContainer>{textState}</TextContainer>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const TextContainer = styled.p`
|
|
44
|
+
font-size: 0.7rem !important;
|
|
45
|
+
color: white;
|
|
46
|
+
text-shadow: 1px 1px 0px #000000;
|
|
47
|
+
letter-spacing: 1.2px;
|
|
48
|
+
`;
|