diff --git a/src/app/Trade.ts b/src/app/Trade.ts index 3a4551a..0a38773 100644 --- a/src/app/Trade.ts +++ b/src/app/Trade.ts @@ -4,8 +4,13 @@ import { multiply } from 'lodash'; import { Player } from '../Player'; import { Herd } from './logic/Herd'; +export type Offer = [AnimalNames, number]; + export class Trade { constructor(private bank: Player) {} + get thisBank(): Player { + return this.bank; + } /** * Gets an offer from player and returns true or false if transaction can be made processed and process it if possible * @param offer accepts tuple with offer containing animal name and quantity to be sold @@ -14,13 +19,17 @@ export class Trade { * @returns true if transaction will be processed, and false otherwise */ processOffer( - offer: [AnimalNames, number], + offer: Offer, { theHerd: playerHerd }: Player, - target: [AnimalNames, number], + target: Offer, ): boolean { + const [offeredAnimal, offeredAnimalCount] = offer; + const [targetAnimal, targetAnimalCount] = target; if ( - playerHerd.getAnimalNumber(offer[0]) < offer[1] || - this.bank.theHerd.getAnimalNumber(target[0]) < target[1] + playerHerd.getAnimalNumber(offeredAnimal) < + offeredAnimalCount || + this.bank.theHerd.getAnimalNumber(targetAnimal) < + targetAnimalCount ) { return false; } @@ -35,17 +44,14 @@ export class Trade { : this.disposeResult(offer, playerHerd, target); } - private calculateValue(offer: [AnimalNames, number]): number { + private calculateValue([animal, count]: Offer): number { return multiply( - ConvertToAnimalObject.convertToAnimalObject(offer[0]).theValue, - offer[1], + ConvertToAnimalObject.convertToAnimalObject(animal).theValue, + count, ); } - private adjustOffer( - offer: [AnimalNames, number], - target: [AnimalNames, number], - ): void { + private adjustOffer(offer: Offer, target: Offer): void { offer[1] -= 1; if (this.calculateValue(offer) <= this.calculateValue(target)) { return; @@ -57,9 +63,9 @@ export class Trade { * updates players and banks herd */ private disposeResult( - [animalSold, quantitySold]: [AnimalNames, number], + [animalSold, quantitySold]: Offer, playerHerd: Herd, - [animalBought, quantityBought]: [AnimalNames, number], + [animalBought, quantityBought]: Offer, ): boolean { playerHerd.addAnimalsToHerd(animalSold, -quantitySold); playerHerd.addAnimalsToHerd(animalBought, quantityBought); diff --git a/src/app/components/EmptyModal.ts b/src/app/components/EmptyModal.ts new file mode 100644 index 0000000..df6de9a --- /dev/null +++ b/src/app/components/EmptyModal.ts @@ -0,0 +1,31 @@ +import { Render } from '../utils/Render'; +import { pull } from 'lodash'; + +export abstract class EmptyModal { + modal: HTMLElement; + modalContainer: HTMLElement; + constructor() { + this.modalContainer = Render.elementFactory('div', { + className: 'modal__container', + }); + this.modal = Render.elementFactory( + 'div', + { className: 'modal' }, + this.modalContainer, + ); + } + + /** + * Hides modal - adds class with display:none + */ + protected hideModal(): void { + this.modal.classList.add('modal--hidden'); + } + + /** + * Shows modal - removes class with display:none + */ + protected showModal(): void { + this.modal.classList.remove('modal--hidden'); + } +} diff --git a/src/app/components/ModalBasic.ts b/src/app/components/ModalBasic.ts index ce3060d..0fc28c1 100644 --- a/src/app/components/ModalBasic.ts +++ b/src/app/components/ModalBasic.ts @@ -1,20 +1,8 @@ import { Render } from '../utils/Render'; import { Button } from './Button'; +import { EmptyModal } from './EmptyModal'; -export class ModalBasic { - modal: HTMLElement; - modalContainer: HTMLElement; - constructor() { - this.modalContainer = Render.elementFactory('div', { - className: 'modal__container', - }); - this.modal = Render.elementFactory( - 'div', - { className: 'modal' }, - this.modalContainer, - ); - } - +export class ModalBasic extends EmptyModal { /** * Creates and appends the main modal structure. * @param {string} heading Heading to be render inside the modal. diff --git a/src/app/components/TradeModal.ts b/src/app/components/TradeModal.ts new file mode 100644 index 0000000..432844d --- /dev/null +++ b/src/app/components/TradeModal.ts @@ -0,0 +1,220 @@ +import { AnimalNames } from '~src/Enums/AnimalNamesEnum'; +import { Player } from '~src/Player'; +import { Herd } from '../logic/Herd'; +import { Offer, Trade } from '../Trade'; +import { Render } from '../utils/Render'; +import { EmptyModal } from './EmptyModal'; + +export class TradeModal extends EmptyModal { + tradeForm: HTMLElement; + playerView: HTMLElement; + bankView: HTMLElement; + warning: HTMLElement; + backButton: HTMLElement; + player: Player; + + constructor(private trade: Trade, firstPlayer: Player) { + super(); + this.player = firstPlayer; + this.playerView = Render.elementFactory('div', { + className: 'trade__player-wrapper', + }); + this.bankView = Render.elementFactory('div', { + className: 'trade__player-wrapper', + }); + this.tradeForm = Render.elementFactory( + 'form', + { + action: '', + method: 'post', + className: 'trade', + }, + this.playerView, + Render.elementFactory('input', { + type: 'submit', + value: 'Trade', + className: 'trade__submit', + }), + this.bankView, + ); + this.warning = Render.elementFactory('p', { + className: 'warning', + }); + this.backButton = Render.elementFactory('button', {}, 'back'); + Render.childrenInjector( + this.modalContainer, + this.tradeForm, + this.warning, + this.backButton, + ); + } + + /** + * Creates TradeModal and returns it as HTMLElement. + */ + createModal(): HTMLElement { + Render.childrenInjector( + this.bankView, + this.createHerdView(this.trade.thisBank, true), + ); + Render.childrenInjector( + this.playerView, + this.createHerdView(this.player), + ); + this.tradeForm.addEventListener('submit', this.handleSubmit); + this.modal.addEventListener('keydown', this.clearWarning); + this.modal.addEventListener('click', this.clearWarning); + this.backButton.addEventListener('click', () => this.hideModal()); + return this.modal; + } + + /** + * Sets next player and his herd in the TradeModal. + */ + setNextPlayer(player: Player): void { + this.player = player; + Render.removeAllChildren(this.playerView); + this.playerView.appendChild(this.createHerdView(this.player)); + this.updateBank(); + this.showModal(); + } + + private updateBank(): void { + Render.removeAllChildren(this.bankView); + this.bankView.appendChild( + this.createHerdView(this.trade.thisBank, true), + ); + } + + private createHerdView( + { theHerd: herd, theName: name }: Player, + isBank = false, + ): HTMLElement { + const herdElement = Render.elementFactory( + 'div', + { + className: 'trade__player-herd', + }, + ...this.generateAnimalRows(herd, isBank), + ); + const container = Render.elementFactory( + 'div', + { + className: 'trade__player', + }, + Render.elementFactory( + 'H2', + { className: 'trade__player-heading' }, + `${name}`, + ), + herdElement, + ); + return container; + } + + private generateAnimalRows( + herd: Herd, + isBank: boolean, + ): HTMLElement[] { + const animalsRows: HTMLElement[] = herd.theAnimals.reduce( + (animalsElements: HTMLElement[], [animal, count]) => { + if (count > 0) { + animalsElements.push( + Render.elementFactory( + 'div', + { className: 'trade__row' }, + Render.elementFactory( + 'label', + { + for: `${isBank ? 'bank' : 'player'}_${ + animal.theName + }`, + }, + `${animal.theName}: ${count}`, + ), + Render.elementFactory('input', { + type: 'number', + id: `${isBank ? 'bank' : 'player'}_${animal.theName}`, + name: `${isBank ? 'bank' : 'player'}_${ + animal.theName + }`, + min: '0', + max: `${count}`, + }), + ), + ); + } + return animalsElements; + }, + [], + ); + + return animalsRows; + } + + private formDataIntoTuples(formData: FormData): [Offer[], Offer[]] { + const offer: Offer[] = []; + const target: Offer[] = []; + for (const [key, value] of formData.entries()) { + const numberOfAnimals = parseInt(value.toString()); + const [player, animal] = key.split('_'); + if (numberOfAnimals > 0) { + switch (player) { + case 'player': { + offer.push([animal as AnimalNames, numberOfAnimals]); + break; + } + case 'bank': { + target.push([animal as AnimalNames, numberOfAnimals]); + break; + } + } + } + } + return [offer, target]; + } + + private processTrade([offer, target]: [Offer[], Offer[]]): boolean { + if (offer.length === 1 && target.length === 1) { + const [[offeredAnimal]] = offer; + const [[targetAnimal]] = target; + if (this.trade.processOffer(offer[0], this.player, target[0])) { + return true; + } + this.displayWarning( + `The value ratio of the ${offeredAnimal}s to ${targetAnimal}s is not correct`, + ); + return false; + } + if (offer.length > 1 || target.length > 1) { + this.displayWarning( + 'To much types of animals, allowed one type for one type', + ); + return false; + } + if (offer.length <= 0 || target.length <= 0) { + this.displayWarning( + 'There need to be at least one animal on both sides', + ); + return false; + } + return false; + } + + private displayWarning(message: string): void { + this.warning.textContent = message; + } + + private handleSubmit = (event: Event): void => { + event.preventDefault(); + const formData = new FormData(event.target as HTMLFormElement); + const data = this.formDataIntoTuples(formData); + if (this.processTrade(data)) { + this.hideModal(); + } + }; + + private clearWarning = (): void => { + this.warning.textContent = ''; + }; +} diff --git a/src/app/manuals/TradeModalDemo.ts b/src/app/manuals/TradeModalDemo.ts new file mode 100644 index 0000000..b5dc6dc --- /dev/null +++ b/src/app/manuals/TradeModalDemo.ts @@ -0,0 +1,49 @@ +import { AnimalNames } from '~src/Enums/AnimalNamesEnum'; +import { Player } from '~src/Player'; +import { TradeModal } from '../components/TradeModal'; +import { Bank } from '../logic/Bank'; +import { Trade } from '../Trade'; +import { Render } from '../utils/Render'; + +//type TradeModalDemo.playDemo() in init in App.ts +export class TradeModalDemo { + static playDemo(): void { + const trade = new Trade(new Bank()); + const player = new Player( + 'Donald', + './static/images/avatars/dog.png', + ); + player.theHerd.addAnimalsToHerd(AnimalNames.RABBIT, 20); + player.theHerd.addAnimalsToHerd(AnimalNames.SHEEP, 5); + player.theHerd.addAnimalsToHerd(AnimalNames.PIG, 5); + player.theHerd.addAnimalsToHerd(AnimalNames.COW, 2); + player.theHerd.addAnimalsToHerd(AnimalNames.HORSE, 1); + const modal = new TradeModal(trade, player); + Render.render('#sf-app', modal.createModal()); + + const player2 = new Player( + 'Gerwazy', + './static/images/avatars/dog.png', + ); + player2.theHerd.addAnimalsToHerd(AnimalNames.RABBIT, 6); + player2.theHerd.addAnimalsToHerd(AnimalNames.SHEEP, 1); + player2.theHerd.addAnimalsToHerd(AnimalNames.PIG, 2); + player2.theHerd.addAnimalsToHerd(AnimalNames.COW, 1); + player2.theHerd.addAnimalsToHerd(AnimalNames.HORSE, 1); + + const player3 = new Player( + 'Eustachy', + './static/images/avatars/dog.png', + ); + player3.theHerd.addAnimalsToHerd(AnimalNames.RABBIT, 8); + player3.theHerd.addAnimalsToHerd(AnimalNames.SHEEP, 3); + player3.theHerd.addAnimalsToHerd(AnimalNames.PIG, 1); + player3.theHerd.addAnimalsToHerd(AnimalNames.COW, 2); + player3.theHerd.addAnimalsToHerd(AnimalNames.HORSE, 0); + + setTimeout(() => { + modal.setNextPlayer(player2); + setTimeout(() => modal.setNextPlayer(player3), 15000); + }, 15000); + } +} diff --git a/styles/components/_components.scss b/styles/components/_components.scss index 603d81c..83e5bcb 100644 --- a/styles/components/_components.scss +++ b/styles/components/_components.scss @@ -3,4 +3,5 @@ @import './components/button'; @import './components/modal'; @import './menu'; +@import './tradeModal'; @import './playerPanel'; diff --git a/styles/components/_modal.scss b/styles/components/_modal.scss index 9a7ab06..c817f93 100644 --- a/styles/components/_modal.scss +++ b/styles/components/_modal.scss @@ -39,4 +39,8 @@ } &__button { } + + &--hidden { + display: none; + } } diff --git a/styles/components/_tradeModal.scss b/styles/components/_tradeModal.scss new file mode 100644 index 0000000..a127deb --- /dev/null +++ b/styles/components/_tradeModal.scss @@ -0,0 +1,29 @@ +.trade { + display: grid; + height: 100%; + grid-template-columns: 2fr 1fr 2fr; + + &__player-herd { + display: flex; + flex-direction: column; + row-gap: 1em; + } + + &__row { + display: grid; + } + + &__player-heading { + padding-bottom: 1em; + text-transform: uppercase; + } + + &__submit { + height: 4em; + } +} + +.warning { + background-color: yellow; + font-size: 26px; +} diff --git a/tsconfig.json b/tsconfig.json index 0801227..3a6dde5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "lib": [ "es6", + "dom.iterable", "DOM" ] /* Specify library files to be included in the compilation. */, "allowJs": true /* Allow javascript files to be compiled. */,