538 lines
15 KiB
TypeScript
538 lines
15 KiB
TypeScript
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,
|
||
Necromancer,
|
||
Monk,
|
||
Thinker,
|
||
Bard
|
||
}
|
||
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<MD2LevelUpReward>) {
|
||
Object.assign(this, config);
|
||
|
||
}
|
||
level: number = 1;
|
||
needExp: number = 0;
|
||
currentExp = 0
|
||
extraHp = 0
|
||
extraMp = 0
|
||
extraRareToken = 0
|
||
extraEpicToken = 0
|
||
}
|
||
export class DrawingBag<T extends IDrawingItem> {
|
||
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<MobInfo> = {}
|
||
) {
|
||
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 => `<img src="${i.imageUrl}" class='mr-1' width="40px">`)
|
||
.concat(this.fixedCarriedTreasure?.map(i => `<img src="${i.imageUrl}" class='mr-1' width="40px">`.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<MD2HeroInfo> = {}
|
||
) {
|
||
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 <b>SECOND</b> action.<br>' +
|
||
'You have to apply this to a Mob that is not in the same zone as a hero, and that could not attack otherwise.<br>' +
|
||
'If no Mob has line of sight to a hero, one Mob moves an additional zone instead.<br>'),
|
||
new MD2EnemyPhaseSpecialRule(22, 'Fast Advance', 'Each Mob moves 1 additional zone in its first move action.<br>' +
|
||
'If a Mob doesn’t move, it adds one additional black enemy die to it’s first attack action instead.<br>' +
|
||
'(If it already has all six black enemy dice in its attack pool, it rerolls one blank result on a black die instead.)<br>'),
|
||
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.)<br>' +
|
||
'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<TreasureType>
|
||
spawnMob: Subject<void>
|
||
spawnRoamingMonster: Subject<void>
|
||
runDarknessPhase(): boolean
|
||
}
|
||
export class CoreGameDarknessPhaseRule implements IDarknessPhaseRule {
|
||
round: number = 1;
|
||
frontEndRound: number = 9;
|
||
extraRound: number = 0;
|
||
constructor() {
|
||
}
|
||
addTreasureToken = new Subject<TreasureType>();
|
||
spawnMob = new Subject<void>();
|
||
spawnRoamingMonster = new Subject<void>();
|
||
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;
|
||
|
||
}
|
||
|
||
}
|
||
|