Compare commits

...

18 Commits

Author SHA1 Message Date
cha0s
9853edec44 feat: let there be light! 2024-09-14 17:33:41 -05:00
cha0s
6fd6565c8b refactor: immediate wait 2024-09-14 17:30:54 -05:00
cha0s
89044ab2cf fix: wait for elapsed 2024-09-14 17:30:42 -05:00
cha0s
fc96f637ff fix: control transfer 2024-09-13 17:54:49 -05:00
cha0s
a54aed9b89 perf: skip debug render 2024-09-12 20:00:20 -05:00
cha0s
51d8f333ed perf: only update relevant dom entities 2024-09-12 16:24:54 -05:00
cha0s
565c2a4095 perf: memo 2024-09-12 16:24:26 -05:00
cha0s
e8ba520a5c refactor: input 2024-09-12 15:52:07 -05:00
cha0s
b9428658b5 refactor: chat state 2024-09-12 14:31:05 -05:00
cha0s
1e0d5d0b95 chore: dep 2024-09-10 23:08:43 -05:00
cha0s
04a0230248 refactor: predictor next to client, smoothing 2024-09-08 17:21:41 -05:00
cha0s
336ec04f66 feat: dialogue pages 2024-09-07 12:07:05 -05:00
cha0s
de8ff49270 tweak: dialogue 2024-09-07 10:09:02 -05:00
cha0s
3bed694b2a fix: damage font 2024-09-06 21:52:44 -05:00
cha0s
2e7d1af189 chore: interpolate scale 2024-09-06 21:20:40 -05:00
cha0s
503a7d2514 perf: refs 2024-09-06 21:20:28 -05:00
cha0s
3b88ab8969 perf: smoother interpolation 2024-09-06 19:48:26 -05:00
cha0s
b912fee2e7 fix: reset interpolator on change 2024-09-06 13:33:13 -05:00
32 changed files with 520 additions and 444 deletions

View File

@ -1,47 +1,27 @@
export default class Interpolator { let authoritative = [];
duration = 0; let duration = 0;
latest; let last = performance.now();
location = 0; let latest = null;
penultimate; let location = 0;
tracking = []; let penultimate;
accept(state) { let tracking = [];
const packet = state;
if ('Tick' !== packet.type) { const interpolate = () => {
const now = performance.now();
const elapsed = (now - last) / 1000;
last = now;
if (authoritative.length > 0) {
for (const packet of authoritative) {
postMessage(packet); postMessage(packet);
return;
} }
this.penultimate = this.latest; authoritative = [];
this.latest = packet;
this.tracking = [];
if (this.penultimate) {
this.duration = this.penultimate.payload.elapsed;
const [from, to] = [this.penultimate.payload.ecs, this.latest.payload.ecs];
for (const entityId in from) {
for (const componentName in from[entityId]) {
if (
['Camera', 'Position'].includes(componentName)
&& to[entityId]?.[componentName]
) {
this.tracking.push({
entityId,
componentName,
properties: ['x', 'y'],
});
} }
} if (tracking.length > 0) {
} location += elapsed;
} const fraction = location / duration;
this.location = 0; const [from, to] = [penultimate.payload.ecs, latest.payload.ecs];
}
interpolate(elapsed) {
if (0 === this.tracking.length) {
return undefined;
}
this.location += elapsed;
const fraction = Math.min(1, this.location / this.duration);
const [from, to] = [this.penultimate.payload.ecs, this.latest.payload.ecs];
const interpolated = {}; const interpolated = {};
for (const {entityId, componentName, properties} of this.tracking) { for (const {entityId, componentName, properties} of tracking) {
if (!interpolated[entityId]) { if (!interpolated[entityId]) {
interpolated[entityId] = {}; interpolated[entityId] = {};
} }
@ -64,48 +44,76 @@ export default class Interpolator {
); );
} }
} }
return {
type: 'Tick',
payload: {
ecs: interpolated,
elapsed,
frame: this.penultimate.payload.frame + fraction,
},
};
}
}
let handle;
const interpolator = new Interpolator();
let last;
const interpolate = (now) => {
const elapsed = (now - last) / 1000;
last = now;
const interpolated = interpolator.interpolate(elapsed);
if (interpolated) {
handle = requestAnimationFrame(interpolate);
postMessage(interpolated);
}
else {
handle = null;
}
}
onmessage = async (event) => {
interpolator.accept(event.data);
if (interpolator.penultimate && 'Tick' === event.data.type) {
postMessage({ postMessage({
type: 'Tick', type: 'Tick',
payload: { payload: {
ecs: interpolator.penultimate.payload.ecs, ...penultimate.payload,
elapsed: last ? (performance.now() - last) / 1000 : 0, ecs: interpolated,
frame: interpolator.penultimate.payload.frame, elapsed,
frame: penultimate.payload.frame + fraction,
},
});
}
requestAnimationFrame(interpolate);
}
requestAnimationFrame(interpolate);
onmessage = async (event) => {
const packet = event.data;
switch (packet.type) {
case 'EcsChange': {
authoritative = [];
latest = null;
tracking = [];
postMessage(packet);
break;
}
case 'Tick': {
penultimate = latest;
latest = packet;
if (penultimate) {
duration = penultimate.payload.elapsed;
location = 0;
tracking = [];
const [from, to] = [penultimate.payload.ecs, latest.payload.ecs];
for (const entityId in from) {
for (const componentName in from[entityId]) {
if (
['Camera', 'Position'].includes(componentName)
&& to[entityId]?.[componentName]
) {
tracking.push({
entityId,
componentName,
properties: ['x', 'y'],
});
}
if (
['Sprite'].includes(componentName)
&& to[entityId]?.[componentName]
) {
tracking.push({
entityId,
componentName,
properties: ['alpha', 'scaleX', 'scaleY'],
});
}
}
}
authoritative.push({
type: 'Tick',
payload: {
...penultimate.payload,
elapsed: last ? (performance.now() - last) / 1000 : 0,
}, },
}); });
if (!handle) {
last = performance.now(); last = performance.now();
handle = requestAnimationFrame(interpolate); }
break;
}
default: {
postMessage(packet);
break;
} }
} }
}; };

View File

@ -17,7 +17,13 @@ export default class LocalClient extends Client {
{type: 'module'}, {type: 'module'},
); );
this.interpolator.addEventListener('message', (event) => { this.interpolator.addEventListener('message', (event) => {
this.accept(event.data); const packet = event.data;
if (CLIENT_PREDICTION) {
this.predictor.postMessage([1, packet]);
}
else {
this.accept(packet);
}
}); });
} }
if (CLIENT_PREDICTION) { if (CLIENT_PREDICTION) {
@ -35,12 +41,7 @@ export default class LocalClient extends Client {
break; break;
} }
case 1: { case 1: {
if (CLIENT_INTERPOLATION) {
this.interpolator.postMessage(packet);
}
else {
this.accept(packet); this.accept(packet);
}
break; break;
} }
} }
@ -54,12 +55,12 @@ export default class LocalClient extends Client {
} }
this.throughput.$$down += event.data.byteLength; this.throughput.$$down += event.data.byteLength;
const packet = decode(event.data); const packet = decode(event.data);
if (CLIENT_PREDICTION) { if (CLIENT_INTERPOLATION) {
this.predictor.postMessage([1, packet]);
}
else if (CLIENT_INTERPOLATION) {
this.interpolator.postMessage(packet); this.interpolator.postMessage(packet);
} }
else if (CLIENT_PREDICTION) {
this.predictor.postMessage([1, packet]);
}
else { else {
this.accept(packet); this.accept(packet);
} }

View File

