@rpg-engine/long-bow 0.8.237 → 0.8.240

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.237",
3
+ "version": "0.8.240",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -24,7 +24,7 @@
24
24
  "build": "tsdx build",
25
25
  "test": "tsdx test --passWithNoTests",
26
26
  "lint": "tsdx lint",
27
- "prepare": "tsdx build",
27
+ "prepare": "echo skip",
28
28
  "size": "size-limit",
29
29
  "analyze": "size-limit --why",
30
30
  "storybook": "start-storybook -p 6006",
@@ -64,13 +64,19 @@
64
64
  "@storybook/addon-links": "6.5.10",
65
65
  "@storybook/addons": "6.5.10",
66
66
  "@storybook/react": "6.5.10",
67
+ "@swc/core": "1.3.94",
68
+ "@swc/jest": "0.2.29",
69
+ "@testing-library/react": "12.1.5",
67
70
  "@types/react": "^18.0.14",
68
71
  "@types/react-dom": "^18.0.5",
69
72
  "@types/styled-components": "^5.1.24",
70
73
  "@types/uuid": "^8.3.4",
74
+ "@vitejs/plugin-react": "1.3.2",
71
75
  "babel-loader": "^8.2.3",
72
76
  "babel-plugin-styled-components": "^2.0.6",
73
77
  "husky": "^7.0.4",
78
+ "jest": "29.7.0",
79
+ "jest-environment-jsdom": "29.7.0",
74
80
  "pretty-quick": "^3.1.3",
75
81
  "react": "^17.0.2",
76
82
  "react-dom": "^17.0.2",
@@ -83,18 +89,12 @@
83
89
  "tslib": "^2.3.1",
84
90
  "typescript": "^4.6.2",
85
91
  "uuid": "^8.3.2",
86
- "@swc/jest": "0.2.29",
87
- "@testing-library/react": "12.1.5",
88
- "jest": "29.7.0",
89
- "jest-environment-jsdom": "29.7.0",
90
- "vite": "2.9.16",
91
- "@vitejs/plugin-react": "1.3.2",
92
- "@swc/core": "1.3.94"
92
+ "vite": "2.9.16"
93
93
  },
