diff --git a/package-lock.json b/package-lock.json index 9331559..bbd963b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "beautiful-react-hooks": "^3.12.2", "classnames": "^2.3.2", "cross-env": "^7.0.3", + "cyber-dice": "github:blopa/cyber-dice", "is-mobile": "^3.1.1", "jest": "^27.5.1", "phaser": "^3.55.2", @@ -9422,6 +9423,15 @@ "node": ">=0.10.0" } }, + "node_modules/cyber-dice": { + "version": "1.0.10", + "resolved": "git+ssh://git@github.com/blopa/cyber-dice.git#8809ffc4346c3b9a818a7696b3616dd833e810e0", + "license": "MIT", + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -35739,6 +35749,11 @@ "array-find-index": "^1.0.1" } }, + "cyber-dice": { + "version": "git+ssh://git@github.com/blopa/cyber-dice.git#8809ffc4346c3b9a818a7696b3616dd833e810e0", + "from": "cyber-dice@github:blopa/cyber-dice", + "requires": {} + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/package.json b/package.json index 39f76ec..38ebe42 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "beautiful-react-hooks": "^3.12.2", "classnames": "^2.3.2", "cross-env": "^7.0.3", + "cyber-dice": "github:blopa/cyber-dice", "is-mobile": "^3.1.1", "jest": "^27.5.1", "phaser": "^3.55.2", diff --git a/src/components/Battle/Battle.jsx b/src/components/Battle/Battle.jsx new file mode 100644 index 0000000..ba339d6 --- /dev/null +++ b/src/components/Battle/Battle.jsx @@ -0,0 +1,140 @@ +import { useEffect, useRef, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; + +// Selectors +import { + selectBattleItems, + selectBattleOnHover, + selectBattleSetters, + selectBattleOnSelect, +} from '../../zustand/battle/selectBattle'; + +// Constants +import { + ENTER_KEY, + ARROW_UP_KEY, + ARROW_DOWN_KEY, + ARROW_LEFT_KEY, + ARROW_RIGHT_KEY, +} from '../../constants'; + +// Utils +import { getTranslationVariables } from '../../utils/utils'; + +// Store +import { useGameStore } from '../../zustand/store'; + +// Styles +import styles from './Battle.module.scss'; + +// Components +import BattleDiceViewer from '../BattleDiceViewer/BattleDiceViewer'; + +function Battle() { + const battleListRef = useRef(); + + // TODO for now only works for four items + const battleItems = useGameStore(selectBattleItems); + + const [selectedItemIndex, setSelectedItemIndex] = useState(0); + const onSelected = useGameStore(selectBattleOnSelect); + const onHover = useGameStore(selectBattleOnHover); + const { setBattleItemsListDom } = useGameStore(selectBattleSetters); + + useEffect(() => { + setBattleItemsListDom(battleListRef.current); + }, [battleListRef, setBattleItemsListDom]); + + useEffect(() => { + onHover?.(selectedItemIndex); + }, [onHover, selectedItemIndex]); + + useEffect(() => { + const handleKeyPressed = (e) => { + switch (e.code) { + case ENTER_KEY: { + onSelected(battleItems[selectedItemIndex], selectedItemIndex); + break; + } + + case ARROW_UP_KEY: { + const increment = selectedItemIndex === 1 ? 1 : -2; + setSelectedItemIndex( + Math.max(0, selectedItemIndex + increment) + ); + + break; + } + + case ARROW_DOWN_KEY: { + const increment = selectedItemIndex === battleItems.length / 2 ? -1 : 2; + setSelectedItemIndex( + Math.min(3, selectedItemIndex + increment) + ); + + break; + } + + case ARROW_LEFT_KEY: { + setSelectedItemIndex( + Math.max(0, selectedItemIndex - 1) + ); + + break; + } + + case ARROW_RIGHT_KEY: { + setSelectedItemIndex( + Math.min(3, selectedItemIndex + 1) + ); + + break; + } + + default: { + break; + } + } + }; + + window.addEventListener('keydown', handleKeyPressed); + + return () => window.removeEventListener('keydown', handleKeyPressed); + }, [battleItems, onSelected, selectedItemIndex]); + + return ( +
+ + +
+ ); +} + +export default Battle; diff --git a/src/components/Battle/Battle.module.scss b/src/components/Battle/Battle.module.scss new file mode 100644 index 0000000..7c0e29e --- /dev/null +++ b/src/components/Battle/Battle.module.scss @@ -0,0 +1,44 @@ +.battle-wrapper { + height: 100%; + + &.paused { + background-color: rgb(0 0 0 / 70%); + } +} + +.battle-items-wrapper { + user-select: none; + user-drag: none; + position: absolute; + font-size: calc(var(--game-zoom) * 10px); + list-style: none; + image-rendering: pixelated; + font-family: "Press Start 2P", serif; + text-transform: uppercase; + display: flex; + flex-wrap: wrap; + margin: 0; + padding: 0; + bottom: 0; + background-color: #83a37d; + border: calc(var(--game-zoom) * 1px) solid #53814b; + outline-offset: calc(var(--game-zoom) * -1px); + justify-content: center; +} + +.battle-item { + cursor: pointer; + padding: 3% 0; + margin: 0.22%; + text-align: center; + border: calc(var(--game-zoom) * 1px) solid #000000; + min-width: calc(((var(--game-width) * var(--game-zoom) / 2) - (var(--game-zoom) * 2) - (var(--game-width) * var(--game-zoom) * 0.008)) * 1px); + + &.fewer-items { + width: calc(((var(--game-width) * var(--game-zoom)) - (var(--game-zom) * 2)) * 1px); + } + + &.selected-battle-item { + background-color: #53814b; + } +} diff --git a/src/components/BattleDice/BattleDice.jsx b/src/components/BattleDice/BattleDice.jsx new file mode 100644 index 0000000..6098962 --- /dev/null +++ b/src/components/BattleDice/BattleDice.jsx @@ -0,0 +1,33 @@ +import { useMemo, useState } from 'react'; +import { DiceWithAnimation } from 'cyber-dice'; + +// Store +import { useGameStore } from '../../zustand/store'; + +// Selectors +import { selectGameZoom } from '../../zustand/game/selectGameData'; + +function BattleDice() { + const gameZoom = useGameStore(selectGameZoom); + + const [shouldAnimateDice, setShouldAnimateDice] = useState(true); + const [randomNumber, setRandomNumber] = useState(Math.floor(Math.random() * 6) + 1); + const diceSize = useMemo(() => 80 * gameZoom, [gameZoom]); + + const animationEndHandler = () => { + setShouldAnimateDice(false); + console.log(randomNumber); + }; + + return ( + + ); +} + +export default BattleDice; diff --git a/src/components/BattleDice/BattleDice.module.scss b/src/components/BattleDice/BattleDice.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/BattleDiceViewer/BattleDiceViewer.jsx b/src/components/BattleDiceViewer/BattleDiceViewer.jsx new file mode 100644 index 0000000..2c98e5b --- /dev/null +++ b/src/components/BattleDiceViewer/BattleDiceViewer.jsx @@ -0,0 +1,80 @@ +import { useMemo } from 'react'; +import { Dice } from 'cyber-dice'; + +// Styles +import styles from './BattleDiceViewer.module.scss'; + +// Store +import { useGameStore } from '../../zustand/store'; + +// Selectors +import { selectGameHeight, selectGameZoom } from '../../zustand/game/selectGameData'; +import { selectHeroEquipedInventoryDice } from '../../zustand/hero/selectHeroData'; +import { selectBattleHoveredItem, selectBattleItemsListDOM } from '../../zustand/battle/selectBattle'; + +function BattleDiceViewer() { + const equipedDice = useGameStore(selectHeroEquipedInventoryDice); + const itemsListDOM = useGameStore(selectBattleItemsListDOM); + const hoveredItem = useGameStore(selectBattleHoveredItem); + const gameHeight = useGameStore(selectGameHeight); + const gameZoom = useGameStore(selectGameZoom); + const availableScreenHeight = useMemo( + // eslint-disable-next-line no-unsafe-optional-chaining + () => gameHeight - (itemsListDOM?.offsetHeight || 0) / gameZoom, + [gameZoom, gameHeight, itemsListDOM?.offsetHeight] + ); + + const baseDiceSize = useMemo( + () => Math.min((availableScreenHeight / 5), 40), [availableScreenHeight] + ); + const diceSize = useMemo(() => baseDiceSize * gameZoom, [gameZoom, baseDiceSize]); + const diceMargin = useMemo(() => (diceSize * 1.25), [diceSize]); + + if (equipedDice.length === 0 || hoveredItem === null || hoveredItem === 3) { + return null; + } + + const { faces: diceFaces } = equipedDice[hoveredItem]; + return ( +
+ +
+ ); +} + +export default BattleDiceViewer; diff --git a/src/components/BattleDiceViewer/BattleDiceViewer.module.scss b/src/components/BattleDiceViewer/BattleDiceViewer.module.scss new file mode 100644 index 0000000..1647b42 --- /dev/null +++ b/src/components/BattleDiceViewer/BattleDiceViewer.module.scss @@ -0,0 +1,18 @@ +.dice-faces-list-wrapper { + list-style: none; + margin: 0; + padding: 0; + padding-inline-start: 0; + position: absolute; + left: 55%; + transform: translateX(-25%); + width: 100%; +} + +.dice-faces-wrapper { + padding: 0; +} + +.face { + position: absolute; +} \ No newline at end of file diff --git a/src/components/ReactWrapper.jsx b/src/components/ReactWrapper.jsx index afa6d91..77de9b6 100644 --- a/src/components/ReactWrapper.jsx +++ b/src/components/ReactWrapper.jsx @@ -14,10 +14,12 @@ import useMutationObserver from '../hooks/useMutationObserver'; import DialogBox from './DialogBox/DialogBox'; import GameMenu from './GameMenu/GameMenu'; import GameText from './GameText/GameText'; +import Battle from './Battle/Battle'; // Selectors import { selectGameCanvasElement } from '../zustand/game/selectGameData'; import { selectDialogMessages } from '../zustand/dialog/selectDialog'; +import { selectBattleItems } from '../zustand/battle/selectBattle'; import { selectMenuItems } from '../zustand/menu/selectMenu'; import { selectTexts } from '../zustand/text/selectText'; @@ -25,6 +27,7 @@ function ReactWrapper() { const canvas = useGameStore(selectGameCanvasElement); const dialogMessages = useGameStore(selectDialogMessages); const menuItems = useGameStore(selectMenuItems); + const battleItems = useGameStore(selectBattleItems); const gameTexts = useGameStore(selectTexts); // const s = useGameStore((store) => store); // console.log(s); @@ -76,6 +79,9 @@ function ReactWrapper() { style={inlineStyles} // onClick={handleWrapperClicked} > + {battleItems.length > 0 && ( + + )} 0} /> {menuItems.length > 0 && ( diff --git a/src/constants.js b/src/constants.js index 90411dc..ff71a5e 100644 --- a/src/constants.js +++ b/src/constants.js @@ -43,6 +43,24 @@ export const ARROW_UP_KEY = 'ArrowUp'; export const ARROW_RIGHT_KEY = 'ArrowRight'; export const ARROW_DOWN_KEY = 'ArrowDown'; +// Battle +// export const MELEE_ITEM_INDEX = 0; +// export const MAGIC_ITEM_INDEX = 1; +// export const DEFEND_ITEM_INDEX = 2; +// export const RUN_ITEM_INDEX = 3; +export const ROCK_BATTLE_ITEM = 'rock'; +export const PAPER_BATTLE_ITEM = 'paper'; +export const SCISSORS_BATTLE_ITEM = 'scissors'; + +export const ATTACK_BATTLE_ITEM = 'attack'; +export const ITEMS_BATTLE_ITEM = 'items'; +export const DEFENSE_BATTLE_ITEM = 'defense'; +export const RUN_BATTLE_ITEM = 'run'; +export const DICE_1_BATTLE_ITEM = 'dice_1'; +export const DICE_2_BATTLE_ITEM = 'dice_2'; +export const DICE_3_BATTLE_ITEM = 'dice_3'; +export const RETURN_BATTLE_ITEM = 'return'; + // DOM identifiers export const GAME_CONTENT_ID = 'game-content'; diff --git a/src/game/scenes/BattleScene.js b/src/game/scenes/BattleScene.js new file mode 100644 index 0000000..7ec3f8e --- /dev/null +++ b/src/game/scenes/BattleScene.js @@ -0,0 +1,41 @@ +// Utils +import { getSelectorData } from '../../utils/utils'; + +// Selectors +import { selectGameSetters, selectGameWidth } from '../../zustand/game/selectGameData'; +import { selectBattleEnemies } from '../../zustand/battle/selectBattle'; + +export const scene = {}; + +export const key = 'BattleScene'; + +export function create() { + const { addGameCameraSizeUpdateCallback } = getSelectorData(selectGameSetters); + const backgroundImage = scene.add.image(0, 0, 'background_grass').setOrigin(0, 0); + const gameWidth = getSelectorData(selectGameWidth); + backgroundImage.setScale(gameWidth / backgroundImage.width); + + addGameCameraSizeUpdateCallback(() => { + const gameWidth = getSelectorData(selectGameWidth); + backgroundImage.setScale(gameWidth / backgroundImage.width); + }); + + const enemies = getSelectorData(selectBattleEnemies); + enemies.forEach(({ sprite, position }) => { + // TODO do this https://medium.com/@junhongwang/sprite-outline-with-phaser-3-9c17190b04bc + // const outline = scene.add.image(position.x, position.y, sprite) + // .setScale(3.5) + // .setTintFill(0x85F9DC) + // .setVisible(false); + const enemy = scene.add.image(position.x, position.y, sprite).setScale(3); + // enemy.outline = outline; + // enemy.setInteractive(); + // enemy.on('pointerover', () => { + // enemy.outline.setVisible(true); + // }); + // enemy.on('pointerout', () => { + // enemy.outline.setVisible(false); + // }); + enemies.push(enemy); + }); +} diff --git a/src/utils/sceneHelpers.js b/src/utils/sceneHelpers.js index 26c8b0e..d33b478 100644 --- a/src/utils/sceneHelpers.js +++ b/src/utils/sceneHelpers.js @@ -16,11 +16,22 @@ import { DOWN_DIRECTION, RIGHT_DIRECTION, KEY_SPRITE_NAME, + RUN_BATTLE_ITEM, HERO_SPRITE_NAME, COIN_SPRITE_NAME, + ROCK_BATTLE_ITEM, ENEMY_SPRITE_NAME, + PAPER_BATTLE_ITEM, + ITEMS_BATTLE_ITEM, HEART_SPRITE_NAME, + RETURN_BATTLE_ITEM, + DICE_1_BATTLE_ITEM, + DICE_2_BATTLE_ITEM, + DICE_3_BATTLE_ITEM, + ATTACK_BATTLE_ITEM, + DEFENSE_BATTLE_ITEM, CRYSTAL_SPRITE_NAME, + SCISSORS_BATTLE_ITEM, IDLE_FRAME_POSITION_KEY, } from '../constants'; @@ -33,6 +44,7 @@ import { } from './utils'; // Selectors +import { selectBattleSetters } from '../zustand/battle/selectBattle'; import { selectDialogMessages, selectDialogSetters } from '../zustand/dialog/selectDialog'; import { selectMapKey, selectTilesets, selectMapSetters } from '../zustand/map/selectMapData'; import { @@ -303,6 +315,122 @@ export const handleObjectsLayer = (scene) => { }]); }); + enemy.on('pointerdown', () => { + scene.scene.moveBelow('GameScene', 'BattleScene'); + scene.scene.pause('GameScene'); + scene.scene.launch('BattleScene'); + + const { + setBattleItems, + setBattleEnemies, + setBattleOnHover, + setBattleOnSelect, + setBattleHoveredItem, + } = getSelectorData(selectBattleSetters); + + const { + addHeroInventoryDice, + } = getSelectorData(selectHeroSetters); + + setBattleItems([ + ATTACK_BATTLE_ITEM, + ITEMS_BATTLE_ITEM, + DEFENSE_BATTLE_ITEM, + RUN_BATTLE_ITEM, + ]); + + setBattleEnemies([ + { + sprite: 'enemy_01', + position: { x: 200, y: 140 }, + types: [ROCK_BATTLE_ITEM], + health: 100, + attack: 10, + }, + { + sprite: 'enemy_02', + position: { x: 300, y: 140 }, + types: [PAPER_BATTLE_ITEM], + health: 100, + attack: 10, + }, + { + sprite: 'enemy_03', + position: { x: 400, y: 160 }, + types: [SCISSORS_BATTLE_ITEM], + health: 100, + attack: 10, + }, + ]); + + setBattleOnSelect((item, itemIndex) => { + switch (item) { + case ATTACK_BATTLE_ITEM: { + const items = [ + DICE_1_BATTLE_ITEM, + DICE_2_BATTLE_ITEM, + DICE_3_BATTLE_ITEM, + RETURN_BATTLE_ITEM, + ]; + + setBattleItems(items); + setBattleOnHover((itemIndex) => { + setBattleHoveredItem(itemIndex); + }); + + [ + { + equiped: true, + faces: [1, 2, 3, 4, 5, 6], + }, + { + equiped: true, + faces: [2, 2, 2, 2, 2, 2], + }, + { + equiped: true, + faces: [3, 3, 3, 3, 3, 3], + }, + ].forEach((dice) => { + addHeroInventoryDice(dice); + }); + + setBattleOnSelect((item, itemIndex) => { + switch (item) { + case DICE_1_BATTLE_ITEM: { + break; + } + case DICE_2_BATTLE_ITEM: { + break; + } + case DICE_3_BATTLE_ITEM: { + break; + } + case RETURN_BATTLE_ITEM: + default: { + break; + } + } + }); + + break; + } + case ITEMS_BATTLE_ITEM: { + break; + } + case DEFENSE_BATTLE_ITEM: { + break; + } + case RUN_BATTLE_ITEM: + default: { + break; + } + } + + // setBattleItems([]); + }); + }); + const enemyActionHeroCollider = scene.physics.add.overlap( enemy, scene.heroSprite.actionCollider, diff --git a/src/zustand/battle/selectBattle.js b/src/zustand/battle/selectBattle.js new file mode 100644 index 0000000..91c6e34 --- /dev/null +++ b/src/zustand/battle/selectBattle.js @@ -0,0 +1,23 @@ +export const selectBattleItems = (state) => state.battle.items; + +export const selectBattleEnemies = (state) => state.battle.enemies; + +export const selectBattleSkills = (state) => state.battle.skills; + +export const selectBattleOnSelect = (state) => state.battle.onSelect; + +export const selectBattleOnHover = (state) => state.battle.onHover; + +export const selectBattleItemsListDOM = (state) => state.battle.itemsListDOM; + +export const selectBattlePickedItem = (state) => state.battle.pickedItem; + +export const selectBattleHoveredItem = (state) => state.battle.hoveredItem; + +export const selectBattleEnemiesPickedItem = (state) => state.battle.enemiesPickedItem; + +export const selectBattleAttackDice = (state) => state.battle.attackDice; + +export const selectBattleDefenseDice = (state) => state.battle.defenseDice; + +export const selectBattleSetters = (state) => state.battle.setters; diff --git a/src/zustand/battle/setBattle.js b/src/zustand/battle/setBattle.js new file mode 100644 index 0000000..646f3e4 --- /dev/null +++ b/src/zustand/battle/setBattle.js @@ -0,0 +1,74 @@ +export default (set) => ({ + setBattleItems: (items) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + items, + }, + })), + setBattlePickedItem: (pickedItem) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + pickedItem, + }, + })), + setBattleHoveredItem: (hoveredItem) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + hoveredItem, + }, + })), + setBattleEnemiesPickedItem: (enemiesPickedItem) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + enemiesPickedItem, + }, + })), + setBattleEnemies: (enemies) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + enemies, + }, + })), + setBattleSkills: (skills) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + skills, + }, + })), + setBattleOnSelect: (onSelect) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + onSelect, + }, + })), + setBattleOnHover: (onHover) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + onHover, + }, + })), + setBattleItemsListDom: (itemsListDOM) => + set((state) => ({ + ...state, + battle: { + ...state.battle, + itemsListDOM, + }, + })), +}); diff --git a/src/zustand/store.js b/src/zustand/store.js index 81b4d2c..b30d549 100644 --- a/src/zustand/store.js +++ b/src/zustand/store.js @@ -8,6 +8,7 @@ import setLoadedAssets from './assets/setLoadedAssets'; import setGameData from './game/setGameData'; import setHeroData from './hero/setHeroData'; import setDialog from './dialog/setDialog'; +import setBattle from './battle/setBattle'; import setMapData from './map/setMapData'; import setMenu from './menu/setMenu'; import setText from './text/setText'; @@ -51,6 +52,19 @@ const store = createStore((set) => ({ characterName: '', setters: setDialog(set), }, + battle: { + items: [], + enemies: [], + skills: [], + onSelect: null, + onHover: null, + pickedItem: null, + hoveredItem: null, + enemiesPickedItem: null, + attackDice: [], + defenseDice: [], + setters: setBattle(set), + }, menu: { items: [], position: 'center',