diff --git a/README.md b/README.md index e73b8c8..47e70c7 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ God, am I getting tired of typing that. - `COLLECTIBLE_FIND_PERCENT_BOOST` - the percent boost to collectible find events happening (default: 0) - `RESOURCE_FIND_PERCENT_BOOST` - the percent boost to resource find events happening (default: 0) - `LOCATION_FIND_PERCENT_BOOST` - the percent boost to location find events happening (default: 0) +- `MONSTER_FIND_PERCENT_BOOST` - the percent boost to monster find events happening (default: 0) - `CRAFTING_SPEED_MULTIPLIER` - the multiplier for crafting speed [percent] (default: 100) - `GAMEANALYTICS_KEY` - the game key for GameAnalytics - `GAMEANALYTICS_SECRET` - the secret key for GameAnalytics diff --git a/client/src/app/components/item-elements/item-elements.component.ts b/client/src/app/components/item-elements/item-elements.component.ts index 75e4c38..8c1614b 100644 --- a/client/src/app/components/item-elements/item-elements.component.ts +++ b/client/src/app/components/item-elements/item-elements.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; -import { IEquipment, IItem } from '@interfaces'; +import { Element, IEquipment, IItem } from '@interfaces'; @Component({ selector: 'app-item-elements', @@ -7,7 +7,7 @@ import { IEquipment, IItem } from '@interfaces'; styleUrls: ['./item-elements.component.scss'], }) export class ItemElementsComponent implements OnInit { - public elements: string[] = []; + public elements: Element[] = []; @Input({ required: true }) item!: IItem; diff --git a/client/src/app/components/modals/location-stats/location-stats.component.ts b/client/src/app/components/modals/location-stats/location-stats.component.ts index 2c0a9fa..03231cc 100644 --- a/client/src/app/components/modals/location-stats/location-stats.component.ts +++ b/client/src/app/components/modals/location-stats/location-stats.component.ts @@ -13,6 +13,7 @@ export class LocationStatsModalComponent implements OnInit { public readonly displayStats: LocationStat[] = [ LocationStat.XPGain, LocationStat.CoinGain, + LocationStat.TaxRate, LocationStat.ExploreSpeed, LocationStat.ItemFind, LocationStat.Wave, @@ -20,6 +21,7 @@ export class LocationStatsModalComponent implements OnInit { LocationStat.LocationFind, LocationStat.CollectibleFind, LocationStat.ResourceFind, + LocationStat.MonsterFind, ]; public readonly statNames: Record = { @@ -32,6 +34,7 @@ export class LocationStatsModalComponent implements OnInit { [LocationStat.LocationFind]: 'Location Find Chance', [LocationStat.CollectibleFind]: 'Collectible Find Chance', [LocationStat.ResourceFind]: 'Resource Find Chance', + [LocationStat.MonsterFind]: 'Monster Find Chance', [LocationStat.TaxRate]: 'Tax Rate', }; @@ -48,6 +51,7 @@ export class LocationStatsModalComponent implements OnInit { [LocationStat.LocationFind]: (value) => `${value}%`, [LocationStat.CollectibleFind]: (value) => `${value}%`, [LocationStat.ResourceFind]: (value) => `${value}%`, + [LocationStat.MonsterFind]: (value) => `${value}%`, [LocationStat.TaxRate]: (value) => `${value}%`, }; diff --git a/client/src/app/components/monster-icon/monster-icon.component.html b/client/src/app/components/monster-icon/monster-icon.component.html new file mode 100644 index 0000000..90011ea --- /dev/null +++ b/client/src/app/components/monster-icon/monster-icon.component.html @@ -0,0 +1,6 @@ + diff --git a/client/src/app/components/monster-icon/monster-icon.component.scss b/client/src/app/components/monster-icon/monster-icon.component.scss new file mode 100644 index 0000000..6519e6a --- /dev/null +++ b/client/src/app/components/monster-icon/monster-icon.component.scss @@ -0,0 +1,23 @@ + +:host { + display: inline-block; + + width: 64px; + height: 64px; + max-width: 64px; + max-height: 64px; + + &.xsmall { + width: 16px; + height: 16px; + max-width: 16px; + max-height: 16px; + } + + &.small { + width: 32px; + height: 32px; + max-width: 32px; + max-height: 32px; + } +} diff --git a/client/src/app/components/monster-icon/monster-icon.component.ts b/client/src/app/components/monster-icon/monster-icon.component.ts new file mode 100644 index 0000000..243c71f --- /dev/null +++ b/client/src/app/components/monster-icon/monster-icon.component.ts @@ -0,0 +1,20 @@ +import { Component, HostBinding, Input, OnInit } from '@angular/core'; +import { IMonster } from '@interfaces'; + +@Component({ + selector: 'app-monster-icon', + templateUrl: './monster-icon.component.html', + styleUrls: ['./monster-icon.component.scss'], +}) +export class MonsterIconComponent implements OnInit { + @Input({ required: true }) monster!: IMonster; + @Input() size: 'xsmall' | 'small' | 'normal' = 'normal'; + + @HostBinding('class') get sizeClass() { + return this.size; + } + + constructor() {} + + ngOnInit() {} +} diff --git a/client/src/app/helpers/data-grabber.interceptor.ts b/client/src/app/helpers/data-grabber.interceptor.ts index 20bd3e4..79503ba 100644 --- a/client/src/app/helpers/data-grabber.interceptor.ts +++ b/client/src/app/helpers/data-grabber.interceptor.ts @@ -18,6 +18,7 @@ import { ApplyDiscoveriesPatches, SetDiscoveries, } from '@stores/discoveries/discoveries.actions'; +import { ApplyFightPatches, SetFight } from '@stores/fight/fight.actions'; import { ApplyInventoryPatches, SetInventory, @@ -116,6 +117,16 @@ export class DataGrabberInterceptor implements HttpInterceptor { } } + if (body.fight) { + if (isArray(body.fight)) { + if (body.fight.length > 0) { + this.store.dispatch(new ApplyFightPatches(body.fight)); + } + } else { + this.store.dispatch(new SetFight(body.fight)); + } + } + if (body.notifications) { this.store.dispatch(new SetNotifications(body.notifications)); } diff --git a/client/src/app/pages/explore/explore.page.html b/client/src/app/pages/explore/explore.page.html index 5509c2c..d2c5f20 100644 --- a/client/src/app/pages/explore/explore.page.html +++ b/client/src/app/pages/explore/explore.page.html @@ -45,7 +45,10 @@ -
+
+ + + +
+ You came across + + {{ actionInfo.actionData.formation.name }}! + + Prepare for combat! +
+ +
+ + + + + + + + +
+ {{ actionInfo.actionData.formation.name }} +
+ +
+ {{ + actionInfo.actionData.formation.monsters.length + }} monsters +
+
+
+
+ + + + {{ actionInfo.text }}! + + +
+
+
+
+ + + You didn't find anything of interest. + + +
Go! diff --git a/client/src/app/pages/explore/explore.page.scss b/client/src/app/pages/explore/explore.page.scss index 8d08efd..e03fcbf 100644 --- a/client/src/app/pages/explore/explore.page.scss +++ b/client/src/app/pages/explore/explore.page.scss @@ -42,6 +42,10 @@ } .action-card { + &.text-only { + padding: 8px; + } + .action-avatar, .action-center, .action-action { display: flex; flex-direction: column; @@ -73,3 +77,11 @@ } } } + +.formation-card { + .action-center { + .formation-name { + font-weight: bold; + } + } +} diff --git a/client/src/app/pages/explore/explore.page.ts b/client/src/app/pages/explore/explore.page.ts index 3d60946..a7355cc 100644 --- a/client/src/app/pages/explore/explore.page.ts +++ b/client/src/app/pages/explore/explore.page.ts @@ -3,6 +3,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { INotificationAction, IPlayerLocation } from '@interfaces'; import { Select } from '@ngxs/store'; import { ActionsService } from '@services/actions.service'; +import { ContentService } from '@services/content.service'; import { GameplayService } from '@services/gameplay.service'; import { VisualService } from '@services/visual.service'; import { PlayerStore } from '@stores'; @@ -28,6 +29,7 @@ export class ExplorePage implements OnInit { constructor( private gameplayService: GameplayService, + private contentService: ContentService, public actionsService: ActionsService, public visualService: VisualService, ) {} @@ -56,4 +58,8 @@ export class ExplorePage implements OnInit { explore() { this.gameplayService.explore().subscribe(); } + + getMonster(monsterId: string) { + return this.contentService.getMonster(monsterId)!; + } } diff --git a/client/src/app/services/content.service.ts b/client/src/app/services/content.service.ts index 5937acd..337ccba 100644 --- a/client/src/app/services/content.service.ts +++ b/client/src/app/services/content.service.ts @@ -2,10 +2,13 @@ import { Injectable } from '@angular/core'; import { environment } from '@environment'; import { ICollectible, + ICombatAbility, IEquipment, IItem, IJob, ILocation, + IMonster, + IMonsterFormation, IRecipe, IResource, } from '@interfaces'; @@ -22,6 +25,9 @@ export class ContentService { equipment: {}, resources: {}, recipes: {}, + abilities: {}, + monsters: {}, + formations: {}, }; public get maxPortraits() { @@ -56,6 +62,18 @@ export class ContentService { return this.content.recipes; } + public get abilities(): Record { + return this.content.abilities; + } + + public get monsters(): Record { + return this.content.monsters; + } + + public get formations(): Record { + return this.content.formations; + } + constructor(private assetService: AssetService) {} public async init() { @@ -117,4 +135,16 @@ export class ContentService { public getRecipes(): IRecipe[] { return Object.values(this.recipes); } + + public getFormation(formation: string): IMonsterFormation | undefined { + return this.formations[formation]; + } + + public getMonster(monster: string): IMonster | undefined { + return this.monsters[monster]; + } + + public getAbility(ability: string): ICombatAbility | undefined { + return this.abilities[ability]; + } } diff --git a/client/src/app/shared.module.ts b/client/src/app/shared.module.ts index ccb8cb5..5e9d1cb 100644 --- a/client/src/app/shared.module.ts +++ b/client/src/app/shared.module.ts @@ -18,6 +18,7 @@ import { ChooseAvatarModalComponent } from '@components/modals/choose-avatar/cho import { CompareItemsModalComponent } from '@components/modals/compare-items/compare-items.component'; import { LocationStatsModalComponent } from '@components/modals/location-stats/location-stats.component'; import { MarketModalComponent } from '@components/modals/market/market.component'; +import { MonsterIconComponent } from '@components/monster-icon/monster-icon.component'; import { StoreTextComponent } from '@components/store-text/store-text.component'; import { RelativeTimePipe } from '@helpers/relativetime.pipe'; import { IonicModule } from '@ionic/angular'; @@ -36,6 +37,7 @@ const components = [ HeaderBarComponent, AvatarComponent, ItemIconComponent, + MonsterIconComponent, ItemRarityComponent, ItemStatsComponent, ItemElementsComponent, diff --git a/client/src/stores/fight/fight.actions.ts b/client/src/stores/fight/fight.actions.ts new file mode 100644 index 0000000..ad10e48 --- /dev/null +++ b/client/src/stores/fight/fight.actions.ts @@ -0,0 +1,12 @@ +import { IFight } from '@interfaces'; +import * as jsonpatch from 'fast-json-patch'; + +export class SetFight { + static type = '[Fight] Set'; + constructor(public fight: IFight) {} +} + +export class ApplyFightPatches { + static type = '[Fight] Apply Patches'; + constructor(public patches: jsonpatch.Operation[]) {} +} diff --git a/client/src/stores/fight/fight.attachments.ts b/client/src/stores/fight/fight.attachments.ts new file mode 100644 index 0000000..ae8f090 --- /dev/null +++ b/client/src/stores/fight/fight.attachments.ts @@ -0,0 +1,8 @@ +import { applyFightPatches, setFight } from '@stores/fight/fight.functions'; +import { IAttachment } from '../../interfaces'; +import { ApplyFightPatches, SetFight } from './fight.actions'; + +export const attachments: IAttachment[] = [ + { action: SetFight, handler: setFight }, + { action: ApplyFightPatches, handler: applyFightPatches }, +]; diff --git a/client/src/stores/fight/fight.functions.ts b/client/src/stores/fight/fight.functions.ts new file mode 100644 index 0000000..ade3858 --- /dev/null +++ b/client/src/stores/fight/fight.functions.ts @@ -0,0 +1,30 @@ +import { IFightStore } from '@interfaces'; +import { StateContext } from '@ngxs/store'; +import { applyPatch } from 'fast-json-patch'; +import { ApplyFightPatches, SetFight } from './fight.actions'; + +export const defaultStore: () => IFightStore = () => ({ + version: 0, + fight: { + id: '', + attackers: [], + defenders: [], + involvedPlayers: [], + tiles: [], + }, +}); + +export function setFight(ctx: StateContext, { fight }: SetFight) { + ctx.patchState({ fight }); +} + +export function applyFightPatches( + ctx: StateContext, + { patches }: ApplyFightPatches, +) { + const fight = ctx.getState().fight; + + applyPatch(fight, patches); + + ctx.patchState({ fight }); +} diff --git a/client/src/stores/fight/fight.migrations.ts b/client/src/stores/fight/fight.migrations.ts new file mode 100644 index 0000000..9a0d8cd --- /dev/null +++ b/client/src/stores/fight/fight.migrations.ts @@ -0,0 +1,11 @@ +import { IFightStore } from '@interfaces'; + +export const fightStoreMigrations = [ + { + version: 0, + migrate: (state: IFightStore) => ({ + ...state, + version: 1, + }), + }, +].map((x) => ({ ...x, key: 'fight' })); diff --git a/client/src/stores/fight/fight.store.ts b/client/src/stores/fight/fight.store.ts new file mode 100644 index 0000000..77f882b --- /dev/null +++ b/client/src/stores/fight/fight.store.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { Selector, State } from '@ngxs/store'; +import { attachAction } from '@seiyria/ngxs-attach-action'; + +import { IFightStore } from '@interfaces'; +import { defaultStore } from './fight.functions'; + +import { getStateHelpers } from '@helpers/store-context'; +import { attachments } from './fight.attachments'; + +@State({ + name: 'fight', + defaults: defaultStore(), +}) +@Injectable() +export class FightStore { + constructor() { + const helpers = getStateHelpers(); + attachments.forEach(({ action, handler }) => { + attachAction(FightStore, action, (ctx, action) => { + handler(ctx, action, helpers); + }); + }); + } + + @Selector() + static fight(state: IFightStore) { + return state.fight; + } +} diff --git a/server/src/app.module.ts b/server/src/app.module.ts index dae1538..d45f459 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -19,6 +19,7 @@ import { StatsModule } from './modules/stats/stats.module'; import { UserModule } from './modules/user/user.module'; import { JWT_CONFIG } from '@modules/config/jwt-config'; +import { FightModule } from '@modules/fight/fight.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { JwtModule } from '@nestjs/jwt'; import { HttpExceptionFilter } from '@utils/http-exception.filter'; @@ -70,6 +71,7 @@ const isProduction = process.env.NODE_ENV === 'production'; AchievementsModule, ContentModule, InventoryModule, + FightModule, EventEmitterModule.forRoot(), GameplayModule, { diff --git a/server/src/interfaces/api.ts b/server/src/interfaces/api.ts index 35f1de0..6554cc7 100644 --- a/server/src/interfaces/api.ts +++ b/server/src/interfaces/api.ts @@ -3,6 +3,7 @@ import * as jsonpatch from 'fast-json-patch'; import { Achievements } from '@modules/achievements/achievements.schema'; import { Crafting } from '@modules/crafting/crafting.schema'; import { Discoveries } from '@modules/discoveries/discoveries.schema'; +import { Fight } from '@modules/fight/fight.schema'; import { Inventory } from '@modules/inventory/inventory.schema'; import { InventoryItem } from '@modules/inventory/inventoryitem.schema'; import { Player } from '@modules/player/player.schema'; @@ -22,6 +23,7 @@ export interface IFullUser { inventory: Inventory; crafting: Crafting; items: InventoryItem[]; + currentFight: Fight; actions: Array<{ type: string } & any>; } @@ -35,4 +37,5 @@ export interface IPatchUser { inventory: jsonpatch.Operation[]; items: jsonpatch.Operation[]; crafting: jsonpatch.Operation[]; + fight: jsonpatch.Operation[]; } diff --git a/server/src/modules/config/mikro-orm-config.ts b/server/src/modules/config/mikro-orm-config.ts index 0260287..cd5391d 100644 --- a/server/src/modules/config/mikro-orm-config.ts +++ b/server/src/modules/config/mikro-orm-config.ts @@ -2,6 +2,7 @@ import { MikroOrmModuleOptions } from '@mikro-orm/nestjs'; import { Achievements } from '@modules/achievements/achievements.schema'; import { Crafting } from '@modules/crafting/crafting.schema'; import { Discoveries } from '@modules/discoveries/discoveries.schema'; +import { Fight } from '@modules/fight/fight.schema'; import { Inventory } from '@modules/inventory/inventory.schema'; import { InventoryItem } from '@modules/inventory/inventoryitem.schema'; import { MarketItem } from '@modules/market/marketitem.schema'; @@ -35,6 +36,7 @@ function mikroOrmConfigFactory( Crafting, MarketItem, MarketSale, + Fight, ], dbName: process.env.NODE_ENV === 'production' ? 'ateoat' : 'ateoattest', type: 'mongo', diff --git a/server/src/modules/content/constants.service.ts b/server/src/modules/content/constants.service.ts index caf5257..45ec3c5 100644 --- a/server/src/modules/content/constants.service.ts +++ b/server/src/modules/content/constants.service.ts @@ -18,6 +18,7 @@ export class ConstantsService { public readonly collectibleFindPercentBoost: number = 0; public readonly resourceFindPercentBoost: number = 0; public readonly locationFindPercentBoost: number = 0; + public readonly monsterFindPercentBoost: number = 0; public readonly craftingSpeedMultiplier: number = 100; @@ -66,6 +67,10 @@ export class ConstantsService { 'LOCATION_FIND_PERCENT_BOOST', 0, ); + this.monsterFindPercentBoost = +this.configService.get( + 'MONSTER_FIND_PERCENT_BOOST', + 0, + ); this.craftingSpeedMultiplier = +this.configService.get( 'CRAFTING_SPEED_MULTIPLIER', diff --git a/server/src/modules/content/content.service.ts b/server/src/modules/content/content.service.ts index 82914ce..32457f3 100644 --- a/server/src/modules/content/content.service.ts +++ b/server/src/modules/content/content.service.ts @@ -2,10 +2,13 @@ import { Injectable } from '@nestjs/common'; import { ICollectible, + ICombatAbility, IEquipment, IItem, IJob, ILocation, + IMonster, + IMonsterFormation, IRecipe, IResource, } from '@interfaces'; @@ -30,6 +33,9 @@ export class ContentService { equipment: {}, resources: {}, recipes: {}, + abilities: {}, + monsters: {}, + formations: {}, }; private get locations(): Record { @@ -56,6 +62,18 @@ export class ContentService { return this.content.recipes; } + private get abilities(): Record { + return this.content.abilities; + } + + private get monsters(): Record { + return this.content.monsters; + } + + private get formations(): Record { + return this.content.formations; + } + constructor(private logger: Logger) {} public async reloadContent() { @@ -132,4 +150,20 @@ export class ContentService { public getRecipe(item: string): IRecipe | undefined { return this.recipes[item]; } + + public allFormations(): IMonsterFormation[] { + return Object.values(this.formations); + } + + public getFormation(formation: string): IMonsterFormation | undefined { + return this.formations[formation]; + } + + public getMonster(monster: string): IMonster | undefined { + return this.monsters[monster]; + } + + public getAbility(ability: string): ICombatAbility | undefined { + return this.abilities[ability]; + } } diff --git a/server/src/modules/fight/fight.controller.ts b/server/src/modules/fight/fight.controller.ts new file mode 100644 index 0000000..e062fc0 --- /dev/null +++ b/server/src/modules/fight/fight.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('fight') +export class FightController {} diff --git a/server/src/modules/fight/fight.module.ts b/server/src/modules/fight/fight.module.ts new file mode 100644 index 0000000..dbb30c5 --- /dev/null +++ b/server/src/modules/fight/fight.module.ts @@ -0,0 +1,13 @@ +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { Fight } from '@modules/fight/fight.schema'; +import { FightService } from '@modules/fight/fight.service'; +import { Module } from '@nestjs/common'; +import { FightController } from './fight.controller'; + +@Module({ + imports: [MikroOrmModule.forFeature([Fight])], + controllers: [FightController], + providers: [FightService], + exports: [FightService], +}) +export class FightModule {} diff --git a/server/src/modules/fight/fight.schema.ts b/server/src/modules/fight/fight.schema.ts new file mode 100644 index 0000000..3239ec5 --- /dev/null +++ b/server/src/modules/fight/fight.schema.ts @@ -0,0 +1,41 @@ +import { IFight, IFightCharacter, IFightTile } from '@interfaces'; +import { + Entity, + PrimaryKey, + Property, + SerializedPrimaryKey, +} from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; + +@Entity() +export class Fight implements IFight { + @PrimaryKey({ hidden: true }) + _id!: ObjectId; + + @SerializedPrimaryKey({ hidden: true }) + id!: string; + + @Property({ hidden: true }) + involvedPlayers: string[]; + + @Property() + attackers: IFightCharacter[]; + + @Property() + defenders: IFightCharacter[]; + + @Property() + tiles: IFightTile[][]; + + constructor( + involvedPlayers: string[], + attackers: IFightCharacter[], + defenders: IFightCharacter[], + tiles: IFightTile[][], + ) { + this.involvedPlayers = involvedPlayers; + this.attackers = attackers; + this.defenders = defenders; + this.tiles = tiles; + } +} diff --git a/server/src/modules/fight/fight.service.ts b/server/src/modules/fight/fight.service.ts new file mode 100644 index 0000000..57c5ece --- /dev/null +++ b/server/src/modules/fight/fight.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FightService {} diff --git a/server/src/modules/gameplay/gameplay.controller.ts b/server/src/modules/gameplay/gameplay.controller.ts index 768aae4..634355b 100644 --- a/server/src/modules/gameplay/gameplay.controller.ts +++ b/server/src/modules/gameplay/gameplay.controller.ts @@ -81,6 +81,13 @@ export class GameplayController { return this.gameplayService.takeItem(user.userId); } + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: 'Explore Event: Fight monsters' }) + @Post('fight') + async fight(@User() user): Promise> { + return this.gameplayService.startFight(user.userId); + } + @UseGuards(JwtAuthGuard) @ApiOperation({ summary: 'Sell an item' }) @Post('item/sell') diff --git a/server/src/modules/gameplay/gameplay.module.ts b/server/src/modules/gameplay/gameplay.module.ts index 28cc467..eb46b4d 100644 --- a/server/src/modules/gameplay/gameplay.module.ts +++ b/server/src/modules/gameplay/gameplay.module.ts @@ -1,6 +1,7 @@ import { ContentModule } from '@modules/content/content.module'; import { CraftingModule } from '@modules/crafting/crafting.module'; import { DiscoveriesModule } from '@modules/discoveries/discoveries.module'; +import { FightModule } from '@modules/fight/fight.module'; import { GameplayController } from '@modules/gameplay/gameplay.controller'; import { GameplayService } from '@modules/gameplay/gameplay.service'; import { InventoryModule } from '@modules/inventory/inventory.module'; @@ -19,6 +20,7 @@ import { Module } from '@nestjs/common'; InventoryModule, PlayerModule, CraftingModule, + FightModule, ], providers: [GameplayService], exports: [GameplayService], diff --git a/server/src/modules/gameplay/gameplay.service.ts b/server/src/modules/gameplay/gameplay.service.ts index 3d457e7..877b3ae 100644 --- a/server/src/modules/gameplay/gameplay.service.ts +++ b/server/src/modules/gameplay/gameplay.service.ts @@ -19,6 +19,7 @@ import { Crafting } from '@modules/crafting/crafting.schema'; import { CraftingService } from '@modules/crafting/crafting.service'; import { Discoveries } from '@modules/discoveries/discoveries.schema'; import { DiscoveriesService } from '@modules/discoveries/discoveries.service'; +import { FightService } from '@modules/fight/fight.service'; import { Inventory } from '@modules/inventory/inventory.schema'; import { InventoryService } from '@modules/inventory/inventory.service'; import { NotificationService } from '@modules/notification/notification.service'; @@ -36,7 +37,8 @@ type ExploreResult = | 'Item' | 'Discovery' | 'Collectible' - | 'Resource'; + | 'Resource' + | 'Monster'; const createFilledArray = (length: number, fill: ExploreResult) => Array(length).fill(fill); @@ -55,6 +57,7 @@ export class GameplayService { private readonly events: EventEmitter2, private readonly notificationService: NotificationService, private readonly playerHelper: PlayerHelperService, + private readonly fights: FightService, ) {} async explore(userId: string): Promise> { @@ -64,6 +67,10 @@ export class GameplayService { if (player.location.cooldown > Date.now()) return { player: [], discoveries: [] }; + if (player.action?.actionData?.stopExplore) { + throw new ForbiddenException("You can't explore right now!"); + } + const discoveries = await this.discoveriesService.getDiscoveriesForUser( userId, ); @@ -105,10 +112,15 @@ export class GameplayService { 'Collectible', ), ...createFilledArray( - this.constantsService.collectibleFindPercentBoost + + this.constantsService.resourceFindPercentBoost + foundLocation.baseStats.resourceFind || 0, 'Resource', ), + ...createFilledArray( + this.constantsService.monsterFindPercentBoost + + foundLocation.baseStats.monsterFind || 0, + 'Monster', + ), ]; if (choices.length < 100) { @@ -210,6 +222,10 @@ export class GameplayService { if (exploreResult === 'Item') { await this.playerService.handleFindItem(playerRef, foundLocation); } + + if (exploreResult === 'Monster') { + await this.playerService.handleFindMonster(playerRef, foundLocation); + } }, ); @@ -498,6 +514,13 @@ export class GameplayService { return { player: playerPatches, inventory: inventoryPatches }; } + async startFight(userId: string): Promise> { + // this.fights.startFight(userId); + // TODO: start fight + + return {}; + } + async sellItem( userId: string, instanceId: string, diff --git a/server/src/modules/player/player.service.ts b/server/src/modules/player/player.service.ts index 468cd56..9f3d8f5 100644 --- a/server/src/modules/player/player.service.ts +++ b/server/src/modules/player/player.service.ts @@ -216,7 +216,7 @@ export class PlayerService { return found[0]; } - async handleRandomWave(player: Player) { + async handleRandomWave(player: Player): Promise { const randomPlayer = await this.getRandomOnlinePlayerAtLocation( player.userId, player.location.current, @@ -237,7 +237,7 @@ export class PlayerService { }); } - async handleFindResource(player: Player, location: ILocation): Promise { + async handleFindResource(player: Player, location: ILocation): Promise { const resourceRarityCommonality: Record = { Common: 100, Uncommon: 75, @@ -275,7 +275,7 @@ export class PlayerService { async handleFindCollectible( player: Player, location: ILocation, - ): Promise { + ): Promise { const collectibleRarityCommonality: Record = { Common: 100, Uncommon: 75, @@ -312,7 +312,7 @@ export class PlayerService { }); } - async handleFindItem(player: Player, location: ILocation): Promise { + async handleFindItem(player: Player, location: ILocation): Promise { const randomItemForLocation = sample( this.contentService .allEquipment() @@ -336,4 +336,25 @@ export class PlayerService { urlData: {}, }); } + + async handleFindMonster(player: Player, location: ILocation): Promise { + const randomFormationForLocation = sample( + this.contentService + .allFormations() + .filter((formation) => formation.location === location.name), + ); + + if (!randomFormationForLocation) return; + + this.setPlayerAction(player, { + text: 'Fight', + action: 'fight', + actionData: { + formation: randomFormationForLocation, + stopExplore: true, + }, + url: 'gameplay/fight', + urlData: {}, + }); + } } diff --git a/shared/interfaces/combat.ts b/shared/interfaces/combat.ts index fb02d7c..1cae380 100644 --- a/shared/interfaces/combat.ts +++ b/shared/interfaces/combat.ts @@ -17,3 +17,27 @@ export interface ICombatAbility { elements: Element[]; statScaling: Partial>; } + +export interface IFightTile { + containedCharacters: string[]; +} + +export interface IFightCharacter { + userId?: string; + monsterId?: string; + characterId: string; + modifiedStats: Record; +} + +export interface IFight { + id: string; + involvedPlayers: string[]; + attackers: IFightCharacter[]; + defenders: IFightCharacter[]; + tiles: IFightTile[][]; +} + +export interface IFightStore { + version: number; + fight: IFight; +} diff --git a/shared/interfaces/item.ts b/shared/interfaces/item.ts index 7717c03..1eef549 100644 --- a/shared/interfaces/item.ts +++ b/shared/interfaces/item.ts @@ -1,4 +1,4 @@ -import { Rarity, Stat } from './buildingblocks'; +import { Element, Rarity, Stat } from './buildingblocks'; export type Armor = 'body' | 'feet' | 'head' | 'legs' | 'shoulders' | 'waist'; diff --git a/shared/interfaces/location.ts b/shared/interfaces/location.ts index 78f8f63..9fd1f50 100644 --- a/shared/interfaces/location.ts +++ b/shared/interfaces/location.ts @@ -8,6 +8,7 @@ export enum LocationStat { Wave = 'wave', CollectibleFind = 'collectibleFind', ResourceFind = 'resourceFind', + MonsterFind = 'monsterFind', TaxRate = 'taxRate', } diff --git a/shared/interfaces/monster.ts b/shared/interfaces/monster.ts index 8fdc2c5..c581260 100644 --- a/shared/interfaces/monster.ts +++ b/shared/interfaces/monster.ts @@ -17,7 +17,6 @@ export interface IMonsterReward { export interface IMonster { itemId: string; - location: string; job: string; level: number; sprite: number; @@ -26,3 +25,14 @@ export interface IMonster { abilities: ICombatMonsterAbility[]; rewards: IMonsterReward; } + +export interface IMonsterGroup { + monster: string; +} + +export interface IMonsterFormation { + name: string; + itemId: string; + location: string; + monsters: IMonsterGroup[]; +}