feat: shop
This commit is contained in:
parent
512c5a470a
commit
f673833a1d
|
@ -97,21 +97,25 @@ class ItemProxy {
|
||||||
get projection() {
|
get projection() {
|
||||||
return this.json.projection;
|
return this.json.projection;
|
||||||
}
|
}
|
||||||
|
get price() {
|
||||||
|
return this.json.price;
|
||||||
|
}
|
||||||
get qty() {
|
get qty() {
|
||||||
return this.instance.slots[this.slot].qty;
|
return this.instance.slots[this.slot].qty;
|
||||||
}
|
}
|
||||||
set qty(qty) {
|
set qty(qty) {
|
||||||
const {instance} = this;
|
const {instance} = this;
|
||||||
if (qty <= 0) {
|
if (qty <= 0) {
|
||||||
this.Component.markChange(instance.entity, 'cleared', {[this.slot]: true});
|
instance.clear(this.slot);
|
||||||
delete instance.slots[this.slot];
|
|
||||||
delete instance.$$items[this.slot];
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
instance.slots[this.slot].qty = qty;
|
instance.slots[this.slot].qty = qty;
|
||||||
this.Component.markChange(instance.entity, 'qtyUpdated', {[this.slot]: qty});
|
this.Component.markChange(instance.entity, 'qtyUpdated', {[this.slot]: qty});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
get source() {
|
||||||
|
return this.instance.slots[this.slot].source;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Inventory extends Component {
|
export default class Inventory extends Component {
|
||||||
|
@ -169,6 +173,11 @@ export default class Inventory extends Component {
|
||||||
const Component = this;
|
const Component = this;
|
||||||
return class InventoryInstance extends Instance {
|
return class InventoryInstance extends Instance {
|
||||||
$$items = {};
|
$$items = {};
|
||||||
|
clear(slot) {
|
||||||
|
Component.markChange(this.entity, 'cleared', {[slot]: true});
|
||||||
|
delete this.slots[slot];
|
||||||
|
delete this.$$items[slot];
|
||||||
|
}
|
||||||
item(slot) {
|
item(slot) {
|
||||||
return this.$$items[slot];
|
return this.$$items[slot];
|
||||||
}
|
}
|
||||||
|
@ -185,7 +194,7 @@ export default class Inventory extends Component {
|
||||||
if (!slots[slot]) {
|
if (!slots[slot]) {
|
||||||
slots[slot] = stack;
|
slots[slot] = stack;
|
||||||
this.$$items[slot] = new ItemProxy(Component, this, slot);
|
this.$$items[slot] = new ItemProxy(Component, this, slot);
|
||||||
await this.$$items[slot].load();
|
await this.$$items[slot].load(stack.source);
|
||||||
Component.markChange(this.entity, 'given', {[slot]: slots[slot]});
|
Component.markChange(this.entity, 'given', {[slot]: slots[slot]});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
3
app/ecs/components/shop.js
Normal file
3
app/ecs/components/shop.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export default class Shop extends Component {}
|
7
app/ecs/components/wallet.js
Normal file
7
app/ecs/components/wallet.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
|
export default class Wallet extends Component {
|
||||||
|
static properties = {
|
||||||
|
gold: {type: 'uint32'},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import {memo} from 'react';
|
import {memo} from 'react';
|
||||||
|
|
||||||
import styles from './bag.module.css';
|
import styles from './bag.module.css';
|
||||||
|
import gridStyles from './grid.module.css';
|
||||||
|
|
||||||
import Grid from './grid.jsx';
|
import Grid from './grid.jsx';
|
||||||
|
|
||||||
|
@ -8,8 +9,9 @@ import Grid from './grid.jsx';
|
||||||
* Inventory bag. 10-40 slots of inventory.
|
* Inventory bag. 10-40 slots of inventory.
|
||||||
*/
|
*/
|
||||||
function Bag({
|
function Bag({
|
||||||
|
highlighted,
|
||||||
isInventoryOpen,
|
isInventoryOpen,
|
||||||
onActivate,
|
onSlotMouseDown,
|
||||||
slots,
|
slots,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -17,11 +19,20 @@ function Bag({
|
||||||
className={styles.bag}
|
className={styles.bag}
|
||||||
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, left: '-440px'}}
|
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, left: '-440px'}}
|
||||||
>
|
>
|
||||||
|
<style>{`
|
||||||
|
.${styles.bag} .${gridStyles.highlighted} {
|
||||||
|
background-color: rgba(255, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.${styles.bag} .${gridStyles.highlighted} button {
|
||||||
|
border: 2.5px dashed rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<Grid
|
<Grid
|
||||||
color="rgba(02, 02, 28, 0.6)"
|
color="rgba(02, 02, 28, 0.6)"
|
||||||
columns={10}
|
columns={10}
|
||||||
|
highlighted={highlighted}
|
||||||
label="Bag"
|
label="Bag"
|
||||||
onActivate={onActivate}
|
onSlotMouseDown={onSlotMouseDown}
|
||||||
slots={slots}
|
slots={slots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {memo} from 'react';
|
import {memo} from 'react';
|
||||||
|
|
||||||
import styles from './external.module.css';
|
import styles from './external.module.css';
|
||||||
|
import gridStyles from './grid.module.css';
|
||||||
|
|
||||||
import Grid from './grid.jsx';
|
import Grid from './grid.jsx';
|
||||||
|
|
||||||
|
@ -8,8 +9,9 @@ import Grid from './grid.jsx';
|
||||||
* External inventory.
|
* External inventory.
|
||||||
*/
|
*/
|
||||||
function External({
|
function External({
|
||||||
|
highlighted,
|
||||||
isInventoryOpen,
|
isInventoryOpen,
|
||||||
onActivate,
|
onSlotMouseDown,
|
||||||
slots,
|
slots,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -17,11 +19,20 @@ function External({
|
||||||
className={styles.external}
|
className={styles.external}
|
||||||
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, top: '450px'}}
|
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, top: '450px'}}
|
||||||
>
|
>
|
||||||
|
<style>{`
|
||||||
|
.${styles.external} .${gridStyles.highlighted} {
|
||||||
|
background-color: rgba(0, 255, 0, 0.4);
|
||||||
|
}
|
||||||
|
.${styles.external} .${gridStyles.highlighted} button {
|
||||||
|
border: 2.5px dashed rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<Grid
|
<Grid
|
||||||
color="rgba(57, 02, 02, 0.6)"
|
color="rgba(57, 02, 02, 0.6)"
|
||||||
columns={10}
|
columns={10}
|
||||||
|
highlighted={highlighted}
|
||||||
label="Chest"
|
label="Chest"
|
||||||
onActivate={onActivate}
|
onSlotMouseDown={onSlotMouseDown}
|
||||||
slots={slots}
|
slots={slots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
.external {
|
.external {
|
||||||
left: 20px;
|
--nothing: 0;
|
||||||
opacity: 1;
|
|
||||||
position: absolute;
|
|
||||||
top: 274px;
|
|
||||||
transition: top 150ms, opacity 200ms;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,19 @@ export default function Grid({
|
||||||
active = -1,
|
active = -1,
|
||||||
color,
|
color,
|
||||||
columns,
|
columns,
|
||||||
|
highlighted,
|
||||||
label,
|
label,
|
||||||
onActivate,
|
onSlotMouseDown,
|
||||||
slots,
|
slots,
|
||||||
}) {
|
}) {
|
||||||
const Slots = slots.map((slot, i) => (
|
const Slots = slots.map((slot, i) => (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
[styles.slot, active === i && styles.active]
|
[
|
||||||
|
styles.slot,
|
||||||
|
active === i && styles.active,
|
||||||
|
highlighted && highlighted.includes(i) && styles.highlighted,
|
||||||
|
]
|
||||||
.filter(Boolean).join(' ')
|
.filter(Boolean).join(' ')
|
||||||
}
|
}
|
||||||
key={i}
|
key={i}
|
||||||
|
@ -23,25 +28,14 @@ export default function Grid({
|
||||||
<Slot
|
<Slot
|
||||||
icon={slot?.icon}
|
icon={slot?.icon}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
onActivate(i)
|
onSlotMouseDown(i)
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onMouseUp={(event) => {
|
onMouseUp={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onDragOver={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
}}
|
|
||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
if (!slot) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
|
||||||
event.dataTransfer.setData('silphius/item', i);
|
|
||||||
onActivate(i);
|
|
||||||
}}
|
|
||||||
onDrop={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
onActivate(i);
|
|
||||||
}}
|
}}
|
||||||
qty={slot?.qty}
|
qty={slot?.qty}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -43,3 +43,7 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
--nothing: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -10,8 +10,9 @@ import Grid from './grid.jsx';
|
||||||
*/
|
*/
|
||||||
function Hotbar({
|
function Hotbar({
|
||||||
active,
|
active,
|
||||||
|
highlighted,
|
||||||
hotbarIsHidden,
|
hotbarIsHidden,
|
||||||
onActivate,
|
onSlotMouseDown,
|
||||||
slots,
|
slots,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
@ -23,13 +24,20 @@ function Hotbar({
|
||||||
.${styles.hotbar} .${gridStyles.label} {
|
.${styles.hotbar} .${gridStyles.label} {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.${styles.hotbar} .${gridStyles.highlighted} {
|
||||||
|
background-color: rgba(255, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.${styles.hotbar} .${gridStyles.highlighted} button {
|
||||||
|
border: 2.5px dashed rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<Grid
|
<Grid
|
||||||
active={active}
|
active={active}
|
||||||
color="rgba(02, 02, 57, 0.6)"
|
color="rgba(02, 02, 57, 0.6)"
|
||||||
columns={10}
|
columns={10}
|
||||||
|
highlighted={highlighted}
|
||||||
label={slots[active] && slots[active].label}
|
label={slots[active] && slots[active].label}
|
||||||
onActivate={onActivate}
|
onSlotMouseDown={onSlotMouseDown}
|
||||||
slots={slots}
|
slots={slots}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
86
app/react/components/dom/trade.jsx
Normal file
86
app/react/components/dom/trade.jsx
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import styles from './trade.module.css';
|
||||||
|
|
||||||
|
function reducePrice(r, item) {
|
||||||
|
return r + item.price * item.qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Trade({
|
||||||
|
isInventoryOpen,
|
||||||
|
losing: losingUnfiltered,
|
||||||
|
onTradeAccepted,
|
||||||
|
gaining: gainingUnfiltered,
|
||||||
|
wallet,
|
||||||
|
}) {
|
||||||
|
const losing = losingUnfiltered.filter(Boolean);
|
||||||
|
const gaining = gainingUnfiltered.filter(Boolean);
|
||||||
|
const nop = 0 === losing.length && 0 === gaining.length;
|
||||||
|
let earn = losing.reduce(reducePrice, 0) - gaining.reduce(reducePrice, 0);
|
||||||
|
const canAfford = -earn <= wallet;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.trade}
|
||||||
|
style={isInventoryOpen ? {transition: 'opacity 50ms'} : {opacity: 0, top: '450px'}}
|
||||||
|
>
|
||||||
|
<div className={styles.tradeInner}>
|
||||||
|
{
|
||||||
|
nop
|
||||||
|
? (
|
||||||
|
<div className={styles.nop}>Let's trade!</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ul className={styles.summary}>
|
||||||
|
{losing.length > 0 && (
|
||||||
|
<li className={styles.lose}>
|
||||||
|
Shop receives:
|
||||||
|
{' '}
|
||||||
|
<ul className={styles.items}>
|
||||||
|
{losing.map(({label, qty}, i) => (
|
||||||
|
<li key={i}>{label} x<span className={styles.qty}>{qty}</span></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{gaining.length > 0 && (
|
||||||
|
<li className={styles.gain}>
|
||||||
|
You receive:
|
||||||
|
{' '}
|
||||||
|
<ul className={styles.items}>
|
||||||
|
{gaining.map(({label, qty}, i) => (
|
||||||
|
<li key={i}>{label} x<span className={styles.qty}>{qty}</span></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li className={styles.subtotal}>
|
||||||
|
{
|
||||||
|
earn > 0
|
||||||
|
? (
|
||||||
|
<span className={styles.cost}>You <span className={styles.make}>earn <span className={styles.amount}>{earn}</span></span><span className={styles.gold}>🄶</span></span>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
earn < 0
|
||||||
|
? <span className={styles.cost}>You <span className={styles.pay}>pay <span className={styles.amount}>{-earn}</span></span><span className={styles.gold}>🄶</span></span>
|
||||||
|
: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
className={styles.agree}
|
||||||
|
disabled={!canAfford}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onTradeAccepted(event);
|
||||||
|
}}
|
||||||
|
title={canAfford ? "Finish the transaction" : `Transaction requires ${-earn}g but you only have ${wallet}g`}
|
||||||
|
>
|
||||||
|
{canAfford ? "Agree" : "Can't afford"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Trade;
|
129
app/react/components/dom/trade.module.css
Normal file
129
app/react/components/dom/trade.module.css
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
.trade {
|
||||||
|
border: 2.5px solid #444444;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin: 16px 40px 0 8px;
|
||||||
|
text-shadow:
|
||||||
|
1px 0 0 black,
|
||||||
|
0 1px 0 black,
|
||||||
|
-1px 0 0 black,
|
||||||
|
0 -1px 0 black
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tradeInner {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border: 2.5px solid #999999;
|
||||||
|
display: flex;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
height: 100%;
|
||||||
|
padding: 2.5px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agree {
|
||||||
|
background-color: rgba(255, 90, 0, 1);
|
||||||
|
border: 2.5px solid #cccccc;
|
||||||
|
box-shadow: inset 0 0 10px #666666;
|
||||||
|
color: white;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2.5px;
|
||||||
|
right: 2.5px;
|
||||||
|
text-shadow:
|
||||||
|
1px 0 0 black,
|
||||||
|
0 1px 0 black,
|
||||||
|
-1px 0 0 black,
|
||||||
|
0 -1px 0 black
|
||||||
|
;
|
||||||
|
transition: box-shadow 200ms;
|
||||||
|
&:disabled {
|
||||||
|
background-color: #333333;
|
||||||
|
border-color: #777777;
|
||||||
|
color: #777777;
|
||||||
|
}
|
||||||
|
&:not(:disabled):hover {
|
||||||
|
box-shadow: inset 0 0 10px #222222;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2.5px;
|
||||||
|
width: 100%;
|
||||||
|
> li {
|
||||||
|
padding: 2.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain {
|
||||||
|
background-color: rgba(0, 255, 0, 0.5);
|
||||||
|
border: 2.5px solid rgba(0, 255, 0, 0.5);
|
||||||
|
max-height: 40.5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lose {
|
||||||
|
background-color: rgba(255, 0, 0, 0.5);
|
||||||
|
border: 2.5px solid rgba(255, 0, 0, 0.5);
|
||||||
|
max-height: 40.5px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: inline;
|
||||||
|
li {
|
||||||
|
display: inline;
|
||||||
|
&:not(:last-child):after {
|
||||||
|
content: ', ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.make {
|
||||||
|
color: rgb(0, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gold {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty {
|
||||||
|
font-family: Joystix;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cost {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2.5px;
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 1.5em;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nop {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 2em;
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
17
app/react/components/dom/wallet.jsx
Normal file
17
app/react/components/dom/wallet.jsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import styles from './wallet.module.css';
|
||||||
|
|
||||||
|
function Wallet({gold}) {
|
||||||
|
const goldString = gold.toString();
|
||||||
|
const pad = ''.padStart(7 - goldString.length, '0');
|
||||||
|
return (
|
||||||
|
<span className={styles.wallet}>
|
||||||
|
<span className={styles.number}>
|
||||||
|
<span className={styles.pad}>{pad}</span>
|
||||||
|
<span className={styles.amount}>{goldString}</span>
|
||||||
|
</span>
|
||||||
|
<span className={styles.gold}>🄶</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Wallet;
|
19
app/react/components/dom/wallet.module.css
Normal file
19
app/react/components/dom/wallet.module.css
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.wallet {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
text-shadow: 1px 0 0 black, 0 1px 0 black, -1px 0 0 black, 0 -1px 0 black;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gold {
|
||||||
|
color: gold;
|
||||||
|
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-family: Joystix;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad {
|
||||||
|
color: #555555;
|
||||||
|
}
|
|
@ -15,6 +15,8 @@ import DateTime from './dom/datetime.jsx';
|
||||||
import Dom from './dom/dom.jsx';
|
import Dom from './dom/dom.jsx';
|
||||||
import Entities from './dom/entities.jsx';
|
import Entities from './dom/entities.jsx';
|
||||||
import External from './dom/external.jsx';
|
import External from './dom/external.jsx';
|
||||||
|
import Trade from './dom/trade.jsx';
|
||||||
|
import Wallet from './dom/wallet.jsx';
|
||||||
import HotBar from './dom/hotbar.jsx';
|
import HotBar from './dom/hotbar.jsx';
|
||||||
import Pixi from './pixi/pixi.jsx';
|
import Pixi from './pixi/pixi.jsx';
|
||||||
import Devtools from './devtools.jsx';
|
import Devtools from './devtools.jsx';
|
||||||
|
@ -62,7 +64,11 @@ function Ui({disconnected}) {
|
||||||
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
|
||||||
const [externalInventory, setExternalInventory] = useState();
|
const [externalInventory, setExternalInventory] = useState();
|
||||||
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
const [externalInventorySlots, setExternalInventorySlots] = useState();
|
||||||
|
const [gaining, setGaining] = useState([]);
|
||||||
|
const [losing, setLosing] = useState([]);
|
||||||
|
const [wallet, setWallet] = useState(0);
|
||||||
const [particleWorker, setParticleWorker] = useState();
|
const [particleWorker, setParticleWorker] = useState();
|
||||||
|
const [trading, setTrading] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let handle;
|
let handle;
|
||||||
if (disconnected) {
|
if (disconnected) {
|
||||||
|
@ -245,6 +251,9 @@ function Ui({disconnected}) {
|
||||||
if (update.MainEntity) {
|
if (update.MainEntity) {
|
||||||
mainEntityRef.current = id;
|
mainEntityRef.current = id;
|
||||||
}
|
}
|
||||||
|
if (update.Wallet && mainEntityRef.current === id) {
|
||||||
|
setWallet(update.Wallet.gold);
|
||||||
|
}
|
||||||
if (update.Inventory) {
|
if (update.Inventory) {
|
||||||
if (mainEntityRef.current === id) {
|
if (mainEntityRef.current === id) {
|
||||||
setBufferSlot(entity.Inventory.item(0));
|
setBufferSlot(entity.Inventory.item(0));
|
||||||
|
@ -267,6 +276,7 @@ function Ui({disconnected}) {
|
||||||
newInventorySlots[i] = entity.Inventory.item(i);
|
newInventorySlots[i] = entity.Inventory.item(i);
|
||||||
}
|
}
|
||||||
setExternalInventory(entity.id)
|
setExternalInventory(entity.id)
|
||||||
|
setTrading(!!entity.Shop);
|
||||||
setExternalInventorySlots(newInventorySlots);
|
setExternalInventorySlots(newInventorySlots);
|
||||||
setIsInventoryOpen(true);
|
setIsInventoryOpen(true);
|
||||||
setHotbarIsHidden(false);
|
setHotbarIsHidden(false);
|
||||||
|
@ -277,6 +287,9 @@ function Ui({disconnected}) {
|
||||||
else if (update.Inventory.closed) {
|
else if (update.Inventory.closed) {
|
||||||
setExternalInventory();
|
setExternalInventory();
|
||||||
setExternalInventorySlots();
|
setExternalInventorySlots();
|
||||||
|
setGaining([]);
|
||||||
|
setLosing([]);
|
||||||
|
setTrading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mainEntityRef.current === id) {
|
if (mainEntityRef.current === id) {
|
||||||
|
@ -371,25 +384,67 @@ function Ui({disconnected}) {
|
||||||
mainEntityRef,
|
mainEntityRef,
|
||||||
scale,
|
scale,
|
||||||
]);
|
]);
|
||||||
const hotbarOnActivate = useCallback((i) => {
|
const hotbarOnSlotMouseDown = useCallback((i) => {
|
||||||
keepHotbarOpen();
|
keepHotbarOpen();
|
||||||
|
if (trading) {
|
||||||
|
const index = losing.indexOf(i + 1);
|
||||||
|
if (-1 === index) {
|
||||||
|
losing.push(i + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
losing.splice(index, 1);
|
||||||
|
}
|
||||||
|
setLosing([...losing]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
client.send({
|
client.send({
|
||||||
type: 'Action',
|
type: 'Action',
|
||||||
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 1]},
|
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 1]},
|
||||||
});
|
});
|
||||||
}, [client, keepHotbarOpen, mainEntityRef]);
|
}
|
||||||
const bagOnActivate = useCallback((i) => {
|
}, [client, keepHotbarOpen, losing, mainEntityRef, trading]);
|
||||||
|
const bagOnSlotMouseDown = useCallback((i) => {
|
||||||
|
if (trading) {
|
||||||
|
const index = losing.indexOf(i + 11);
|
||||||
|
if (-1 === index) {
|
||||||
|
losing.push(i + 11);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
losing.splice(index, 1);
|
||||||
|
}
|
||||||
|
setLosing([...losing]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
client.send({
|
client.send({
|
||||||
type: 'Action',
|
type: 'Action',
|
||||||
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 11]},
|
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 11]},
|
||||||
});
|
});
|
||||||
}, [client, mainEntityRef]);
|
}
|
||||||
const externalInventoryOnActivate = useCallback((i) => {
|
}, [client, losing, mainEntityRef, trading]);
|
||||||
|
const externalInventoryOnSlotMouseDown = useCallback((i) => {
|
||||||
|
if (trading) {
|
||||||
|
const index = gaining.indexOf(i);
|
||||||
|
if (-1 === index) {
|
||||||
|
gaining.push(i);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gaining.splice(index, 1);
|
||||||
|
}
|
||||||
|
setGaining([...gaining]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
client.send({
|
client.send({
|
||||||
type: 'Action',
|
type: 'Action',
|
||||||
payload: {type: 'swapSlots', value: [0, externalInventory, i]},
|
payload: {type: 'swapSlots', value: [0, externalInventory, i]},
|
||||||
});
|
});
|
||||||
}, [client, externalInventory]);
|
}
|
||||||
|
}, [client, externalInventory, gaining, trading]);
|
||||||
|
const onTradeAccepted = useCallback(() => {
|
||||||
|
client.send({
|
||||||
|
type: 'Action',
|
||||||
|
payload: {type: 'acceptTrade', value: {gaining, losing}},
|
||||||
|
});
|
||||||
|
}, [client, gaining, losing]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pixiRef.current) {
|
if (!pixiRef.current) {
|
||||||
return;
|
return;
|
||||||
|
@ -510,21 +565,37 @@ function Ui({disconnected}) {
|
||||||
<Dom>
|
<Dom>
|
||||||
<HotBar
|
<HotBar
|
||||||
active={activeSlot}
|
active={activeSlot}
|
||||||
|
highlighted={trading && losing.filter((i) => i < 11).map((i) => i - 1)}
|
||||||
hotbarIsHidden={hotbarIsHidden}
|
hotbarIsHidden={hotbarIsHidden}
|
||||||
onActivate={hotbarOnActivate}
|
onSlotMouseDown={hotbarOnSlotMouseDown}
|
||||||
slots={hotbarSlots}
|
slots={hotbarSlots}
|
||||||
/>
|
/>
|
||||||
<Bag
|
<Bag
|
||||||
|
highlighted={trading && losing.filter((i) => i >= 11).map((i) => i - 11)}
|
||||||
isInventoryOpen={isInventoryOpen}
|
isInventoryOpen={isInventoryOpen}
|
||||||
onActivate={bagOnActivate}
|
onSlotMouseDown={bagOnSlotMouseDown}
|
||||||
slots={inventorySlots}
|
slots={inventorySlots}
|
||||||
/>
|
/>
|
||||||
{externalInventory && (
|
{externalInventory && (
|
||||||
|
<div className={styles.external}>
|
||||||
<External
|
<External
|
||||||
|
highlighted={trading && gaining}
|
||||||
isInventoryOpen={isInventoryOpen}
|
isInventoryOpen={isInventoryOpen}
|
||||||
onActivate={externalInventoryOnActivate}
|
onSlotMouseDown={externalInventoryOnSlotMouseDown}
|
||||||
slots={externalInventorySlots}
|
slots={externalInventorySlots}
|
||||||
/>
|
/>
|
||||||
|
{trading && (
|
||||||
|
<Trade
|
||||||
|
isInventoryOpen={isInventoryOpen}
|
||||||
|
onTradeAccepted={onTradeAccepted}
|
||||||
|
gaining={gaining.map((slot) => externalInventorySlots[slot])}
|
||||||
|
losing={losing.map((slot) => {
|
||||||
|
return slot < 11 ? hotbarSlots[slot - 1] : inventorySlots[slot - 11];
|
||||||
|
})}
|
||||||
|
wallet={wallet}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Entities
|
<Entities
|
||||||
camera={camera}
|
camera={camera}
|
||||||
|
@ -550,6 +621,7 @@ function Ui({disconnected}) {
|
||||||
<Disconnected />
|
<Disconnected />
|
||||||
)}
|
)}
|
||||||
<DateTime />
|
<DateTime />
|
||||||
|
<Wallet gold={wallet} />
|
||||||
</Dom>
|
</Dom>
|
||||||
</div>
|
</div>
|
||||||
{devtoolsIsOpen && (
|
{devtoolsIsOpen && (
|
||||||
|
|
|
@ -22,3 +22,13 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.external {
|
||||||
|
display: flex;
|
||||||
|
left: 20px;
|
||||||
|
opacity: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 274px;
|
||||||
|
transition: top 150ms, opacity 200ms;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -152,6 +152,7 @@ export default async function createHomestead(id) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Position: {x: 200, y: 200},
|
Position: {x: 200, y: 200},
|
||||||
|
Shop: {},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
anchorX: 0.5,
|
anchorX: 0.5,
|
||||||
anchorY: 0.7,
|
anchorY: 0.7,
|
||||||
|
|
|
@ -49,6 +49,9 @@ export default async function createPlayer(id) {
|
||||||
},
|
},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
|
Wallet: {
|
||||||
|
gold: 1000,
|
||||||
|
},
|
||||||
Wielder: {
|
Wielder: {
|
||||||
activeSlot: 1,
|
activeSlot: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -166,6 +166,8 @@ export default class Engine {
|
||||||
Interacts,
|
Interacts,
|
||||||
Interlocutor,
|
Interlocutor,
|
||||||
Inventory,
|
Inventory,
|
||||||
|
Player,
|
||||||
|
Wallet,
|
||||||
Wielder,
|
Wielder,
|
||||||
} = entity;
|
} = entity;
|
||||||
const ecs = this.ecses[Ecs.path];
|
const ecs = this.ecses[Ecs.path];
|
||||||
|
@ -231,6 +233,33 @@ export default class Engine {
|
||||||
Controlled[payload.type] = payload.value;
|
Controlled[payload.type] = payload.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'acceptTrade': {
|
||||||
|
const {losing, gaining} = payload.value;
|
||||||
|
const gainingSlots = gaining.filter((slot) => Player.openInventory.item(slot));
|
||||||
|
const losingSlots = losing.filter((slot) => Inventory.item(slot));
|
||||||
|
const nop = 0 === gainingSlots.length && 0 === losingSlots.length;
|
||||||
|
const gainingItems = gainingSlots.map((slot) => Player.openInventory.item(slot));
|
||||||
|
const losingItems = losingSlots.map((slot) => Inventory.item(slot));
|
||||||
|
if (nop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const reducePrice = (r, item) => {
|
||||||
|
return r + item.price * item.qty;
|
||||||
|
};
|
||||||
|
const earn = losingItems.reduce(reducePrice, 0) - gainingItems.reduce(reducePrice, 0);
|
||||||
|
const canAfford = -earn <= Wallet.gold;
|
||||||
|
if (!canAfford) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const slot of losingSlots) {
|
||||||
|
Inventory.clear(slot);
|
||||||
|
}
|
||||||
|
for (const item of gainingItems) {
|
||||||
|
Inventory.give({qty: item.qty, source: item.source});
|
||||||
|
}
|
||||||
|
Wallet.gold += earn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'swapSlots': {
|
case 'swapSlots': {
|
||||||
if (!Controlled.locked) {
|
if (!Controlled.locked) {
|
||||||
const [l, other, r] = payload.value;
|
const [l, other, r] = payload.value;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/brush/brush.png",
|
"icon": "/resources/brush/brush.png",
|
||||||
"label": "Brush",
|
"label": "Brush",
|
||||||
|
"price": 100,
|
||||||
"start": "/resources/brush/start.js"
|
"start": "/resources/brush/start.js"
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/furball/furball.png",
|
"icon": "/resources/furball/furball.png",
|
||||||
"label": "Fur Ball"
|
"label": "Fur Ball",
|
||||||
|
"price": 5
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/hoe/icon.png",
|
"icon": "/resources/hoe/icon.png",
|
||||||
"label": "Hoe",
|
"label": "Hoe",
|
||||||
|
"price": 100,
|
||||||
"projectionCheck": "/resources/hoe/projection-check.js",
|
"projectionCheck": "/resources/hoe/projection-check.js",
|
||||||
"projection": {
|
"projection": {
|
||||||
"distance": [3, -1],
|
"distance": [3, -1],
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/magic-swords/icon.png",
|
"icon": "/resources/magic-swords/icon.png",
|
||||||
"label": "Magic swords",
|
"label": "Magic swords",
|
||||||
|
"price": 2000,
|
||||||
"start": "/resources/magic-swords/start.js"
|
"start": "/resources/magic-swords/start.js"
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/potion/icon.png",
|
"icon": "/resources/potion/icon.png",
|
||||||
"label": "Potion",
|
"label": "Potion",
|
||||||
|
"price": 50,
|
||||||
"start": "/resources/potion/start.js"
|
"start": "/resources/potion/start.js"
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/tomato-seeds/icon.png",
|
"icon": "/resources/tomato-seeds/icon.png",
|
||||||
"label": "Tomato Seeds",
|
"label": "Tomato Seeds",
|
||||||
|
"price": 20,
|
||||||
"projection": {
|
"projection": {
|
||||||
"distance": [1, -1],
|
"distance": [1, -1],
|
||||||
"grid": [
|
"grid": [
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/tomato/tomato.png"
|
"icon": "/resources/tomato/tomato.png",
|
||||||
|
"label": "Tomato",
|
||||||
|
"price": 20
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"icon": "/resources/watering-can/icon.png",
|
"icon": "/resources/watering-can/icon.png",
|
||||||
"label": "Watering Can",
|
"label": "Watering Can",
|
||||||
|
"price": 100,
|
||||||
"projectionCheck": "/resources/watering-can/projection-check.js",
|
"projectionCheck": "/resources/watering-can/projection-check.js",
|
||||||
"projection": {
|
"projection": {
|
||||||
"distance": [3, -1],
|
"distance": [3, -1],
|
||||||
|
|
|
@ -14,11 +14,11 @@ export default {
|
||||||
decorators: [
|
decorators: [
|
||||||
(Hotbar, ctx) => {
|
(Hotbar, ctx) => {
|
||||||
const [, updateArgs] = useArgs();
|
const [, updateArgs] = useArgs();
|
||||||
const {onActivate} = ctx.args;
|
const {onSlotMouseDown} = ctx.args;
|
||||||
ctx.args.onActivate = (i) => {
|
ctx.args.onSlotMouseDown = (i) => {
|
||||||
updateArgs({active: i});
|
updateArgs({active: i});
|
||||||
if (onActivate) {
|
if (onSlotMouseDown) {
|
||||||
onActivate(i);
|
onSlotMouseDown(i);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Hotbar();
|
return Hotbar();
|
||||||
|
@ -33,7 +33,7 @@ export default {
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
args: {
|
args: {
|
||||||
active: 0,
|
active: 0,
|
||||||
onActivate: fn(),
|
onSlotMouseDown: fn(),
|
||||||
slots,
|
slots,
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user