Compare commits

...

10 Commits

Author SHA1 Message Date
cha0s
3eb94f2ef8 refactor: continuous direction 2024-07-24 09:28:35 -05:00
cha0s
cbe5f473a6 perf: nothing for nothing 2024-07-24 02:46:44 -05:00
cha0s
49f46b4b00 fix: clear undefined slots 2024-07-24 02:39:25 -05:00
cha0s
a0943c8bbb refactor: upper chat bounds 2024-07-24 02:31:06 -05:00
cha0s
80c1f78c6a fix: server logic 2024-07-24 02:23:12 -05:00
cha0s
52f19b1d89 fix: to each their own 2024-07-24 01:50:33 -05:00
cha0s
3d7b0fc14e fun: hearts 2024-07-24 01:40:10 -05:00
cha0s
c45d24b909 fun: inventory + bag 2024-07-23 23:27:08 -05:00
cha0s
f197658199 fix: grid group 2024-07-23 23:22:07 -05:00
cha0s
fb1a286a86 fix: destroyer 2024-07-23 21:40:28 -05:00
39 changed files with 565 additions and 133 deletions

View File

@ -3,6 +3,32 @@ import Component from '@/ecs/component.js';
export default class Controlled extends Component { export default class Controlled extends Component {
instanceFromSchema() { instanceFromSchema() {
return class ControlledInstance extends super.instanceFromSchema() { return class ControlledInstance extends super.instanceFromSchema() {
directionMove(direction) {
const x = Math.cos(direction);
if (x > 0) {
this.moveLeft = 0;
this.moveRight = x;
}
else {
this.moveLeft = -x;
this.moveRight = 0;
}
const y = Math.sin(direction);
if (y > 0) {
this.moveUp = 0;
this.moveDown = y;
}
else {
this.moveUp = -y;
this.moveDown = 0;
}
}
stop() {
this.moveRight = 0;
this.moveDown = 0;
this.moveLeft = 0;
this.moveUp = 0;
}
toJSON() { toJSON() {
return {}; return {};
} }

View File

@ -1,7 +1,18 @@
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
import {HALF_PI, TAU} from '@/util/math.js';
export default class Direction extends Component { export default class Direction extends Component {
instanceFromSchema() {
return class DirectionInstance extends super.instanceFromSchema() {
static quantize(d, n) {
return Math.floor(((d + (TAU / (n * 2))) % TAU) / (TAU / n));
}
quantize(n) {
return this.constructor.quantize(this.direction, n);
}
};
}
static properties = { static properties = {
direction: {type: 'uint8'}, direction: {defaultValue: HALF_PI, type: 'float32'},
}; };
} }

View File

@ -1,16 +1,6 @@
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
export default class Emitter extends Component { export default class Emitter extends Component {
mergeDiff(original, update) {
const merged = {};
if (update.emit) {
merged.emit = {
...original.emit,
...update.emit,
}
}
return merged;
}
instanceFromSchema() { instanceFromSchema() {
const Component = this; const Component = this;
return class EmitterInstance extends super.instanceFromSchema() { return class EmitterInstance extends super.instanceFromSchema() {
@ -21,4 +11,14 @@ export default class Emitter extends Component {
} }
}; };
} }
mergeDiff(original, update) {
const merged = {};
if (update.emit) {
merged.emit = {
...original.emit,
...update.emit,
}
}
return merged;
}
} }

View File

@ -8,18 +8,19 @@ export default class Interacts extends Component {
const {Direction, Position} = ecs.get(this.entity); const {Direction, Position} = ecs.get(this.entity);
let x0 = Position.x - 8; let x0 = Position.x - 8;
let y0 = Position.y - 8; let y0 = Position.y - 8;
if (0 === Direction.direction) { const direction = Direction.quantize(4);
y0 -= 12 if (0 === direction) {
}
if (1 === Direction.direction) {
x0 += 12 x0 += 12
} }
if (2 === Direction.direction) { if (1 === direction) {
y0 += 12 y0 += 12
} }
if (3 === Direction.direction) { if (2 === direction) {
x0 -= 12 x0 -= 12
} }
if (3 === direction) {
y0 -= 12
}
return {x0, x1: x0 + 15, y0, y1: y0 + 15}; return {x0, x1: x0 + 15, y0, y1: y0 + 15};
} }
toJSON() { toJSON() {

View File

@ -33,21 +33,21 @@ class ItemProxy {
let startY = position.y; let startY = position.y;
switch (direction) { switch (direction) {
case 0: case 0:
startX += projection.distance[1];
startY -= projection.distance[0];
break;
case 1:
startX += projection.distance[0]; startX += projection.distance[0];
startY += projection.distance[1]; startY += projection.distance[1];
break; break;
case 2: case 1:
startX -= projection.distance[1]; startX -= projection.distance[1];
startY += projection.distance[0]; startY += projection.distance[0];
break; break;
case 3: case 2:
startX -= projection.distance[0]; startX -= projection.distance[0];
startY -= projection.distance[1]; startY -= projection.distance[1];
break; break;
case 3:
startX += projection.distance[1];
startY -= projection.distance[0];
break;
} }
const projected = []; const projected = [];
for (const row in projection.grid) { for (const row in projection.grid) {
@ -58,17 +58,17 @@ class ItemProxy {
let axe; let axe;
switch (direction) { switch (direction) {
case 0: case 0:
axe = [column, row];
break;
case 1:
axe = [-row, column]; axe = [-row, column];
break; break;
case 2: case 1:
axe = [-column, -row]; axe = [-column, -row];
break; break;
case 3: case 2:
axe = [row, -column]; axe = [row, -column];
break; break;
case 3:
axe = [column, row];
break;
} }
const x = startX + parseInt(axe[0]); const x = startX + parseInt(axe[0]);
const y = startY + parseInt(axe[1]); const y = startY + parseInt(axe[1]);
@ -91,6 +91,9 @@ class ItemProxy {
get icon() { get icon() {
return this.json.icon; return this.json.icon;
} }
get label() {
return this.json.label;
}
get projection() { get projection() {
return this.json.projection; return this.json.projection;
} }
@ -191,6 +194,12 @@ export default class Inventory extends Component {
const tmp = [$$items[l], slots[l]]; const tmp = [$$items[l], slots[l]];
[$$items[l], slots[l]] = [$$items[r], slots[r]]; [$$items[l], slots[l]] = [$$items[r], slots[r]];
[$$items[r], slots[r]] = tmp; [$$items[r], slots[r]] = tmp;
if (undefined === slots[l]) {
delete slots[l];
}
if (undefined === slots[r]) {
delete slots[r];
}
Component.markChange(this.entity, 'swapped', [[l, r]]); Component.markChange(this.entity, 'swapped', [[l, r]]);
} }
} }

View File

@ -1,4 +1,5 @@
import {System} from '@/ecs/index.js'; import {System} from '@/ecs/index.js';
import {HALF_PI} from '@/util/math.js';
export default class ControlDirection extends System { export default class ControlDirection extends System {
@ -9,16 +10,16 @@ export default class ControlDirection extends System {
continue; continue;
} }
if (moveUp > 0) { if (moveUp > 0) {
Direction.direction = 0; Direction.direction = HALF_PI * 3;
} }
if (moveDown > 0) { if (moveDown > 0) {
Direction.direction = 2; Direction.direction = HALF_PI * 1;
} }
if (moveLeft > 0) { if (moveLeft > 0) {
Direction.direction = 3; Direction.direction = HALF_PI * 2;
} }
if (moveRight > 0) { if (moveRight > 0) {
Direction.direction = 1; Direction.direction = HALF_PI * 0;
} }
} }
} }

View File

@ -25,12 +25,12 @@ export default class SpriteDirection extends System {
} }
if (Direction) { if (Direction) {
const name = { const name = {
0: 'up', 0: 'right',
1: 'right', 1: 'down',
2: 'down', 2: 'left',
3: 'left', 3: 'up',
}; };
parts.push(name[Direction.direction]); parts.push(name[Direction.quantize(4)]);
} }
if (parts.length > 0) { if (parts.length > 0) {
Sprite.animation = parts.join(':'); Sprite.animation = parts.join(':');

View File

@ -20,11 +20,11 @@ export default function Tiles({eventsChannel}) {
const [ecs] = useEcs(); const [ecs] = useEcs();
useEffect(() => { useEffect(() => {
if (!ecs) { if (!ecs) {
return false; return;
} }
const master = ecs.get(1); const master = ecs.get(1);
if (!master) { if (!master) {
return false; return;
} }
const {TileLayers} = master; const {TileLayers} = master;
const {area, tileSize} = TileLayers.layer(0); const {area, tileSize} = TileLayers.layer(0);

View File

@ -0,0 +1,54 @@
import styles from './bag.module.css';
import Slot from './slot.jsx';
/**
* Inventory bag. 10-40 slots of inventory.
*/
export default function Bag({
isInventoryOpen,
slots,
}) {
const Slots = slots.map((slot, i) => (
<div
className={
[styles.slotWrapper]
.filter(Boolean).join(' ')
}
key={i}
>
<Slot
icon={slot?.icon}
// onMouseDown={(event) => {
// onActivate(i)
// event.stopPropagation();
// }}
// onMouseUp={(event) => {
// event.stopPropagation();
// }}
// onDragOver={(event) => {
// event.preventDefault();
// }}
// onDragStart={(event) => {
// if (!slot) {
// event.preventDefault();
// }
// event.dataTransfer.setData('silphius/item', i);
// onActivate(i);
// }}
// onDrop={(event) => {
// event.preventDefault();
// onActivate(i);
// }}
qty={slot?.qty}
/>
</div>
));
return (
<div
className={styles.bag}
style={isInventoryOpen ? {transition: 'none'} : {left: '-440px'}}
>
{Slots}
</div>
);
}

View File

@ -0,0 +1,31 @@
.bag {
align-self: left;
--border: calc(var(--unit) * 3px);
background-color: rgba(02, 02, 28, 0.6);
border: var(--border) solid #444444;
box-sizing: border-box;
display: inline-block;
left: calc(var(--unit) * 20px);
line-height: 0;
position: absolute;
top: calc(var(--unit) * 90px);
transition: left 150ms;
max-width: 430.5px;
}
.slotWrapper {
border: var(--border) solid #999999;
box-sizing: border-box;
display: inline-block;
line-height: 0;
padding: 0;
&:not(:nth-child(10n)) {
border-right: none;
}
&:not(:nth-last-of-type(-n+10)) {
border-bottom: none;
}
&:hover {
background-color: rgba(0, 0, 0, 0.2);
}
}

View File

@ -9,8 +9,3 @@
user-select: text; user-select: text;
width: 100%; width: 100%;
} }
@font-face {
font-family: "Cookbook";
src: url("/assets/fonts/Cookbook.woff") format("woff");
}

View File

@ -5,7 +5,7 @@
background-color: #00000044; background-color: #00000044;
border: 1px solid #333333; border: 1px solid #333333;
color: #ffffff; color: #ffffff;
font-family: "Cookbook"; font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
font-size: 16px; font-size: 16px;
margin: 4px; margin: 4px;
padding: 0; padding: 0;

View File

@ -97,7 +97,7 @@ export default function Dialogue({
position.y * scale - camera.y, position.y * scale - camera.y,
RESOLUTION.y - bounds.y * scale - 16, RESOLUTION.y - bounds.y * scale - 16,
), ),
bounds.y * scale + 88, bounds.y * scale + 16,
); );
return ( return (
<div <div

View File

@ -14,6 +14,9 @@ export default function Dialogues({camera, dialogues, scale}) {
/> />
); );
} }
if (0 === elements.length) {
return false;
}
return <div className={styles.dialogues}>{elements}</div>; return <div className={styles.dialogues}>{elements}</div>;
} }

View File

@ -2,8 +2,3 @@
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif; font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
font-size: 22px; font-size: 22px;
} }
@font-face {
font-family: "Cookbook";
src: url("/assets/fonts/Cookbook.woff") format("woff");
}

View File

@ -4,7 +4,12 @@ import Slot from './slot.jsx';
/** /**
* The hotbar. 10 slots of inventory with an active selection. * The hotbar. 10 slots of inventory with an active selection.
*/ */
export default function Hotbar({active, onActivate, slots}) { export default function Hotbar({
active,
hotbarIsHidden,
onActivate,
slots,
}) {
const Slots = slots.map((slot, i) => ( const Slots = slots.map((slot, i) => (
<div <div
className={ className={
@ -43,7 +48,9 @@ export default function Hotbar({active, onActivate, slots}) {
return ( return (
<div <div
className={styles.hotbar} className={styles.hotbar}
style={hotbarIsHidden ? {top: '-50px'} : {transition: 'none'}}
> >
<p className={styles.label}>{slots[active] && slots[active].label}</p>
{Slots} {Slots}
</div> </div>
); );

View File

@ -1,13 +1,32 @@
.hotbar { .hotbar {
align-self: center; align-self: left;
--border: calc(var(--unit) * 3px); --border: calc(var(--unit) * 3px);
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(02, 02, 57, 0.6);
border: var(--border) solid #444444; border: var(--border) solid #444444;
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
left: calc(var(--unit) * 20px);
line-height: 0; line-height: 0;
position: absolute; position: absolute;
top: calc(var(--unit) * 25px); top: calc(var(--unit) * 20px);
transition: top 150ms;
}
.label {
background-color: transparent;
color: white;
font-family: Cookbook, Georgia, 'Times New Roman', Times, serif;
left: 50%;
margin: 0;
position: absolute;
text-shadow:
0px -1px 0px black,
1px 0px 0px black,
0px 1px 0px black,
-1px 0px 0px black
;
top: -17.5px;
transform: translateX(-50%);
} }
.slotWrapper { .slotWrapper {

View File

@ -108,7 +108,7 @@ export default function Ecs({applyFilters, camera, monopolizers, scale}) {
if (entity) { if (entity) {
const {Direction, Position, Wielder} = entity; const {Direction, Position, Wielder} = entity;
setPosition(Position.toJSON()); setPosition(Position.toJSON());
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.direction)); setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
} }
}, [ecs, mainEntity, scale]); }, [ecs, mainEntity, scale]);
useEffect(() => { useEffect(() => {

View File

@ -7,6 +7,8 @@ import {Sprite} from '@pixi/sprite';
import {useRadians} from '@/react/context/radians.js'; import {useRadians} from '@/react/context/radians.js';
import {deferredLighting} from './lights.js';
const tileSize = {x: 16, y: 16}; const tileSize = {x: 16, y: 16};
const radius = 9; const radius = 9;
@ -68,6 +70,7 @@ const TargetingGridInternal = PixiComponent('TargetingGrid', {
container.mask = area; container.mask = area;
const top = new Container(); const top = new Container();
top.addChild(container, area); top.addChild(container, area);
top.parentGroup = deferredLighting.diffuseGroup;
return top; return top;
}, },
applyProps: ({children: [container]}, oldProps, {x, y, radians}) => { applyProps: ({children: [container]}, oldProps, {x, y, radians}) => {

View File

@ -11,6 +11,7 @@ import addKeyListener from './add-key-listener.js';
import ClientEcs from './client-ecs.js'; import ClientEcs from './client-ecs.js';
import Disconnected from './dom/disconnected.jsx'; import Disconnected from './dom/disconnected.jsx';
import Chat from './dom/chat/chat.jsx'; import Chat from './dom/chat/chat.jsx';
import Bag from './dom/bag.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 HotBar from './dom/hotbar.jsx'; import HotBar from './dom/hotbar.jsx';
@ -18,6 +19,11 @@ import Pixi from './pixi/pixi.jsx';
import Devtools from './devtools.jsx'; import Devtools from './devtools.jsx';
import styles from './ui.module.css'; import styles from './ui.module.css';
const KEY_MAP = {
keyDown: 1,
keyUp: 0,
};
function emptySlots() { function emptySlots() {
return Array(10).fill(undefined); return Array(10).fill(undefined);
} }
@ -50,6 +56,9 @@ function Ui({disconnected}) {
const [chatHistoryCaret, setChatHistoryCaret] = useState(-1); const [chatHistoryCaret, setChatHistoryCaret] = useState(-1);
const [chatMessages, setChatMessages] = useState({}); const [chatMessages, setChatMessages] = useState({});
const [pendingMessage, setPendingMessage] = useState(''); const [pendingMessage, setPendingMessage] = useState('');
const [hotbarIsHidden, setHotbarIsHidden] = useState(true);
const [hotbarHideHandle, setHotbarHideHandle] = useState();
const [isInventoryOpen, setIsInventoryOpen] = useState(false);
useEffect(() => { useEffect(() => {
async function setEcsStuff() { async function setEcsStuff() {
const {default: Components} = await import('@/ecs/components/index.js'); const {default: Components} = await import('@/ecs/components/index.js');
@ -76,6 +85,44 @@ function Ui({disconnected}) {
clearTimeout(handle); clearTimeout(handle);
}; };
}, [disconnected]); }, [disconnected]);
useEffect(() => {
return addKeyListener(document.body, ({type, payload}) => {
if (chatInputRef.current) {
chatInputRef.current.focus();
}
if (chatIsOpen) {
return;
}
let actionPayload;
switch (payload) {
case 'w': {
actionPayload = {type: 'moveUp', value: KEY_MAP[type]};
break;
}
case 'a': {
actionPayload = {type: 'moveLeft', value: KEY_MAP[type]};
break;
}
case 's': {
actionPayload = {type: 'moveDown', value: KEY_MAP[type]};
break;
}
case 'd': {
actionPayload = {type: 'moveRight', value: KEY_MAP[type]};
break;
}
}
if (actionPayload) {
client.send({
type: 'Action',
payload: actionPayload,
});
}
});
}, [
chatIsOpen,
client,
]);
useEffect(() => { useEffect(() => {
return addKeyListener(document.body, ({event, type, payload}) => { return addKeyListener(document.body, ({event, type, payload}) => {
if ('Escape' === payload && 'keyDown' === type && chatIsOpen) { if ('Escape' === payload && 'keyDown' === type && chatIsOpen) {
@ -88,10 +135,6 @@ function Ui({disconnected}) {
if (chatIsOpen) { if (chatIsOpen) {
return; return;
} }
const KEY_MAP = {
keyDown: 1,
keyUp: 0,
};
let actionPayload; let actionPayload;
switch (payload) { switch (payload) {
case '-': case '-':
@ -123,26 +166,25 @@ function Ui({disconnected}) {
} }
break; break;
} }
case 'w': {
actionPayload = {type: 'moveUp', value: KEY_MAP[type]};
break;
}
case 'a': {
actionPayload = {type: 'moveLeft', value: KEY_MAP[type]};
break;
}
case 's': {
actionPayload = {type: 'moveDown', value: KEY_MAP[type]};
break;
}
case 'd': {
actionPayload = {type: 'moveRight', value: KEY_MAP[type]};
break;
}
case ' ': { case ' ': {
actionPayload = {type: 'use', value: KEY_MAP[type]}; actionPayload = {type: 'use', value: KEY_MAP[type]};
break; break;
} }
case 'Tab': {
if ('keyDown' === type) {
if (isInventoryOpen) {
setHotbarIsHidden(true);
}
else {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
}
setIsInventoryOpen(!isInventoryOpen);
}
break
}
case 'Enter': { case 'Enter': {
if ('keyDown' === type) { if ('keyDown' === type) {
setChatIsOpen(true); setChatIsOpen(true);
@ -161,60 +203,150 @@ function Ui({disconnected}) {
} }
case '1': { case '1': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 1}; actionPayload = {type: 'changeSlot', value: 1};
} }
break; break;
} }
case '2': { case '2': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 2}; actionPayload = {type: 'changeSlot', value: 2};
} }
break; break;
} }
case '3': { case '3': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 3}; actionPayload = {type: 'changeSlot', value: 3};
} }
break; break;
} }
case '4': { case '4': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 4}; actionPayload = {type: 'changeSlot', value: 4};
} }
break; break;
} }
case '5': { case '5': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 5}; actionPayload = {type: 'changeSlot', value: 5};
} }
break; break;
} }
case '6': { case '6': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 6}; actionPayload = {type: 'changeSlot', value: 6};
} }
break; break;
} }
case '7': { case '7': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 7}; actionPayload = {type: 'changeSlot', value: 7};
} }
break; break;
} }
case '8': { case '8': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 8}; actionPayload = {type: 'changeSlot', value: 8};
} }
break; break;
} }
case '9': { case '9': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 9}; actionPayload = {type: 'changeSlot', value: 9};
} }
break; break;
} }
case '0': { case '0': {
if ('keyDown' === type) { if ('keyDown' === type) {
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
actionPayload = {type: 'changeSlot', value: 10}; actionPayload = {type: 'changeSlot', value: 10};
} }
break; break;
@ -227,7 +359,17 @@ function Ui({disconnected}) {
}); });
} }
}); });
}, [chatIsOpen, client, debug, devtoolsIsOpen, monopolizers, setDebug, setScale]); }, [
chatIsOpen,
client,
debug,
devtoolsIsOpen,
hotbarHideHandle,
isInventoryOpen,
monopolizers,
setDebug,
setScale,
]);
usePacket('EcsChange', async () => { usePacket('EcsChange', async () => {
setEcs(new ClientEcs({Components, Systems})); setEcs(new ClientEcs({Components, Systems}));
setMainEntity(undefined); setMainEntity(undefined);
@ -387,6 +529,15 @@ function Ui({disconnected}) {
event.preventDefault(); event.preventDefault();
return; return;
} }
if (!isInventoryOpen) {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
setHotbarHideHandle(setTimeout(() => {
setHotbarIsHidden(true);
}, 4000));
}
if (event.deltaY > 0) { if (event.deltaY > 0) {
client.send({ client.send({
type: 'Action', type: 'Action',
@ -411,6 +562,7 @@ function Ui({disconnected}) {
<Dom> <Dom>
<HotBar <HotBar
active={activeSlot} active={activeSlot}
hotbarIsHidden={hotbarIsHidden}
onActivate={(i) => { onActivate={(i) => {
client.send({ client.send({
type: 'Action', type: 'Action',
@ -419,6 +571,10 @@ function Ui({disconnected}) {
}} }}
slots={hotbarSlots} slots={hotbarSlots}
/> />
<Bag
isInventoryOpen={isInventoryOpen}
slots={Array(30).fill(undefined)}
/>
<Entities <Entities
camera={camera} camera={camera}
scale={scale} scale={scale}

View File

@ -26,3 +26,8 @@ body {
border-radius: 20px; border-radius: 20px;
border: 3px solid #333; border: 3px solid #333;
} }
@font-face {
font-family: "Cookbook";
src: url("/assets/fonts/Cookbook.woff") format("woff");
}

View File

@ -116,7 +116,8 @@ export default async function createHomestead(id) {
], ],
}, },
Controlled: {}, Controlled: {},
Direction: {direction: 2}, Direction: {},
Emitter: {},
Forces: {}, Forces: {},
Interactive: { Interactive: {
interacting: 1, interacting: 1,
@ -167,6 +168,7 @@ export default async function createHomestead(id) {
}, },
], ],
collisionStartScript: ` collisionStartScript: `
if (other.Player) {
ecs.switchEcs( ecs.switchEcs(
other, other,
'town', 'town',
@ -177,6 +179,7 @@ export default async function createHomestead(id) {
}, },
}, },
); );
}
`, `,
}, },

View File

@ -14,7 +14,7 @@ export default async function createPlayer(id) {
], ],
}, },
Controlled: {}, Controlled: {},
Direction: {direction: 2}, Direction: {},
Ecs: {path: ['homesteads', `${id}`].join('/')}, Ecs: {path: ['homesteads', `${id}`].join('/')},
Emitter: {}, Emitter: {},
Forces: {}, Forces: {},

View File

@ -38,9 +38,10 @@ export default async function createTown() {
}, },
], ],
collisionStartScript: ` collisionStartScript: `
if (other.Player) {
ecs.switchEcs( ecs.switchEcs(
other, other,
['homesteads', '0'].join('/'), ['homesteads', other.Player.id].join('/'),
{ {
Position: { Position: {
x: 20, x: 20,
@ -48,6 +49,7 @@ export default async function createTown() {
}, },
}, },
); );
}
`, `,
}, },
Position: {x: 952, y: 480}, Position: {x: 952, y: 480},

