Enable pieces rotation in dice set and board

landing-page-layout
MiguelMLorente 2024-12-03 23:03:28 +01:00
parent 949969bda3
commit b7561d804c
13 changed files with 166 additions and 23 deletions

View File

@ -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,
)!;
}

View File

@ -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,
}, },
], ],
}); });

View File

@ -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);

View File

@ -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(

BIN
web/public/rotate-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -16,6 +16,7 @@ const GamePage = () => {
pieceId: getRandomPieceId(), pieceId: getRandomPieceId(),
isSelected: false, isSelected: false,
isDisabled: false, isDisabled: false,
rotation: 0,
}; };
}), }),
); );

View File

@ -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 {

View File

@ -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>
); );

View File

@ -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);

View File

@ -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 (
<div className="dice-set"> <React.Fragment>
{dice.map((die) => ( <div className="dice-set-actions">
<Die die={die} handleDieClick={handleDieClick} /> <img
))} src="./rotate-icon.png"
</div> onClick={() => handleRotateButton(-90)}
></img>
<img
src="./rotate-icon.png"
className="icon-inverted"
onClick={() => handleRotateButton(90)}
></img>
</div>
<div className="dice-set">
{dice.map((die) => (
<Die die={die} handleDieClick={handleDieClick} />
))}
</div>
</React.Fragment>
); );
}; };

View File

@ -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);
} }

View File

@ -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>
); );

View File

@ -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;
}[] }[]
> >
>; >;