Compare commits

..

No commits in common. "9853edec4472fe1c3d9f2063b68830d5243c145f" and "29df4c3dcba1eb3349c977068469f939d83a795f" have entirely different histories.

32 changed files with 472 additions and 548 deletions

View File

@ -1,27 +1,47 @@
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) {
export default class Interpolator {
duration = 0;
latest;
location = 0;
penultimate;
tracking = [];
accept(state) {
const packet = state;
if ('Tick' !== packet.type) {
postMessage(packet);
return;
}
authoritative = [];
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;
}
if (tracking.length > 0) {
location += elapsed;
const fraction = location / duration;
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 = {};
for (const {entityId, componentName, properties} of tracking) {
for (const {entityId, componentName, properties} of this.tracking) {
if (!interpolated[entityId]) {
interpolated[entityId] = {};
}
@ -44,76 +64,48 @@ const interpolate = () => {
);
}
}
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: {
...penultimate.payload,
ecs: interpolated,
elapsed,
frame: penultimate.payload.frame + fraction,
ecs: interpolator.penultimate.payload.ecs,
elapsed: last ? (performance.now() - last) / 1000 : 0,
frame: interpolator.penultimate.payload.frame,
},
});
}
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;
if (!handle) {
last = performance.now();
handle = requestAnimationFrame(interpolate);
}
}
};

View File

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

View File