View File

@ -63,6 +63,7 @@ export default class Engine {
if (entity !== connectedPlayer.entity) { if (entity !== connectedPlayer.entity) {
continue; continue;
} }
const {id} = entity.Player;
// remove entity link to connection to start queueing actions and pause updates // remove entity link to connection to start queueing actions and pause updates
delete connectedPlayer.entity; delete connectedPlayer.entity;
// forget previous state // forget previous state
@ -93,6 +94,7 @@ export default class Engine {
} }
// recreate the entity in the new ECS and again associate it with the connection // recreate the entity in the new ECS and again associate it with the connection
connectedPlayer.entity = engine.ecses[path].get(await engine.ecses[path].create(dumped)); connectedPlayer.entity = engine.ecses[path].get(await engine.ecses[path].create(dumped));
connectedPlayer.entity.Player.id = id
} }
} }
} }
@ -226,11 +228,12 @@ export default class Engine {
await this.loadEcs(entityJson.Ecs.path); await this.loadEcs(entityJson.Ecs.path);
} }
const ecs = this.ecses[entityJson.Ecs.path]; const ecs = this.ecses[entityJson.Ecs.path];
const entity = await ecs.create(entityJson); const entity = ecs.get(await ecs.create(entityJson));
entity.Player.id = id
this.connectedPlayers.set( this.connectedPlayers.set(
connection, connection,
{ {
entity: ecs.get(entity), entity,
id, id,
memory: { memory: {
chunks: new Map(), chunks: new Map(),

View File

@ -45,7 +45,12 @@ export const {
SQRT2, SQRT2,
} = Math; } = Math;
export const SQRT_2_2 = Math.sqrt(2) / 2;
export const EIGHTH_PI = Math.PI / 8;
export const QUARTER_PI = Math.PI / 4;
export const HALF_PI = Math.PI / 2;
export const TAU = Math.PI * 2; export const TAU = Math.PI * 2;
export const PI_180 = Math.PI / 180;
export function bresenham({x: x1, y: y1}, {x: x2, y: y2}) { export function bresenham({x: x1, y: y1}, {x: x2, y: y2}) {
const points = []; const points = [];

View File

@ -1,4 +1,5 @@
{ {
"icon": "/assets/brush/brush.png", "icon": "/assets/brush/brush.png",
"label": "Brush",
"start": "/assets/brush/start.js" "start": "/assets/brush/start.js"
} }

View File

@ -1,7 +1,7 @@
const {Collider, Controlled, Interacts, Inventory, Sound, Sprite} = wielder const {Collider, Controlled, Interacts, Inventory, Sound, Sprite} = wielder
const entities = Collider.closest(Interacts.aabb()); const entities = Collider.closest(Interacts.aabb());
for (const entity of entities) { for (const entity of entities) {
const {Tags} = entity; const {Emitter, Position, Tags} = entity;
if (Tags && Tags.has('kittan')) { if (Tags && Tags.has('kittan')) {
Controlled.locked = 1 Controlled.locked = 1
const [, direction] = Sprite.animation.split(':') const [, direction] = Sprite.animation.split(':')
@ -17,6 +17,94 @@ for (const entity of entities) {
source: '/assets/furball/furball.json', source: '/assets/furball/furball.json',
}); });
Controlled.locked = 0; Controlled.locked = 0;
const heartParticles = {
behaviors: [
{
type: 'moveAcceleration',
config: {
accel: {
x: 0,
y: -100,
},
minStart: 0,
maxStart: 0,
rotate: false,
}
},
{
type: 'moveSpeed',
config: {
speed: {
list: [
{
time: 0,
value: 30
},
{
time: 1,
value: 0
}
]
}
}
},
{
type: 'scale',
config: {
scale: {
list: [
{
value: 0.5,
time: 0,
},
{
value: 0.125,
time: 1,
},
]
}
}
},
{
type: 'textureSingle',
config: {
texture: '/assets/heart/heart.png',
}
},
],
lifetime: {
min: 0.5,
max: 0.5,
},
frequency: 0.1,
emitterLifetime: 0.25,
pos: {
x: 0,
y: 0
},
rotation: 180,
};
Emitter.emit({
...heartParticles,
behaviors: [
...heartParticles.behaviors,
{
type: 'spawnShape',
config: {
type: 'rect',
data: {
x: Position.x - 8,
y: Position.y,
w: 16,
h: 1,
}
}
}
]
})
break; break;
} }
} }

View File

@ -1,3 +1,4 @@
{ {
"icon": "/assets/furball/furball.png" "icon": "/assets/furball/furball.png",
"label": "Fur Ball"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

View File

@ -1,5 +1,6 @@
{ {
"icon": "/assets/hoe/icon.png", "icon": "/assets/hoe/icon.png",
"label": "Hoe",
"projectionCheck": "/assets/hoe/projection-check.js", "projectionCheck": "/assets/hoe/projection-check.js",
"projection": { "projection": {
"distance": [3, -1], "distance": [3, -1],

View File

@ -1,5 +1,5 @@
const {Direction, Position, Wielder} = wielder const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction) const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) { if (projected?.length > 0) {
const {Controlled, Emitter, Sound, Sprite} = wielder const {Controlled, Emitter, Sound, Sprite} = wielder

View File

@ -1,15 +1,16 @@
entity.Direction.direction = Math.floor(Math.random() * 4); entity.Direction.direction = Math.random() * Math.TAU;
entity.Controlled.directionMove(entity.Direction.direction);
const map = {0: 'moveUp', 1: 'moveRight', 2: 'moveDown', 3: 'moveLeft'};
entity.Controlled[map[entity.Direction.direction]] = 1;
await wait(0.25 + Math.random() * 2.25); await wait(0.25 + Math.random() * 2.25);
entity.Controlled[map[entity.Direction.direction]] = 0;
entity.Controlled.stop();
entity.Sprite.isAnimating = 0; entity.Sprite.isAnimating = 0;
await wait(1 + Math.random() * 3); await wait(1 + Math.random() * 3);
entity.Direction.direction = Math.floor(Math.random() * 4); entity.Direction.direction = Math.random() * Math.TAU;
await wait(0.5 + Math.random() * 2.5); await wait(0.5 + Math.random() * 2.5);

View File

@ -1,4 +1,5 @@
{ {
"icon": "/assets/potion/icon.png", "icon": "/assets/potion/icon.png",
"label": "Potion",
"start": "/assets/potion/start.js" "start": "/assets/potion/start.js"
} }

View File

@ -1,5 +1,5 @@
const {Direction, Position, Wielder} = wielder const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction) const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) { if (projected?.length > 0) {
const {Controlled, Emitter, Sound, Sprite} = wielder const {Controlled, Emitter, Sound, Sprite} = wielder
const {TileLayers} = ecs.get(1) const {TileLayers} = ecs.get(1)
@ -120,9 +120,10 @@ if (projected?.length > 0) {
Sound.play('/assets/sow.wav'); Sound.play('/assets/sow.wav');
Sprite.animation = ['moving', direction].join(':'); Sprite.animation = ['moving', direction].join(':');
const directionMap = {0: 'right', 1: 'down', 2: 'left', 3: 'up'};
for (let i = 0; i < 6; ++i) { for (let i = 0; i < 6; ++i) {
Direction.direction = Math.floor(Math.random() * 4); Direction.direction = Math.HALF_PI * Math.floor(Math.random() * 4);
Sprite.animation = ['moving', 0 === Direction.direction ? 'up' : (1 === Direction.direction ? 'right' : (2 === Direction.direction ? 'down' : 'left'))].join(':'); Sprite.animation = ['moving', directionMap[Direction.quantize(4)]].join(':');
await wait(0.125); await wait(0.125);
} }

View File

@ -1,5 +1,6 @@
{ {
"icon": "/assets/tomato-seeds/icon.png", "icon": "/assets/tomato-seeds/icon.png",
"label": "Tomato Seeds",
"projection": { "projection": {
"distance": [1, -1], "distance": [1, -1],
"grid": [ "grid": [

View File

@ -1,5 +1,5 @@
const {Direction, Position, Wielder} = wielder const {Direction, Position, Wielder} = wielder
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction) const projected = Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))
if (projected?.length > 0) { if (projected?.length > 0) {
const {Controlled, Emitter, Sound, Sprite} = wielder const {Controlled, Emitter, Sound, Sprite} = wielder

View File

@ -1,5 +1,6 @@
{ {
"icon": "/assets/watering-can/icon.png", "icon": "/assets/watering-can/icon.png",
"label": "Watering Can",
"projectionCheck": "/assets/watering-can/projection-check.js", "projectionCheck": "/assets/watering-can/projection-check.js",
"projection": { "projection": {
"distance": [3, -1], "distance": [3, -1],

View File

@ -3,6 +3,9 @@ import compression from 'compression';
import express from 'express'; import express from 'express';
import morgan from 'morgan'; import morgan from 'morgan';
// patch pixi server context
import('./app/server/pixi-context.js');
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP; const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
@ -32,6 +35,20 @@ else {
server = createServer(serverOptions, app); server = createServer(serverOptions, app);
} }
// immediately start listening and queueing up connections
let resolve, promise = new Promise((res) => {
resolve = res;
});
app.use(async (req, res, next) => {
await promise;
next();
});
const port = process.env.PORT || 3000;
server.listen(port, () =>
console.log(`Express server listening at http${isInsecure ? '' : 's'}://localhost:${port}`)
);
// possibly load dev server and build the request handler up front
const viteDevServer = isProduction const viteDevServer = isProduction
? undefined ? undefined
: await import('vite').then((vite) => : await import('vite').then((vite) =>
@ -39,26 +56,18 @@ const viteDevServer = isProduction
server: {middlewareMode: {server}}, server: {middlewareMode: {server}},
}) })
); );
const build = () => (
let websocketBuilt = false;
const remixHandler = createRequestHandler({
build: async () => {
// patch pixi server context
import('./app/server/pixi-context.js');
const ssr = await (
viteDevServer viteDevServer
? viteDevServer.ssrLoadModule('virtual:remix/server-build') ? viteDevServer.ssrLoadModule('virtual:remix/server-build')
: import('./build/server/index.js') : import('./build/server/index.js')
); );
if (!websocketBuilt) { const ssr = await build();
await ssr.entry.module.websocket(server, viteDevServer); await ssr.entry.module.websocket(server, viteDevServer);
websocketBuilt = true; const remixHandler = createRequestHandler({
} build: () => ssr,
return ssr;
},
}); });
// configure middleware
app.use(compression()); app.use(compression());
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
@ -85,7 +94,5 @@ app.use(morgan('tiny'));
// handle SSR requests // handle SSR requests
app.all('*', remixHandler); app.all('*', remixHandler);
const port = process.env.PORT || 3000; // finally let requests resolve
server.listen(port, () => resolve();
console.log(`Express server listening at http${isInsecure ? '' : 's'}://localhost:${port}`)
);