Enable pieces rotation in dice set and board
parent
035b7da335
commit
5fc02aa6a6
|
|
@ -11,3 +11,20 @@ export const directions: Direction[] = [
|
||||||
Direction.EAST,
|
Direction.EAST,
|
||||||
Direction.WEST,
|
Direction.WEST,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function rotateDirection(
|
||||||
|
initialDirection: Direction,
|
||||||
|
rotationAngle: 0 | 90 | 180 | 270,
|
||||||
|
): Direction {
|
||||||
|
const angleToDirectionMap: Record<Direction, 0 | 90 | 180 | 270> = {
|
||||||
|
[Direction.NORTH]: 0,
|
||||||
|
[Direction.EAST]: 90,
|
||||||
|
[Direction.SOUTH]: 180,
|
||||||
|
[Direction.WEST]: 270,
|
||||||
|
};
|
||||||
|
const finalAngle =
|
||||||
|
(360 + angleToDirectionMap[initialDirection] - rotationAngle) % 360;
|
||||||
|
return (Object.keys(angleToDirectionMap) as Direction[]).find(
|
||||||
|
(direction) => angleToDirectionMap[direction] === finalAngle,
|
||||||
|
)!;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ const STRAIGHT_RAIL: Piece = new Piece({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const TURN_RAIL: Piece = new Piece({
|
const TURN_RAIL: Piece = new Piece({
|
||||||
useInternalTracks: false,
|
useInternalTracks: false,
|
||||||
trackDefinitions: [
|
trackDefinitions: [
|
||||||
|
|
@ -166,7 +165,7 @@ const DEAD_END_STATION_ROAD: Piece = new Piece({
|
||||||
trackDefinitions: [
|
trackDefinitions: [
|
||||||
{
|
{
|
||||||
startPoint: Direction.EAST,
|
startPoint: Direction.EAST,
|
||||||
type: TrackType.RAIL,
|
type: TrackType.ROAD,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -232,7 +231,6 @@ const FOUR_WAY_WITH_ONE_RAIL: Piece = new Piece({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const FOUR_WAY_TURNING_CROSSING: Piece = new Piece({
|
const FOUR_WAY_TURNING_CROSSING: Piece = new Piece({
|
||||||
useInternalTracks: true,
|
useInternalTracks: true,
|
||||||
internalNodeType: InternalNodeType.STATION,
|
internalNodeType: InternalNodeType.STATION,
|
||||||
|
|
@ -316,7 +314,7 @@ const FOUR_WAY_CROSS_ROAD: Piece = new Piece({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
startPoint: Direction.WEST,
|
startPoint: Direction.WEST,
|
||||||
type: TrackType.RAIL,
|
type: TrackType.ROAD,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { CellType } from "../constants/CellType";
|
import { CellType } from "../constants/CellType";
|
||||||
import { Direction, directions } from "../constants/Direction";
|
import { Direction } from "../constants/Direction";
|
||||||
import { ExitType } from "../constants/ExitType";
|
import { ExitType } from "../constants/ExitType";
|
||||||
import { PieceId, pieceMap } from "../constants/Pieces";
|
import { PieceId, pieceMap } from "../constants/Pieces";
|
||||||
import { TrackType } from "../constants/TrackType";
|
|
||||||
import { Exit } from "./Exit";
|
import { Exit } from "./Exit";
|
||||||
import { ExternalNode } from "./ExternalNode";
|
import { ExternalNode } from "./ExternalNode";
|
||||||
import { InternalNode } from "./InternalNode";
|
import { InternalNode } from "./InternalNode";
|
||||||
|
|
@ -33,9 +32,12 @@ export class Cell {
|
||||||
return node!;
|
return node!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public placePiece(pieceId: PieceId) {
|
/**
|
||||||
|
* @param rotation in degrees counter-clockwise
|
||||||
|
*/
|
||||||
|
public placePiece(pieceId: PieceId, rotation: 0 | 90 | 180 | 270) {
|
||||||
if (this.placedPiece !== undefined) return;
|
if (this.placedPiece !== undefined) return;
|
||||||
const piece: Piece = pieceMap[pieceId];
|
const piece: Piece = pieceMap[pieceId].rotate(rotation);
|
||||||
|
|
||||||
this.validatePiecePlacement(piece);
|
this.validatePiecePlacement(piece);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Direction } from "../constants/Direction";
|
import { Direction, rotateDirection } from "../constants/Direction";
|
||||||
import { TrackType } from "../constants/TrackType";
|
import { TrackType } from "../constants/TrackType";
|
||||||
import { Cell } from "./Cell";
|
import { Cell } from "./Cell";
|
||||||
import { InternalNode } from "./InternalNode";
|
import { InternalNode } from "./InternalNode";
|
||||||
|
|
@ -65,6 +65,33 @@ export class Piece {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rotate(rotation: 0 | 90 | 180 | 270): Piece {
|
||||||
|
return new Piece({
|
||||||
|
useInternalTracks: this.internalNode !== undefined,
|
||||||
|
internalNodeType: this.internalNode?.type,
|
||||||
|
trackDefinitions: Array.from(this.tracks).map((track) => {
|
||||||
|
if (track.joinedPoints.secondPoint instanceof InternalNode) {
|
||||||
|
return {
|
||||||
|
type: track.type,
|
||||||
|
startPoint: rotateDirection(
|
||||||
|
track.joinedPoints.firstPoint,
|
||||||
|
rotation,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: track.type,
|
||||||
|
startPoint: rotateDirection(
|
||||||
|
track.joinedPoints.firstPoint,
|
||||||
|
rotation,
|
||||||
|
),
|
||||||
|
endPoint: rotateDirection(track.joinedPoints.secondPoint, rotation),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toPlacedPiece(cell: Cell) {
|
toPlacedPiece(cell: Cell) {
|
||||||
return new PlacedPiece(
|
return new PlacedPiece(
|
||||||
new Set(
|
new Set(
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -16,6 +16,7 @@ const GamePage = () => {
|
||||||
pieceId: getRandomPieceId(),
|
pieceId: getRandomPieceId(),
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
rotation: 0,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,18 @@
|
||||||
&:nth-child(3) {
|
&:nth-child(3) {
|
||||||
top: -40px;
|
top: -40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.rotate-90 {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-180 {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-270 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.house {
|
&.house {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Cell, directions, Exit, PieceId } from "interface";
|
import { Cell, directions, Exit, PieceId } from "interface";
|
||||||
import "./BoardCell.scss";
|
import "./BoardCell.scss";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface BoardCellProps {
|
export interface BoardCellProps {
|
||||||
cell: Cell;
|
cell: Cell;
|
||||||
|
|
@ -7,6 +8,7 @@ export interface BoardCellProps {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}[];
|
}[];
|
||||||
setDice: React.Dispatch<
|
setDice: React.Dispatch<
|
||||||
React.SetStateAction<
|
React.SetStateAction<
|
||||||
|
|
@ -14,6 +16,7 @@ export interface BoardCellProps {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}[]
|
}[]
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|
@ -22,11 +25,15 @@ export interface BoardCellProps {
|
||||||
|
|
||||||
const BoardCell = (props: BoardCellProps) => {
|
const BoardCell = (props: BoardCellProps) => {
|
||||||
const { cell, dice, setDice, refreshBoardRender } = props;
|
const { cell, dice, setDice, refreshBoardRender } = props;
|
||||||
|
const [pieceRotationAngle, setPieceRotationAngle] = useState(0);
|
||||||
const handleBoardCellClick = () => {
|
const handleBoardCellClick = () => {
|
||||||
const selectedDie = dice.find((die) => die.isSelected);
|
const selectedDie = dice.find((die) => die.isSelected);
|
||||||
if (!selectedDie) return;
|
if (!selectedDie) return;
|
||||||
try {
|
try {
|
||||||
cell.placePiece(selectedDie.pieceId);
|
cell.placePiece(
|
||||||
|
selectedDie.pieceId,
|
||||||
|
selectedDie.rotation as 0 | 90 | 180 | 270,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return;
|
return;
|
||||||
|
|
@ -36,12 +43,13 @@ const BoardCell = (props: BoardCellProps) => {
|
||||||
return die !== selectedDie
|
return die !== selectedDie
|
||||||
? die
|
? die
|
||||||
: {
|
: {
|
||||||
pieceId: selectedDie.pieceId,
|
...selectedDie,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
setPieceRotationAngle(selectedDie.rotation);
|
||||||
refreshBoardRender();
|
refreshBoardRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -64,7 +72,10 @@ const BoardCell = (props: BoardCellProps) => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{cell.placedPiece && (
|
{cell.placedPiece && (
|
||||||
<img className="piece" src={`pieces/${cell.placedPiece.id}.jpeg`}></img>
|
<img
|
||||||
|
className={`piece rotate-${pieceRotationAngle}`}
|
||||||
|
src={`pieces/${cell.placedPiece.id}.jpeg`}
|
||||||
|
></img>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,25 @@
|
||||||
|
.dice-set-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 50px 50px 0;
|
||||||
|
width: max-content;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 3px solid green;
|
||||||
|
box-shadow: 0 0 10px green;
|
||||||
|
border-radius: 20%;
|
||||||
|
padding: 5px;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
&.icon-inverted {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dice-set {
|
.dice-set {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, auto);
|
grid-template-columns: repeat(2, auto);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { PieceId } from "interface";
|
import { PieceId } from "interface";
|
||||||
import "./DiceSet.scss";
|
import "./DiceSet.scss";
|
||||||
import Die from "./Die";
|
import Die from "./Die";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
export interface DiceSetProps {
|
export interface DiceSetProps {
|
||||||
dice: {
|
dice: {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}[];
|
}[];
|
||||||
setDice: React.Dispatch<
|
setDice: React.Dispatch<
|
||||||
React.SetStateAction<
|
React.SetStateAction<
|
||||||
|
|
@ -14,6 +16,7 @@ export interface DiceSetProps {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}[]
|
}[]
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|
@ -24,6 +27,7 @@ const DiceSet = (props: DiceSetProps) => {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}) => {
|
}) => {
|
||||||
if (die.isDisabled) return;
|
if (die.isDisabled) return;
|
||||||
const newDiceState = dice.map((oldDie) => {
|
const newDiceState = dice.map((oldDie) => {
|
||||||
|
|
@ -36,12 +40,37 @@ const DiceSet = (props: DiceSetProps) => {
|
||||||
setDice(newDiceState);
|
setDice(newDiceState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRotateButton = (rotation: number) => {
|
||||||
|
const newDiceState = dice.map((die) => {
|
||||||
|
if (!die.isSelected) return die;
|
||||||
|
const rotationAngle = (die.rotation + rotation + 360) % 360;
|
||||||
|
return {
|
||||||
|
...die,
|
||||||
|
rotation: rotationAngle,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setDice(newDiceState);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="dice-set-actions">
|
||||||
|
<img
|
||||||
|
src="./rotate-icon.png"
|
||||||
|
onClick={() => handleRotateButton(-90)}
|
||||||
|
></img>
|
||||||
|
<img
|
||||||
|
src="./rotate-icon.png"
|
||||||
|
className="icon-inverted"
|
||||||
|
onClick={() => handleRotateButton(90)}
|
||||||
|
></img>
|
||||||
|
</div>
|
||||||
<div className="dice-set">
|
<div className="dice-set">
|
||||||
{dice.map((die) => (
|
{dice.map((die) => (
|
||||||
<Die die={die} handleDieClick={handleDieClick} />
|
<Die die={die} handleDieClick={handleDieClick} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,18 @@
|
||||||
&.disabled {
|
&.disabled {
|
||||||
box-shadow: 0 0 20px grey;
|
box-shadow: 0 0 20px grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.rotate-90 {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-180 {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rotate-270 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: transform 0.5s cubic-bezier(.47,1.64,.41,.8);
|
||||||
}
|
}
|
||||||
|
|
@ -2,23 +2,31 @@ import { PieceId } from "interface";
|
||||||
import "./Die.scss";
|
import "./Die.scss";
|
||||||
|
|
||||||
interface DieProps {
|
interface DieProps {
|
||||||
die: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean };
|
die: {
|
||||||
|
pieceId: PieceId;
|
||||||
|
isSelected: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
|
};
|
||||||
handleDieClick: (die: {
|
handleDieClick: (die: {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Die = (props: DieProps) => {
|
const Die = (props: DieProps) => {
|
||||||
const { die, handleDieClick } = props;
|
const { die, handleDieClick } = props;
|
||||||
const { pieceId, isSelected, isDisabled } = die;
|
const { pieceId, isSelected, isDisabled, rotation } = die;
|
||||||
|
const className =
|
||||||
|
"dice" +
|
||||||
|
(isSelected ? " selected" : "") +
|
||||||
|
(isDisabled ? " disabled" : "") +
|
||||||
|
` rotate-${rotation}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={className} onClick={() => handleDieClick(die)}>
|
||||||
className={`dice${isSelected ? " selected" : ""}${isDisabled ? " disabled" : ""}`}
|
|
||||||
onClick={() => handleDieClick(die)}
|
|
||||||
>
|
|
||||||
<img src={`pieces/${pieceId}.jpeg`}></img>
|
<img src={`pieces/${pieceId}.jpeg`}></img>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export interface GameBoardProps {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}[];
|
}[];
|
||||||
setDice: React.Dispatch<
|
setDice: React.Dispatch<
|
||||||
React.SetStateAction<
|
React.SetStateAction<
|
||||||
|
|
@ -15,6 +16,7 @@ export interface GameBoardProps {
|
||||||
pieceId: PieceId;
|
pieceId: PieceId;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
rotation: number;
|
||||||
}[]
|
}[]
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue