Skip to content

Commit

Permalink
pointer and command controls
Browse files Browse the repository at this point in the history
  • Loading branch information
Grzegorz Tańczyk committed Apr 21, 2024
1 parent dbfccf8 commit 826bb0c
Show file tree
Hide file tree
Showing 20 changed files with 627 additions and 28 deletions.
376 changes: 375 additions & 1 deletion games/nukes/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion games/nukes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"react-router-dom": "^6.21.1",
"styled-components": "^6.1.6",
"typescript": "^5.2.2",
"vite": "^5.0.0"
"vite": "^5.0.0",
"vite-plugin-checker": "^0.6.4"
}
}
33 changes: 33 additions & 0 deletions games/nukes/src/controls-render/launch-highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import styled from 'styled-components';
import { EntityType } from '../world/world-state-types';
import { useSelectedObject } from '../controls/selection';
import { usePointer } from '../controls/pointer';

export function LaunchHighlight() {
const selectedObject = useSelectedObject();
const pointer = usePointer();

if (selectedObject?.type !== EntityType.LAUNCH_SITE) {
return null;
}

return (
<HighlightContainer
style={
{
'--x': pointer.x,
'--y': pointer.y,
} as React.CSSProperties
}
>
{pointer.x}, {pointer.y}
</HighlightContainer>
);
}

const HighlightContainer = styled.div`
position: absolute;
transform: translate(calc(var(--x) * 1px), calc(var(--y) * 1px));
pointer-events: none;
color: red;
`;
46 changes: 46 additions & 0 deletions games/nukes/src/controls/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useCustomEvent } from '../events';
import { EntityType, Explosion, Missile, WorldState } from '../world/world-state-types';
import { usePointer } from './pointer';
import { useSelectedObject } from './selection';

export function Command({
worldState,
setWorldState,
}: {
worldState: WorldState;
setWorldState: (worldState: WorldState) => void;
}) {
const selectedObject = useSelectedObject();
const pointer = usePointer();

useCustomEvent('world-click', () => {
if (selectedObject?.type !== EntityType.LAUNCH_SITE || pointer.pointingObjects.length === 0) {
return;
}

const missile: Missile = {
id: Math.random() + '',
launch: selectedObject.position,
launchTimestamp: worldState.timestamp,

target: pointer.pointingObjects[0].position,
targetTimestamp: worldState.timestamp + 10,
};

const explosion: Explosion = {
id: Math.random() + '',
startTimestamp: missile.targetTimestamp,
endTimestamp: missile.targetTimestamp + 5,
position: missile.target,
radius: 30,
};

setWorldState({
...worldState,
missiles: [...worldState.missiles, missile],
explosions: [...worldState.explosions, explosion],
});
});

return null;
}
75 changes: 75 additions & 0 deletions games/nukes/src/controls/pointer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { createContext, useContext, useReducer } from 'react';
import { City, LaunchSite } from '../world/world-state-types';

type PointableObject = LaunchSite | City;

type PointerDispatchAction =
| {
type: 'move';
x: number;
y: number;
}
| {
type: 'point' | 'unpoint';
object: PointableObject;
};

type Pointer = {
x: number;
y: number;
pointingObjects: PointableObject[];
};

const initialPointer: Pointer = { x: 0, y: 0, pointingObjects: [] };

const pointerReducer: React.Reducer<Pointer, PointerDispatchAction> = (
pointer: Pointer,
action: PointerDispatchAction,
) => {
if (action.type === 'move') {
return { x: action.x, y: action.y, pointingObjects: pointer.pointingObjects };
} else if (action.type === 'point' && !pointer.pointingObjects.some((object) => object.id === action.object.id)) {
return { x: pointer.x, y: pointer.y, pointingObjects: [...pointer.pointingObjects, action.object] };
} else if (action.type === 'unpoint' && pointer.pointingObjects.some((object) => object.id === action.object.id)) {
return {
x: pointer.x,
y: pointer.y,
pointingObjects: pointer.pointingObjects.filter((object) => object.id === action.object.id),
};
} else {
return pointer;
}
};

