initial commit
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { PointerEvent } from 'react';
|
||||
import type { GameStore } from '../game/GameStore';
|
||||
import type { LogDrop, TreeState } from '../game/types';
|
||||
import { CHARACTER_SPRITE, LOG_SPRITE, TREE_SPRITE } from '../game/sprites';
|
||||
import PixelSprite from './PixelSprite';
|
||||
|
||||
interface Props {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
// Each entity below is its own observer so mobx can subscribe it only to the fields it actually
|
||||
// reads. Otherwise, with everything read inside GameMap's single observer, the character moving
|
||||
// every animation frame would re-render the whole tree/log list too, not just itself.
|
||||
|
||||
const LogEntity = observer(function LogEntity({ log }: { log: LogDrop }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="entity log-shadow" style={{ left: log.x, top: log.y }} />
|
||||
<div className="entity log" style={{ left: log.x, top: log.y - log.z, zIndex: Math.floor(log.y - log.z) }}>
|
||||
<PixelSprite pixels={LOG_SPRITE} size={20} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const TreeEntity = observer(function TreeEntity({
|
||||
tree,
|
||||
store,
|
||||
onPointerDown,
|
||||
onPointerUp,
|
||||
}: {
|
||||
tree: TreeState;
|
||||
store: GameStore;
|
||||
onPointerDown: (e: PointerEvent<HTMLDivElement>, id: string) => void;
|
||||
onPointerUp: (e: PointerEvent<HTMLDivElement>) => void;
|
||||
}) {
|
||||
const isChopping = tree.id === store.heldTreeId && store.isChopping;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="entity tree-shadow" style={{ left: tree.x, top: tree.y, zIndex: Math.floor(tree.y) - 1 }} />
|
||||
<div
|
||||
className={`entity tree ${isChopping ? 'tree--chopping' : ''}`}
|
||||
style={{ left: tree.x, top: tree.y, zIndex: Math.floor(tree.y) }}
|
||||
onPointerDown={(e) => onPointerDown(e, tree.id)}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
>
|
||||
<PixelSprite pixels={TREE_SPRITE} size={64} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const CharacterEntity = observer(function CharacterEntity({ store }: { store: GameStore }) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="entity character-shadow"
|
||||
style={{ left: store.character.x, top: store.character.y, zIndex: Math.floor(store.character.y) - 1 }}
|
||||
/>
|
||||
<div
|
||||
className={`entity character ${store.isMoving ? 'character--walking' : ''}`}
|
||||
style={{ left: store.character.x, top: store.character.y, zIndex: Math.floor(store.character.y) }}
|
||||
>
|
||||
<PixelSprite pixels={CHARACTER_SPRITE} size={48} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
function GameMap({ store }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container)
|
||||
return;
|
||||
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
if (width > 0 && height > 0) {
|
||||
store.initialize(width, height);
|
||||
store.start();
|
||||
setReady(true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
store.stop();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('blur', store.release);
|
||||
window.addEventListener('beforeunload', store.save);
|
||||
return () => {
|
||||
window.removeEventListener('blur', store.release);
|
||||
window.removeEventListener('beforeunload', store.save);
|
||||
};
|
||||
}, [store]);
|
||||
|
||||
function handleTreePointerDown(e: PointerEvent<HTMLDivElement>, id: string) {
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
// Stop this from also bubbling to the map's own pointer handlers below (which would
|
||||
// otherwise interpret the same press as "walk to this empty ground point").
|
||||
e.stopPropagation();
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
store.holdTree(id);
|
||||
}
|
||||
|
||||
function handleTreePointerUp(e: PointerEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
store.release();
|
||||
}
|
||||
|
||||
function handleMapPointerDown(e: PointerEvent<HTMLDivElement>) {
|
||||
if (e.button !== 0)
|
||||
return;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
store.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="game-map"
|
||||
ref={containerRef}
|
||||
onPointerDown={handleMapPointerDown}
|
||||
onPointerUp={store.release}
|
||||
onPointerCancel={store.release}
|
||||
>
|
||||
{ready && (
|
||||
<>
|
||||
{store.logDrops.map((log) => (
|
||||
<LogEntity key={log.id} log={log} />
|
||||
))}
|
||||
|
||||
{store.trees.map((tree) => (
|
||||
<TreeEntity
|
||||
key={tree.id}
|
||||
tree={tree}
|
||||
store={store}
|
||||
onPointerDown={handleTreePointerDown}
|
||||
onPointerUp={handleTreePointerUp}
|
||||
/>
|
||||
))}
|
||||
|
||||
<CharacterEntity store={store} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(GameMap);
|
||||
Reference in New Issue
Block a user