94
94
  "dependencies": {
95
95
  "@capacitor/core": "^6.1.0",
96
96
  "@rollup/plugin-image": "^2.1.1",
97
- "@rpg-engine/shared": "^0.10.119",
97
+ "@rpg-engine/shared": "0.11.37",
98
98
  "dayjs": "^1.11.2",
99
99
  "font-awesome": "^4.7.0",
100
100
  "fs-extra": "^10.1.0",
@@ -0,0 +1,60 @@
1
+ import {
2
+ IItem,
3
+ ISoulGemSchema,
4
+ ItemContainerType,
5
+ ItemRarities,
6
+ ItemSubType,
7
+ SoulGemCategory,
8
+ SoulGemsBlueprint,
9
+ } from '@rpg-engine/shared';
10
+ import { generateContextMenu } from '../itemContainerHelper';
11
+
12
+ const soulGem: ISoulGemSchema = {
13
+ key: SoulGemsBlueprint.Fire,
14
+ name: 'Fire Soul Gem',
15
+ category: SoulGemCategory.Offensive,
16
+ rarity: ItemRarities.Common,
17
+ damageBonus: 0.05,
18
+ cooldownReduction: 0,
19
+ manaCostReduction: 0,
20
+ radiusBonus: 0,
21
+ durationBonus: 0,
22
+ };
23
+ const item = { subType: ItemSubType.SoulGem, soulGem } as IItem;
24
+ const attach = { id: 'attach-soul-gem', text: 'Attach gem' };
25
+
26
+ describe('Soul Gem inventory context action', () => {
27
+ it('offers Attach gem exactly once for a carried Soul Gem', () => {
28
+ expect(
29
+ generateContextMenu(item, ItemContainerType.Inventory).filter(
30
+ (action) => action.id === attach.id
31
+ )
32
+ ).toEqual([attach]);
33
+ });
34
+
35
+ it.each([
36
+ ItemContainerType.Depot,
37
+ ItemContainerType.Loot,
38
+ ItemContainerType.MapContainer,
39
+ ItemContainerType.Equipment,
40
+ null,
41
+ ])('does not offer attachment outside inventory: %s', (containerType) => {
42
+ expect(generateContextMenu(item, containerType)).not.toContainEqual(attach);
43
+ });
44
+
45
+ it('requires canonical Soul Gem metadata', () => {
46
+ expect(
47
+ generateContextMenu(
48
+ { ...item, soulGem: undefined },
49
+ ItemContainerType.Inventory
50
+ )
51
+ ).not.toContainEqual(attach);
52
+ });
53
+
54
+ it('requires the Soul Gem subtype', () => {
55
+ const ordinaryItem = { soulGem } as IItem;
56
+ expect(
57
+ generateContextMenu(ordinaryItem, ItemContainerType.Inventory)
58
+ ).not.toContainEqual(attach);
59
+ });
60
+ });
@@ -8,6 +8,7 @@ import {
8
8
  ItemContainerType,
9
9
  ItemSocketEvents,
10
10
  ItemSocketEventsDisplayLabels,
11
+ ItemSubType,
11
12
  ItemType
12
13
  } from '@rpg-engine/shared';
13
14
 
@@ -207,5 +208,13 @@ export const generateContextMenu = (
207
208
  contextActionMenu.push({ id: ItemSocketEvents.DetachGem, text: "Detach Gems" });
208
209
  }
209
210
 
211
+ if (
212
+ itemContainerType === ItemContainerType.Inventory &&
213
+ item.subType === ItemSubType.SoulGem &&
214
+ item.soulGem
215
+ ) {
216
+ contextActionMenu.push({ id: 'attach-soul-gem', text: 'Attach gem' });
217
+ }
218
+
210
219
  return contextActionMenu;
211
220
  };