@ -27,8 +27,17 @@ 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) {
@ -37,7 +46,7 @@ function applyClientActions(elapsed) {
const {Controlled} = main;
const finished = [];
for (const [id, action] of actions) {
if (0 === action.finished && !action.ack) {
if (Stage.UNACK === action.stage) {
if (!Controlled.locked) {
switch (action.action.type) {
case 'moveUp':
@ -51,7 +60,7 @@ function applyClientActions(elapsed) {
}
action.steps.push(elapsed);
}
if (1 === action.finished) {
if (Stage.FINISHING === action.stage) {
if (!Controlled.locked) {
switch (action.action.type) {
case 'moveUp':
@ -63,9 +72,9 @@ function applyClientActions(elapsed) {
}
}
}
action.finished = 2;
action.stage = Stage.FINISHED;
}
if (action.ack && 2 === action.finished) {
if (Stage.FINISHED === action.stage) {
action.steps.shift();
if (0 === action.steps.length) {
finished.push(id);
@ -104,13 +113,13 @@ onmessage = async (event) => {
if (0 === packet.payload.value) {
const ack = pending.get(packet.payload.type);
const action = actions.get(ack);
action.finished = 1;
action.stage = Stage.FINISHING;
pending.delete(packet.payload.type);
}
else {
const tx = {
action: packet.payload,
ack: false,finished: 0,
stage: Stage.UNACK,
steps: [],
};
packet.payload.ack = Math.random();
@ -130,12 +139,11 @@ onmessage = async (event) => {
switch (packet.type) {
case 'ActionAck': {
const action = actions.get(packet.payload.ack);
action.ack = true;
action.stage = Stage.ACK;
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: 6 * realSecondsPerGameHour, type: 'uint16'},
irlSeconds: {defaultValue: 10 * realSecondsPerGameHour, type: 'uint16'},
};
}

View File

@ -14,14 +14,14 @@ export default function Devtools({
eventsChannel,
}) {
const client = useClient();
const mainEntityRef = useMainEntity();
const [mainEntity] = useMainEntity();
const [mainEntityJson, setMainEntityJson] = useState('');
const onEcsTick = useCallback((payload, ecs) => {
if (!mainEntityRef.current) {
if (!mainEntity) {
return;
}
setMainEntityJson(JSON.stringify(ecs.get(mainEntityRef.current), null, 2));
}, [mainEntityRef]);
setMainEntityJson(JSON.stringify(ecs.get(mainEntity), null, 2));
}, [mainEntity]);
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 ecsRef = useEcs();
const [ecs] = useEcs();
useEffect(() => {
if (!ecsRef.current) {
if (!ecs) {
return;
}
const master = ecsRef.current.get(1);
const master = ecs.get(1);
if (!master) {
return;
}
@ -56,10 +56,10 @@ export default function Tiles({eventsChannel}) {
eventsChannel.removeListener('click', onClick);
};
});
if (!ecsRef.current) {
if (!ecs) {
return false;
}
const master = ecsRef.current.get(1);
const master = ecs.get(1);
if (!master) {
return false;
}

View File

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

View File

@ -8,6 +8,7 @@ export default function Chat({
chatHistoryCaret,
chatInputRef,
chatMessages,
onClose,
message,
pendingMessage,
setChatHistoryCaret,
@ -32,6 +33,7 @@ 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;
font-family: Joystix, 'Courier New', Courier, monospace;
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
.dialogue {
background-color: #02023999;
border: solid 1px #999999;
border: solid 3px white;
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,7 +45,6 @@ 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,
@ -65,7 +64,7 @@ export default function Entities({
},
};
if (dialogues[key].monopolizer) {
monopolizers.push(monopolizer);
setMonopolizers((monopolizers) => [...monopolizers, monopolizer]);
}
dialogues[key].onClose = () => {
setEntities((entities) => ({
@ -73,10 +72,14 @@ export default function Entities({
[id]: ecs.rebuild(id),
}));
if (dialogues[key].monopolizer) {
const index = monopolizers.indexOf(monopolizer);
if (-1 !== index) {
setMonopolizers((monopolizers) => {
const index = monopolizers.indexOf(monopolizer);
if (-1 === index) {
return monopolizers;
}
monopolizers.splice(index, 1);
}
return [...monopolizers];
});
}
delete dialogues[key];
};
@ -92,7 +95,7 @@ export default function Entities({
...updating,
};
});
}, [monopolizers, setChatMessages]);
}, [setChatMessages, setMonopolizers]);
useEcsTick(onEcsTick);
const renderables = [];
for (const id in entities) {

View File

@ -1,5 +1,3 @@
import {memo} from 'react';
import styles from './hotbar.module.css';
import gridStyles from './grid.module.css';
@ -8,7 +6,7 @@ import Grid from './grid.jsx';
/**
* The hotbar. 10 slots of inventory with an active selection.
*/
function Hotbar({
export default function Hotbar({
active,
hotbarIsHidden,
onActivate,
@ -35,5 +33,3 @@ function Hotbar({
</div>
);
}
export default memo(Hotbar);

View File

@ -1,4 +1,4 @@
import {Container, useApp} from '@pixi/react';
import {Container} 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 app = useApp();
const mainEntityRef = useMainEntity();
const [mainEntity] = 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(mainEntityRef.current);
const entity = ecs.get(mainEntity);
for (const id in payload) {
const update = payload[id];
switch (id) {
@ -28,88 +28,7 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
setLayers(Object.values(master.TileLayers.$$layersProxies));
}
if (update.Time) {
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;
setHour(Math.round(ecs.get(1).Time.hour * 60) / 60);
}
if (update.Water) {
setWater(master.Water.water);
@ -123,7 +42,7 @@ export default function Ecs({camera, monopolizers, particleWorker, scale}) {
setPosition(Position.toJSON());
setProjected(Wielder.activeItem()?.project(Position.tile, Direction.quantize(4)));
}
}, [app.ambientLight, mainEntityRef]);
}, [mainEntity]);
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 ecsRef = useEcs();
const [ecs] = useEcs();
const containerRef = useRef();
const latestTick = useRef();
const entities = useRef({});
const pool = useRef([]);
const mainEntityRef = useMainEntity();
const [mainEntity] = 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, ecs) => {
const updateEntities = useCallback((payload) => {
for (const id in payload) {
if ('1' === id) {
continue;
@ -43,15 +43,14 @@ 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));
entities.current[id].setDebug(false);
if (mainEntityRef.current === id) {
entities.current[id].reset(ecs.get(id), debug);
if (mainEntity === id) {
entities.current[id].setMainEntity();
}
}
entities.current[id].update(payload[id], containerRef.current);
}
}, [mainEntityRef])
}, [debug, ecs, mainEntity])
useEffect(() => {
if (0 === pool.current.length) {
for (let i = 0; i < 1000; ++i) {
@ -70,25 +69,25 @@ export default function Entities({monopolizers, particleWorker}) {
}
entities.current = {};
});
useEcsTick(updateEntities);
const onEcsTickEntities = useCallback((payload) => {
updateEntities(payload);
}, [updateEntities]);
useEcsTick(onEcsTickEntities);
useEffect(() => {
if (!particleWorker) {
if (!ecs || !particleWorker) {
return;
}
async function onMessage(diff) {
if (!ecsRef.current) {
return;
}
latestTick.current = Promise.resolve(latestTick.current).then(async () => {
await ecsRef.current.apply(diff.data);
updateEntities(diff.data, ecsRef.current);
await ecs.apply(diff.data);
updateEntities(diff.data);
});
}
particleWorker.addEventListener('message', onMessage);
return () => {
particleWorker.removeEventListener('message', onMessage);
};
}, [ecsRef, particleWorker, updateEntities]);
}, [ecs, particleWorker, updateEntities]);
const onEcsTickParticles = useCallback((payload) => {
for (const id in payload) {
const update = payload[id];
@ -101,7 +100,7 @@ export default function Entities({monopolizers, particleWorker}) {
}, [particleWorker]);
useEcsTick(onEcsTickParticles);
const onEcsTickInteractions = useCallback((payload, ecs) => {
const main = ecs.get(mainEntityRef.current);
const main = ecs.get(mainEntity);
if (main) {
if (willInteractWith.current !== main.Interacts.willInteractWith) {
if (entities.current[willInteractWith.current]) {
@ -116,7 +115,7 @@ export default function Entities({monopolizers, particleWorker}) {
: [];
}
}
}, [interactionFilters, mainEntityRef, monopolizers]);
}, [interactionFilters, mainEntity, monopolizers]);
useEcsTick(onEcsTickInteractions);
return (
<Container

View File

@ -43,12 +43,13 @@ export default class Entity {
this.debug.parent.removeChild(this.debug);
this.attached = false;
}
reset(entity) {
reset(entity, debug) {
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);
@ -96,7 +97,6 @@ export default class Entity {
0xffffff - 0x2244cc,
0,
);
this.container.addChild(this.light);
}
this.light.brightness = Light.brightness;
}
@ -178,16 +178,14 @@ export default class Entity {
}
}
}
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.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,8 +24,9 @@ export const ApplicationStageLights = {
init: function() {
const {stage} = this;
deferredLighting.addToStage(stage);
this.ambientLight = new AmbientLight(0xffffff, 1);
stage.addChild(this.ambientLight);
// const ambientLight = new AmbientLight(0x2244cc, 0.25);
const ambientLight = new AmbientLight(0xffffff, 1);
stage.addChild(ambientLight);
},
},
};

View File

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

View File

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

View File

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

View File

@ -289,7 +289,11 @@ export default async function createHomestead(id) {
interacting: 1,
interactScript: `
const lines = [
'Mind your own business, buddy.\\n\\ner, I mean, <shake>MEEHHHHHH</shake>',
'mrowwr',
'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)];
subject.Interlocutor.dialogue({

View File

@ -27,7 +27,6 @@ const textEncoder = new TextEncoder();
export default class Engine {
ackingActions = new Map();
connectedPlayers = new Map();
ecses = {};
frame = 0;
@ -90,13 +89,7 @@ export default class Engine {
// dump entity state with updates for the transition
const dumped = {
...entity.toJSON(),
// manually transfer control
Controlled: {
moveUp: entity.Controlled.moveUp,
moveRight: entity.Controlled.moveRight,
moveDown: entity.Controlled.moveDown,
moveLeft: entity.Controlled.moveLeft,
},
Controlled: entity.Controlled,
Ecs: {path},
...updates,
};
@ -110,7 +103,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
});
}
}
@ -257,15 +250,6 @@ 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,
{
@ -319,7 +303,6 @@ export default class Engine {
if (!connectedPlayer) {
return;
}
this.ackingActions.delete(connection);
this.connectedPlayers.delete(connection);
this.incomingActions.delete(connection);
const {entity, heartbeat, id} = connectedPlayer;
@ -462,12 +445,6 @@ 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 = 30;
export const UPS = 15;

View File

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

View File

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

11
package-lock.json generated
View File

@ -23,7 +23,6 @@
"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",
@ -54,6 +53,7 @@
"@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,6 +8989,7 @@
"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"
},
@ -9006,6 +9007,7 @@
"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",
@ -9019,6 +9021,7 @@
"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"
},
@ -12402,7 +12405,8 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/ismobilejs": {
"version": "1.1.1",
@ -16864,6 +16868,7 @@
"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"
}
@ -19203,6 +19208,7 @@
"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"
},
@ -19214,6 +19220,7 @@
"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,7 +30,6 @@
"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",
@ -61,6 +60,7 @@
"@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();
await wait(0);
}