@rpg-engine/long-bow 0.8.137 → 0.8.139

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.137",
3
+ "version": "0.8.139",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -24,6 +24,17 @@ const TRANSACTION_TYPE_LABELS: Record<string, string> = {
24
24
  AdminAdjustment: 'Admin',
25
25
  };
26
26
 
27
+ const TRANSACTION_TYPE_COLORS: Record<string, string> = {
28
+ Purchase: '#22c55e', // green — DC coming in
29
+ Transfer: '#60a5fa', // blue — P2P transfer
30
+ MarketplaceSale: '#fbbf24', // gold — sold item
31
+ MarketplacePurchase: '#f97316', // orange — bought item
32
+ StorePurchase: '#a78bfa', // purple — store
33
+ Fee: '#ef4444', // red — cost
34
+ Refund: '#06b6d4', // cyan — money back
35
+ AdminAdjustment: '#9ca3af', // gray — admin
36
+ };
37
+
27
38
  const TRANSACTION_TYPE_OPTIONS = [
28
39
  { value: '', label: 'All Types' },
29
40
  { value: 'Purchase', label: 'Purchase' },
@@ -101,16 +112,17 @@ export const DCHistoryPanel: React.FC<IDCHistoryPanelProps> = ({
101
112
  {transactions.map((tx) => {
102
113
  const isCredit = tx.amount > 0;
103
114
  const label = TRANSACTION_TYPE_LABELS[tx.type] ?? tx.type;
115
+ const color = TRANSACTION_TYPE_COLORS[tx.type] ?? '#f59e0b';
104
116
  const subtitle = tx.note ?? (tx.relatedCharacterName ? tx.relatedCharacterName : '');
105
117
 
106
118
  return (
107
- <TransactionRow key={tx._id}>
119
+ <TransactionRow key={tx._id} style={{ borderLeft: `3px solid ${color}` }}>
108
120
  <TxLeft>
109
- <TxType>{label}</TxType>
121
+ <TxType style={{ color }}>{label}</TxType>
110
122
  {subtitle ? <TxNote>{subtitle}</TxNote> : null}
111
123
  </TxLeft>
112
124
  <TxRight>
113
- <TxAmount $credit={isCredit}>
125
+ <TxAmount style={{ color: isCredit ? '#4CAF50' : '#D04648' }}>
114
126
  {isCredit ? '+' : ''}{tx.amount} DC
115
127
  </TxAmount>
116
128
  <TxDate>{formatDate(tx.createdAt)}</TxDate>
@@ -227,14 +239,12 @@ const TxRight = styled.div`
227
239
 
228
240
  const TxType = styled.span`
229
241
  font-size: 7px;
230
- color: #f59e0b;
231
242
  font-family: 'Press Start 2P', cursive;
232
243
  `;
233
244
 
234
- const TxAmount = styled.span<{ $credit: boolean }>`
245
+ const TxAmount = styled.span`
235
246
  font-size: 8px;
236
247
  font-family: 'Press Start 2P', cursive;
237
- color: ${({ $credit }) => ($credit ? uiColors.green : uiColors.red)};
238
248
  white-space: nowrap;
239
249
  `;
240
250
 
@@ -64,7 +64,7 @@ export const DCWalletModal: React.FC<IDCWalletModalProps> = ({
64
64
  {onBuyDC && (
65
65
  <BuyButton onPointerDown={onBuyDC} title="Buy Definya Coins">
66
66
  <FaShoppingCart />
67
- <BuyButtonLabel>Buy DC</BuyButtonLabel>
67
+ <BuyButtonLabel>Buy More DC</BuyButtonLabel>
68
68
  </BuyButton>
69
69
  )}
70
70
  </BalanceContent>
@@ -1,10 +1,11 @@
1
- import { IEquipmentSet, IMarketplaceItem } from '@rpg-engine/shared';
1
+ import { goldToDC, IEquipmentSet, IMarketplaceItem } from '@rpg-engine/shared';
2
2
  import React, { useEffect, useRef, useState } from 'react';
3
3
  import { AiFillCaretRight } from 'react-icons/ai';
4
4
  import styled from 'styled-components';
5
5
  import { ConfirmModal } from '../ConfirmModal';
6
6
  import { Dropdown } from '../Dropdown';
7
7
  import { Input } from '../Input';
8
+ import { MarketplaceBuyModal, MarketplacePaymentMethod } from './MarketplaceBuyModal';
8
9
  import { MarketplaceRows } from './MarketplaceRows';
9
10
  import { itemRarityOptions, itemTypeOptions, orderByOptions } from './filters';
10
11
 
@@ -26,11 +27,13 @@ export interface IBuyPanelProps {
26
27
  onChangePriceInput: (value: [number | undefined, number | undefined]) => void;
27
28
  scale?: number;
28
29
  equipmentSet?: IEquipmentSet | null;
29
- onMarketPlaceItemBuy?: (marketPlaceItemId: string) => void;
30
+ onMarketPlaceItemBuy?: (marketPlaceItemId: string, paymentMethod?: MarketplacePaymentMethod) => void;
30
31
  characterId: string;
31
32
  enableHotkeys?: () => void;
32
33
  disableHotkeys?: () => void;
33
34
  currentPage: number;
35
+ dcBalance?: number;
36
+ dcToGoldSwapRate?: number;
34
37
  }
35
38
 
36
39
  export const BuyPanel: React.FC<IBuyPanelProps> = ({
@@ -50,6 +53,8 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
50
53
  enableHotkeys,
51
54
  disableHotkeys,
52
55
  currentPage,
56
+ dcBalance = 0,
57
+ dcToGoldSwapRate = 0,
53
58
  }) => {
54
59
  const [name, setName] = useState('');
55
60
  const [mainLevel, setMainLevel] = useState<
@@ -70,9 +75,30 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
70
75
  itemsContainer.current?.scrollTo(0, 0);
71
76
  }, [currentPage]);
72
77
 
78
+ const buyingItem = buyingItemId
79
+ ? items.find(i => i._id === buyingItemId)
80
+ : null;
81
+ const hasDCBalance = dcBalance > 0 && dcToGoldSwapRate > 0;
82
+
83
+ const getDCEquivalentPrice = (goldPrice: number): number =>
84
+ dcToGoldSwapRate > 0 ? goldToDC(goldPrice) : 0;
85
+
73
86
  return (
74
87
  <>
75
- {buyingItemId && (
88
+ {buyingItemId && buyingItem && hasDCBalance && (
89
+ <MarketplaceBuyModal
90
+ goldPrice={buyingItem.price}
91
+ dcEquivalentPrice={getDCEquivalentPrice(buyingItem.price)}
92
+ dcBalance={dcBalance}
93
+ onClose={() => setBuyingItemId(null)}
94
+ onConfirm={(paymentMethod) => {
95
+ onMarketPlaceItemBuy?.(buyingItemId, paymentMethod);
96
+ setBuyingItemId(null);
97
+ enableHotkeys?.();
98
+ }}
99
+ />
100
+ )}
101
+ {buyingItemId && !hasDCBalance && (
76
102
  <ConfirmModal
77
103
  onClose={setBuyingItemId.bind(null, null)}
78
104
  onConfirm={() => {
@@ -216,6 +242,7 @@ export const BuyPanel: React.FC<IBuyPanelProps> = ({
216
242
  atlasJSON={atlasJSON}
217
243
  item={item}
218
244
  itemPrice={price}
245
+ dcEquivalentPrice={dcToGoldSwapRate > 0 ? getDCEquivalentPrice(price) : undefined}
219
246
  equipmentSet={equipmentSet}
220
247
  onMarketPlaceItemBuy={setBuyingItemId.bind(null, _id)}
221
248
  disabled={owner === characterId}
@@ -5,6 +5,7 @@ import { Button, ButtonTypes } from '../Button';
5
5
  import { DraggableContainer } from '../DraggableContainer';
6
6
  import { Pager } from '../Pager';
7
7
  import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
8
+ import { MarketplacePaymentMethod } from './MarketplaceBuyModal';
8
9
  import { BuyPanel } from './BuyPanel';
9
10
  import { ManagmentPanel } from './ManagmentPanel';
10
11
 
@@ -26,7 +27,7 @@ export interface IMarketPlaceProps {
26
27
  onChangePriceInput: (value: [number | undefined, number | undefined]) => void;
27
28
  scale?: number;
28
29
  equipmentSet?: IEquipmentSet | null;
29
- onMarketPlaceItemBuy?: (marketPlaceItemId: string) => void;
30
+ onMarketPlaceItemBuy?: (marketPlaceItemId: string, paymentMethod?: MarketplacePaymentMethod) => void;
30
31
  onMarketPlaceItemRemove?: (marketPlaceItemId: string) => void;
31
32
  availableGold: number;
32
33
  selectedItemToSell: IItem | null;
@@ -41,6 +42,8 @@ export interface IMarketPlaceProps {
41
42
  currentPage: number;
42
43
  itemsPerPage: number;
43
44
  onPageChange: (page: number) => void;
45
+ dcBalance?: number;
46
+ dcToGoldSwapRate?: number;
44
47
  }
45
48
 
46
49
  export const Marketplace: React.FC<IMarketPlaceProps> = props => {
@@ -0,0 +1,231 @@
1
+ import { formatDCAmount } from '@rpg-engine/shared';
2
+ import React, { useCallback, useState } from 'react';
3
+ import { FaTimes } from 'react-icons/fa';
4
+ import styled from 'styled-components';
5
+ import ModalPortal from '../Abstractions/ModalPortal';
6
+ import { Button, ButtonTypes } from '../Button';
7
+
8
+ export type MarketplacePaymentMethod = 'gold' | 'dc';
9
+
10
+ export interface IMarketplaceBuyModalProps {
11
+ goldPrice: number;
12
+ dcEquivalentPrice: number;
13
+ dcBalance: number;
14
+ onConfirm: (paymentMethod: MarketplacePaymentMethod) => void;
15
+ onClose: () => void;
16
+ }
17
+
18
+ export const MarketplaceBuyModal: React.FC<IMarketplaceBuyModalProps> = ({
19
+ goldPrice,
20
+ dcEquivalentPrice,
21
+ dcBalance,
22
+ onConfirm,
23
+ onClose,
24
+ }) => {
25
+ const [selected, setSelected] = useState<MarketplacePaymentMethod>('gold');
26
+ const hasSufficientDC = dcBalance >= dcEquivalentPrice;
27
+
28
+ const stopPropagation = useCallback(
29
+ (e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => {
30
+ e.stopPropagation();
31
+ },
32
+ []
33
+ );
34
+
35
+ const handleConfirm = useCallback(() => {
36
+ onConfirm(selected);
37
+ }, [selected, onConfirm]);
38
+
39
+ return (
40
+ <ModalPortal>
41
+ <Overlay onPointerDown={onClose} />
42
+ <ModalContainer>
43
+ <ModalContent
44
+ onClick={stopPropagation as React.MouseEventHandler}
45
+ onTouchStart={stopPropagation as React.TouchEventHandler}
46
+ onPointerDown={stopPropagation as React.PointerEventHandler}
47
+ >
48
+ <Header>
49
+ <Title>Confirm Purchase</Title>
50
+ <CloseButton onPointerDown={onClose} aria-label="Close">
51
+ <FaTimes />
52
+ </CloseButton>
53
+ </Header>
54
+
55
+ <Options>
56
+ <RadioOption
57
+ $selected={selected === 'gold'}
58
+ onPointerDown={() => setSelected('gold')}
59
+ >
60
+ <RadioCircle $selected={selected === 'gold'} />
61
+ <OptionText>
62
+ <OptionLabel>Gold</OptionLabel>
63
+ <OptionSub>{goldPrice.toLocaleString()} gold</OptionSub>
64
+ </OptionText>
65
+ </RadioOption>
66
+
67
+ <RadioOption
68
+ $selected={selected === 'dc'}
69
+ $disabled={!hasSufficientDC}
70
+ onPointerDown={() => hasSufficientDC && setSelected('dc')}
71
+ >
72
+ <RadioCircle $selected={selected === 'dc'} />
73
+ <OptionText>
74
+ <OptionLabel $disabled={!hasSufficientDC}>Definya Coin</OptionLabel>
75
+ <OptionSub>
76
+ {formatDCAmount(dcEquivalentPrice)} DC
77
+ {' '}
78
+ <BalanceHint $insufficient={!hasSufficientDC}>
79
+ ({formatDCAmount(dcBalance)} available)
80
+ </BalanceHint>
81
+ </OptionSub>
82
+ </OptionText>
83
+ </RadioOption>
84
+ </Options>
85
+
86
+ <ConfirmRow>
87
+ <Button buttonType={ButtonTypes.RPGUIButton} onPointerDown={handleConfirm}>
88
+ Confirm
89
+ </Button>
90
+ </ConfirmRow>
91
+ </ModalContent>
92
+ </ModalContainer>
93
+ </ModalPortal>
94
+ );
95
+ };
96
+
97
+ const Overlay = styled.div`
98
+ position: fixed;
99
+ inset: 0;
100
+ background: rgba(0, 0, 0, 0.65);
101
+ z-index: 1000;
102
+ `;
103
+
104
+ const ModalContainer = styled.div`
105
+ position: fixed;
106
+ inset: 0;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ z-index: 1001;
111
+ pointer-events: none;
112
+ `;
113
+
114
+ const ModalContent = styled.div`
115
+ background: #1a1a2e;
116
+ border: 2px solid #f59e0b;
117
+ border-radius: 8px;
118
+ padding: 20px 24px 24px;
119
+ min-width: 300px;
120
+ max-width: 90%;
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 16px;
124
+ pointer-events: auto;
125
+ animation: scaleIn 0.15s ease-out;
126
+
127
+ @keyframes scaleIn {
128
+ from { transform: scale(0.85); opacity: 0; }
129
+ to { transform: scale(1); opacity: 1; }
130
+ }
131
+ `;
132
+
133
+ const Header = styled.div`
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: space-between;
137
+ `;
138
+
139
+ const Title = styled.h3`
140
+ margin: 0;
141
+ font-family: 'Press Start 2P', cursive;
142
+ font-size: 0.7rem;
143
+ color: #fef08a;
144
+ `;
145
+
146
+ const CloseButton = styled.button`
147
+ background: none;
148
+ border: none;
149
+ color: rgba(255, 255, 255, 0.6);
150
+ cursor: pointer;
151
+ font-size: 1rem;
152
+ padding: 4px;
153
+ display: flex;
154
+ align-items: center;
155
+
156
+ &:hover {
157
+ color: #ffffff;
158
+ }
159
+ `;
160
+
161
+ const Options = styled.div`
162
+ display: flex;
163
+ flex-direction: column;
164
+ gap: 8px;
165
+ `;
166
+
167
+ const RadioOption = styled.div<{ $selected: boolean; $disabled?: boolean }>`
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 12px;
171
+ padding: 10px 12px;
172
+ border: 1px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.15)')};
173
+ border-radius: 6px;
174
+ background: ${({ $selected }) => ($selected ? 'rgba(245,158,11,0.1)' : 'transparent')};
175
+ cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
176
+ opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)};
177
+ transition: border-color 0.15s, background 0.15s;
178
+
179
+ &:hover {
180
+ border-color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.15)' : '#f59e0b')};
181
+ }
182
+ `;
183
+
184
+ const RadioCircle = styled.div<{ $selected: boolean }>`
185
+ width: 16px;
186
+ height: 16px;
187
+ border-radius: 50%;
188
+ border: 2px solid ${({ $selected }) => ($selected ? '#f59e0b' : 'rgba(255,255,255,0.4)')};
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ flex-shrink: 0;
193
+
194
+ &::after {
195
+ content: '';
196
+ width: 8px;
197
+ height: 8px;
198
+ border-radius: 50%;
199
+ background: #f59e0b;
200
+ opacity: ${({ $selected }) => ($selected ? 1 : 0)};
201
+ transition: opacity 0.15s;
202
+ }
203
+ `;
204
+
205
+ const OptionText = styled.div`
206
+ display: flex;
207
+ flex-direction: column;
208
+ gap: 3px;
209
+ `;
210
+
211
+ const OptionLabel = styled.span<{ $disabled?: boolean }>`
212
+ font-family: 'Press Start 2P', cursive;
213
+ font-size: 0.65rem;
214
+ color: ${({ $disabled }) => ($disabled ? 'rgba(255,255,255,0.4)' : '#ffffff')};
215
+ `;
216
+
217
+ const OptionSub = styled.span`
218
+ font-family: 'Press Start 2P', cursive;
219
+ font-size: 0.5rem;
220
+ color: rgba(255, 255, 255, 0.55);
221
+ `;
222
+
223
+ const BalanceHint = styled.span<{ $insufficient: boolean }>`
224
+ color: ${({ $insufficient }) => ($insufficient ? '#ef4444' : 'rgba(255, 255, 255, 0.4)')};
225
+ `;
226
+
227
+ const ConfirmRow = styled.div`
228
+ display: flex;
229
+ justify-content: center;
230
+ margin-top: 4px;
231
+ `;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ formatDCAmount,
2
3
  getItemTextureKeyPath,
3
4
  IEquipmentSet,
4
5
  IItem,
@@ -19,6 +20,7 @@ export interface IMarketPlaceRowsPropos {
19
20
  atlasIMG: any;
20
21
  item: IItem;
21
22
  itemPrice: number;
23
+ dcEquivalentPrice?: number;
22
24
  equipmentSet?: IEquipmentSet | null;
23
25
  scale?: number;
24
26
  onMarketPlaceItemBuy?: () => void;
@@ -31,6 +33,7 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
31
33
  atlasIMG,
32
34
  item,
33
35
  itemPrice,
36
+ dcEquivalentPrice,
34
37
  equipmentSet,
35
38
  scale,
36
39
  onMarketPlaceItemBuy,
@@ -99,11 +102,14 @@ export const MarketplaceRows: React.FC<IMarketPlaceRowsPropos> = ({
99
102
  </GoldContainer>
100
103
  <PriceValue>
101
104
  <p>
102
- <Ellipsis maxLines={1} maxWidth="200px" fontSize="10px">
105
+ <Ellipsis maxLines={1} maxWidth="120px" fontSize="10px">
103
106
  ${itemPrice}
104
107
  </Ellipsis>
105
108
  </p>
106
109
  </PriceValue>
110
+ {dcEquivalentPrice !== undefined && (
111
+ <DCPriceLabel>{formatDCAmount(dcEquivalentPrice)} DC</DCPriceLabel>
112
+ )}
107
113
  </ItemIconContainer>
108
114
  <ButtonContainer>
109
115
  <Button
@@ -176,6 +182,13 @@ const SpriteContainer = styled.div`
176
182
  left: 0.5rem;
177
183
  `;
178
184
 
185
+ const DCPriceLabel = styled.span`
186
+ margin-left: 8px;
187
+ color: #fef08a;
188
+ font-size: 0.65rem;
189
+ white-space: nowrap;
190
+ `;
191
+
179
192
  const PriceValue = styled.div`
180
193
  margin-left: 40px;
181
194
  `;
@@ -168,6 +168,8 @@ export const Store: React.FC<IStoreProps> = ({
168
168
  packs={packs.filter(pack => pack.priceUSD >= 9.99)}
169
169
  onAddToCart={handleAddPackToCart}
170
170
  onSelectPack={setSelectedPack}
171
+ atlasJSON={atlasJSON}
172
+ atlasIMG={atlasIMG}
171
173
  />
172
174
  ),
173
175
  },
@@ -176,9 +178,11 @@ export const Store: React.FC<IStoreProps> = ({
176
178
  title: packsTabLabel,
177
179
  content: customPacksContent ?? (
178
180
  <StorePacksSection
179
- packs={packs.filter(pack => pack.priceUSD < 9.99)}
181
+ packs={hidePremiumTab ? packs : packs.filter(pack => pack.priceUSD < 9.99)}
180
182
  onAddToCart={handleAddPackToCart}
181
183
  onSelectPack={setSelectedPack}
184
+ atlasJSON={atlasJSON}
185
+ atlasIMG={atlasIMG}
182
186
  />
183
187
  ),
184
188
  },
@@ -252,7 +256,6 @@ export const Store: React.FC<IStoreProps> = ({
252
256
  {onShowHistory && (
253
257
  <CTAButton
254
258
  icon={<FaHistory />}
255
- label="History"
256
259
  onClick={onShowHistory}
257
260
  />
258
261
  )}
@@ -3,6 +3,7 @@ import React, { useCallback } from 'react';
3
3
  import { FaCartPlus } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { CTAButton } from '../../shared/CTAButton/CTAButton';
6
+ import { SpriteFromAtlas } from '../../shared/SpriteFromAtlas';
6
7
  import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
7
8
  import { usePackFiltering } from '../../../hooks/usePackFiltering';
8
9
 
@@ -10,12 +11,16 @@ interface IStorePacksSectionProps {
10
11
  packs: IItemPack[];
11
12
  onAddToCart: (pack: IItemPack) => void;
12
13
  onSelectPack?: (pack: IItemPack) => void;
14
+ atlasJSON?: any;
15
+ atlasIMG?: string;
13
16
  }
14
17
 
15
18
  export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
16
19
  packs,
17
20
  onAddToCart,
18
21
  onSelectPack,
22
+ atlasJSON,
23
+ atlasIMG,
19
24
  }) => {
20
25
  const { searchQuery, setSearchQuery, filteredPacks } = usePackFiltering(packs);
21
26
 
@@ -24,11 +29,22 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
24
29
  return imageUrl.default || imageUrl.src;
25
30
  };
26
31
 
32
+ const renderPackIcon = useCallback(
33
+ (pack: IItemPack) => {
34
+ const imgSrc = getImageSrc(pack.image);
35
+ if (atlasJSON && atlasIMG && imgSrc && atlasJSON?.frames?.[imgSrc]) {
36
+ return <SpriteFromAtlas atlasJSON={atlasJSON} atlasIMG={atlasIMG} spriteKey={imgSrc} width={40} height={40} imgScale={1.2} centered />;
37
+ }
38
+ return <img src={imgSrc} alt={pack.title} />;
39
+ },
40
+ [atlasJSON, atlasIMG]
41
+ );
42
+
27
43
  const renderPack = useCallback(
28
44
  (pack: IItemPack) => (
29
45
  <PackRow key={pack.key} onClick={() => onSelectPack?.(pack)}>
30
46
  <PackIconContainer>
31
- <img src={getImageSrc(pack.image)} alt={pack.title} />
47
+ {renderPackIcon(pack)}
32
48
  </PackIconContainer>
33
49
 
34
50
  <PackDetails>
package/src/index.tsx CHANGED
@@ -35,6 +35,7 @@ export * from './components/itemSelector/ItemSelector';
35
35
  export * from './components/Leaderboard/Leaderboard';
36
36
  export * from './components/ListMenu';
37
37
  export * from './components/Marketplace/Marketplace';
38
+ export * from './components/Marketplace/MarketplaceBuyModal';
38
39
  export * from './components/Marketplace/MarketplaceRows';
39
40
  export * from './components/Multitab/TabBody';
40
41
  export * from './components/Multitab/TabsContainer';