@ -27,17 +27,8 @@ const Flow = {
DOWN: 1, DOWN: 1,
}; };
const Stage = {
UNACK: 0,
ACK: 1,
FINISHING: 2,
FINISHED: 3,
};
const actions = new Map(); const actions = new Map();
let ecs = new PredictionEcs({Components, Systems}); let ecs = new PredictionEcs({Components, Systems});
let mainEntityId = 0; let mainEntityId = 0;
function applyClientActions(elapsed) { function applyClientActions(elapsed) {
@ -46,7 +37,7 @@ function applyClientActions(elapsed) {
const {Controlled} = main; const {Controlled} = main;
const finished = []; const finished = [];
for (const [id, action] of actions) { for (const [id, action] of actions) {
if (Stage.UNACK === action.stage) { if (0 === action.finished && !action.ack) {
if (!Controlled.locked) { if (!Controlled.locked) {
switch (action.action.type) { switch (action.action.type) {
case 'moveUp': case 'moveUp':
@ -60,7 +51,7 @@ function applyClientActions(elapsed) {
} }
action.steps.push(elapsed); action.steps.push(elapsed);
} }
if (Stage.FINISHING === action.stage) { if (1 === action.finished) {
if (!Controlled.locked) { if (!Controlled.locked) {
switch (action.action.type) { switch (action.action.type) {
case 'moveUp': case 'moveUp':
@ -72,9 +63,9 @@ function applyClientActions(elapsed) {
} }
} }
} }
action.stage = Stage.FINISHED; action.finished = 2;
} }
if (Stage.FINISHED === action.stage) { if (action.ack && 2 === action.finished) {
action.steps.shift(); action.steps.shift();
if (0 === action.steps.length) { if (0 === action.steps.length) {
finished.push(id); finished.push(id);
@ -113,13 +104,13 @@ onmessage = async (event) => {
if (0 === packet.payload.value) { if (0 === packet.payload.value) {
const ack = pending.get(packet.payload.type); const ack = pending.get(packet.payload.type);
const action = actions.get(ack); const action = actions.get(ack);
action.stage = Stage.FINISHING; action.finished = 1;
pending.delete(packet.payload.type); pending.delete(packet.payload.type);
} }
else { else {
const tx = { const tx = {
action: packet.payload, action: packet.payload,
stage: Stage.UNACK, ack: false,finished: 0,
steps: [], steps: [],
}; };
packet.payload.ack = Math.random(); packet.payload.ack = Math.random();
@ -139,11 +130,12 @@ onmessage = async (event) => {
switch (packet.type) { switch (packet.type) {
case 'ActionAck': { case 'ActionAck': {
const action = actions.get(packet.payload.ack); const action = actions.get(packet.payload.ack);
action.stage = Stage.ACK; action.ack = true;
return; return;
} }
case 'EcsChange': { case 'EcsChange': {
ecs = new PredictionEcs({Components, Systems}); ecs = new PredictionEcs({Components, Systems});
mainEntityId = 0;
break; break;
} }
case 'Tick': { case 'Tick': {

View File

@ -18,6 +18,6 @@ export default class Time extends Component {
}; };
} }
static properties = { static properties = {
irlSeconds: {defaultValue: 10 * realSecondsPerGameHour, type: 'uint16'}, irlSeconds: {defaultValue: 6 * realSecondsPerGameHour, type: 'uint16'},
}; };
} }

View File

@ -14,14 +14,14 @@ export default function Devtools({
eventsChannel, eventsChannel,
}) { }) {
const client = useClient(); const client = useClient();
const [mainEntity] = useMainEntity(); const mainEntityRef = useMainEntity();
const [mainEntityJson, setMainEntityJson] = useState(''); const [mainEntityJson, setMainEntityJson] = useState('');
const onEcsTick = useCallback((payload, ecs) => { const onEcsTick = useCallback((payload, ecs) => {
if (!mainEntity) { if (!mainEntityRef.current) {
return; return;
} }
setMainEntityJson(JSON.stringify(ecs.get(mainEntity), null, 2)); setMainEntityJson(JSON.stringify(ecs.get(mainEntityRef.current), null, 2));
}, [mainEntity]); }, [mainEntityRef]);
useEcsTick(onEcsTick); useEcsTick(onEcsTick);
return ( return (
<div className={styles.devtools}> <div className={styles.devtools}>

View File

@ -17,12 +17,12 @@ export default function Tiles({eventsChannel}) {
const [layer, setLayer] = useState(0); const [layer, setLayer] = useState(0);
const [brush, setBrush] = useState(0); const [brush, setBrush] = useState(0);
const [stamp, setStamp] = useState([]); const [stamp, setStamp] = useState([]);
const [ecs] = useEcs(); const ecsRef = useEcs();
useEffect(() => { useEffect(() => {
if (!ecs) { if (!ecsRef.current) {
return; return;
} }
const master = ecs.get(1); const master = ecsRef.current.get(1);
if (!master) { if (!master) {
return; return;
} }
@ -56,10 +56,10 @@ export default function Tiles({eventsChannel}) {
eventsChannel.removeListener('click', onClick); eventsChannel.removeListener('click', onClick);
}; };
}); });
if (!ecs) { if (!ecsRef.current) {
return false; return false;
} }
const master = ecs.get(1); const master = ecsRef.current.get(1);
if (!master) { if (!master) {
return false; return false;
} }

View File

@ -1,3 +1,5 @@
import {memo} from 'react';
import styles from './bag.module.css'; import styles from './bag.module.css';
import Grid from './grid.jsx'; import Grid from './grid.jsx';
@ -5,7 +7,7 @@ import Grid from './grid.jsx';
/** /**
* Inventory bag. 10-40 slots of inventory. * Inventory bag. 10-40 slots of inventory.
*/ */
export default function Bag({ function Bag({
isInventoryOpen, isInventoryOpen,
onActivate, onActivate,
slots, slots,
@ -25,3 +27,5 @@ export default function Bag({
</div> </div>
); );
} }
export default memo(Bag);

View File

@ -8,7 +8,6 @@ export default function Chat({
chatHistoryCaret, chatHistoryCaret,
chatInputRef, chatInputRef,
chatMessages, chatMessages,
onClose,
message, message,
pendingMessage, pendingMessage,
setChatHistoryCaret, setChatHistoryCaret,
@ -33,7 +32,6 @@ export default function Chat({
<Input <Input
chatHistory={chatHistory} chatHistory={chatHistory}
chatHistoryCaret={chatHistoryCaret} chatHistoryCaret={chatHistoryCaret}
onClose={onClose}
message={message} message={message}
chatInputRef={chatInputRef} chatInputRef={chatInputRef}
pendingMessage={pendingMessage} pendingMessage={pendingMessage}

View File

@ -59,5 +59,5 @@
} }
.damages { .damages {
font-family: Joystix, 'Courier New', Courier, monospace; font-family: Joystix;
} }

View File

@ -1,7 +1,7 @@
.caret { .caret {
position: absolute; position: absolute;
fill: #ffffff; fill: #999999;
stroke: #00000044; stroke: #02023999;
stroke-width: 2px; stroke-width: 2px;
left: 50%; left: 50%;
top: 50%; top: 50%;

View File

@ -18,36 +18,63 @@ export default function Dialogue({
const ref = useRef(); const ref = useRef();
const [dimensions, setDimensions] = useState({h: 0, w: 0}); const [dimensions, setDimensions] = useState({h: 0, w: 0});
const [caret, setCaret] = useState(0); const [caret, setCaret] = useState(0);
const [page, setPage] = useState(0);
const radians = useRadians(); const radians = useRadians();
const [pageLetters, setPageLetters] = useState([]);
useEffect(() => { useEffect(() => {
setCaret(0);
setPageLetters(dialogue.letters.filter((letter) => letter.page === page));
}, [dialogue, page]);
useEffect(() => {
if (0 === pageLetters.length) {
return;
}
return dialogue.addSkipListener(() => { return dialogue.addSkipListener(() => {
if (caret >= dialogue.letters.length - 1) { // at end
dialogue.onClose(); if (caret >= pageLetters.length - 1) {
if (page < dialogue.pages - 1) {
setPage((page) => page + 1);
} }
else { else {
setCaret(dialogue.letters.length - 1); dialogue.onClose();
}
}
// skip to end
else {
setCaret(pageLetters.length - 1);
} }
}); });
}, [caret, dialogue]); }, [caret, dialogue, page, pageLetters]);
useEffect(() => { useEffect(() => {
const {params} = dialogue.letters[caret]; if (0 === pageLetters.length) {
return;
}
const {params} = pageLetters[caret];
let handle; let handle;
if (caret >= dialogue.letters.length - 1) { // start lingering timeout
if (caret >= pageLetters.length - 1) {
const {linger} = dialogue; const {linger} = dialogue;
if (!linger) { if (!linger) {
return; return;
} }
handle = setTimeout(() => { handle = setTimeout(() => {
if (page < dialogue.pages - 1) {
setPage((page) => page + 1);
}
else {
dialogue.onClose(); dialogue.onClose();
}
}, linger * 1000); }, linger * 1000);
} }
else { else {
// jump to next caret spot
let jump = caret; let jump = caret;
while (0 === dialogue.letters[jump].params.rate.frequency && jump < dialogue.letters.length - 1) { while (0 === pageLetters[jump].params.rate.frequency && jump < pageLetters.length - 1) {
jump += 1; jump += 1;
} }
setCaret(jump); setCaret(jump);
if (jump < dialogue.letters.length - 1) { if (jump < pageLetters.length - 1) {
// wait for next jump
handle = setTimeout(() => { handle = setTimeout(() => {
setCaret(caret + 1); setCaret(caret + 1);
}, params.rate.frequency * 1000) }, params.rate.frequency * 1000)
@ -56,7 +83,7 @@ export default function Dialogue({
return () => { return () => {
clearTimeout(handle); clearTimeout(handle);
} }
}, [caret, dialogue]); }, [caret, dialogue, page, pageLetters]);
const updateDimensions = useCallback(() => { const updateDimensions = useCallback(() => {
if (ref.current) { if (ref.current) {
const {height, width} = ref.current.getBoundingClientRect(); const {height, width} = ref.current.getBoundingClientRect();
@ -65,8 +92,8 @@ export default function Dialogue({
}, [domScale]); }, [domScale]);
useAnimationFrame(updateDimensions); useAnimationFrame(updateDimensions);
const localRender = useMemo( const localRender = useMemo(
() => render(dialogue.letters, styles.letter), () => render(pageLetters, styles.letter),
[dialogue.letters], [pageLetters],
); );
let position = 'function' === typeof dialogue.position let position = 'function' === typeof dialogue.position
? dialogue.position() ? dialogue.position()

View File

@ -1,6 +1,6 @@
.dialogue { .dialogue {
background-color: #02023999; background-color: #02023999;
border: solid 3px white; border: solid 1px #999999;
border-radius: 8px; border-radius: 8px;
color: white; color: white;
left: 0; left: 0;

View File

@ -9,9 +9,9 @@ import Entity from './entity.jsx';
export default function Entities({ export default function Entities({
camera, camera,
monopolizers,
scale, scale,
setChatMessages, setChatMessages,
setMonopolizers,
}) { }) {
const [entities, setEntities] = useState({}); const [entities, setEntities] = useState({});
usePacket('EcsChange', async () => { usePacket('EcsChange', async () => {
@ -29,9 +29,9 @@ export default function Entities({
deleting[id] = true; deleting[id] = true;
continue; continue;
} }
updating[id] = ecs.get(id);
const {dialogue} = update.Interlocutor || {}; const {dialogue} = update.Interlocutor || {};
if (dialogue) { if (dialogue) {
updating[id] = ecs.get(id);
const {dialogues} = updating[id].Interlocutor; const {dialogues} = updating[id].Interlocutor;
for (const key in dialogue) { for (const key in dialogue) {
dialogues[key] = dialogue[key]; dialogues[key] = dialogue[key];
@ -45,6 +45,7 @@ export default function Entities({
dialogues[key].position = () => updating[id].Position; dialogues[key].position = () => updating[id].Position;
} }
dialogues[key].letters = parseLetters(dialogues[key].body); dialogues[key].letters = parseLetters(dialogues[key].body);
dialogues[key].pages = 1 + dialogues[key].letters.at(-1).page;
setChatMessages((chatMessages) => ({ setChatMessages((chatMessages) => ({
[[id, key].join('-')]: dialogues[key].letters, [[id, key].join('-')]: dialogues[key].letters,
...chatMessages, ...chatMessages,
@ -64,7 +65,7 @@ export default function Entities({
}, },
}; };
if (dialogues[key].monopolizer) { if (dialogues[key].monopolizer) {
setMonopolizers((monopolizers) => [...monopolizers, monopolizer]); monopolizers.push(monopolizer);
} }
dialogues[key].onClose = () => { dialogues[key].onClose = () => {
setEntities((entities) => ({ setEntities((entities) => ({
@ -72,14 +73,10 @@ export default function Entities({
[id]: ecs.rebuild(id), [id]: ecs.rebuild(id),
})); }));
if (dialogues[key].monopolizer) { if (dialogues[key].monopolizer) {
setMonopolizers((monopolizers) => {
const index = monopolizers.indexOf(monopolizer); const index = monopolizers.indexOf(monopolizer);
if (-1 === index) { if (-1 !== index) {
return monopolizers;
}
monopolizers.splice(index, 1); monopolizers.splice(index, 1);
return [...monopolizers]; }
});
} }
delete dialogues[key]; delete dialogues[key];
}; };
@ -95,7 +92,7 @@ export default function Entities({
...updating, ...updating,
}; };
}); });
}, [setChatMessages, setMonopolizers]); }, [monopolizers, setChatMessages]);
useEcsTick(onEcsTick); useEcsTick(onEcsTick);
const renderables = []; const renderables = [];
for (const id in entities) { for (const id in entities) {

View File

@ -1,3 +1,5 @@
import {memo} from 'react';
import styles from './hotbar.module.css'; import styles from './hotbar.module.css';
import gridStyles from './grid.module.css'; import gridStyles from './grid.module.css';
@ -6,7 +8,7 @@ import Grid from './grid.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({ function Hotbar({
active, active,
hotbarIsHidden, hotbarIsHidden,
onActivate, onActivate,
@ -33,3 +35,5 @@ export default function Hotbar({
</div> </div>
); );
} }
export default memo(Hotbar);

View File

@ -1,4 +1,4 @@
import {Container} from '@pixi/react'; import {Container, useApp} from '@pixi/react';
import {useCallback, useState} from 'react'; import {useCallback, useState} from 'react';
import {useEcsTick} from '@/react/context/ecs.js'; import {useEcsTick} from '@/react/context/ecs.js';
@ -11,14 +11,14 @@ import TileLayer from './tile-layer.jsx';
import Water from './water.jsx'; import Water from './water.jsx';
export default function Ecs({camera, monopolizers, particleWorker, scale}) { export default function Ecs({camera, monopolizers, particleWorker, scale}) {
const [mainEntity] = useMainEntity(); const app = useApp();
const mainEntityRef = useMainEntity();
const [layers, setLayers] = useState([]); const [layers, setLayers] = useState([]);
const [hour, setHour] = useState(10);
const [projected, setProjected] = useState([]); const [projected, setProjected] = useState([]);
const [position, setPosition] = useState({x: 0, y: 0}); const [position, setPosition] = useState({x: 0, y: 0});
const [water, setWater] = useState(); const [water, setWater] = useState();
const onEcsTick = useCallback((payload, ecs) => { const onEcsTick = useCallback((payload, ecs) => {
const entity = ecs.get(mainEntity); const entity = ecs.get(mainEntityRef.current);
for (const id in payload) { for (const id in payload) {
const update = payload[id]; const update = payload[id];
switch (id) { switch (id) {
@ -28,7 +28,88 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
setLayers(Object.values(master.TileLayers.$$layersProxies)); setLayers(Object.values(master.TileLayers.$$layersProxies));
} }
if (update.Time) { if (update.Time) {
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60); const {hour} = ecs.get(1).Time;
let brightness, color;
// 0 - 5 night
// 21 - 0 night
if (
(hour >= 0 && hour < 5)
|| hour >= 21
) {
brightness = 0.25;
color = 0x2244cc;
}
// 5 - 6 blue
// 20 - 21 blue
if (
(hour >= 5 && hour < 6)
|| (hour >= 20 && hour < 21)
) {
const mag = hour < 20 ? (hour - 5) : (21 - hour);
brightness = 0.25 + (0.75 * mag);
color = 0x2244cc;
}
// 6 - 7 morning golden
if (hour >= 6 && hour < 7) {
if (hour < 6.25) {
const [r, g, b] = [0xff - 0x22, 0xd3 - 0x44, 0x7a - 0xcc];
const fraction = ((hour - 6) * 4);
brightness = 1 + (fraction * 0.25);
color = (
(0x22 + (r * fraction)) << 16
| (0x44 + (g * fraction)) << 8
| (0xcc + (b * fraction))
);
}
else if (hour >= 6.75) {
const [r, g, b] = [0xff - 0xff, 0xd3 - 0xff, 0x7a - 0xff];
const fraction = ((7 - hour) * 4);
brightness = 1 + (fraction * 0.25);
color = (
(0xff + (r * fraction)) << 16
| (0xff + (g * fraction)) << 8
| (0xff + (b * fraction))
);
}
else {
brightness = 1.25;
color = 0xffd37a;
}
}
// 19 - 20 evening golden
if (hour >= 19 && hour < 20) {
if (hour < 19.25) {
const [r, g, b] = [0xff - 0xff, 0xd3 - 0xff, 0x7a - 0xff];
const fraction = ((hour - 19) * 4);
brightness = 1 + (fraction * 0.25);
color = (
(0xff + (r * fraction)) << 16
| (0xff + (g * fraction)) << 8
| (0xff + (b * fraction))
);
}
else if (hour >= 19.75) {
const [r, g, b] = [0xff - 0x22, 0xd3 - 0x44, 0x7a - 0xcc];
const fraction = ((20 - hour) * 4);
brightness = 1 + (fraction * 0.25);
color = (
(0x22 + (r * fraction)) << 16
| (0x44 + (g * fraction)) << 8
| (0xcc + (b * fraction))
);
}
else {
brightness = 1.25;
color = 0xffd37a;
}
}
// 7 - 19 day
if (hour >= 7 && hour < 19) {
brightness = 1;
color = 0xffffff;
}
app.ambientLight.brightness = brightness;
app.ambientLight.color = color;
} }
if (update.Water) { if (update.Water) {
setWater(master.Water.water); setWater(master.Water.water);
@ -42,7 +123,7 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
setPosition(Position.toJSON()); setPosition(Position.toJSON());
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4))); setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
} }
}, [mainEntity]); }, [app.ambientLight, mainEntityRef]);
useEcsTick(onEcsTick); useEcsTick(onEcsTick);
return ( return (
<Container <Container

View File

@ -13,12 +13,12 @@ import Entity from './entity.js';
export default function Entities({monopolizers, particleWorker}) { export default function Entities({monopolizers, particleWorker}) {
const [debug] = useDebug(); const [debug] = useDebug();
const [ecs] = useEcs(); const ecsRef = useEcs();
const containerRef = useRef(); const containerRef = useRef();
const latestTick = useRef(); const latestTick = useRef();
const entities = useRef({}); const entities = useRef({});
const pool = useRef([]); const pool = useRef([]);
const [mainEntity] = useMainEntity(); const mainEntityRef = useMainEntity();
const radians = useRadians(); const radians = useRadians();
const willInteractWith = useRef(0); const willInteractWith = useRef(0);
const [interactionFilters] = useState([ const [interactionFilters] = useState([
@ -28,7 +28,7 @@ export default function Entities({monopolizers, particleWorker}) {
const pulse = (Math.cos(radians / 4) + 1) * 0.5; const pulse = (Math.cos(radians / 4) + 1) * 0.5;
interactionFilters[0].brightness = (pulse * 0.75) + 1; interactionFilters[0].brightness = (pulse * 0.75) + 1;
interactionFilters[1].outerStrength = pulse * 0.5; interactionFilters[1].outerStrength = pulse * 0.5;
const updateEntities = useCallback((payload) => { const updateEntities = useCallback((payload, ecs) => {
for (const id in payload) { for (const id in payload) {
if ('1' === id) { if ('1' === id) {
continue; continue;
@ -43,14 +43,15 @@ export default function Entities({monopolizers, particleWorker}) {
entities.current[id] = pool.current.length > 0 entities.current[id] = pool.current.length > 0
? pool.current.pop() ? pool.current.pop()
: new Entity(); : new Entity();
entities.current[id].reset(ecs.get(id), debug); entities.current[id].reset(ecs.get(id));
if (mainEntity === id) { entities.current[id].setDebug(false);
if (mainEntityRef.current === id) {
entities.current[id].setMainEntity(); entities.current[id].setMainEntity();
} }
} }
entities.current[id].update(payload[id], containerRef.current); entities.current[id].update(payload[id], containerRef.current);
} }
}, [debug, ecs, mainEntity]) }, [mainEntityRef])
useEffect(() => { useEffect(() => {
if (0 === pool.current.length) { if (0 === pool.current.length) {
for (let i = 0; i < 1000; ++i) { for (let i = 0; i < 1000; ++i) {
@ -69,25 +70,25 @@ export default function Entities({monopolizers, particleWorker}) {
} }
entities.current = {}; entities.current = {};
}); });
const onEcsTickEntities = useCallback((payload) => { useEcsTick(updateEntities);
updateEntities(payload);
}, [updateEntities]);
useEcsTick(onEcsTickEntities);
useEffect(() => { useEffect(() => {
if (!ecs || !particleWorker) { if (!particleWorker) {
return; return;
} }
async function onMessage(diff) { async function onMessage(diff) {
if (!ecsRef.current) {
return;
}
latestTick.current = Promise.resolve(latestTick.current).then(async () => { latestTick.current = Promise.resolve(latestTick.current).then(async () => {
await ecs.apply(diff.data); await ecsRef.current.apply(diff.data);
updateEntities(diff.data); updateEntities(diff.data, ecsRef.current);
}); });
} }
particleWorker.addEventListener('message', onMessage); particleWorker.addEventListener('message', onMessage);
return () => { return () => {
particleWorker.removeEventListener('message', onMessage); particleWorker.removeEventListener('message', onMessage);
}; };
}, [ecs, particleWorker, updateEntities]); }, [ecsRef, particleWorker, updateEntities]);
const onEcsTickParticles = useCallback((payload) => { const onEcsTickParticles = useCallback((payload) => {
for (const id in payload) { for (const id in payload) {
const update = payload[id]; const update = payload[id];
@ -100,7 +101,7 @@ export default function Entities({monopolizers, particleWorker}) {
}, [particleWorker]); }, [particleWorker]);
useEcsTick(onEcsTickParticles); useEcsTick(onEcsTickParticles);
const onEcsTickInteractions = useCallback((payload, ecs) => { const onEcsTickInteractions = useCallback((payload, ecs) => {
const main = ecs.get(mainEntity); const main = ecs.get(mainEntityRef.current);
if (main) { if (main) {
if (willInteractWith.current !== main.Interacts.willInteractWith) { if (willInteractWith.current !== main.Interacts.willInteractWith) {
if (entities.current[willInteractWith.current]) { if (entities.current[willInteractWith.current]) {
@ -115,7 +116,7 @@ export default function Entities({monopolizers, particleWorker}) {
: []; : [];
} }
} }
}, [interactionFilters, mainEntity, monopolizers]); }, [interactionFilters, mainEntityRef, monopolizers]);
useEcsTick(onEcsTickInteractions); useEcsTick(onEcsTickInteractions);
return ( return (
<Container <Container

View File

@ -43,13 +43,12 @@ export default class Entity {
this.debug.parent.removeChild(this.debug); this.debug.parent.removeChild(this.debug);
this.attached = false; this.attached = false;
} }
reset(entity, debug) { reset(entity) {
this.entity = entity; this.entity = entity;
if (this.light) { if (this.light) {
this.container.removeChild(this.light); this.container.removeChild(this.light);
this.light = undefined; this.light = undefined;
} }
this.setDebug(debug);
this.isMainEntity = false; this.isMainEntity = false;
if (this.interactionAabb) { if (this.interactionAabb) {
this.debug.removeChild(this.interactionAabb); this.debug.removeChild(this.interactionAabb);
@ -97,6 +96,7 @@ export default class Entity {
0xffffff - 0x2244cc, 0xffffff - 0x2244cc,
0, 0,
); );
this.container.addChild(this.light);
} }
this.light.brightness = Light.brightness; this.light.brightness = Light.brightness;
} }
@ -178,6 +178,7 @@ export default class Entity {
} }
} }
} }
if (this.debug.alpha) {
if (this.entity.Collider) { if (this.entity.Collider) {
this.colliderAabb.redraw(this.entity.Collider.aabb); this.colliderAabb.redraw(this.entity.Collider.aabb);
} }
@ -187,6 +188,7 @@ export default class Entity {
if (this.isMainEntity) { if (this.isMainEntity) {
this.interactionAabb.redraw(this.entity.Interacts.aabb()); this.interactionAabb.redraw(this.entity.Interacts.aabb());
} }
}
if (this.attached || !container) { if (this.attached || !container) {
return; return;
} }

View File

@ -24,9 +24,8 @@ export const ApplicationStageLights = {
init: function() { init: function() {
const {stage} = this; const {stage} = this;
deferredLighting.addToStage(stage); deferredLighting.addToStage(stage);
// const ambientLight = new AmbientLight(0x2244cc, 0.25); this.ambientLight = new AmbientLight(0xffffff, 1);
const ambientLight = new AmbientLight(0xffffff, 1); stage.addChild(this.ambientLight);
stage.addChild(ambientLight);
}, },
}, },
}; };

View File

@ -26,7 +26,7 @@ const KEY_MAP = {
}; };
function emptySlots() { function emptySlots() {
return Array(50).fill(undefined); return Array(10).fill(undefined);
} }
const devEventsChannel = new EventEmitter(); const devEventsChannel = new EventEmitter();
@ -37,20 +37,21 @@ function Ui({disconnected}) {
const chatInputRef = useRef(); const chatInputRef = useRef();
const latestTick = useRef(); const latestTick = useRef();
const gameRef = useRef(); const gameRef = useRef();
const [mainEntity, setMainEntity] = useMainEntity(); const mainEntityRef = useMainEntity();
const [debug, setDebug] = useDebug(); const [, setDebug] = useDebug();
const [ecs, setEcs] = useEcs(); const ecsRef = useEcs();
const [showDisconnected, setShowDisconnected] = useState(false); const [showDisconnected, setShowDisconnected] = useState(false);
const [bufferSlot, setBufferSlot] = useState(); const [bufferSlot, setBufferSlot] = useState();
const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false); const [devtoolsIsOpen, setDevtoolsIsOpen] = useState(false);
const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y; const ratio = (RESOLUTION.x * (devtoolsIsOpen ? 2 : 1)) / RESOLUTION.y;
const [camera, setCamera] = useState({x: 0, y: 0}); const [camera, setCamera] = useState({x: 0, y: 0});
const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
const [inventorySlots, setInventorySlots] = useState(emptySlots()); const [inventorySlots, setInventorySlots] = useState(emptySlots());
const [activeSlot, setActiveSlot] = useState(0); const [activeSlot, setActiveSlot] = useState(0);
const [scale, setScale] = useState(2); const [scale, setScale] = useState(2);
const [Components, setComponents] = useState(); const [Components, setComponents] = useState();
const [Systems, setSystems] = useState(); const [Systems, setSystems] = useState();
const [monopolizers, setMonopolizers] = useState([]); const monopolizers = useRef([]);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [chatIsOpen, setChatIsOpen] = useState(false); const [chatIsOpen, setChatIsOpen] = useState(false);
const [chatHistory, setChatHistory] = useState([]); const [chatHistory, setChatHistory] = useState([]);
@ -58,7 +59,7 @@ function Ui({disconnected}) {
const [chatMessages, setChatMessages] = useState({}); const [chatMessages, setChatMessages] = useState({});
const [pendingMessage, setPendingMessage] = useState(''); const [pendingMessage, setPendingMessage] = useState('');
const [hotbarIsHidden, setHotbarIsHidden] = useState(true); const [hotbarIsHidden, setHotbarIsHidden] = useState(true);
const [hotbarHideHandle, setHotbarHideHandle] = useState(); const hotbarHideHandle = useRef();
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();
@ -67,8 +68,8 @@ function Ui({disconnected}) {
class ClientEcsPerf extends ClientEcs { class ClientEcsPerf extends ClientEcs {
markChange() {} markChange() {}
} }
setEcs(new ClientEcsPerf({Components, Systems})); ecsRef.current = new ClientEcsPerf({Components, Systems});
}, [Components, Systems, setEcs]); }, [ecsRef, Components, Systems]);
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');
@ -95,201 +96,137 @@ 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,
]);
const keepHotbarOpen = useCallback(() => { const keepHotbarOpen = useCallback(() => {
if (!isInventoryOpen) { if (!isInventoryOpen) {
setHotbarIsHidden(false); setHotbarIsHidden(false);
if (hotbarHideHandle) { if (hotbarHideHandle.current) {
clearTimeout(hotbarHideHandle); clearTimeout(hotbarHideHandle.current);
} }
setHotbarHideHandle(setTimeout(() => { hotbarHideHandle.current = setTimeout(() => {
setHotbarIsHidden(true); setHotbarIsHidden(true);
}, 4000)); }, 4000);
} }
}, [hotbarHideHandle, isInventoryOpen]); }, [isInventoryOpen]);
useEffect(() => { useEffect(() => {
return addKeyListener(document.body, ({event, type, payload}) => { return addKeyListener(document.body, ({event, payload, type}) => {
if ('Escape' === payload && 'keyDown' === type && chatIsOpen) { const actionMap = {
setChatIsOpen(false); 'd': {type: 'moveRight'},
's': {type: 'moveDown'},
'a': {type: 'moveLeft'},
'w': {type: 'moveUp'},
'e': {type: 'interact'},
'1': {type: 'changeSlot', payload: 1},
'2': {type: 'changeSlot', payload: 2},
'3': {type: 'changeSlot', payload: 3},
'4': {type: 'changeSlot', payload: 4},
'5': {type: 'changeSlot', payload: 5},
'6': {type: 'changeSlot', payload: 6},
'7': {type: 'changeSlot', payload: 7},
'8': {type: 'changeSlot', payload: 8},
'9': {type: 'changeSlot', payload: 9},
'0': {type: 'changeSlot', payload: 10},
'`': {type: 'openInventory'},
'-': {type: 'zoomOut'},
'+': {type: 'zoomIn'},
'=': {type: 'zoomIn'},
'F3': {type: 'debug'},
'F4': {type: 'devtools'},
'Enter': {type: 'openChat'},
'Escape': {type: 'closeChat'},
};
const action = actionMap[payload];
if (!action) {
return; return;
} }
if (chatInputRef.current) { if (chatInputRef.current) {
chatInputRef.current.focus(); if ('closeChat' === action.type && 'keyDown' === type) {
setChatIsOpen(false);
return;
} }
if (chatIsOpen) { chatInputRef.current.focus();
return; return;
} }
let actionPayload; let actionPayload;
switch (payload) { switch (action.type) {
case '-': case 'interact': {
if ('keyDown' === type) { if ('keyDown' === type) {
setScale((scale) => scale > 1 ? scale - 1 : 1); if (monopolizers.current.length > 0) {
} monopolizers.current[0].trigger();
break;
case '=':
case '+':
if ('keyDown' === type) {
setScale((scale) => scale < 4 ? Math.floor(scale + 1) : 4);
}
break;
case 'F3': {
if (event) {
event.preventDefault();
}
if ('keyDown' === type) {
setDebug(!debug);
}
break;
}
case 'F4': {
if (event) {
event.preventDefault();
}
if ('keyDown' === type) {
setDevtoolsIsOpen(!devtoolsIsOpen);
}
break;
}
case '`': {
if (event) {
event.preventDefault();
}
if ('keyDown' === type) {
if (isInventoryOpen) {
setHotbarIsHidden(true);
}
else {
setHotbarIsHidden(false);
if (hotbarHideHandle) {
clearTimeout(hotbarHideHandle);
}
}
setIsInventoryOpen(!isInventoryOpen);
}
break
}
case 'Enter': {
if ('keyDown' === type) {
setChatIsOpen(true);
}
break
}
case 'e': {
if (KEY_MAP[type]) {
if (monopolizers.length > 0) {
monopolizers[0].trigger();
break; break;
} }
} }
actionPayload = {type: 'interact', value: KEY_MAP[type]}; actionPayload = {type: 'interact', value: KEY_MAP[type]};
break; break;
} }
case '1': { case 'moveRight':
case 'moveDown':
case 'moveLeft':
case 'moveUp': {
actionPayload = {type: action.type, value: 'keyDown' === type ? 1 : 0};
break;
}
case 'changeSlot': {
if ('keyDown' === type) { if ('keyDown' === type) {
keepHotbarOpen(); keepHotbarOpen();
actionPayload = {type: 'changeSlot', value: 1}; actionPayload = {type: 'changeSlot', value: action.payload};
} }
break; break;
} }
case '2': { case 'openInventory': {
if (event) {
event.preventDefault();
}
if ('keyDown' === type) { if ('keyDown' === type) {
keepHotbarOpen(); setIsInventoryOpen((isInventoryOpen) => {
actionPayload = {type: 'changeSlot', value: 2}; if (isInventoryOpen) {
setHotbarIsHidden(true);
} }
break; else {
setHotbarIsHidden(false);
if (hotbarHideHandle.current) {
clearTimeout(hotbarHideHandle.current);
} }
case '3': { }
return !isInventoryOpen;
});
}
return;
}
case 'zoomIn': {
if ('keyDown' === type) { if ('keyDown' === type) {
keepHotbarOpen(); setScale((scale) => scale < 4 ? Math.floor(scale + 1) : 4);
actionPayload = {type: 'changeSlot', value: 3};
} }
break; return;
} }
case '4': { case 'zoomOut': {
if ('keyDown' === type) { if ('keyDown' === type) {
keepHotbarOpen(); setScale((scale) => scale > 1 ? scale - 1 : 1);
actionPayload = {type: 'changeSlot', value: 4};
} }
break; return;
}
case 'debug': {
if (event) {
event.preventDefault();
} }
case '5': {
if ('keyDown' === type) { if ('keyDown' === type) {
keepHotbarOpen(); setDebug((debug) => !debug);
actionPayload = {type: 'changeSlot', value: 5};
} }
break; return;
}
case 'devtools': {
if (event) {
event.preventDefault();
} }
case '6': {
if ('keyDown' === type) { if ('keyDown' === type) {
keepHotbarOpen(); setDevtoolsIsOpen((devtoolsIsOpen) => !devtoolsIsOpen);
actionPayload = {type: 'changeSlot', value: 6};
} }
break; return;
} }
case '7': { case 'openChat': {
if ('keyDown' === type) { if ('keyDown' === type) {
keepHotbarOpen(); setChatIsOpen(true);
actionPayload = {type: 'changeSlot', value: 7};
} }
break; return;
}
case '8': {
if ('keyDown' === type) {
keepHotbarOpen();
actionPayload = {type: 'changeSlot', value: 8};
}
break;
}
case '9': {
if ('keyDown' === type) {
keepHotbarOpen();
actionPayload = {type: 'changeSlot', value: 9};
}
break;
}
case '0': {
if ('keyDown' === type) {
keepHotbarOpen();
actionPayload = {type: 'changeSlot', value: 10};
}
break;
} }
} }
if (actionPayload) { if (actionPayload) {
@ -299,36 +236,24 @@ function Ui({disconnected}) {
}); });
} }
}); });
}, [ }, [client, keepHotbarOpen, setDebug]);
chatIsOpen,
client,
debug,
devtoolsIsOpen,
hotbarHideHandle,
isInventoryOpen,
keepHotbarOpen,
monopolizers,
setDebug,
setScale,
]);
const onEcsChangePacket = useCallback(() => { const onEcsChangePacket = useCallback(() => {
refreshEcs(); refreshEcs();
setMainEntity(undefined); mainEntityRef.current = undefined;
setMonopolizers([]); monopolizers.current = [];
}, [refreshEcs, setMainEntity]); }, [refreshEcs, mainEntityRef]);
usePacket('EcsChange', onEcsChangePacket); usePacket('EcsChange', onEcsChangePacket);
const onTickPacket = useCallback(async (payload, client) => { const onTickPacket = useCallback(async (payload, client) => {
if (0 === Object.keys(payload.ecs).length) { if (0 === Object.keys(payload.ecs).length) {
return; return;
} }
latestTick.current = Promise.resolve(latestTick.current).then(async () => { latestTick.current = Promise.resolve(latestTick.current).then(async () => {
await ecs.apply(payload.ecs); await ecsRef.current.apply(payload.ecs);
client.emitter.invoke(':Ecs', payload.ecs); client.emitter.invoke(':Ecs', payload.ecs);
}); });
}, [ecs]); }, [ecsRef]);
usePacket('Tick', onTickPacket); usePacket('Tick', onTickPacket);
const onEcsTick = useCallback((payload, ecs) => { const onEcsTick = useCallback((payload, ecs) => {
let localMainEntity = mainEntity;
for (const id in payload) { for (const id in payload) {
const entity = ecs.get(id); const entity = ecs.get(id);
const update = payload[id]; const update = payload[id];
@ -336,14 +261,21 @@ function Ui({disconnected}) {
continue; continue;
} }
if (update.MainEntity) { if (update.MainEntity) {
setMainEntity(localMainEntity = id); mainEntityRef.current = id;
} }
if (update.Inventory) { if (update.Inventory) {
if (localMainEntity === id) { if (mainEntityRef.current === id) {
setBufferSlot(entity.Inventory.item(0)); setBufferSlot(entity.Inventory.item(0));
setHotbarSlots(() => {
const newHotbarSlots = [];
for (let i = 1; i < 11; ++i) {
newHotbarSlots.push(entity.Inventory.item(i));
}
return newHotbarSlots;
});
const newInventorySlots = emptySlots(); const newInventorySlots = emptySlots();
for (let i = 1; i < 41; ++i) { for (let i = 11; i < 41; ++i) {
newInventorySlots[i - 1] = entity.Inventory.item(i); newInventorySlots[i - 11] = entity.Inventory.item(i);
} }
setInventorySlots(newInventorySlots); setInventorySlots(newInventorySlots);
} }
@ -356,8 +288,8 @@ function Ui({disconnected}) {
setExternalInventorySlots(newInventorySlots); setExternalInventorySlots(newInventorySlots);
setIsInventoryOpen(true); setIsInventoryOpen(true);
setHotbarIsHidden(false); setHotbarIsHidden(false);
if (hotbarHideHandle) { if (hotbarHideHandle.current) {
clearTimeout(hotbarHideHandle); clearTimeout(hotbarHideHandle.current);
} }
} }
else if (update.Inventory.closed) { else if (update.Inventory.closed) {
@ -365,13 +297,13 @@ function Ui({disconnected}) {
setExternalInventorySlots(); setExternalInventorySlots();
} }
} }
if (localMainEntity === id) { if (mainEntityRef.current === id) {
if (update.Wielder && 'activeSlot' in update.Wielder) { if (update.Wielder && 'activeSlot' in update.Wielder) {
setActiveSlot(update.Wielder.activeSlot); setActiveSlot(update.Wielder.activeSlot);
} }
} }
} }
}, [hotbarHideHandle, mainEntity, setMainEntity]); }, [mainEntityRef]);
useEcsTick(onEcsTick); useEcsTick(onEcsTick);
const onEcsTickParticles = useCallback((payload, ecs) => { const onEcsTickParticles = useCallback((payload, ecs) => {
if (!('1' in payload) || particleWorker) { if (!('1' in payload) || particleWorker) {
@ -415,15 +347,15 @@ function Ui({disconnected}) {
}, []); }, []);
useEcsTick(onEcsTickAabbs); useEcsTick(onEcsTickAabbs);
const onEcsTickCamera = useCallback((payload, ecs) => { const onEcsTickCamera = useCallback((payload, ecs) => {
if (mainEntity) { if (mainEntityRef.current) {
const mainEntityEntity = ecs.get(mainEntity); const mainEntityEntity = ecs.get(mainEntityRef.current);
const x = Math.round((mainEntityEntity.Camera.x * scale) - RESOLUTION.x / 2); const x = Math.round((mainEntityEntity.Camera.x * scale) - RESOLUTION.x / 2);
const y = Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2); const y = Math.round((mainEntityEntity.Camera.y * scale) - RESOLUTION.y / 2);
if (x !== camera.x || y !== camera.y) { if (x !== camera.x || y !== camera.y) {
setCamera({x, y}); setCamera({x, y});
} }
} }
}, [camera, mainEntity, scale]); }, [camera, mainEntityRef, scale]);
useEcsTick(onEcsTickCamera); useEcsTick(onEcsTickCamera);
useEffect(() => { useEffect(() => {
function onContextMenu(event) { function onContextMenu(event) {
@ -435,15 +367,15 @@ function Ui({disconnected}) {
}; };
}, []); }, []);
const computePosition = useCallback(({clientX, clientY}) => { const computePosition = useCallback(({clientX, clientY}) => {
if (!gameRef.current || !mainEntity) { if (!gameRef.current || !mainEntityRef.current) {
return; return;
} }
const {top, left, width} = gameRef.current.getBoundingClientRect(); const {top, left, width} = gameRef.current.getBoundingClientRect();
const master = ecs.get(1); const master = ecsRef.current.get(1);
if (!master) { if (!master) {
return; return;
} }
const {Camera} = ecs.get(mainEntity); const {Camera} = ecsRef.current.get(mainEntityRef.current);
const size = width / RESOLUTION.x; const size = width / RESOLUTION.x;
const camera = { const camera = {
x: ((Camera.x * scale) - (RESOLUTION.x / 2)), x: ((Camera.x * scale) - (RESOLUTION.x / 2)),
@ -454,10 +386,23 @@ function Ui({disconnected}) {
y: (((clientY - top) / size) + camera.y) / scale, y: (((clientY - top) / size) + camera.y) / scale,
}; };
}, [ }, [
ecs, ecsRef,
mainEntity, mainEntityRef,
scale, scale,
]); ]);
const hotbarOnActivate = useCallback((i) => {
keepHotbarOpen();
client.send({
type: 'Action',
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 1]},
});
}, [client, keepHotbarOpen, mainEntityRef]);
const bagOnActivate = useCallback((i) => {
client.send({
type: 'Action',
payload: {type: 'swapSlots', value: [0, mainEntityRef.current, i + 11]},
});
}, [client, mainEntityRef]);
return ( return (
<div <div
className={styles.ui} className={styles.ui}
@ -479,7 +424,7 @@ function Ui({disconnected}) {
<div <div
className={[styles.game, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')} className={[styles.game, devtoolsIsOpen && styles.devtoolsIsOpen].filter(Boolean).join(' ')}
onMouseDown={(event) => { onMouseDown={(event) => {
if (chatIsOpen) { if (chatInputRef.current) {
event.preventDefault(); event.preventDefault();
return; return;
} }
@ -500,8 +445,8 @@ function Ui({disconnected}) {
} }
break; break;
case 2: case 2:
if (monopolizers.length > 0) { if (monopolizers.current.length > 0) {
monopolizers[0].trigger(); monopolizers.current[0].trigger();
break; break;
} }
client.send({ client.send({
@ -512,7 +457,7 @@ function Ui({disconnected}) {
} }
}} }}
onMouseUp={(event) => { onMouseUp={(event) => {
if (chatIsOpen) { if (chatInputRef.current) {
event.preventDefault(); event.preventDefault();
return; return;
} }
@ -534,18 +479,18 @@ function Ui({disconnected}) {
event.preventDefault(); event.preventDefault();
}} }}
onWheel={(event) => { onWheel={(event) => {
if (chatIsOpen) { if (chatInputRef.current) {
event.preventDefault(); event.preventDefault();
return; return;
} }
if (!isInventoryOpen) { if (!isInventoryOpen) {
setHotbarIsHidden(false); setHotbarIsHidden(false);
if (hotbarHideHandle) { if (hotbarHideHandle.current) {
clearTimeout(hotbarHideHandle); clearTimeout(hotbarHideHandle.current);
} }
setHotbarHideHandle(setTimeout(() => { hotbarHideHandle.current = setTimeout(() => {
setHotbarIsHidden(true); setHotbarIsHidden(true);
}, 4000)); }, 4000);
} }
if (event.deltaY > 0) { if (event.deltaY > 0) {
client.send({ client.send({
@ -564,7 +509,7 @@ function Ui({disconnected}) {
> >
<Pixi <Pixi
camera={camera} camera={camera}
monopolizers={monopolizers} monopolizers={monopolizers.current}
particleWorker={particleWorker} particleWorker={particleWorker}
scale={scale} scale={scale}
/> />
@ -572,24 +517,13 @@ function Ui({disconnected}) {
<HotBar <HotBar
active={activeSlot} active={activeSlot}
hotbarIsHidden={hotbarIsHidden} hotbarIsHidden={hotbarIsHidden}
onActivate={(i) => { onActivate={hotbarOnActivate}
keepHotbarOpen(); slots={hotbarSlots}
client.send({
type: 'Action',
payload: {type: 'swapSlots', value: [0, mainEntity, i + 1]},
});
}}
slots={inventorySlots.slice(0, 10)}
/> />
<Bag <Bag
isInventoryOpen={isInventoryOpen} isInventoryOpen={isInventoryOpen}
onActivate={(i) => { onActivate={bagOnActivate}
client.send({ slots={inventorySlots}
type: 'Action',
payload: {type: 'swapSlots', value: [0, mainEntity, i + 11]},
});
}}
slots={inventorySlots.slice(10, 20)}
/> />
{externalInventory && ( {externalInventory && (
<External <External
@ -607,7 +541,7 @@ function Ui({disconnected}) {
camera={camera} camera={camera}
scale={scale} scale={scale}
setChatMessages={setChatMessages} setChatMessages={setChatMessages}
setMonopolizers={setMonopolizers} monopolizers={monopolizers.current}
/> />
{chatIsOpen && ( {chatIsOpen && (
<Chat <Chat
@ -621,9 +555,6 @@ function Ui({disconnected}) {
setChatHistoryCaret={setChatHistoryCaret} setChatHistoryCaret={setChatHistoryCaret}
setMessage={setMessage} setMessage={setMessage}
setPendingMessage={setPendingMessage} setPendingMessage={setPendingMessage}
onClose={() => {
setChatIsOpen(false);
}}
/> />
)} )}
{showDisconnected && ( {showDisconnected && (

View File

@ -11,12 +11,12 @@ export function useEcs() {
} }
export function useEcsTick(fn) { export function useEcsTick(fn) {
const [ecs] = useEcs(); const ecsRef = useEcs();
const memo = useCallback((payload) => { const memo = useCallback((payload) => {
if (!ecs) { if (!ecsRef.current) {
return; return;
} }
fn(payload, ecs); fn(payload, ecsRef.current);
}, [ecs, fn]); }, [ecsRef, fn]);
usePacket(':Ecs', memo); usePacket(':Ecs', memo);
} }

View File

@ -29,10 +29,10 @@ body {
@font-face { @font-face {
font-family: "Cookbook"; font-family: "Cookbook";
src: url("/assets/fonts/Cookbook.woff") format("woff"); src: url("/assets/fonts/Cookbook.woff");
} }
@font-face { @font-face {
font-family: "Joystix"; font-family: "Joystix";
src: url("/assets/fonts/Joystix.ttf") format("ttf"); src: url("/assets/fonts/Joystix.woff");
} }

View File

@ -1,5 +1,5 @@
import {json} from "@remix-run/node"; import {json} from "@remix-run/node";
import {useCallback, useEffect, useState} from 'react'; import {useCallback, useEffect, useRef, useState} from 'react';
import {useOutletContext, useParams} from 'react-router-dom'; import {useOutletContext, useParams} from 'react-router-dom';
import Ui from '@/react/components/ui.jsx'; import Ui from '@/react/components/ui.jsx';
@ -22,10 +22,9 @@ export default function PlaySpecific() {
const Client = useOutletContext(); const Client = useOutletContext();
const assetsTuple = useState({}); const assetsTuple = useState({});
const [client, setClient] = useState(); const [client, setClient] = useState();
const mainEntityTuple = useState(); const mainEntityRef = useRef();
const setMainEntity = mainEntityTuple[1];
const debugTuple = useState(false); const debugTuple = useState(false);
const ecsTuple = useState(); const ecsRef = useRef();
const [disconnected, setDisconnected] = useState(false); const [disconnected, setDisconnected] = useState(false);
const params = useParams(); const params = useParams();
const [type, url] = params['*'].split('/'); const [type, url] = params['*'].split('/');
@ -109,7 +108,7 @@ export default function PlaySpecific() {
if (!client || !disconnected) { if (!client || !disconnected) {
return; return;
} }
setMainEntity(undefined); mainEntityRef.current = undefined;
async function reconnect() { async function reconnect() {
await client.connect(url); await client.connect(url);
} }
@ -118,7 +117,7 @@ export default function PlaySpecific() {
return () => { return () => {
clearInterval(handle); clearInterval(handle);
}; };
}, [client, disconnected, setMainEntity, url]); }, [client, disconnected, mainEntityRef, url]);
// useEffect(() => { // useEffect(() => {
// let source = true; // let source = true;
// async function play() { // async function play() {
@ -144,8 +143,8 @@ export default function PlaySpecific() {
// }, []) // }, [])
return ( return (
<ClientContext.Provider value={client}> <ClientContext.Provider value={client}>
<MainEntityContext.Provider value={mainEntityTuple}> <MainEntityContext.Provider value={mainEntityRef}>
<EcsContext.Provider value={ecsTuple}> <EcsContext.Provider value={ecsRef}>
<DebugContext.Provider value={debugTuple}> <DebugContext.Provider value={debugTuple}>
<AssetsContext.Provider value={assetsTuple}> <AssetsContext.Provider value={assetsTuple}>
<RadiansContext.Provider value={radians}> <RadiansContext.Provider value={radians}>

View File

@ -289,11 +289,7 @@ export default async function createHomestead(id) {
interacting: 1, interacting: 1,
interactScript: ` interactScript: `
const lines = [ const lines = [
'mrowwr', 'Mind your own business, buddy.\\n\\ner, I mean, <shake>MEEHHHHHH</shake>',
'p<shake>rrr</shake>o<wave>wwwww</wave>',
'mew<rate frequency={0.5}> </rate>mew!',
'me<wave>wwwww</wave>',
'\\\\*pu<shake>rrrrr</shake>\\\\*',
]; ];
const line = lines[Math.floor(Math.random() * lines.length)]; const line = lines[Math.floor(Math.random() * lines.length)];
subject.Interlocutor.dialogue({ subject.Interlocutor.dialogue({

View File

@ -27,6 +27,7 @@ const textEncoder = new TextEncoder();
export default class Engine { export default class Engine {
ackingActions = new Map();
connectedPlayers = new Map(); connectedPlayers = new Map();
ecses = {}; ecses = {};
frame = 0; frame = 0;
@ -89,7 +90,13 @@ export default class Engine {
// dump entity state with updates for the transition // dump entity state with updates for the transition
const dumped = { const dumped = {
...entity.toJSON(), ...entity.toJSON(),
Controlled: entity.Controlled, // manually transfer control
Controlled: {
moveUp: entity.Controlled.moveUp,
moveRight: entity.Controlled.moveRight,
moveDown: entity.Controlled.moveDown,
moveLeft: entity.Controlled.moveLeft,
},
Ecs: {path}, Ecs: {path},
...updates, ...updates,
}; };
@ -103,7 +110,7 @@ export default class Engine {
Promise.all(promises).then(async () => { Promise.all(promises).then(async () => {
// 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 connectedPlayer.entity.Player.id = id;
}); });
} }
} }
@ -250,6 +257,15 @@ export default class Engine {
} }
} }
if (payload.ack) { if (payload.ack) {
if (!this.ackingActions.has(connection)) {
this.ackingActions.set(connection, []);
}
this.ackingActions.get(connection).push({
type: 'ActionAck',
payload: {
ack: payload.ack,
},
})
this.server.send( this.server.send(
connection, connection,
{ {
@ -303,6 +319,7 @@ export default class Engine {
if (!connectedPlayer) { if (!connectedPlayer) {
return; return;
} }
this.ackingActions.delete(connection);
this.connectedPlayers.delete(connection); this.connectedPlayers.delete(connection);
this.incomingActions.delete(connection); this.incomingActions.delete(connection);
const {entity, heartbeat, id} = connectedPlayer; const {entity, heartbeat, id} = connectedPlayer;
@ -445,6 +462,12 @@ export default class Engine {
update(elapsed) { update(elapsed) {
for (const [connection, {entity}] of this.connectedPlayers) { for (const [connection, {entity}] of this.connectedPlayers) {
if (this.ackingActions.has(connection)) {
for (const ack of this.ackingActions.get(connection)) {
this.server.send(connection, ack);
}
this.ackingActions.delete(connection);
}
if (!entity) { if (!entity) {
continue; continue;
} }

View File

@ -17,4 +17,4 @@ export const SERVER_LATENCY = 0;
export const TPS = 60; export const TPS = 60;
export const UPS = 15; export const UPS = 30;

View File

@ -27,6 +27,7 @@ function computeParams(ancestors) {
export function parseLetters(source) { export function parseLetters(source) {
let letters = []; let letters = [];
let page = 0;
try { try {
const tree = parser.parse(source); const tree = parser.parse(source);
tree.dialogue = { tree.dialogue = {
@ -43,6 +44,10 @@ export function parseLetters(source) {
} }
break; break;
} }
case 'paragraph': {
page += 1;
break;
}
case 'text': { case 'text': {
const params = computeParams(ancestors); const params = computeParams(ancestors);
const split = node.value.split(''); const split = node.value.split('');
@ -51,7 +56,12 @@ export function parseLetters(source) {
for (const name in params) { for (const name in params) {
indices[name] = i + params[name].length; indices[name] = i + params[name].length;
} }
letters.push({character: split[i], indices, params}); letters.push({
character: split[i],
indices,
page: page - 1,
params,
});
} }
for (const name in params) { for (const name in params) {
params[name].length += split.length; params[name].length += split.length;

View File

@ -48,10 +48,20 @@ export default class Script {
Math: MathUtil, Math: MathUtil,
Promise: PromiseUtil, Promise: PromiseUtil,
transition, transition,
wait: (seconds) => ( wait: (seconds = 0) => (
new Promise((resolve) => { new PromiseUtil.Ticker(
setTimeout(resolve, seconds * 1000); (resolve) => {
}) if (0 === seconds) {
resolve();
}
},
(elapsed, resolve) => {
seconds -= elapsed;
if (seconds <= 0) {
resolve();
}
}
)
), ),
}; };
} }

11
package-lock.json generated
View File

@ -23,6 +23,7 @@
"acorn": "^8.12.0", "acorn": "^8.12.0",
"alea": "^1.0.1", "alea": "^1.0.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"isbot": "^4.1.0", "isbot": "^4.1.0",
@ -53,7 +54,6 @@
"@storybook/test": "^8.1.6", "@storybook/test": "^8.1.6",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"@vitest/coverage-v8": "^1.6.0", "@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
@ -8989,7 +8989,6 @@
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.1" "cross-spawn": "^7.0.1"
}, },
@ -9007,7 +9006,6 @@
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@ -9021,7 +9019,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
}, },
@ -12405,8 +12402,7 @@
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"dev": true
}, },
"node_modules/ismobilejs": { "node_modules/ismobilejs": {
"version": "1.1.1", "version": "1.1.1",
@ -16868,7 +16864,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -19208,7 +19203,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
}, },
@ -19220,7 +19214,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }

View File

@ -30,6 +30,7 @@
"acorn": "^8.12.0", "acorn": "^8.12.0",
"alea": "^1.0.1", "alea": "^1.0.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"isbot": "^4.1.0", "isbot": "^4.1.0",
@ -60,7 +61,6 @@
"@storybook/test": "^8.1.6", "@storybook/test": "^8.1.6",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",
"@vitest/coverage-v8": "^1.6.0", "@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",

Binary file not shown.

Binary file not shown.

View File

@ -87,5 +87,5 @@ while (shots.length > 0) {
for (let i = 0; i < destroying.length; ++i) { for (let i = 0; i < destroying.length; ++i) {
shots.splice(shots.indexOf(destroying[i]), 1); shots.splice(shots.indexOf(destroying[i]), 1);
} }
await wait(0); await wait();
} }