diff --git a/interface/constants/Direction.ts b/interface/constants/Direction.ts index 6bc4347..d44deda 100644 --- a/interface/constants/Direction.ts +++ b/interface/constants/Direction.ts @@ -11,3 +11,20 @@ export const directions: Direction[] = [ Direction.EAST, Direction.WEST, ]; + +export function rotateDirection( + initialDirection: Direction, + rotationAngle: 0 | 90 | 180 | 270, +): Direction { + const angleToDirectionMap: Record = { + [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, + )!; +} diff --git a/interface/constants/Pieces.ts b/interface/constants/Pieces.ts index e71f635..e4bddc3 100644 --- a/interface/constants/Pieces.ts +++ b/interface/constants/Pieces.ts @@ -13,7 +13,6 @@ const STRAIGHT_RAIL: Piece = new Piece({ }, ], }); - const TURN_RAIL: Piece = new Piece({ useInternalTracks: false, trackDefinitions: [ @@ -166,7 +165,7 @@ const DEAD_END_STATION_ROAD: Piece = new Piece({ trackDefinitions: [ { 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({ useInternalTracks: true, internalNodeType: InternalNodeType.STATION, @@ -316,7 +314,7 @@ const FOUR_WAY_CROSS_ROAD: Piece = new Piece({ }, { startPoint: Direction.WEST, - type: TrackType.RAIL, + type: TrackType.ROAD, }, ], }); diff --git a/interface/types/Cell.ts b/interface/types/Cell.ts index 9bac0ba..6412cc5 100644 --- a/interface/types/Cell.ts +++ b/interface/types/Cell.ts @@ -1,8 +1,7 @@ import { CellType } from "../constants/CellType"; -import { Direction, directions } from "../constants/Direction"; +import { Direction } from "../constants/Direction"; import { ExitType } from "../constants/ExitType"; import { PieceId, pieceMap } from "../constants/Pieces"; -import { TrackType } from "../constants/TrackType"; import { Exit } from "./Exit"; import { ExternalNode } from "./ExternalNode"; import { InternalNode } from "./InternalNode"; @@ -33,9 +32,12 @@ export class Cell { 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; - const piece: Piece = pieceMap[pieceId]; + const piece: Piece = pieceMap[pieceId].rotate(rotation); this.validatePiecePlacement(piece); diff --git a/interface/types/Piece.ts b/interface/types/Piece.ts index f684cf4..953c04d 100644 --- a/interface/types/Piece.ts +++ b/interface/types/Piece.ts @@ -1,4 +1,4 @@ -import { Direction } from "../constants/Direction"; +import { Direction, rotateDirection } from "../constants/Direction"; import { TrackType } from "../constants/TrackType"; import { Cell } from "./Cell"; 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) { return new PlacedPiece( new Set( diff --git a/web/public/rotate-icon.png b/web/public/rotate-icon.png new file mode 100644 index 0000000..5af53ca Binary files /dev/null and b/web/public/rotate-icon.png differ diff --git a/web/src/pages/game/GamePage.tsx b/web/src/pages/game/GamePage.tsx index 499bd51..4b06b3d 100644 --- a/web/src/pages/game/GamePage.tsx +++ b/web/src/pages/game/GamePage.tsx @@ -16,6 +16,7 @@ const GamePage = () => { pieceId: getRandomPieceId(), isSelected: false, isDisabled: false, + rotation: 0, }; }), ); diff --git a/web/src/pages/game/components/BoardCell.scss b/web/src/pages/game/components/BoardCell.scss index a7822ed..1250a30 100644 --- a/web/src/pages/game/components/BoardCell.scss +++ b/web/src/pages/game/components/BoardCell.scss @@ -15,6 +15,18 @@ &:nth-child(3) { top: -40px; } + + &.rotate-90 { + transform: rotate(-90deg); + } + + &.rotate-180 { + transform: rotate(180deg); + } + + &.rotate-270 { + transform: rotate(90deg); + } } &.house { diff --git a/web/src/pages/game/components/BoardCell.tsx b/web/src/pages/game/components/BoardCell.tsx index 220e7f6..3170438 100644 --- a/web/src/pages/game/components/BoardCell.tsx +++ b/web/src/pages/game/components/BoardCell.tsx @@ -1,5 +1,6 @@ import { Cell, directions, Exit, PieceId } from "interface"; import "./BoardCell.scss"; +import { useState } from "react"; export interface BoardCellProps { cell: Cell; @@ -7,6 +8,7 @@ export interface BoardCellProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[]; setDice: React.Dispatch< React.SetStateAction< @@ -14,6 +16,7 @@ export interface BoardCellProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[] > >; @@ -22,11 +25,15 @@ export interface BoardCellProps { const BoardCell = (props: BoardCellProps) => { const { cell, dice, setDice, refreshBoardRender } = props; + const [pieceRotationAngle, setPieceRotationAngle] = useState(0); const handleBoardCellClick = () => { const selectedDie = dice.find((die) => die.isSelected); if (!selectedDie) return; try { - cell.placePiece(selectedDie.pieceId); + cell.placePiece( + selectedDie.pieceId, + selectedDie.rotation as 0 | 90 | 180 | 270, + ); } catch (error) { console.log(error); return; @@ -36,12 +43,13 @@ const BoardCell = (props: BoardCellProps) => { return die !== selectedDie ? die : { - pieceId: selectedDie.pieceId, + ...selectedDie, isSelected: false, isDisabled: true, }; }), ); + setPieceRotationAngle(selectedDie.rotation); refreshBoardRender(); }; @@ -64,7 +72,10 @@ const BoardCell = (props: BoardCellProps) => { ); })} {cell.placedPiece && ( - + )} ); diff --git a/web/src/pages/game/components/DiceSet.scss b/web/src/pages/game/components/DiceSet.scss index 1152b08..87776c6 100644 --- a/web/src/pages/game/components/DiceSet.scss +++ b/web/src/pages/game/components/DiceSet.scss @@ -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 { display: grid; grid-template-columns: repeat(2, auto); diff --git a/web/src/pages/game/components/DiceSet.tsx b/web/src/pages/game/components/DiceSet.tsx index 8e10fe0..1779190 100644 --- a/web/src/pages/game/components/DiceSet.tsx +++ b/web/src/pages/game/components/DiceSet.tsx @@ -1,12 +1,14 @@ import { PieceId } from "interface"; import "./DiceSet.scss"; import Die from "./Die"; +import React from "react"; export interface DiceSetProps { dice: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[]; setDice: React.Dispatch< React.SetStateAction< @@ -14,6 +16,7 @@ export interface DiceSetProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[] > >; @@ -24,6 +27,7 @@ const DiceSet = (props: DiceSetProps) => { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }) => { if (die.isDisabled) return; const newDiceState = dice.map((oldDie) => { @@ -36,12 +40,37 @@ const DiceSet = (props: DiceSetProps) => { 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 ( -
- {dice.map((die) => ( - - ))} -
+ +
+ handleRotateButton(-90)} + > + handleRotateButton(90)} + > +
+
+ {dice.map((die) => ( + + ))} +
+
); }; diff --git a/web/src/pages/game/components/Die.scss b/web/src/pages/game/components/Die.scss index d96f7f8..b3f4195 100644 --- a/web/src/pages/game/components/Die.scss +++ b/web/src/pages/game/components/Die.scss @@ -15,4 +15,18 @@ &.disabled { 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); } \ No newline at end of file diff --git a/web/src/pages/game/components/Die.tsx b/web/src/pages/game/components/Die.tsx index f6644b0..ba8474f 100644 --- a/web/src/pages/game/components/Die.tsx +++ b/web/src/pages/game/components/Die.tsx @@ -2,23 +2,31 @@ import { PieceId } from "interface"; import "./Die.scss"; interface DieProps { - die: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean }; + die: { + pieceId: PieceId; + isSelected: boolean; + isDisabled: boolean; + rotation: number; + }; handleDieClick: (die: { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }) => void; } const Die = (props: DieProps) => { 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 ( -
handleDieClick(die)} - > +
handleDieClick(die)}>
); diff --git a/web/src/pages/game/components/GameBoard.tsx b/web/src/pages/game/components/GameBoard.tsx index a713e7d..0997041 100644 --- a/web/src/pages/game/components/GameBoard.tsx +++ b/web/src/pages/game/components/GameBoard.tsx @@ -8,6 +8,7 @@ export interface GameBoardProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[]; setDice: React.Dispatch< React.SetStateAction< @@ -15,6 +16,7 @@ export interface GameBoardProps { pieceId: PieceId; isSelected: boolean; isDisabled: boolean; + rotation: number; }[] > >;