diff --git a/index.html b/index.html
index 8915e34..4a972d5 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,10 @@
-
-
);
});
-const TreeEntity = observer(function TreeEntity({ tree, store }: { tree: TreeState; store: GameStore }) {
- const isChopping = tree.id === store.heldTreeId && store.isChopping;
+const NODE_SPRITES = { tree: TREE_SPRITE, bush: BUSH_SPRITE };
+const NODE_SIZES = { tree: 64, bush: 40 };
+
+const NodeEntity = observer(function NodeEntity({ node, store }: { node: ResourceNode; store: GameStore }) {
+ const isChopping = node.id === store.heldNodeId && store.isChopping;
return (
);
@@ -62,13 +78,27 @@ const CharacterEntity = observer(function CharacterEntity({ store }: { store: Ga
function GameMap({ store }: Props) {
const containerRef = useRef
(null);
const [ready, setReady] = useState(false);
+ const isRotatedRef = useRef(false);
+
+ useEffect(() => {
+ const mql = window.matchMedia(ROTATE_QUERY);
+ isRotatedRef.current = mql.matches;
+ const handleChange = (e: MediaQueryListEvent) => {
+ isRotatedRef.current = e.matches;
+ };
+ mql.addEventListener('change', handleChange);
+ return () => mql.removeEventListener('change', handleChange);
+ }, []);
useEffect(() => {
const container = containerRef.current;
if (!container)
return;
- const { width, height } = container.getBoundingClientRect();
+ // getBoundingClientRect() reports the physical (rotated) box, whose width/height are swapped
+ // relative to the app's own logical layout — see ROTATE_QUERY above.
+ const rect = container.getBoundingClientRect();
+ const [width, height] = isRotatedRef.current ? [rect.height, rect.width] : [rect.width, rect.height];
if (width > 0 && height > 0) {
store.initialize(width, height);
store.start();
@@ -90,31 +120,45 @@ function GameMap({ store }: Props) {
};
}, [store]);
- /** Finds the tree (if any) the pointer is currently over, by DOM hit-testing at its client
- * coordinates. */
- function treeIdAtPoint(clientX: number, clientY: number): string | null {
- const hit = document.elementFromPoint(clientX, clientY)?.closest('[data-tree-id]');
- return hit?.getAttribute('data-tree-id') ?? null;
+ /** Finds the node (if any) the pointer is currently over, by DOM hit-testing at its client
+ * coordinates. elementFromPoint uses physical screen coordinates and already accounts for any
+ * CSS transform, so this needs no rotation correction (unlike toLocalPoint below). */
+ function nodeIdAtPoint(clientX: number, clientY: number): string | null {
+ const hit = document.elementFromPoint(clientX, clientY)?.closest('[data-node-id]');
+ return hit?.getAttribute('data-node-id') ?? null;
+ }
+
+ /** Converts a pointer event's physical screen position into the map's own logical (x, y) —
+ * i.e. what the game world uses for the character's target position. When the map is rotated
+ * (see ROTATE_QUERY), physical x/y are swapped and one is mirrored relative to logical x/y, so
+ * a plain subtraction of rect.left/rect.top would point the character the wrong way. */
+ function toLocalPoint(e: PointerEvent, rect: DOMRect): { x: number; y: number } {
+ const physicalX = e.clientX - rect.left;
+ const physicalY = e.clientY - rect.top;
+ if (isRotatedRef.current)
+ return { x: physicalY, y: rect.width - physicalX };
+ return { x: physicalX, y: physicalY };
}
/** The character's action is purely a function of the current pointer state: if the primary
* button isn't held, nothing happens; otherwise it heads for whatever's under the cursor right
- * now — a tree to chop, or open ground to walk to. Shared by pointerdown/move/up so panning the
- * cursor across a tree mid-drag starts/stops chopping it, and releasing the button stops it. */
+ * now — a node to chop, or open ground to walk to. Shared by pointerdown/move/up so panning the
+ * cursor across a node mid-drag starts/stops chopping it, and releasing the button stops it. */
function followPointer(e: PointerEvent) {
if ((e.buttons & 1) === 0) {
store.release();
return;
}
- const treeId = treeIdAtPoint(e.clientX, e.clientY);
- if (treeId) {
- store.holdTree(treeId);
+ const nodeId = nodeIdAtPoint(e.clientX, e.clientY);
+ if (nodeId) {
+ store.holdNode(nodeId);
return;
}
const rect = e.currentTarget.getBoundingClientRect();
- store.moveTo(e.clientX - rect.left, e.clientY - rect.top);
+ const { x, y } = toLocalPoint(e, rect);
+ store.moveTo(x, y);
}
function handlePointerDown(e: PointerEvent) {
@@ -133,12 +177,12 @@ function GameMap({ store }: Props) {
>
{ready && (
<>
- {store.logDrops.map((log) => (
-
+ {store.resourceDrops.map((drop) => (
+
))}
- {store.trees.map((tree) => (
-
+ {store.nodes.map((node) => (
+
))}
diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx
index e22b25b..5d01c7b 100644
--- a/src/components/Topbar.tsx
+++ b/src/components/Topbar.tsx
@@ -1,6 +1,6 @@
import { observer } from 'mobx-react-lite';
import type { GameStore } from '../game/GameStore';
-import { COIN_SPRITE, LOG_SPRITE } from '../game/sprites';
+import { COIN_SPRITE, LOG_SPRITE, TWIG_SPRITE } from '../game/sprites';
import PixelSprite from './PixelSprite';
interface Props {
@@ -15,13 +15,17 @@ function Topbar({ store }: Props) {
Уровень {store.level}