@@ -90,7 +90,7 @@ export const LoginStreakPanel: React.FC<ILoginStreakPanelProps> = ({
90
90
  {milestones.map((milestone) => (
91
91
  <MilestoneCard key={milestone.day} $reached={milestone.reached}>
92
92
  <MilestoneDayLabel>Day {milestone.day}</MilestoneDayLabel>
93
- <MilestoneRewardName>{milestone.quantity}x {milestone.itemName}</MilestoneRewardName>
93
+ <MilestoneRewardName>{milestone.rewards.map(reward => `${reward.quantity}x ${reward.itemName}`).join(', ')}</MilestoneRewardName>
94
94
  <MilestoneBadge $reached={milestone.reached}>{milestone.reached ? '✓' : '○'}</MilestoneBadge>
95
95
  </MilestoneCard>
96
96
  ))}
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { cleanup, render, screen } from '@testing-library/react';
3
+ import { LoginStreakPanel } from '../LoginStreakPanel';
4
+
5
+ jest.mock('../../DraggableContainer', () => ({
6
+ DraggableContainer: ({ children }: { children: React.ReactNode }) => <>{children}</>,
7
+ }));
8
+ jest.mock('../../SimpleProgressBar', () => ({ SimpleProgressBar: () => null }));
9
+
10
+ afterEach(cleanup);
11
+
12
+ it('shows every reward from the shared milestone payload', () => {
13
+ render(
14
+ <LoginStreakPanel
15
+ streak={7}
16
+ longestStreak={7}
17
+ xpBonusPercent={15}
18
+ maxXpBonusPercent={50}
19
+ maxBonusDay={30}
20
+ isConsecutive
21
+ onClose={jest.fn()}
22
+ milestones={[{
23
+ day: 7,
24
+ reached: true,
25
+ rewards: [
26
+ { itemKey: 'healing-potion', itemName: 'Healing Potion', quantity: 5 },
27
+ { itemKey: 'mana-potion', itemName: 'Mana Potion', quantity: 3 },
28
+ ],
29
+ }]}
30
+ />
31
+ );
32
+ expect(screen.getByText('5x Healing Potion, 3x Mana Potion')).toBeTruthy();
33
+ });
@@ -1,6 +1,6 @@
1
1
  import { ISpell, ItemRarities } from '@rpg-engine/shared';
2
2
  import React from 'react';
3
- import styled, { css } from 'styled-components';
3
+ import styled, { css, keyframes } from 'styled-components';
4
4
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
5
5
  import { SpellInfoWrapper } from './cards/SpellInfoWrapper';
6
6
 
@@ -87,6 +87,11 @@ export const Spell: React.FC<ISpellProps> = ({
87
87
  );
88
88
  };
89
89
 
90
+ const assignmentPulse = keyframes`
91
+ 0%, 100% { background-color: rgba(255, 247, 215, .5); }
92
+ 50% { background-color: rgba(255, 252, 235, .9); }
93
+ `;
94
+
90
95
  const Card = styled.button<{ $assigning: boolean }>`
91
96
  box-sizing: border-box;
92
97
  display: flex;
@@ -104,6 +109,12 @@ const Card = styled.button<{ $assigning: boolean }>`
104
109
  font: inherit;
105
110
  cursor: pointer;
106
111
  transition: background .12s, border-color .12s;
112
+ animation-name: ${({ $assigning }) => $assigning ? assignmentPulse : 'none'};
113
+ animation-duration: 1.4s;
114
+ animation-timing-function: ease-in-out;
115
+ animation-iteration-count: infinite;
116
+ &:disabled { animation-name: none; }
117
+ @media (prefers-reduced-motion: reduce) { animation-name: none; }
107
118
  &:hover:not(:disabled) { background: rgba(255, 252, 235, .9); border-color: #8a5a14; box-shadow: inset 0 0 0 1px #8a5a14; }
108
119
  &:focus-visible { outline: 3px solid #3a2110; outline-offset: 2px; }
109
120
  /* RPGUI greys and recolors every :disabled (0,2,0): keep the dashed card and its red/blue stats */
@@ -100,3 +100,29 @@ it('positions focused details visibly without waiting for a mouse movement', ()
100
100
  expect(tooltip.style.opacity).toBe('1');
101
101
  expect(tooltip.style.transform).toBe('translate(170px, 120px)');
102
102
  });
103
+
104
+ it('closes mouse details after leaving a clicked card without blurring it', () => {
105
+ render(<SpellInfoWrapper spell={mockSpells[0]}><button>Cast</button></SpellInfoWrapper>);
106
+ const button = screen.getByRole('button');
107
+ // Mouse clicks focus buttons without enabling keyboard focus-visible.
108
+ const matches = button.matches.bind(button);
109
+ jest.spyOn(button, 'matches').mockImplementation(selector => selector === ':focus-visible' ? false : matches(selector));
110
+ fireEvent.mouseEnter(button);
111
+ act(() => button.focus());
112
+ fireEvent.click(button);
113
+ expect(screen.getByRole('tooltip')).toBeTruthy();
114
+ fireEvent.mouseLeave(button);
115
+ expect(screen.queryByRole('tooltip')).toBeNull();
116
+ expect(document.activeElement).toBe(button);
117
+ });
118
+
119
+ it('keeps keyboard details when the pointer leaves a keyboard-focused card', () => {
120
+ render(<SpellInfoWrapper spell={mockSpells[0]}><button>Cast</button></SpellInfoWrapper>);
121
+ const button = screen.getByRole('button');
122
+ jest.spyOn(button, 'matches').mockReturnValue(true);
123
+ act(() => button.focus());
124
+ fireEvent.mouseLeave(button);
125
+ expect(screen.getByRole('tooltip')).toBeTruthy();
126
+ act(() => button.blur());
127
+ expect(screen.queryByRole('tooltip')).toBeNull();
128
+ });
@@ -69,6 +69,8 @@ it('blocks cast on low skills, mana and cooldown but permits assignment without
69
69
  rerender(<Spell {...card} charSkillLevels={{ strength: 20 }} charMana={0} isSettingShortcut />);
70
70
  fireEvent.click(screen.getByRole('button'));
71
71
  expect(onClick).toHaveBeenCalledWith(spell.key);
72
+ rerender(<Spell {...card} charSkillLevels={{ strength: 2 }} isSettingShortcut />);
73
+ expect(getComputedStyle(screen.getByRole('button')).animationName).toBe('none');
72
74
  });
73
75
 
74
76
  it('clears an occupied shortcut without starting assignment', () => {
@@ -111,3 +113,13 @@ it('spells out requirement, mana cost and cooldown and only prompts when assigni
111
113
  expect(screen.getAllByText('Requires Magic Resistance 4')).toHaveLength(1);
112
114
  expect((screen.getByRole('button') as HTMLButtonElement).disabled).toBe(true);
113
115
  });
116
+
117
+ it('pulses only assignable cards until assignment ends', () => {
118
+ render(<Spellbook {...props} />);
119
+ expect(getComputedStyle(screen.getByRole('button', { name: 'Cast Spell 8' })).animationName).toBe('none');
120
+ fireEvent.click(screen.getByRole('button', { name: 'Set shortcut 1' }));
121
+ expect(getComputedStyle(screen.getByRole('button', { name: 'Assign Spell 8' })).animationName).not.toBe('none');
122
+ expect(getComputedStyle(screen.getByRole('button', { name: 'Assign Spell 8' })).animationName).not.toBe('');
123
+ fireEvent.click(screen.getByRole('button', { name: 'Cancel assignment' }));
124
+ expect(getComputedStyle(screen.getByRole('button', { name: 'Cast Spell 8' })).animationName).toBe('none');
125
+ });
@@ -65,7 +65,9 @@ export const SpellInfoWrapper: React.FC<ISpellInfoWrapperProps> = ({
65
65
  if (!isTooltipMobileVisible) setIsTooltipVisible(true);
66
66
  }}
67
67
  onMouseLeave={event => {
68
- if (!event.currentTarget.contains(document.activeElement)) setIsTooltipVisible(false);
68
+ if (!event.currentTarget.contains(document.activeElement) || !document.activeElement?.matches(':focus-visible')) {
69
+ setIsTooltipVisible(false);
70
+ }
69
71
  }}
70
72
  onTouchStart={handleTouchStart}
71
73
  onTouchEnd={clearHold}
@@ -16,10 +16,10 @@ export default meta;
16
16
  type Story = StoryObj<ILoginStreakPanelProps>;
17
17
 
18
18
  const mockMilestones: ILoginStreakMilestone[] = [
19
- { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
20
- { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: true },
21
- { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: false },
22
- { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: false },
19
+ { day: 7, rewards: [{ itemKey: 'light-healing-potion', itemName: 'Light Healing Potion', quantity: 5 }], reached: true },
20
+ { day: 14, rewards: [{ itemKey: 'greater-healing-potion', itemName: 'Greater Healing Potion', quantity: 3 }], reached: true },
21
+ { day: 21, rewards: [{ itemKey: 'light-mana-potion', itemName: 'Light Mana Potion', quantity: 5 }], reached: false },
22
+ { day: 30, rewards: [{ itemKey: 'greater-mana-potion', itemName: 'Greater Mana Potion', quantity: 3 }], reached: false },
23
23
  ];
24
24
 
25
25
  const Template = (args: ILoginStreakPanelProps) => (
@@ -53,10 +53,10 @@ export const InProgress: Story = {
53
53
  maxBonusDay: 30,
54
54
  isConsecutive: true,
55
55
  milestones: [
56
- { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
57
- { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: false },
58
- { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: false },
59
- { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: false },
56
+ { day: 7, rewards: [{ itemKey: 'light-healing-potion', itemName: 'Light Healing Potion', quantity: 5 }], reached: true },
57
+ { day: 14, rewards: [{ itemKey: 'greater-healing-potion', itemName: 'Greater Healing Potion', quantity: 3 }], reached: false },
58
+ { day: 21, rewards: [{ itemKey: 'light-mana-potion', itemName: 'Light Mana Potion', quantity: 5 }], reached: false },
59
+ { day: 30, rewards: [{ itemKey: 'greater-mana-potion', itemName: 'Greater Mana Potion', quantity: 3 }], reached: false },
60
60
  ],
61
61
  milestoneRewardGranted: null,
62
62
  onClose: () => console.log('close'),
@@ -88,10 +88,10 @@ export const MilestoneReached: Story = {
88
88
  maxBonusDay: 30,
89
89
  isConsecutive: true,
90
90
  milestones: [
91
- { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
92
- { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: false },
93
- { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: false },
94
- { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: false },
91
+ { day: 7, rewards: [{ itemKey: 'light-healing-potion', itemName: 'Light Healing Potion', quantity: 5 }], reached: true },
92
+ { day: 14, rewards: [{ itemKey: 'greater-healing-potion', itemName: 'Greater Healing Potion', quantity: 3 }], reached: false },
93
+ { day: 21, rewards: [{ itemKey: 'light-mana-potion', itemName: 'Light Mana Potion', quantity: 5 }], reached: false },
94
+ { day: 30, rewards: [{ itemKey: 'greater-mana-potion', itemName: 'Greater Mana Potion', quantity: 3 }], reached: false },
95
95
  ],
96
96
  milestoneRewardGranted: { itemName: 'Light Healing Potion', quantity: 5 },
97
97
  onClose: () => console.log('close'),
@@ -108,10 +108,10 @@ export const MaxBonus: Story = {
108
108
  maxBonusDay: 30,
109
109
  isConsecutive: true,
110
110
  milestones: [
111
- { day: 7, itemName: 'Light Healing Potion', quantity: 5, reached: true },
112
- { day: 14, itemName: 'Greater Healing Potion', quantity: 3, reached: true },
113
- { day: 21, itemName: 'Light Mana Potion', quantity: 5, reached: true },
114
- { day: 30, itemName: 'Greater Mana Potion', quantity: 3, reached: true },
111
+ { day: 7, rewards: [{ itemKey: 'light-healing-potion', itemName: 'Light Healing Potion', quantity: 5 }], reached: true },
112
+ { day: 14, rewards: [{ itemKey: 'greater-healing-potion', itemName: 'Greater Healing Potion', quantity: 3 }], reached: true },
113
+ { day: 21, rewards: [{ itemKey: 'light-mana-potion', itemName: 'Light Mana Potion', quantity: 5 }], reached: true },
114
+ { day: 30, rewards: [{ itemKey: 'greater-mana-potion', itemName: 'Greater Mana Potion', quantity: 3 }], reached: true },
115
115
  ],
116
116
  milestoneRewardGranted: null,
117
117
  onClose: () => console.log('close'),
@@ -1,4 +1,4 @@
1
- import { QuestStatus, QuestType } from '@rpg-engine/shared';
1
+ import { IQuest, QuestStatus, QuestType } from '@rpg-engine/shared';
2
2
  import { Story } from '@storybook/react';
3
3
  import React from 'react';
4
4
  import {
@@ -20,7 +20,7 @@ const Template: Story<IQuestListProps> = args => (
20
20
 
21
21
  export const Default = Template.bind({});
22
22
 
23
- const IQuestMock = [
23
+ const IQuestMock: IQuest[] = [
24
24
  {
25
25
  _id: '6317996c77f2420047a20b55',
26
26
  rewards: [