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 {
duration = 0;
latest;
location = 0;
penultimate;
tracking = [];
accept(state) {
const packet = state;
if ('Tick' !== packet.type) {
let authoritative = [];
let duration = 0;
let last = performance.now();
let latest = null;
let location = 0;
let penultimate;
let tracking = [];
const interpolate = () => {
const now = performance.now();
const elapsed = (now - last) / 1000;
last = now;
if (authoritative.length > 0) {
for (const packet of authoritative) {
postMessage(packet);
return;
}
this.penultimate = this.latest;
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'],
});
}
}
}
}
this.location = 0;
authoritative = [];
}
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];
if (tracking.length > 0) {
location += elapsed;
const fraction = location / duration;
const [from, to] = [penultimate.payload.ecs, latest.payload.ecs];
const interpolated = {};
for (const {entityId, componentName, properties} of this.tracking) {
for (const {entityId, componentName, properties} of tracking) {
if (!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({
type: 'Tick',
payload: {
ecs: interpolator.penultimate.payload.ecs,
elapsed: last ? (performance.now() - last) / 1000 : 0,
frame: interpolator.penultimate.payload.frame,
...penultimate.payload,
ecs: interpolated,
elapsed,
frame: penultimate.payload.frame + fraction,
},
});
if (!handle) {
last = performance.now();
handle = requestAnimationFrame(interpolate);
}
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,
},
});
last = performance.now();
}
break;
}
default: {
postMessage(packet);
break;
}
}
};

View File

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

View File

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

View File

@ -18,6 +18,6 @@ export default class Time extends Component {
};
}
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,
}) {
const client = useClient();
const [mainEntity] = useMainEntity();
const mainEntityRef = useMainEntity();
const [mainEntityJson, setMainEntityJson] = useState('');
const onEcsTick = useCallback((payload, ecs) => {
if (!mainEntity) {
if (!mainEntityRef.current) {
return;
}
setMainEntityJson(JSON.stringify(ecs.get(mainEntity), null, 2));
}, [mainEntity]);
setMainEntityJson(JSON.stringify(ecs.get(mainEntityRef.current), null, 2));
}, [mainEntityRef]);
useEcsTick(onEcsTick);
return (
<div className={styles.devtools}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import {memo} from 'react';
import styles from './hotbar.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.
*/
export default function Hotbar({
function Hotbar({
active,
hotbarIsHidden,
onActivate,
@ -33,3 +35,5 @@ export default function Hotbar({
</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 {useEcsTick} from '@/react/context/ecs.js';
@ -11,14 +11,14 @@ import TileLayer from './tile-layer.jsx';
import Water from './water.jsx';
export default function Ecs({camera, monopolizers, particleWorker, scale}) {
const [mainEntity] = useMainEntity();
const app = useApp();
const mainEntityRef = useMainEntity();
const [layers, setLayers] = useState([]);
const [hour, setHour] = useState(10);
const [projected, setProjected] = useState([]);
const [position, setPosition] = useState({x: 0, y: 0});
const [water, setWater] = useState();
const onEcsTick = useCallback((payload, ecs) => {
const entity = ecs.get(mainEntity);
const entity = ecs.get(mainEntityRef.current);
for (const id in payload) {
const update = payload[id];
switch (id) {
@ -28,7 +28,88 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
setLayers(Object.values(master.TileLayers.$$layersProxies));
}
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) {
setWater(master.Water.water);
@ -42,7 +123,7 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
setPosition(Position.toJSON());
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
}
}, [mainEntity]);
}, [app.ambientLight, mainEntityRef]);
useEcsTick(onEcsTick);
return (
<Container

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ const textEncoder = new TextEncoder();
export default class Engine {
ackingActions = new Map();
connectedPlayers = new Map();
ecses = {};
frame = 0;
@ -89,7 +90,13 @@ export default class Engine {
// dump entity state with updates for the transition
const dumped = {
...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},
...updates,
};
@ -103,7 +110,7 @@ export default class Engine {
Promise.all(promises).then(async () => {
// 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.Player.id = id
connectedPlayer.entity.Player.id = id;
});
}
}
@ -250,6 +257,15 @@ export default class Engine {
}
}
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(
connection,
{
@ -303,6 +319,7 @@ export default class Engine {
if (!connectedPlayer) {
return;
}
this.ackingActions.delete(connection);
this.connectedPlayers.delete(connection);
this.incomingActions.delete(connection);
const {entity, heartbeat, id} = connectedPlayer;
@ -445,6 +462,12 @@ export default class Engine {
update(elapsed) {
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) {
continue;
}

View File

@ -17,4 +17,4 @@ export const SERVER_LATENCY = 0;
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) {
let letters = [];
let page = 0;
try {
const tree = parser.parse(source);
tree.dialogue = {
@ -43,6 +44,10 @@ export function parseLetters(source) {
}
break;
}
case 'paragraph': {
page += 1;
break;
}
case 'text': {
const params = computeParams(ancestors);
const split = node.value.split('');
@ -51,7 +56,12 @@ export function parseLetters(source) {
for (const name in params) {
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) {
params[name].length += split.length;

View File

@ -48,10 +48,20 @@ export default class Script {
Math: MathUtil,
Promise: PromiseUtil,
transition,
wait: (seconds) => (
new Promise((resolve) => {
setTimeout(resolve, seconds * 1000);
})
wait: (seconds = 0) => (
new PromiseUtil.Ticker(
(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",
"alea": "^1.0.1",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.18.2",
"idb-keyval": "^6.2.1",
"isbot": "^4.1.0",
@ -53,7 +54,6 @@
"@storybook/test": "^8.1.6",
"@vitejs/plugin-react": "^4.3.1",
"@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"eslint": "^8.38.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
@ -8989,7 +8989,6 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
@ -9007,7 +9006,6 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@ -9021,7 +9019,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@ -12405,8 +12402,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/ismobilejs": {
"version": "1.1.1",
@ -16868,7 +16864,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
@ -19208,7 +19203,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@ -19220,7 +19214,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}

View File

@ -30,6 +30,7 @@
"acorn": "^8.12.0",
"alea": "^1.0.1",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.18.2",
"idb-keyval": "^6.2.1",
"isbot": "^4.1.0",
@ -60,7 +61,6 @@
"@storybook/test": "^8.1.6",
"@vitejs/plugin-react": "^4.3.1",
"@vitest/coverage-v8": "^1.6.0",
"cross-env": "^7.0.3",
"eslint": "^8.38.0",
"eslint-plugin-import": "^2.28.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) {
shots.splice(shots.indexOf(destroying[i]), 1);
}
await wait(0);
await wait();
}