import { Observable, Subject } from "rxjs"; import { environment } from "../../../environments/environment"; import { MsgBoxService } from "../../services/msg-box.service"; import { ObjectUtils } from "../../utilities/object-utils"; import { GamePlayer } from "../games.model"; import { MD2Clone } from "./factorys/md2-clone"; import { MobSkill } from "./massive-darkness2.model.boss"; import { MD2DiceSet, MD2MobSkill } from "./massive-darkness2.db.model"; const MD2_IMG_URL = (id: string = null) => { return `${environment.apiUrl}/Files/Images/MD2/${(id ? `${encodeURI(id)}` : '')}` } export enum MobDlgType { Spawn, Activating, BeenAttacked, PreView, Dashboard } export enum RoundPhase { HeroPhase, EnemyPhase, LevelUpPhase, DarknessPhase, BossActivation } export enum TreasureType { Cover, Common, Rare, Epic, Legendary } export enum HeroClass { Berserker, Wizard, Rogue, Ranger, Shaman, Paladin, Druid, } export enum MobType { Mob, RoamingMonster, Boss } export enum MD2Icon { Attack, Defense, Mana, Shadow, EnemySkill, EnemyClaw, Reroll, Fire, Frost, OneHand, TwoHand, Helmet, Armor, Ring, Foot, Melee, Range, Magic, HP, MP, Dice, Arrow, ArrowBullseye, ArrowOverload, SoulToken, Rage, RedDice, BlueDice, GreenDice, YellowDice, OrangeDice, BlackDice, //Below are image based icons TreasureToken = 300, TreasureToken_Common, TreasureToken_Rare, TreasureToken_Epic, TreasureToken_Legendary, HP_Color, Mana_Color, CorruptToken, TimeToken, FireToken, FrozenToken } export enum AttackTarget { Random = 40, LeastHp = 50, LeastMp = 60, HighestHp = 70, HighestMp = 80, LowestLevel = 90, MostCorruption = 200, LeastCorruption = 201 } export enum AttackType { Melee = 15, Range = 16, Magic = 17 } export class AttackInfo { constructor( type: MD2Icon, yellow: number = 0, orange: number = 0, red: number = 0, black: number = 0 ) { this.type = type this.orange = orange this.red = red this.yellow = yellow this.black = black } type: MD2Icon orange: number red: number yellow: number black: number } export class MD2LevelUpReward { constructor(config: Partial) { Object.assign(this, config); } level: number = 1; needExp: number = 0; currentExp = 0 extraHp = 0 extraMp = 0 extraRareToken = 0 extraEpicToken = 0 } export class DrawingBag { constructor(drawingItems: IDrawingItem[] = []) { this.drawingItems = drawingItems; this.removedItems = []; } drawingItems: IDrawingItem[] removedItems: IDrawingItem[] public bagIsEmpty(predicate: (value: T) => boolean = undefined) { if (predicate) { return this.drawingItems.filter(predicate).reduce((sum, current) => sum + current.drawingWeight, 0) == 0; } else { return this.drawingItems.reduce((sum, current) => sum + current.drawingWeight, 0) == 0; } } public Draw(amount: number): T[] { let drawItems: T[] = this.DrawAndRemove(amount); this.RestoreRemoveItems(); return drawItems; } public DrawAndRemove(amount: number = 1, predicate: (value: T) => boolean = undefined): T[] { let drawItems: T[] = []; for (let i = 0; i < amount; i++) { if (!this.bagIsEmpty(predicate)) { let drawItem = null as T; let drawingPool = [] as T[]; if (predicate) { drawingPool = this.drawingItems.filter(predicate) as T[]; } else { drawingPool = this.drawingItems as T[]; } let drawIndex = Math.random() * drawingPool.reduce((sum, current) => sum + current.drawingWeight, 0); let drawCalc = 0; for (let i = 0; i < drawingPool.length; i++) { const item = drawingPool[i]; drawCalc += item.drawingWeight; if (drawCalc >= drawIndex) { drawItem = MD2Clone.CloneDrawingItem(item); drawItem.drawingWeight = 1; break; } } //ObjectUtils.CloneValue this.RemoveItem(drawItem); drawItems.push(drawItem); } else { break; } } return drawItems; } public RestoreRemoveItems() { for (let i = 0; i < this.removedItems.length; i++) { const removedItem = this.removedItems[i]; this.AddItem(removedItem); } this.removedItems = []; } public AddItem(item: IDrawingItem) { let existingItem = this.drawingItems.find(i => i.identifyName == item.identifyName); if (existingItem) { existingItem.drawingWeight += item.drawingWeight; } else { this.drawingItems.push(item); } } public RemoveItem(item: IDrawingItem) { if (item) { if (item.identifyName) { let existingItem = this.drawingItems.find(i => i.identifyName == item.identifyName); if (existingItem) { existingItem.drawingWeight -= item.drawingWeight; let removedItem = this.removedItems.find(i => i.identifyName == item.identifyName); if (removedItem) { removedItem.drawingWeight += item.drawingWeight; } else { this.removedItems.push(item); } } } else { let existingItem = this.drawingItems.find(i => i.name == item.name); if (existingItem) { existingItem.drawingWeight -= item.drawingWeight; let removedItem = this.removedItems.find(i => i.name == item.name); if (removedItem) { removedItem.drawingWeight += item.drawingWeight; } else { this.removedItems.push(item); } } } } } public ClearAllItems() { this.drawingItems = []; this.removedItems = []; } } export interface IMobFactory { mobName: string; generate(level: number): MobInfo; } export interface IDrawingItem { imageUrl: string name: string get identifyName(): string description: string drawingWeight: number } export class DrawingItem implements IDrawingItem { constructor( name: string, description: string, imageUrl: string, drawingWeight: number = 1 ) { this.imageUrl = imageUrl this.name = name this.description = description this.drawingWeight = drawingWeight } get identifyName(): string { return this.name; } imageUrl: string name: string description: string drawingWeight: number } export class TreasureItem extends DrawingItem { constructor(type: TreasureType, itemAmount: number = 1) { super(`${TreasureType[type]} Treasure`, `It's a ${TreasureType[type]} Treasure!`, MD2_IMG_URL(`TreasureToken/${TreasureType[type]}.png`), itemAmount); this.type = type; this.itemAmount = itemAmount; } type: TreasureType; itemAmount: number; get identifyName(): string { return this.name; } } export class MobInfo implements IDrawingItem { constructor( config: Partial = {} ) { Object.assign(this, config); this.description = config.name; this.drawingWeight = 1; this.unitRemainHp = config.hp } type: MobType = MobType.Mob; imageUrl: string standUrl: string leaderImgUrl: string minionImgUrl: string name: string description: string drawingWeight: number level: number; rewardTokens: number; hp: number; hpPerHero: number; mobAmount: number; carriedTreasure: TreasureItem[]; fixedCarriedTreasure: TreasureItem[]; unitRemainHp: number; attackInfos: AttackInfo[]; defenseInfo: MD2DiceSet; skills: MD2MobSkill[]; actions: number = 0; activateDescription: string; fireToken: number = 0; frozenToken: number = 0; corruptionToken: number = 0; uiWounds: number; uiFireTokens: number; uiFrozenTokens: number; uiCorruptionTokens: number; uiAttackedBy: string; extraRule: string; get identifyName(): string { return `${this.name}_${this.level}`; } public get carriedTreasureHtml(): string { if (!this.carriedTreasure) return ''; return this.carriedTreasure.map(i => ``) .concat(this.fixedCarriedTreasure?.map(i => ``.repeat(i.drawingWeight))).join(); } public get totalHp(): number { switch (this.type) { case MobType.Mob: return (this.mobAmount - 1) * this.hp + this.unitRemainHp; case MobType.RoamingMonster: return this.unitRemainHp; case MobType.Boss: default: return this.unitRemainHp; } } public get minionAmount(): number { switch (this.type) { case MobType.Mob: return (this.mobAmount - 1); case MobType.RoamingMonster: case MobType.Boss: default: return 0; } } public get leaderExp(): number { switch (this.type) { case MobType.Mob: return 2; case MobType.RoamingMonster: return 4; case MobType.Boss: default: return 0; } } public activateFunction?: (mob: MobInfo, msgBoxService: MsgBoxService, heros: MD2HeroInfo[]) => void; } export interface MD2HeroProfile { id: string; heroClass: HeroClass; title: string; hp: number; mana: number; skillHtml: string; shadowSkillHtml: string; } export class MD2HeroInfo { constructor( config: Partial = {} ) { Object.assign(this, config); } class: HeroClass; name: string; hp: number; mp: number; ap: number; hpMaximum: number; mpMaximum: number; exp: number = 0; level: number = 1; fireToken: number = 0; frozenToken: number = 0; corruptionToken: number = 0; playerInfo: GamePlayer; imgUrl: string; skillHtml: string; shadowSkillHtml: string; remainActions: number = 3; rage: number = 0; uiActivating = false; uiShowCorruptionToken = false; uiBossFight = false; uiShowAttackBtn = false; public get heroFullName(): string { return `${this.playerInfo.name} (${HeroClass[this.class]} - ${this.name})` } } export class MD2Rules { public static CoreGameLevelBoard = [ new MD2LevelUpReward({ level: 2, needExp: 5, extraHp: 1, extraMp: 0, extraRareToken: 1 }), new MD2LevelUpReward({ level: 3, needExp: 10, extraHp: 1, extraMp: 1, extraEpicToken: 1 }), new MD2LevelUpReward({ level: 4, needExp: 12, extraHp: 2, extraMp: 1, extraEpicToken: 1 }), new MD2LevelUpReward({ level: 5, needExp: 18, extraHp: 2, extraMp: 2, extraEpicToken: 1 }), ]; public static checkCoreGameLevelup(currentLevel: number, currentExp: number): MD2LevelUpReward { let result = null as MD2LevelUpReward; let nextLevel = this.CoreGameLevelBoard.find(r => r.level > currentLevel && currentExp >= r.needExp); if (nextLevel) { result = ObjectUtils.CloneValue(nextLevel) as MD2LevelUpReward; result.currentExp = currentExp - nextLevel.needExp; } return result; } } export class MD2EnemyPhaseSpecialInfo { specialRule: MD2EnemyPhaseSpecialRule specialRules = [ new MD2EnemyPhaseSpecialRule(30, '', ''), new MD2EnemyPhaseSpecialRule(44, 'Surprise Attack', 'One Mob with line of sight to a hero changes its attack profile to ranged during its SECOND action.
' + 'You have to apply this to a Mob that is not in the same zone as a hero, and that could not attack otherwise.
' + 'If no Mob has line of sight to a hero, one Mob moves an additional zone instead.
'), new MD2EnemyPhaseSpecialRule(22, 'Fast Advance', 'Each Mob moves 1 additional zone in its first move action.
' + 'If a Mob doesn’t move, it adds one additional black enemy die to it’s first attack action instead.
' + '(If it already has all six black enemy dice in its attack pool, it rerolls one blank result on a black die instead.)
'), new MD2EnemyPhaseSpecialRule(4, 'A Dark Portal Appears, Guiding Predators To Their Prey', 'First, move one green portal token from the board to the zone with the hero who has the least amount of health tokens. (If there is no green portal token on the board, put one onto the board.)
' + 'Then, move the Mob that is furthest away from any heroes into this new portal zone. In its following activation, this Mob only has one action.'), ] } export class MD2EnemyPhaseSpecialRule implements IDrawingItem { constructor(drawingWeight: number, title: string, description: string) { this.drawingWeight = drawingWeight this.title = title this.description = description } get identifyName(): string { return this.name; } imageUrl: string; name: string; drawingWeight: number; title: string description: string } export interface IDarknessPhaseRule { addTreasureToken: Subject spawnMob: Subject spawnRoamingMonster: Subject runDarknessPhase(): boolean } export class CoreGameDarknessPhaseRule implements IDarknessPhaseRule { round: number = 1; frontEndRound: number = 9; extraRound: number = 0; constructor() { } addTreasureToken = new Subject(); spawnMob = new Subject(); spawnRoamingMonster = new Subject(); runDarknessPhase() { if (this.round >= this.frontEndRound) { this.extraRound++; if (this.extraRound % 4 == 0) { this.spawnRoamingMonster.next(); return false; } else if (this.extraRound % 2 == 0) { this.spawnMob.next(); return false; } return true; } this.round++; switch (this.round) { case 3: case 9: this.spawnRoamingMonster.next(); return false; break; case 4: this.addTreasureToken.next(TreasureType.Rare); break; case 5: case 7: this.spawnMob.next(); return false; break; case 6: case 8: this.addTreasureToken.next(TreasureType.Epic); break; default: break; } return true; } }