const PointerContext = createContext<Pointer>(initialPointer);

const PointerDispatchContext = createContext<React.Dispatch<PointerDispatchAction>>(() => {});

export function PointerContextWrapper({ children }: { children: React.ReactNode }) {
const [selection, reducer] = useReducer(pointerReducer, initialPointer);

return (
<PointerContext.Provider value={selection}>
<PointerDispatchContext.Provider value={reducer}>{children}</PointerDispatchContext.Provider>
</PointerContext.Provider>
);
}

export function usePointer() {
const pointer = useContext(PointerContext);

return pointer;
}

export function usePointerMove() {
const dispatch = useContext(PointerDispatchContext);
return (x: number, y: number) => dispatch({ type: 'move', x, y });
}

export function useObjectPointer() {
const dispatch = useContext(PointerDispatchContext);
return [
(object: PointableObject) => dispatch({ type: 'point', object }),
(object: PointableObject) => dispatch({ type: 'unpoint', object }),
];
}
22 changes: 15 additions & 7 deletions games/nukes/src/controls/selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { LaunchSite } from '../world/world-state-types';

type SelectionDispatchAction =
| {
action: 'clear';
type: 'clear';
}
| {
action: 'set';
type: 'set';
object: LaunchSite;
};

Expand All @@ -21,9 +21,9 @@ const selectionReducer: React.Reducer<Selection, SelectionDispatchAction> = (
selection: Selection,
action: SelectionDispatchAction,
) => {
if (action.action === 'clear') {
if (action.type === 'clear') {
return initialSelection;
} else if (action.action === 'set') {
} else if (action.type === 'set') {
return { ...selection, selectedObject: action.object };
} else {
return selection;
Expand All @@ -34,7 +34,7 @@ const selectionReducer: React.Reducer<Selection, SelectionDispatchAction> = (
const SelectionContext = createContext<Selection>(initialSelection);

// definition of dispatch function for selection context
const SelectionDispatchContext = createContext<React.Dispatch<SelectionDispatchAction>>(selectionReducer);
const SelectionDispatchContext = createContext<React.Dispatch<SelectionDispatchAction>>(() => {});

export function SelectionContextWrapper({ children }: { children: React.ReactNode }) {
const [selection, reducer] = useReducer(selectionReducer, initialSelection);
Expand All @@ -50,11 +50,19 @@ export function useObjectSelection(object: LaunchSite) {
const dispatch = useContext(SelectionDispatchContext);
const selection = useContext(SelectionContext);

return [selection.selectedObject?.id === object.id, () => dispatch({ action: 'set', object })] as const;
return [selection.selectedObject?.id === object.id, () => dispatch({ type: 'set', object })] as const;
}

export function useSelectedObject() {
const selection = useContext(SelectionContext);

return selection.selectedObject;
}

// definition of clear selection function for selection context

export function useClearSelection() {
const dispatch = useContext(SelectionDispatchContext);

return () => dispatch({ action: 'clear' });
return () => dispatch({ type: 'clear' });
}
21 changes: 21 additions & 0 deletions games/nukes/src/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect } from 'react';

export function dispatchCustomEvent<T>(eventName: string, data?: T) {
const event = new CustomEvent(eventName, {
bubbles: true,
detail: data,
});
document.dispatchEvent(event);
}

export function useCustomEvent<T>(eventName: string, callback: (data: T) => void) {
useEffect(() => {
const handler = (event: Event | CustomEvent<T>) => {
callback((event as CustomEvent).detail as T);
};
document.addEventListener(eventName, handler, false);
return () => {
document.removeEventListener(eventName, handler, false);
};
}, [eventName, callback]);
}
3 changes: 0 additions & 3 deletions games/nukes/src/game-states/state-intro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ const Intro: GameStateComponent = ({ setGameState }) => {
return (
<>
<h3>intro</h3>
<button onClick={() => setGameState(GameStateTechMap)}>Map tech demo</button>
<br />
<button onClick={() => setGameState(GameStateTechNuke)}>Nuke tech demo</button>
<br />
<button onClick={() => setGameState(GameStateTechWorld)}>Nuke world demo</button>
</>
Expand Down
21 changes: 15 additions & 6 deletions games/nukes/src/game-states/state-tech-world.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import { createWorldState } from '../world/world-state-create';
import { updateWorldState } from '../world/world-state-updates';
import { WorldState } from '../world/world-state-types';
import { SelectionContextWrapper } from '../controls/selection';
import { WorldStateRender } from '../render/world-state-render';
import { WorldStateRender } from '../world-render/world-state-render';

import { LaunchHighlight } from '../controls-render/launch-highlight';

import { GameState, GameStateComponent } from './types';
import { PointerContextWrapper } from '../controls/pointer';
import { Command } from '../controls/command';

const WorldComponent: GameStateComponent = ({ setGameState }) => {
const WorldComponent: GameStateComponent = ({}) => {
const [worldState, setWorldState] = useState(() => createWorldState());
const updateWorld = useCallback(
(worldState: WorldState, deltaTime: number) => setWorldState(updateWorldState(worldState, deltaTime)),
Expand All @@ -18,10 +22,15 @@ const WorldComponent: GameStateComponent = ({ setGameState }) => {

return (
<SelectionContextWrapper>
<StateContainer>
<TimeControls worldState={worldState} updateWorld={updateWorld} />
<WorldStateRender state={worldState} />
</StateContainer>
<PointerContextWrapper>
<StateContainer>
<Command worldState={worldState} setWorldState={setWorldState} />
<TimeControls worldState={worldState} updateWorld={updateWorld} />
<WorldStateRender state={worldState} />

<LaunchHighlight />
</StateContainer>
</PointerContextWrapper>
</SelectionContextWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import styled from 'styled-components';

import { City } from '../world/world-state-types';
import { useObjectPointer } from '../controls/pointer';

export function CityRender({ city }: { city: City }) {
const [point, unpoint] = useObjectPointer();

return (
<CityContainer
onMouseEnter={() => point(city)}
onMouseLeave={() => unpoint(city)}
style={
{
'--x': city.position.x,
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import styled from 'styled-components';

import { LaunchSite } from '../world/world-state-types';
import { useObjectSelection } from '../controls/selection';
import { useObjectPointer } from '../controls/pointer';

export function LaunchSiteRender({ launchSite }: { launchSite: LaunchSite }) {
const [isSelected, select] = useObjectSelection(launchSite);
const [point, unpoint] = useObjectPointer();

return (
<LaunchSiteContainer
onMouseEnter={() => point(launchSite)}
onMouseLeave={() => unpoint(launchSite)}
onClick={() => select()}
style={
{
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import styled from 'styled-components';

import { State } from '../world/world-state-types';

export function StateRender({ state }: { state: State }) {
export function StateRender(_props: { state: State }) {
return <StateContainer />;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import styled from 'styled-components';

import { WorldState } from '../world/world-state-types';
import { usePointerMove } from '../controls/pointer';

import { SectorRender } from './sector-render';
import { StateRender } from './state-render';
import { CityRender } from './city-render';
import { LaunchSiteRender } from './launch-site-render';
import { MissileRender } from './missile-render';
import { ExplosionRender } from './explosion-render';
import { dispatchCustomEvent } from '../events';

export function WorldStateRender({ state }: { state: WorldState }) {
// wrap this into styled components globl css
const pointerMove = usePointerMove();

return (
<WorldStateContainer>
<WorldStateContainer
onMouseMove={(event) => pointerMove(event.clientX, event.clientY)}
onClick={() => dispatchCustomEvent('world-click')}
>
{state.sectors.map((sector) => (
<SectorRender key={sector.id} sector={sector} />
))}
Expand Down
Loading

0 comments on commit 826bb0c

Please sign in to comment.