Compare commits

..

No commits in common. "3325c6363115eb600b1454fd1144058040b740a7" and "0d3995764bb45fb6379b6062499e233c2ad38486" have entirely different histories.

39 changed files with 227 additions and 1492 deletions

View File

@ -50,11 +50,8 @@ import { MD2MobInfoEditorComponent } from './massive-darkness2/md2-mob-info-main
import { MD2MobInfoDetailComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component';
import { MD2MobSkillEditorComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component';
import { MD2MobLevelEditorComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-level-editor/md2-mob-level-editor.component';
import { MD2BossFightEditorComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-boss-fight-editor/md2-boss-fight-editor.component';
import { MD2PhaseBuffEditorComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-phase-buff-editor/md2-phase-buff-editor.component';
import { MD2HeroProfileMaintenanceComponent } from './massive-darkness2/md2-hero-profile-maintenance/md2-hero-profile-maintenance.component';
import { MD2HeroProfileEditorComponent } from './massive-darkness2/md2-hero-profile-maintenance/md2-hero-profile-editor/md2-hero-profile-editor.component';
import { GameInitDlgComponent } from './massive-darkness2/game-init-dlg/game-init-dlg.component';
@NgModule({
@ -90,11 +87,8 @@ import { GameInitDlgComponent } from './massive-darkness2/game-init-dlg/game-ini
MD2MobInfoDetailComponent,
MD2MobSkillEditorComponent,
MD2MobLevelEditorComponent,
MD2BossFightEditorComponent,
MD2PhaseBuffEditorComponent,
MD2HeroProfileMaintenanceComponent,
MD2HeroProfileEditorComponent,
GameInitDlgComponent
MD2HeroProfileEditorComponent
],
imports: [
CommonModule,

View File

@ -83,7 +83,6 @@ export abstract class MD2Base {
}
abstract refreshUI();
handleSignalRCallback(message: SignalRMessage): void {
console.log('handleSignalRCallback', message);
if (message.from) {
if (message.from.isGroup) {
if (!this.isHeroDashboard) return;
@ -147,8 +146,6 @@ export abstract class MD2Base {
break;
case 'update':
if (this.isHeroDashboard) {
//Before update game info check the current Hero level
let playerHeroLevel = this.md2Service.playerHero?.level;
this.md2Service.info = new MD2GameInfo(JSON.parse(message.parameters['gameInfo']) as MD2GameInfo);
let playerHero = this.md2Service.heros.find(h => h.playerInfo.tabId == this.stateService.loginUserService.sessionTabId);
if (playerHero) {
@ -157,13 +154,6 @@ export abstract class MD2Base {
playerHero.playerInfo.isDisconnected = false;
this.md2Service.playerHero = playerHero;
this.md2Service.broadcastMyHeroInfo();
//When fetch game info, if the hero level is changed, show the level up message
if (playerHeroLevel && playerHeroLevel != playerHero.level) {
//do i--
for (let i = playerHero.level; i > playerHeroLevel; i--) {
this.md2Service.msgBoxService.show(`Level Up Lv.${i}`, { text: 'Please do a skill level up!', icon: ADIcon.INFO });
}
}
}
this.detectChanges();
}
@ -182,11 +172,9 @@ export abstract class MD2Base {
}
break;
case 'sendJoinInfo':
//When hero join the game, or reconnect to the game will receive this message
if (this.isHeroDashboard && this.md2Service.playerHero) {
this.md2Service.playerHero.playerInfo.signalRClientId = message.parameters['signalrconnid'];
//Send fetch game info to the hero
this.md2Service.broadcastFetchGameInfo();
this.md2Service.broadcastMyHeroInfo();
}
break;
default:

View File

@ -3,7 +3,7 @@
<nb-card-body class="g-overflow-hidden">
<div class="row form-group">
<div class="col-md-5 g-height-700px">
<md2-mob-stand-info [mob]="boss" [mode]="mode"></md2-mob-stand-info>
<md2-mob-stand-info [mob]="boss.info" [mode]="mode"></md2-mob-stand-info>
</div>
<div class="col-md-7">
@ -17,11 +17,11 @@
<div class="row">
<div class="col-md-4">
<md2-mob-attack-info [mob]="boss"></md2-mob-attack-info>
<md2-mob-attack-info [mob]="boss.info"></md2-mob-attack-info>
</div>
<div class="col-md-8 MD2IconContainer-lg">
<md2-mob-combat-info [mob]="boss"></md2-mob-combat-info>
<md2-mob-combat-info [mob]="boss.info"></md2-mob-combat-info>
</div>
</div>

View File

@ -6,11 +6,10 @@ import { takeUntil } from 'rxjs/operators';
import { MD2Service } from '../../../../services/MD2/md2.service';
import { MsgBoxService } from '../../../../services/msg-box.service';
import { StateService } from '../../../../services/state.service';
import { MobDlgType, MD2Icon, MD2HeroInfo, RoundPhase, MobInfo } from '../../massive-darkness2.model';
import { MobDlgType, MD2Icon, MD2HeroInfo, RoundPhase } from '../../massive-darkness2.model';
import { MobSkill, IBossFight } from '../../massive-darkness2.model.boss';
import { MD2ComponentBase } from '../../MD2Base';
import { SpawnMobDlgComponent } from '../../mobs/spawn-mob-dlg/spawn-mob-dlg.component';
import { MD2MobInfo, MD2MobSkill } from '../../massive-darkness2.db.model';
@Component({
selector: 'ngx-boss-activation',
@ -18,8 +17,8 @@ import { MD2MobInfo, MD2MobSkill } from '../../massive-darkness2.db.model';
styleUrls: ['./boss-activation.component.scss']
})
export class BossActivationComponent implements OnInit {
boss: MobInfo;
bossAction: MD2MobSkill;
boss: IBossFight;
bossAction: MobSkill;
currentAction: number;
allActions: number;
MobDlgType = MobDlgType;

View File

@ -8,17 +8,17 @@
<div class="col-md-5">
<!-- <img src="{{boss.standUrl}}" class="w-100 bossStandImg"> -->
<md2-mob-stand-info [mob]="boss" [mode]="MobDlgType.PreView"></md2-mob-stand-info>
<md2-mob-stand-info [mob]="boss.info" [mode]="MobDlgType.PreView"></md2-mob-stand-info>
<!-- HP and Mana Bars -->
<div class="hero-stats-overlay">
<div class="stat-bar-overlay hp-bar-overlay">
<div class="stat-bar-label-overlay">
<md2-icon [icon]="MD2Icon.HP_Color" size="sm"></md2-icon>
<span class="stat-value-overlay">{{boss.unitRemainHp}}/{{boss.hp}}</span>
<span class="stat-value-overlay">{{boss.info.unitRemainHp}}/{{boss.info.hp}}</span>
</div>
<div class="stat-progress-bar-overlay">
<div class="stat-progress-fill-overlay hp-fill-overlay"
[style.width.%]="(boss.unitRemainHp / boss.hp) * 100">
[style.width.%]="(boss.info.unitRemainHp / boss.info.hp) * 100">
</div>
</div>
</div>
@ -27,26 +27,26 @@
<div class="col-md-7">
<div class="row">
<div class="col-md">
<adj-number-input name="mob{{boss.name}}" [(ngModel)]="boss.unitRemainHp" minimum="0"
<adj-number-input name="mob{{boss.info.name}}" [(ngModel)]="boss.info.unitRemainHp" minimum="0"
class="mb-3" title="Boss HP" (hitMinimum)="WIN()">
</adj-number-input>
<md2-mob-attack-info [mob]="boss">
<md2-mob-attack-info [mob]="boss.info">
</md2-mob-attack-info>
<md2-mob-def-info [mob]="boss"></md2-mob-def-info>
<md2-mob-def-info [mob]="boss.info"></md2-mob-def-info>
</div>
<div class="col-md-9 bossSpecialRules" *ngIf="boss.bossFightProfile.specialRules">
<div [innerHtml]="boss.bossFightProfile.specialRules"></div>
<div class="col-md-9 h6" *ngIf="boss.extraRules">
<div [innerHtml]="boss.extraRules"></div>
</div>
</div>
<md2-mob-combat-info [mob]="boss"></md2-mob-combat-info>
<md2-mob-combat-info [mob]="boss.info"></md2-mob-combat-info>
<!--
<button nbButton hero status="danger" size="small" (click)="attack(boss)">Attack It</button> -->
<button nbButton hero status="danger" size="small" (click)="attack(boss.info)">Attack It</button> -->
<!-- <label class="MD2Text mt-3" [innerHtml]="boss.combatSkill.skillName">
<!-- <label class="MD2Text mt-3" [innerHtml]="boss.info.combatSkill.skillName">
</label>
<label class="MD2Text" [innerHtml]="boss.combatInfo.skillDescription">
<label class="MD2Text" [innerHtml]="boss.info.combatInfo.skillDescription">
</label> -->
</div>

View File

@ -6,11 +6,6 @@ nb-card {
max-height: 67vh;
object-fit: contain;
}
::ng-deep .bossSpecialRules {
.MD2Icon {
font-size: 30px;
}
}
// HP and Mana Bars Overlay
.hero-stats-overlay {
position: absolute;

View File

@ -39,7 +39,7 @@ export class BossFightComponent extends MD2ComponentBase {
super.ngOnInit();
this.md2Service.heroAttackingSubject.pipe(takeUntil(this.destroy$)).subscribe(result => {
if (this.md2Service.info.isBossFight) {
this.attack(this.boss);
this.attack(this.boss.info);
}
});
}
@ -49,7 +49,7 @@ export class BossFightComponent extends MD2ComponentBase {
this.destroy$.complete();
}
activate() {
this.md2Service.activateBoss();
this.boss.activating();
}
WIN() {
this.msgBoxService.show('Win', { text: 'You Win the Boss Fight', icon: ADIcon.INFO });
@ -65,8 +65,8 @@ export class BossFightComponent extends MD2ComponentBase {
if (mobResult) {
let attackDamage = mobResult.uiWounds;
if (attackDamage) {
this.boss.unitRemainHp -= attackDamage;
if (this.boss.unitRemainHp <= 0) {
this.boss.info.unitRemainHp -= attackDamage;
if (this.boss.info.unitRemainHp <= 0) {
this.WIN();
}
this.cdRef.detectChanges();

View File

@ -1,40 +0,0 @@
<nb-card status="info" size="large">
<nb-card-header>
<img src="{{md2Service.imgUrl('HeroIcon.png')}}" width="40px">
<span class="ml-2 g-font-size-17">Initialize Game</span>
</nb-card-header>
<nb-card-body>
<div class="form-group">
<label class="label">Select Game Bundles:</label>
<div class="form-group" *ngFor="let bundle of bundleOptions">
<nb-checkbox [checked]="isBundleEnabled(bundle.value)" (checkedChange)="toggleBundle(bundle.value)">
{{ bundle.label }}
</nb-checkbox>
</div>
<small class="form-text text-muted" *ngIf="enabledBundles.length === 0">
At least one bundle must be selected.
</small>
</div>
<div class="form-group">
<nb-checkbox [(checked)]="enableMobSpecialRule">
Enable Mob Special Rules
</nb-checkbox>
</div>
<div class="form-group">
<nb-checkbox [(checked)]="enableHeroBetrayal">
Enable Hero Betrayal
</nb-checkbox>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="warning" size="small" (click)="cancel()">
Cancel
</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small"
[disabled]="enabledBundles.length === 0" (click)="submit()">
Initialize
</button>
</nb-card-footer>
</nb-card>

View File

@ -1,12 +0,0 @@
nb-card {
min-width: 400px;
}
.form-group {
margin-bottom: 1rem;
}
nb-checkbox {
display: block;
margin-bottom: 0.5rem;
}

View File

@ -1,73 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { GameBundle } from '../massive-darkness2.db.model';
import { MD2Service } from '../../../services/MD2/md2.service';
import { StringUtils } from '../../../utilities/string-utils';
export interface GameInitConfig {
enabledBundles: GameBundle[];
enableMobSpecialRule: boolean;
enableHeroBetrayal: boolean;
}
@Component({
selector: 'ngx-game-init-dlg',
templateUrl: './game-init-dlg.component.html',
styleUrls: ['./game-init-dlg.component.scss']
})
export class GameInitDlgComponent implements OnInit {
GameBundle = GameBundle;
@Input() initialConfig: GameInitConfig;
enabledBundles: GameBundle[] = [GameBundle.CoreGame];
enableMobSpecialRule: boolean = false;
enableHeroBetrayal: boolean = false;
bundleOptions = [];
constructor(
private dlgRef: NbDialogRef<GameInitDlgComponent>,
public md2Service: MD2Service
) { }
ngOnInit(): void {
//For each GameBundle, create a new option
this.bundleOptions = Object.values(GameBundle).filter(b => !isNaN(Number(b))).map(b => ({
value: b as GameBundle, label: StringUtils.camelToTitle(GameBundle[b as GameBundle] as string)
}));
// Initialize from context if provided
if (this.initialConfig) {
this.enabledBundles = [...this.initialConfig.enabledBundles];
this.enableMobSpecialRule = this.initialConfig.enableMobSpecialRule;
this.enableHeroBetrayal = this.initialConfig.enableHeroBetrayal;
}
}
toggleBundle(bundle: GameBundle): void {
const index = this.enabledBundles.indexOf(bundle);
if (index > -1) {
this.enabledBundles.splice(index, 1);
} else {
this.enabledBundles.push(bundle);
}
}
isBundleEnabled(bundle: GameBundle): boolean {
return this.enabledBundles.includes(bundle);
}
submit(): void {
const config: GameInitConfig = {
enabledBundles: this.enabledBundles,
enableMobSpecialRule: this.enableMobSpecialRule,
enableHeroBetrayal: this.enableHeroBetrayal
};
this.dlgRef.close(config);
}
cancel(): void {
this.dlgRef.close();
}
}

View File

@ -270,24 +270,16 @@
*ngIf="hero.uiActivating">
</adj-number-input>
</div>
<div class="col-6" *ngIf="hero.class==HeroClass.Berserker">
<div class="col-6">
<adj-number-input name="heroRage" [(ngModel)]="hero.rage" minimum="0" maximum="7"
title="{{iconHtml(MD2Icon.Rage,'g-color-google-plus mr-1 g-font-size-18')}}Rage"
(blur)="heroUpdateDebounceTimer.resetTimer()">
(blur)="heroUpdateDebounceTimer.resetTimer()" *ngIf="hero.class==HeroClass.Berserker">
</adj-number-input>
</div>
<div class="col-6" *ngIf="hero.uiShowExtraToken">
<adj-number-input name="heroExtraToken" [(ngModel)]="hero.extraToken" minimum="0"
title="{{hero.uiExtraTokenHtml}} {{hero.uiExtraTokenName}}"
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
</div>
<div class="col-6" *ngIf="hero.uiShowExtraToken2">
<adj-number-input name="heroExtraToken2" [(ngModel)]="hero.extraToken2" minimum="0"
title="{{hero.uiExtraTokenHtml2}} {{hero.uiExtraTokenName2}}"
(blur)="heroUpdateDebounceTimer.resetTimer()">
<adj-number-input name="heroCorruption" [(ngModel)]="hero.corruptionToken" minimum="0"
title="{{imgHtml('Tokens/CorruptToken.png','g-height-18')}} Corruption"
(blur)="heroUpdateDebounceTimer.resetTimer()" *ngIf="hero.uiShowCorruptionToken">
</adj-number-input>
</div>

View File

@ -15,7 +15,6 @@ import { HeroClass, MD2HeroInfo, MD2HeroProfile, MD2Icon } from '../massive-dark
import { MD2Base } from '../MD2Base';
import { MD2HeroProfileService } from '../service/massive-darkness2.service';
import { SignalRService } from '../../../services/signal-r.service';
import { NbToastrService } from '@nebular/theme';
@Component({
selector: 'ngx-hero-dashboard',
@ -47,9 +46,7 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
}
heroUpdateDebounceTimer = new DebounceTimer(1000, () => {
this.broadcastHeroInfo();
})
heroUpdateDebounceTimer = new DebounceTimer(1000, () => { this.broadcastHeroInfo(); })
classOptions: DropDownOption[] = [
new DropDownOption(HeroClass.Berserker, 'Berserker'),
@ -95,8 +92,7 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
private msgBoxService: MsgBoxService,
private signalRService: SignalRService,
private toastrService: NbToastrService
private signalRService: SignalRService
) {
super(md2Service, stateService, route, cdRef);
this.isHeroDashboard = true;
@ -113,10 +109,9 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
this.gameRoomService.joinGameRoom(this.roomId);
//this.fetchGameInfo();
this.signalRService.signalRMessageConnSubject.subscribe(state => {
//fetchGameInfo is called in MD2Base.handleSignalRCallback sendJoinInfo message
// if (state.status == 'connected') {
// this.fetchGameInfo();
// }
if (state.status == 'connected') {
this.fetchGameInfo();
}
});
}
@ -289,6 +284,7 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
this.hero.uiActivating = false;
}
this.broadcastHeroInfo();
}
endActivation() {
if (this.hero.remainActions > 0) {

View File

@ -57,9 +57,7 @@
({{md2Service.highestPlayerLevel}})</label>
</div> -->
<div class="col-12" *ngFor="let hero of md2Service.heros">
<label class='label mr-1'
(click)="adjustHeroValue(hero,'remainActions')">{{hero.playerInfo.name}}
({{heroClassName(hero)}} -
<label class='label mr-1'>{{hero.playerInfo.name}} ({{heroClassName(hero)}} -
{{hero.name}})</label>
<span class="badge badge-primary mr-1"
(click)="adjustHeroValue(hero,'level')">Lv.:{{hero.level}}</span>
@ -75,14 +73,6 @@
<span class="badge mr-1" *ngIf="hero.frozenToken">
<md2-icon [icon]="MD2Icon.FrozenToken" size="sm"></md2-icon>{{hero.frozenToken}}
</span>
<span class="badge mr-1" *ngIf="hero.extraToken">
<span [innerHtml]="hero.uiExtraTokenHtml"></span> {{hero.extraToken}}
</span>
<span class="badge mr-1" *ngIf="hero.extraToken2">
<span [innerHtml]="hero.uiExtraTokenHtml2"></span> {{hero.extraToken2}}
</span>
<span class="badge badge-success mr-1" *ngIf="hero.remainActions>0"
(click)="adjustHeroValue(hero,'remainActions')">Actions:
{{hero.remainActions}}</span>

View File

@ -17,8 +17,6 @@ import { SpawnMobDlgComponent } from './mobs/spawn-mob-dlg/spawn-mob-dlg.compone
import { BossMicheal } from './massive-darkness2.model.boss';
import { MD2InitService } from '../../services/MD2/md2-init.service';
import { NumberUtils } from '../../utilities/number-utils';
import { GameInitDlgComponent, GameInitConfig } from './game-init-dlg/game-init-dlg.component';
import { GameBundle } from './massive-darkness2.db.model';
@Component({
selector: 'ngx-massive-darkness2',
@ -45,62 +43,9 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
ngOnInit(): void {
super.ngOnInit();
this.md2Service.enemyPhaseSubject.pipe(takeUntil(this.destroy$)).subscribe(result => {
this.showEnemyPhaseAction(0);
});
this.showGameInitDialog();
}
private showGameInitDialog() {
// Only show dialog if game hasn't been initialized yet
if (!this.md2Service.initialized) {
const dialogRef = this.msgBoxService.dlgService.open(GameInitDlgComponent, {
closeOnBackdropClick: false,
closeOnEsc: false,
context: {
initialConfig: {
enabledBundles: this.md2Service.info.enabledBundles?.length > 0
? this.md2Service.info.enabledBundles
: [GameBundle.CoreGame],
enableMobSpecialRule: this.md2Service.info.enableMobSpecialRule || false,
enableHeroBetrayal: this.md2Service.info.enableHeroBetrayal || false
}
}
});
dialogRef.onClose.pipe(first()).subscribe((config: GameInitConfig) => {
if (config) {
this.initGameBundles(config);
} else {
// User cancelled, use defaults
this.initGameBundles({
enabledBundles: [GameBundle.CoreGame],
enableMobSpecialRule: false,
enableHeroBetrayal: false
});
}
});
} else {
// Game already initialized, just use current settings
this.initGameBundles({
enabledBundles: this.md2Service.info.enabledBundles,
enableMobSpecialRule: this.md2Service.info.enableMobSpecialRule,
enableHeroBetrayal: this.md2Service.info.enableHeroBetrayal
});
}
}
private initGameBundles(config: GameInitConfig) {
this.md2Service.info.enabledBundles = config.enabledBundles;
this.md2Service.info.enableMobSpecialRule = config.enableMobSpecialRule;
this.md2Service.info.enableHeroBetrayal = config.enableHeroBetrayal;
if (this.md2Service.initialized == false) {
this.gameRoomService.createGameRoom('MD2');
this.md2Service.initialized = true;
}
this.initService.initMobDecks();
this.initService.initTreasureBag();
}
@ -112,7 +57,7 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
inputType: 'number',
inputValue: hero[value].toString()
}).pipe(first()).subscribe(result => {
if (![false, null, undefined].includes(result)) {
if (result) {
hero[value] = Number.parseInt(result);
this.md2Service.broadcastHeroInfoToAll(hero, true);
this.detectChanges();
@ -133,12 +78,10 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
}
showQrCode() {
if (this.md2Service.initialized == false) {
this.gameRoomService.createGameRoom('MD2');
this.md2Service.initialized = true;
}
let initUrl = `${window.location.origin}/games/MD2_Hero/${this.gameRoomService.gameRoomId}`;
this.msgBoxService.show("Scan To Join", { text: `<img src='${this.qrCodeService.QRCodeUrl(initUrl, 5)}'><br><a href='${initUrl}' target='_blank'>Link</a>` });
}
@ -242,7 +185,7 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
public get round(): string {
if (this.md2Service.info.isBossFight) {
return `Boss Fight ${NumberUtils.Ordinal(this.md2Service.info.bossRound)} Round`;
return `Boss Fight ${NumberUtils.Ordinal(this.md2Service.info.boss.rounds)} Round`;
} else {
return NumberUtils.Ordinal(this.md2Service.info.round) + ' Round';
}

View File

@ -8,19 +8,14 @@ export enum MobSkillTarget {
HighestHp = 70,
HighestMp = 80,
LowestLevel = 90,
HighestLevel = 100,
MostExtraToken = 200,
LeastExtraToken,
MostExtraToken2,
LeastExtraToken2
MostCorruption = 200,
LeastCorruption = 201
}
export enum GameBundle {
CoreGame,
HeavenFallen,
Zombicide,
ZombicideWhiteDeath,
DarkBringerPack
Zombiecide
}
export interface MD2MobInfo {
id: string;
@ -31,7 +26,6 @@ export interface MD2MobInfo {
minionImgUrl: string;
mobLevelInfos: MD2MobLevelInfo[];
skills: MD2MobSkill[];
bossFightProfile?: BossFightProfile;
}
export interface MD2MobLevelInfo {
@ -50,10 +44,7 @@ export interface MD2MobLevelInfo {
defenceInfo: MD2DiceSet;
}
export class MD2MobSkill {
constructor(config: Partial<MD2MobSkill> = {}) {
Object.assign(this, config);
}
export interface MD2MobSkill {
id: string;
seq: number;
level: number;
@ -76,30 +67,4 @@ export interface MD2DiceSet {
blue: number | null;
green: number | null;
black: number | null;
}
export interface BossFightProfile {
mobInfoId: string;
id: string;
prerequisite: string;
objective: string;
specialRules: string;
extraTokenName: string;
extraTokenHtml: string;
extraTokenName2: string;
extraTokenHtml2: string;
phaseBuffs: BossFightPhaseBuff[];
}
export interface BossFightPhaseBuff {
id: string;
bossFightProfileId: string;
phase: number;
extraAction: number;
extraAttackDice: MD2DiceSet;
extraDefenceDice: MD2DiceSet;
extraHp: number;
extraTokenCount: number;
extraTokenCount2: number;
enableExtraBuffDescription: boolean;
extraBuffDescription: string;
}

View File

@ -6,65 +6,49 @@ import { StringUtils } from "../../utilities/string-utils";
import { GamePlayer } from "../games.model";
import { MD2HeroInfo, AttackTarget, HeroClass } from "./massive-darkness2.model";
import { MobSkill } from "./massive-darkness2.model.boss";
import { MobSkillTarget } from "./massive-darkness2.db.model";
export class MD2Logic {
public static getTargetHeroByFilter(heros: MD2HeroInfo[], targetType: MobSkillTarget) {
public static getTargetHeroByFilter(heros: MD2HeroInfo[], targetType: AttackTarget) {
return this.getTargetHerosByFilter(heros, targetType, true)[0];
}
public static getTargetHerosByFilter(heros: MD2HeroInfo[], targetType: MobSkillTarget, onlyOne: boolean = false) {
public static getTargetHerosByFilter(heros: MD2HeroInfo[], targetType: AttackTarget, onlyOne: boolean = false) {
let beenAttackedHero = [] as MD2HeroInfo[];
switch (targetType) {
case MobSkillTarget.LeastHp:
case AttackTarget.LeastHp:
let lowestHp = Math.min(...heros.map(h => h.hp));
beenAttackedHero = heros.filter(h => h.hp == lowestHp);
//this.otherAttackTarget = 'attacking the other <b>Lowest HP</b> hero.';
break;
case MobSkillTarget.LeastMp:
case AttackTarget.LeastMp:
let lowestMp = Math.min(...heros.map(h => h.mp));
beenAttackedHero = heros.filter(h => h.mp == lowestMp);
beenAttackedHero = heros.filter(h => h.hp == lowestMp);
//this.otherAttackTarget = 'attacking the other <b>Lowest HP</b> hero.';
break;
case MobSkillTarget.HighestHp:
case AttackTarget.HighestHp:
let highestHp = Math.max(...heros.map(h => h.hp));
beenAttackedHero = heros.filter(h => h.hp == highestHp);
//this.otherAttackTarget = 'attacking the other <b>Highest HP</b> hero.';
break;
case MobSkillTarget.HighestMp:
case AttackTarget.HighestMp:
let highestMp = Math.max(...heros.map(h => h.mp));
beenAttackedHero = heros.filter(h => h.mp == highestMp);
//this.otherAttackTarget = 'attacking the other <b>Highest Mp</b> hero.';
break;
case MobSkillTarget.LowestLevel:
let lowestLevel = Math.min(...heros.map(h => h.level));
case AttackTarget.LowestLevel:
let lowestLevel = Math.max(...heros.map(h => h.level));
beenAttackedHero = heros.filter(h => h.level == lowestLevel);
//this.otherAttackTarget = 'attacking the other <b>Lowest Level</b> hero.';
break;
case MobSkillTarget.HighestLevel:
let highestLevel = Math.max(...heros.map(h => h.level));
beenAttackedHero = heros.filter(h => h.level == highestLevel);
//this.otherAttackTarget = 'attacking the other <b>Lowest Level</b> hero.';
break;
case MobSkillTarget.LeastExtraToken:
case AttackTarget.LeastCorruption:
let leastExtraToken = Math.min(...heros.map(h => h.extraToken));
beenAttackedHero = heros.filter(h => h.extraToken == leastExtraToken);
let leastCor = Math.min(...heros.map(h => h.corruptionToken));
beenAttackedHero = heros.filter(h => h.corruptionToken == leastCor);
break;
case MobSkillTarget.MostExtraToken:
let mostExtraToken = Math.max(...heros.map(h => h.extraToken));
beenAttackedHero = heros.filter(h => h.extraToken == mostExtraToken);
case AttackTarget.MostCorruption:
let mostCor = Math.max(...heros.map(h => h.corruptionToken));
beenAttackedHero = heros.filter(h => h.corruptionToken == mostCor);
break;
case MobSkillTarget.LeastExtraToken2:
let leastExtraToken2 = Math.min(...heros.map(h => h.extraToken2));
beenAttackedHero = heros.filter(h => h.extraToken2 == leastExtraToken2);
break;
case MobSkillTarget.MostExtraToken2:
let mostExtraToken2 = Math.max(...heros.map(h => h.extraToken2));
beenAttackedHero = heros.filter(h => h.extraToken2 == mostExtraToken2);
break;
case MobSkillTarget.Random:
case AttackTarget.Random:
default:
beenAttackedHero = [heros[Math.round(Math.random() * (heros.length - 1))]];
//this.otherAttackTarget = 'Just act like normal.';

View File

@ -6,7 +6,7 @@ import { StringUtils } from "../../utilities/string-utils"
import { BossActivationComponent } from "./boss-fight/boss-activation/boss-activation.component"
import { TreasureType, AttackInfo, AttackType, MD2Icon, MD2HeroInfo, AttackTarget, MobInfo, MobType } from "./massive-darkness2.model"
import { RollingBlackDice } from "./massive-darkness2.model.dice"
import { MD2DiceSet, MD2MobSkill, MobSkillTarget } from "./massive-darkness2.db.model"
import { MD2DiceSet, MD2MobSkill } from "./massive-darkness2.db.model"
export enum MobSkillType {
@ -156,14 +156,14 @@ export class BossMicheal extends BossFight {
bossAction(): Observable<boolean> {
let actionResult = new RollingBlackDice().roll(2);
let actionResult = new RollingBlackDice().roll(this.actionBlackDice);
let actionHtml = '';
let beenAttackedHero = [] as MD2HeroInfo[];
let bossAction: MobSkill;
switch (actionResult.claws) {
case 0:
//Justice From Above
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.MostExtraToken, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.MostCorruption, true);
bossAction = new MobSkill(
{
name: 'Justice From Above',
@ -173,7 +173,7 @@ export class BossMicheal extends BossFight {
break;
case 1:
//Lance Dash
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastExtraToken, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastCorruption, true);
bossAction = new MobSkill({
name: 'Lance Dash',
description:
@ -193,13 +193,12 @@ export class BossMicheal extends BossFight {
default:
break;
}
return null;
//return this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction, currentAction: this.activatedTimes, allActions: this.actions } }).onClose;
return this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction, currentAction: this.activatedTimes, allActions: this.actions } }).onClose;
}
prepareForBossFight(): void {
this.md2Service.heros.forEach(hero => {
hero.uiShowExtraToken = true;
hero.uiShowCorruptionToken = true;
});
this.md2Service.msgBoxService.show('Prepare Boss Fight', {
text: `<h6>Place ${this.md2Service.heros.length * 2} ${this.corruptionTokenHtml} on the Corruption Stone Zones (Shadow
@ -283,7 +282,7 @@ export class BossReaper extends BossFight {
switch (actionResult.claws) {
case 0:
//Justice From Above
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastMp, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastMp, true);
bossAction = new MobSkill(
{
name: 'Soul Drain',
@ -293,7 +292,7 @@ export class BossReaper extends BossFight {
break;
case 1:
//Lance Dash
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastExtraToken, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastCorruption, true);
bossAction = new MobSkill({
name: 'Time Ticking',
description:
@ -314,8 +313,7 @@ export class BossReaper extends BossFight {
default:
break;
}
return null;
//return this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction, currentAction: this.activatedTimes, allActions: this.actions } }).onClose;
return this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction, currentAction: this.activatedTimes, allActions: this.actions } }).onClose;
}
prepareForBossFight(): void {
this.md2Service.msgBoxService.show('Prepare Boss Fight', {

View File

@ -5,7 +5,7 @@ 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 { BossFightProfile, GameBundle, MD2DiceSet, MD2MobSkill } from "./massive-darkness2.db.model";
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 {
@ -307,8 +307,6 @@ export class MobInfo implements IDrawingItem {
this.drawingWeight = 1;
this.unitRemainHp = config.hp
}
id: string;
from: GameBundle;
type: MobType = MobType.Mob;
imageUrl: string
standUrl: string
@ -335,15 +333,12 @@ export class MobInfo implements IDrawingItem {
fireToken: number = 0;
frozenToken: number = 0;
corruptionToken: number = 0;
uiExtraTokenCount: number = 0;
uiExtraTokenCount2: number = 0;
uiWounds: number;
uiFireTokens: number;
uiFrozenTokens: number;
uiCorruptionTokens: number;
uiAttackedBy: string;
extraRule: string;
bossFightProfile?: BossFightProfile;
get identifyName(): string {
return `${this.name}_${this.level}`;
}
@ -414,8 +409,7 @@ export class MD2HeroInfo {
level: number = 1;
fireToken: number = 0;
frozenToken: number = 0;
extraToken: number = 0;
extraToken2: number = 0;
corruptionToken: number = 0;
playerInfo: GamePlayer;
imgUrl: string;
skillHtml: string;
@ -423,15 +417,10 @@ export class MD2HeroInfo {
remainActions: number = 3;
rage: number = 0;
uiActivating = false;
uiExtraTokenHtml: string = '';
uiExtraTokenHtml2: string = '';
uiExtraTokenName: string = '';
uiExtraTokenName2: string = '';
uiShowExtraToken = false;
uiShowExtraToken2 = false;
uiShowCorruptionToken = false;
uiBossFight = false;
uiShowAttackBtn = false;
uiBetrayal = false;
public get heroFullName(): string {
return `${this.playerInfo.name} (${HeroClass[this.class]} - ${this.name})`
}

View File

@ -18,11 +18,10 @@ export class MD2HeroProfileMaintenanceComponent implements OnInit {
MD2Icon = MD2Icon;
public gridData: GridDataResult = { data: [], total: 0 };
private allData: MD2HeroProfile[] = [];
private lastSelectedHero: MD2HeroProfile;
private lastSelectedHeroClass: HeroClass;
public gridState: State = {
skip: 0,
take: 1000,
take: 10,
sort: [{
field: 'title',
dir: 'asc'
@ -78,11 +77,6 @@ export class MD2HeroProfileMaintenanceComponent implements OnInit {
public addHandler(): void {
const editorData = { heroClass: this.lastSelectedHeroClass || HeroClass.Berserker } as MD2HeroProfile;
if (this.lastSelectedHero) {
editorData.heroClass = this.lastSelectedHero.heroClass;
editorData.skillHtml = this.lastSelectedHero.skillHtml;
editorData.shadowSkillHtml = this.lastSelectedHero.shadowSkillHtml;
}
const dialogRef = this.dialogService.open({
title: 'Add New Hero Profile',
content: MD2HeroProfileEditorComponent,
@ -101,7 +95,6 @@ export class MD2HeroProfileMaintenanceComponent implements OnInit {
dialogRef.result.subscribe((result: MD2HeroProfile) => {
if (result) {
this.lastSelectedHero = result;
this.lastSelectedHeroClass = result.heroClass;
this.loadData();
}
@ -115,7 +108,7 @@ export class MD2HeroProfileMaintenanceComponent implements OnInit {
width: '90vw',
height: 600
});
this.lastSelectedHero = dataItem;
const editor = dialogRef.content.instance;
editor.isAdding = false;
editor.data = JSON.parse(JSON.stringify(dataItem));

View File

@ -145,6 +145,46 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn
}).result.subscribe((html: string) => {
if (html && this.editor) {
this.insertAfterSelection(html, true);
return;
// Insert the HTML content at the current cursor position
// Use ProseMirror's dispatch method to insert content at cursor
const view = this.editor.view;
if (view && view.state) {
try {
// Parse the HTML content
const dom = document.createElement('div');
dom.innerHTML = html;
// Use ProseMirror's DOMParser to parse HTML
// Access it from the view's state
const pmState = view.state;
// Use the editor's exec method or manual dispatch
if ((this.editor as any).exec) {
// Try using exec with 'insertHTML' command if available
try {
(this.editor as any).exec('insertHTML', html);
} catch (e) {
throw new Error('insertHTML not supported');
}
} else {
throw new Error('exec method not available');
}
} catch (e) {
console.error('Error inserting HTML:', e);
// Fallback: append to the end
const currentValue = this.editor.value || '';
const newValue = currentValue + ' ' + html;
this.value = newValue;
this.onChange(newValue);
}
} else {
// Fallback: append to the end
const currentValue = this.editor.value || '';
const newValue = currentValue + ' ' + html;
this.value = newValue;
this.onChange(newValue);
}
}
});
}
@ -405,7 +445,7 @@ export const rbjTagNodeSpec: NodeSpec = {
"span",
{
class: classValue,
"md2-icon": md2IconText,
// "rbj-tag-id": node.attrs["rbj-tag-id"],
// "tag-marker": node.attrs["tag-marker"],
// "tag-value": node.attrs["tag-value"],
// "tag-preview": node.attrs["tag-preview"],

View File

@ -37,7 +37,7 @@ import { MD2Service } from '../../../services/MD2/md2.service';
gap: 10px;
max-height: 400px;
overflow-y: auto;
//width:400px;
width:400px;
}
.icon-item {
cursor: pointer;

View File

@ -1,135 +0,0 @@
<div class="k-dialog-content">
<kendo-tabstrip tabPosition="top">
<kendo-tabstrip-tab title="Boss Fight Info" [selected]="true">
<ng-template kendoTabContent>
<form class="k-form" style="padding: 5px;">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="k-label g-cursor-pointer"
(click)="showInsertMD2Icon('extraTokenHtml')">Extra Token Name<span
class="tokenIconDiv" [innerHTML]="model.extraTokenHtml"></span></label>
<input kendoTextBox [(ngModel)]="model.extraTokenName" name="extraTokenName"
class="k-input" placeholder="Enter extra token name" />
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="k-label g-cursor-pointer"
(click)="showInsertMD2Icon('extraTokenHtml2')">Extra Token Name 2<span
class="tokenIconDiv" [innerHTML]="model.extraTokenHtml2"></span></label>
<input kendoTextBox [(ngModel)]="model.extraTokenName2" name="extraTokenName2"
class="k-input" placeholder="Enter extra token name 2" />
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="k-label">Prerequisite</label>
<md2-html-editor [(ngModel)]="model.prerequisite" name="prerequisite"
class="htmlEditor"></md2-html-editor>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="k-label">Objective</label>
<md2-html-editor [(ngModel)]="model.objective" name="objective"
class="htmlEditor"></md2-html-editor>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="k-label">Special Rules</label>
<md2-html-editor [(ngModel)]="model.specialRules" name="specialRules"
class="htmlEditor"></md2-html-editor>
</div>
</div>
</div>
</form>
</ng-template>
</kendo-tabstrip-tab>
<kendo-tabstrip-tab title="Phase Buffs">
<ng-template kendoTabContent>
<div style="padding: 20px;">
<div class="phase-buffs-toolbar" style="margin-bottom: 10px;">
<button kendoButton (click)="addPhaseBuffHandler()" [primary]="true">
<span class="k-icon k-i-plus"></span> Add Phase Buff
</button>
</div>
<kendo-grid #phaseBuffsGrid [data]="phaseBuffsData" [loading]="isLoading"
[pageSize]="phaseBuffsState.take" [skip]="phaseBuffsState.skip" [sortable]="true"
[filterable]="true" [pageable]="true" [height]="400" (remove)="removePhaseBuffHandler($event)"
(dataStateChange)="phaseBuffsState = $event; loadPhaseBuffs()">
<kendo-grid-column field="phase" title="Phase" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.phase }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="extraAction" title="Extra Action" [width]="100">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.extraAction }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="extraHp" title="Extra HP" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.extraHp }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="extraTokenCount" title="Extra Token Count" [width]="120">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.extraTokenCount }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="extraTokenCount2" title="Extra Token Count 2" [width]="140">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.extraTokenCount2 }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="extraBuffDescription" title="Extra Buff Description" [width]="300">
<ng-template kendoGridCellTemplate let-dataItem>
<div *ngIf="dataItem.enableExtraBuffDescription"
[innerHTML]="dataItem.extraBuffDescription">
</div>
<span *ngIf="!dataItem.enableExtraBuffDescription">-</span>
</ng-template>
</kendo-grid-column>
<kendo-grid-command-column title="Actions" [width]="133">
<ng-template kendoGridCellTemplate let-isNew="isNew" let-dataItem="dataItem"
let-rowIndex="rowIndex">
<button kendoButton [primary]="true"
(click)="editPhaseBuffHandler(dataItem)">Edit</button>
<button kendoGridRemoveCommand>Remove</button>
</ng-template>
</kendo-grid-command-column>
</kendo-grid>
</div>
</ng-template>
</kendo-tabstrip-tab>
</kendo-tabstrip>
</div>
<kendo-dialog-actions>
<button kendoButton (click)="close()">Cancel</button>
<button kendoButton [primary]="true" (click)="save()" [disabled]="!isValid || processing">
{{ processing ? 'Saving...' : 'Save' }}
</button>
</kendo-dialog-actions>

View File

@ -1,10 +0,0 @@
// Boss Fight Editor styles
.tokenIconDiv {
margin-left: 5px;
font-size: 30px;
img {
width: 30px;
height: 30px;
object-fit: contain;
}
}

View File

@ -1,216 +0,0 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { DialogRef, DialogContentBase, DialogService } from '@progress/kendo-angular-dialog';
import { GridComponent, GridDataResult } from '@progress/kendo-angular-grid';
import { State } from '@progress/kendo-data-query';
import { first } from 'rxjs/operators';
import { BossFightProfile, BossFightPhaseBuff } from '../../massive-darkness2.db.model';
import { MobSkillType } from '../../massive-darkness2.model.boss';
import { MD2BossFightProfileService, MD2PhaseBuffService } from '../../service/massive-darkness2.service';
import { MsgBoxService } from '../../../../services/msg-box.service';
import { MD2PhaseBuffEditorComponent } from '../md2-phase-buff-editor/md2-phase-buff-editor.component';
import { MD2IconPickerDlgComponent } from '../../md2-html-editor/md2-icon-picker-dlg.component';
@Component({
selector: 'ngx-md2-boss-fight-editor',
templateUrl: './md2-boss-fight-editor.component.html',
styleUrls: ['./md2-boss-fight-editor.component.scss']
})
export class MD2BossFightEditorComponent extends DialogContentBase implements OnInit {
@Input() public data: BossFightProfile;
@Input() public mobInfoId: string;
@ViewChild('phaseBuffsGrid') phaseBuffsGrid: GridComponent;
public model: BossFightProfile;
public phaseBuffs: BossFightPhaseBuff[] = [];
public phaseBuffsData: GridDataResult = { data: [], total: 0 };
public phaseBuffsState: State = {
skip: 0,
take: 10,
sort: [],
filter: {
logic: 'and',
filters: []
}
};
public isLoading: boolean = false;
public processing: boolean = false;
constructor(
public dialog: DialogRef,
private dialogService: DialogService,
private bossFightProfileService: MD2BossFightProfileService,
private phaseBuffService: MD2PhaseBuffService,
private msgBoxService: MsgBoxService
) {
super(dialog);
}
ngOnInit(): void {
this.initializeModel();
}
public initializeModel(): void {
this.model = {
id: this.data?.id || '',
mobInfoId: this.mobInfoId || this.data?.mobInfoId || '',
prerequisite: this.data?.prerequisite || '',
objective: this.data?.objective || '',
specialRules: this.data?.specialRules || '',
extraTokenName: this.data?.extraTokenName || '',
extraTokenHtml: this.data?.extraTokenHtml || '',
extraTokenName2: this.data?.extraTokenName2 || '',
extraTokenHtml2: this.data?.extraTokenHtml2 || '',
phaseBuffs: this.data?.phaseBuffs || []
};
this.phaseBuffs = this.model.phaseBuffs || [];
this.loadPhaseBuffs();
}
public loadPhaseBuffs(): void {
this.phaseBuffsData = {
data: this.phaseBuffs.sort((a, b) => a.phase - b.phase),
total: this.phaseBuffs.length
};
}
public addPhaseBuffHandler(): void {
if (!this.model) return;
const lastPhaseBuff = this.phaseBuffs.length > 0
? this.phaseBuffs.reduce((prev, current) => (prev.phase > current.phase) ? prev : current)
: null;
const nextPhase = lastPhaseBuff ? lastPhaseBuff.phase + 1 : 1;
const newPhaseBuff: BossFightPhaseBuff = {
id: this.generatePhaseBuffId(),
bossFightProfileId: this.model.id,
phase: nextPhase,
extraAction: 0,
extraAttackDice: { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
extraDefenceDice: { type: MobSkillType.Defense, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
extraHp: 0,
extraTokenCount: 0,
extraTokenCount2: 0,
enableExtraBuffDescription: false,
extraBuffDescription: ''
};
this.openPhaseBuffEditor(newPhaseBuff, true);
}
public editPhaseBuffHandler(dataItem: BossFightPhaseBuff): void {
if (!this.model) return;
const phaseBuffCopy: BossFightPhaseBuff = JSON.parse(JSON.stringify(dataItem));
this.openPhaseBuffEditor(phaseBuffCopy, false);
}
private openPhaseBuffEditor(phaseBuff: BossFightPhaseBuff, isNew: boolean): void {
if (!this.model) return;
const dialogRef = this.dialogService.open({
title: isNew ? 'Add New Phase Buff' : 'Edit Phase Buff',
content: MD2PhaseBuffEditorComponent,
width: '80vw',
height: 700
});
const editor = dialogRef.content.instance as MD2PhaseBuffEditorComponent;
editor.isAdding = isNew;
editor.data = phaseBuff;
editor.bossFightProfileId = this.model.id;
setTimeout(() => {
editor.initializeModel();
}, 0);
dialogRef.result.subscribe(result => {
if (result && typeof result === 'object' && 'id' in result) {
this.handlePhaseBuffSaved(result as BossFightPhaseBuff, isNew);
}
});
}
private handlePhaseBuffSaved(result: BossFightPhaseBuff, isNew: boolean): void {
if (!this.model) return;
if (isNew) {
if (!this.phaseBuffs) {
this.phaseBuffs = [];
}
this.phaseBuffs.push(result);
} else {
const index = this.phaseBuffs.findIndex(p => p.id === result.id);
if (index !== -1) {
this.phaseBuffs[index] = result;
}
}
this.model.phaseBuffs = this.phaseBuffs;
this.loadPhaseBuffs();
}
public removePhaseBuffHandler({ dataItem }: { dataItem: BossFightPhaseBuff }): void {
this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(answer => {
if (answer === true) {
this.isLoading = true;
this.phaseBuffService.delete(dataItem.id).pipe(first()).subscribe(result => {
const index = this.phaseBuffs.findIndex(p => p.id === dataItem.id);
if (index !== -1) {
this.phaseBuffs.splice(index, 1);
this.model.phaseBuffs = this.phaseBuffs;
this.loadPhaseBuffs();
}
this.isLoading = false;
});
}
});
}
private generatePhaseBuffId(): string {
return 'phasebuff_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
public close(): void {
this.dialog.close();
}
public save(): void {
if (!this.processing && this.model) {
this.processing = true;
this.model.phaseBuffs = this.phaseBuffs;
this.model.mobInfoId = this.mobInfoId || this.model.mobInfoId;
this.bossFightProfileService.createOrUpdate(this.model).pipe(first()).subscribe(result => {
this.processing = false;
this.dialog.close(result);
}, error => {
this.processing = false;
console.error('Error saving boss fight profile:', error);
});
}
}
showInsertMD2Icon(attributeName: string) {
this.dialogService.open({
title: 'Select MD2 Icon',
content: MD2IconPickerDlgComponent,
width: '800px',
height: 600
}).result.subscribe((html: string) => {
if (html && typeof html === 'string') {
this.model[attributeName] = html;
} else {
this.model[attributeName] = '';
}
});
}
public get isValid(): boolean {
if (!this.model) {
return false;
}
return this.model.mobInfoId !== '';
}
}

View File

@ -183,6 +183,5 @@
</div>
<kendo-dialog-actions>
<button kendoButton *ngIf="mobInfo?.type === MobType.Boss" (click)="editBossFightHandler()">Edit Boss Fight</button>
<button kendoButton [primary]="true" (click)="close()">Close</button>
</kendo-dialog-actions>

View File

@ -3,15 +3,13 @@ import { DialogRef, DialogContentBase, DialogService } from '@progress/kendo-ang
import { GridComponent, GridDataResult } from '@progress/kendo-angular-grid';
import { State } from '@progress/kendo-data-query';
import { first } from 'rxjs/operators';
import { MD2MobInfo, MD2MobLevelInfo, MD2MobSkill, MobSkillTarget, GameBundle, BossFightProfile } from '../../massive-darkness2.db.model';
import { MD2MobInfo, MD2MobLevelInfo, MD2MobSkill, MobSkillTarget, GameBundle } from '../../massive-darkness2.db.model';
import { MobType } from '../../massive-darkness2.model';
import { MobSkillType } from '../../massive-darkness2.model.boss';
import { MD2MobLevelInfoService, MD2MobSkillService } from '../../service/massive-darkness2.service';
import { MsgBoxService } from '../../../../services/msg-box.service';
import { MD2MobSkillEditorComponent } from '../md2-mob-skill-editor/md2-mob-skill-editor.component';
import { MD2MobLevelEditorComponent } from '../md2-mob-level-editor/md2-mob-level-editor.component';
import { MD2BossFightEditorComponent } from '../md2-boss-fight-editor/md2-boss-fight-editor.component';
import { MD2MobInfoService } from '../../service/massive-darkness2.service';
@Component({
selector: 'ngx-md2-mob-info-detail',
@ -20,7 +18,6 @@ import { MD2MobInfoService } from '../../service/massive-darkness2.service';
})
export class MD2MobInfoDetailComponent extends DialogContentBase implements OnInit {
MobSkillType = MobSkillType;
MobType = MobType;
@Input() public mobInfo: MD2MobInfo;
@ViewChild('levelsGrid') levelsGrid: GridComponent;
@ViewChild('skillsGrid') skillsGrid: GridComponent;
@ -57,7 +54,6 @@ export class MD2MobInfoDetailComponent extends DialogContentBase implements OnIn
private dialogService: DialogService,
private mobLevelInfoService: MD2MobLevelInfoService,
private mobSkillService: MD2MobSkillService,
private mobInfoService: MD2MobInfoService,
private msgBoxService: MsgBoxService
) {
super(dialog);
@ -438,53 +434,6 @@ export class MD2MobInfoDetailComponent extends DialogContentBase implements OnIn
return GameBundle[bundle] || '';
}
public editBossFightHandler(): void {
if (!this.mobInfo || this.mobInfo.type !== MobType.Boss) return;
// Ensure bossFightProfile is initialized
if (!this.mobInfo.bossFightProfile) {
this.mobInfo.bossFightProfile = {
id: 'bossfight_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
mobInfoId: this.mobInfo.id,
prerequisite: '',
objective: '',
specialRules: '',
extraTokenName: '',
extraTokenHtml: '',
extraTokenName2: '',
extraTokenHtml2: '',
phaseBuffs: []
};
}
// Create a copy of the boss fight profile for editing
const bossFightProfileCopy: BossFightProfile = JSON.parse(JSON.stringify(this.mobInfo.bossFightProfile));
const dialogRef = this.dialogService.open({
title: `Edit Boss Fight: ${this.mobInfo.name}`,
content: MD2BossFightEditorComponent,
width: '90vw',
height: '90vh'
});
const editor = dialogRef.content.instance as MD2BossFightEditorComponent;
editor.data = bossFightProfileCopy;
editor.mobInfoId = this.mobInfo.id;
setTimeout(() => {
editor.initializeModel();
}, 0);
dialogRef.result.subscribe(result => {
if (result && typeof result === 'object' && 'id' in result) {
// Reload mob info to get updated boss fight profile
this.mobInfoService.getById(this.mobInfo.id).pipe(first()).subscribe(mobInfo => {
this.mobInfo = mobInfo;
});
}
});
}
public close(): void {
this.dialog.close(true);
}

View File

@ -2,7 +2,7 @@ import { Component, Input, OnInit, ViewChild, ChangeDetectorRef } from '@angular
import { DialogRef, DialogContentBase } from '@progress/kendo-angular-dialog';
import { NgForm } from '@angular/forms';
import { first } from 'rxjs/operators';
import { MD2MobInfo, GameBundle, BossFightProfile } from '../../massive-darkness2.db.model';
import { MD2MobInfo, GameBundle } from '../../massive-darkness2.db.model';
import { MobType } from '../../massive-darkness2.model';
import { MD2MobInfoService } from '../../service/massive-darkness2.service';
@ -48,15 +48,9 @@ export class MD2MobInfoEditorComponent extends DialogContentBase implements OnIn
leaderImgUrl: this.data?.leaderImgUrl || '',
minionImgUrl: this.data?.minionImgUrl || '',
mobLevelInfos: this.data?.mobLevelInfos || [],
skills: this.data?.skills || [],
bossFightProfile: this.data?.bossFightProfile
skills: this.data?.skills || []
};
// Initialize bossFightProfile for Boss type mobs if not already set
if (typeValue === MobType.Boss && !this.model.bossFightProfile) {
this.model.bossFightProfile = this.createDefaultBossFightProfile(this.model.id);
}
// Set selected objects for dropdowns
this.selectedMobType = this.mobTypes.find(t => t.value === typeValue) || this.mobTypes[0] || null;
this.selectedGameBundle = this.gameBundles.find(b => b.value === fromValue) || this.gameBundles[0] || null;
@ -64,21 +58,6 @@ export class MD2MobInfoEditorComponent extends DialogContentBase implements OnIn
this.cdr.detectChanges();
}
private createDefaultBossFightProfile(mobInfoId: string): BossFightProfile {
return {
id: 'bossfight_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
mobInfoId: mobInfoId || '',
prerequisite: '',
objective: '',
specialRules: '',
extraTokenName: '',
extraTokenHtml: '',
extraTokenName2: '',
extraTokenHtml2: '',
phaseBuffs: []
};
}
private initializeEnums(): void {
// Initialize MobType options
@ -106,24 +85,13 @@ export class MD2MobInfoEditorComponent extends DialogContentBase implements OnIn
if (this.model.name && !this.processing) {
this.processing = true;
const mobType = this.selectedMobType?.value ?? MobType.Mob;
// Ensure bossFightProfile is initialized for Boss type
let bossFightProfile = this.model.bossFightProfile;
if (mobType === MobType.Boss && !bossFightProfile) {
bossFightProfile = this.createDefaultBossFightProfile(this.model.id);
} else if (mobType !== MobType.Boss) {
bossFightProfile = undefined;
}
// Extract enum values from selected objects
const mobInfo: MD2MobInfo = {
...this.model,
type: mobType,
type: this.selectedMobType?.value ?? MobType.Mob,
from: this.selectedGameBundle?.value ?? GameBundle.CoreGame,
mobLevelInfos: this.data?.mobLevelInfos || [],
skills: this.data?.skills || [],
bossFightProfile: bossFightProfile
skills: this.data?.skills || []
};
this.mobInfoService.createOrUpdate(mobInfo).pipe(first()).subscribe(result => {

View File

@ -11,8 +11,7 @@
<kendo-grid #grid [data]="gridData" [loading]="isLoading" [pageSize]="gridState.take" [skip]="gridState.skip"
[group]="gridState.group" [filter]="gridState.filter" [sort]="gridState.sort" [sortable]="true"
[filterable]="true" [pageable]="true" [selectable]="true" [groupable]="true"
(dataStateChange)="gridState = $event; processGridData()" (edit)="editHandler($event)"
(remove)="removeHandler($event)" (add)="addHandler()">
(dataStateChange)="gridState = $event; processGridData()">
<kendo-grid-toolbar>
<button kendoGridAddCommand>Add new</button>

View File

@ -1,180 +0,0 @@
<div class="k-dialog-content">
<form #form="ngForm" class="k-form">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Phase *</label>
<kendo-numerictextbox [(ngModel)]="model.phase" name="phase" [min]="1" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Extra Action</label>
<kendo-numerictextbox [(ngModel)]="model.extraAction" name="extraAction" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.HP"></md2-icon>Extra HP</label>
<kendo-numerictextbox [(ngModel)]="model.extraHp" name="extraHp" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Extra Token Count</label>
<kendo-numerictextbox [(ngModel)]="model.extraTokenCount" name="extraTokenCount" [min]="0"
[decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Extra Token Count 2</label>
<kendo-numerictextbox [(ngModel)]="model.extraTokenCount2" name="extraTokenCount2" [min]="0"
[decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Enable Extra Buff Description</label>
<input type="checkbox" [(ngModel)]="model.enableExtraBuffDescription" name="enableExtraBuffDescription" />
</div>
</div>
</div>
<div class="row" *ngIf="model.enableExtraBuffDescription">
<div class="col-md-12">
<div class="form-group">
<label class="k-label">Extra Buff Description</label>
<textarea [(ngModel)]="model.extraBuffDescription" name="extraBuffDescription" rows="4" class="k-input" style="width: 100%;"></textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h5>Extra Attack Dice</h5>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.Attack"></md2-icon>Attack Type</label>
<kendo-dropdownlist [(ngModel)]="selectedAttackType" name="extraAttackDice.type"
[data]="attackTypes" [textField]="'text'" [valueField]="'value'">
<ng-template kendoDropDownListItemTemplate let-dataItem>
<span [innerHTML]="dataItem.text"></span>
</ng-template>
<ng-template kendoDropDownListValueTemplate let-dataItem>
<span [innerHTML]="dataItem?.text || ''"></span>
</ng-template>
</kendo-dropdownlist>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.YellowDice"></md2-icon>Yellow Dice</label>
<kendo-numerictextbox [(ngModel)]="model.extraAttackDice.yellow" name="extraAttackDice.yellow"
[min]="0" [decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.OrangeDice"></md2-icon>Orange Dice</label>
<kendo-numerictextbox [(ngModel)]="model.extraAttackDice.orange" name="extraAttackDice.orange"
[min]="0" [decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.RedDice"></md2-icon>Red Dice</label>
<kendo-numerictextbox [(ngModel)]="model.extraAttackDice.red" name="extraAttackDice.red" [min]="0"
[decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.BlackDice"></md2-icon>Black Dice</label>
<kendo-numerictextbox [(ngModel)]="model.extraAttackDice.black" name="extraAttackDice.black"
[min]="0" [decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h5>Extra Defence Dice</h5>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.Defense"></md2-icon>Defence Type</label>
<kendo-dropdownlist [(ngModel)]="selectedDefenceType" name="extraDefenceDice.type"
[data]="attackTypes" [textField]="'text'" [valueField]="'value'">
<ng-template kendoDropDownListItemTemplate let-dataItem>
<span [innerHTML]="dataItem.text"></span>
</ng-template>
<ng-template kendoDropDownListValueTemplate let-dataItem>
<span [innerHTML]="dataItem?.text || ''"></span>
</ng-template>
</kendo-dropdownlist>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.BlueDice"></md2-icon>Blue Dice</label>
<kendo-numerictextbox [(ngModel)]="model.extraDefenceDice.blue" name="extraDefenceDice.blue"
[min]="0" [decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.GreenDice"></md2-icon>Green Dice</label>
<kendo-numerictextbox [(ngModel)]="model.extraDefenceDice.green" name="extraDefenceDice.green"
[min]="0" [decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.BlackDice"></md2-icon>Black Dice</label>
<kendo-numerictextbox [(ngModel)]="model.extraDefenceDice.black" name="extraDefenceDice.black"
[min]="0" [decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
</div>
</form>
</div>
<kendo-dialog-actions>
<button kendoButton (click)="close()">Cancel</button>
<button kendoButton [primary]="true" (click)="save()" [disabled]="!isValid || processing">
{{ processing ? 'Saving...' : 'Save' }}
</button>
</kendo-dialog-actions>

View File

@ -1,114 +0,0 @@
import { Component, Input, OnInit, ViewChild, ChangeDetectorRef } from '@angular/core';
import { DialogRef, DialogContentBase } from '@progress/kendo-angular-dialog';
import { NgForm } from '@angular/forms';
import { first } from 'rxjs/operators';
import { BossFightPhaseBuff, MD2DiceSet } from '../../massive-darkness2.db.model';
import { MobSkillType } from '../../massive-darkness2.model.boss';
import { MD2PhaseBuffService } from '../../service/massive-darkness2.service';
import { MD2Icon } from '../../massive-darkness2.model';
import { MD2Service } from '../../../../services/MD2/md2.service';
@Component({
selector: 'ngx-md2-phase-buff-editor',
templateUrl: './md2-phase-buff-editor.component.html',
styleUrls: ['./md2-phase-buff-editor.component.scss']
})
export class MD2PhaseBuffEditorComponent extends DialogContentBase implements OnInit {
MD2Icon = MD2Icon;
MobSkillType = MobSkillType;
@Input() public data: BossFightPhaseBuff;
@Input() public bossFightProfileId: string;
@Input() public isAdding: boolean = false;
@ViewChild('form') form: NgForm;
public model: BossFightPhaseBuff;
public processing: boolean = false;
public attackTypes: Array<{ value: MobSkillType; text: string }> = [];
public selectedAttackType: { value: MobSkillType; text: string } | null = null;
public selectedDefenceType: { value: MobSkillType; text: string } | null = null;
constructor(
public dialog: DialogRef,
private phaseBuffService: MD2PhaseBuffService,
private cdr: ChangeDetectorRef,
private md2Service: MD2Service
) {
super(dialog);
}
ngOnInit(): void {
this.initializeModel();
this.initializeEnums();
}
public initializeEnums(): void {
this.attackTypes = [
{ value: MobSkillType.Attack, text: 'None' },
{ value: MobSkillType.MeleeAttack, text: this.md2Service.iconHtml(MD2Icon.Melee) + ' Melee Attack' },
{ value: MobSkillType.RangeAttack, text: this.md2Service.iconHtml(MD2Icon.Range) + ' Range Attack' },
{ value: MobSkillType.MagicAttack, text: this.md2Service.iconHtml(MD2Icon.Magic) + ' Magic Attack' },
{ value: MobSkillType.Defense, text: this.md2Service.iconHtml(MD2Icon.Defense) + ' Defense' }
];
this.selectedAttackType = this.attackTypes.find(t => t.value === this.model.extraAttackDice?.type) || this.attackTypes[0] || null;
this.selectedDefenceType = this.attackTypes.find(t => t.value === this.model.extraDefenceDice?.type) || this.attackTypes[4] || null;
}
public initializeModel(): void {
this.model = {
id: this.data?.id || '',
bossFightProfileId: this.bossFightProfileId || this.data?.bossFightProfileId || '',
phase: this.data?.phase ?? 1,
extraAction: this.data?.extraAction ?? 0,
extraAttackDice: this.data?.extraAttackDice
? { ...this.data.extraAttackDice }
: { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
extraDefenceDice: this.data?.extraDefenceDice
? { ...this.data.extraDefenceDice }
: { type: MobSkillType.Defense, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
extraHp: this.data?.extraHp ?? 0,
extraTokenCount: this.data?.extraTokenCount ?? 0,
extraTokenCount2: this.data?.extraTokenCount2 ?? 0,
enableExtraBuffDescription: this.data?.enableExtraBuffDescription ?? false,
extraBuffDescription: this.data?.extraBuffDescription || ''
};
this.cdr.detectChanges();
}
public close(): void {
this.dialog.close();
}
public save(): void {
if (!this.processing) {
this.processing = true;
// Ensure required objects exist
if (!this.model.extraAttackDice) {
this.model.extraAttackDice = { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null };
}
if (!this.model.extraDefenceDice) {
this.model.extraDefenceDice = { type: MobSkillType.Defense, yellow: null, orange: null, red: null, blue: null, green: null, black: null };
}
this.model.extraAttackDice.type = this.selectedAttackType?.value ?? MobSkillType.Attack;
this.model.extraDefenceDice.type = this.selectedDefenceType?.value ?? MobSkillType.Defense;
this.model.bossFightProfileId = this.bossFightProfileId || this.model.bossFightProfileId;
this.phaseBuffService.createOrUpdate(this.model).pipe(first()).subscribe(result => {
this.processing = false;
this.dialog.close(result);
}, error => {
this.processing = false;
console.error('Error saving phase buff:', error);
});
}
}
public get isValid(): boolean {
if (!this.model) {
return false;
}
return this.model.phase > 0 && this.model.bossFightProfileId !== '';
}
}

View File

@ -32,8 +32,8 @@
// HP and Mana Bars Overlay
.hero-stats-overlay {
position: relative;
bottom: 60px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem;
@ -41,6 +41,9 @@
border-radius: 0 0 8px 8px;
z-index: 1;
width: 95%;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.35rem;
}
}
.stat-bar-overlay {

View File

@ -14,7 +14,6 @@ import { StringUtils } from '../../../utilities/string-utils';
import { DrawingBag, DrawingItem, MD2Icon, MobDlgType, MobInfo, TreasureType } from '../massive-darkness2.model';
import { MD2Base, MD2ComponentBase } from '../MD2Base';
import { SpawnMobDlgComponent } from './spawn-mob-dlg/spawn-mob-dlg.component';
import { GameBundle } from '../massive-darkness2.db.model';
@Component({
selector: 'md2-mobs',
@ -89,24 +88,20 @@ export class MobsComponent extends MD2ComponentBase implements OnInit {
}
spawnSpecificMob() {
let mobOptions = this.isRoamingMonster ?
this.md2Service.allRoamingMonsterInfos.map(f => new DropDownOption(f.id, `${StringUtils.camelToTitle(GameBundle[f.from])} - ${f.name}`)) :
this.md2Service.allMobInfos.map(f => new DropDownOption(f.id, `${StringUtils.camelToTitle(GameBundle[f.from])} - ${f.name}`));
mobOptions = mobOptions.sort((a, b) => a.value1.localeCompare(b.value1));
let mobOptions = this.isRoamingMonster ? this.md2Service.allRoamingMonsterInfos.map(f => new DropDownOption(f.name, f.name)) : this.md2Service.allMobInfos.map(f => new DropDownOption(f.name, f.name));
this.msgBoxService.showInputbox('Spawn', '',
{
inputType: 'dropdown', dropDownOptions: mobOptions,
buttons: ADButtons.YesNoCancel,
confirmButtonText: 'Spawn',
cancelButtonText: 'Random'
}).pipe(first()).subscribe(mobId => {
}).pipe(first()).subscribe(mobName => {
if (mobId || mobId === false) {
if (mobName || mobName === false) {
if (!mobId) { mobId = null; }
let result = this.md2Service.spawnMob(this.isRoamingMonster, mobId);
if (!mobName) { mobName = null; }
let result = this.md2Service.spawnMob(this.isRoamingMonster, mobName);
let titleText = result.exitingMob == null ? `${result.mob.description} Shows Up` : `${result.mob.description} Activate One Action Now!`;
let actType = result.exitingMob == null ? MobDlgType.Spawn : MobDlgType.Activating;
let mob = result.exitingMob == null ? result.mob : result.exitingMob;

View File

@ -12,8 +12,7 @@ import { MD2ComponentBase } from '../../MD2Base';
import { ADButtons, ADIcon } from '../../../../ui/alert-dlg/alert-dlg.model';
import { MobSkillType } from '../../massive-darkness2.model.boss';
import { Observable, from, of } from 'rxjs';
import { MD2MobSkill, MobSkillTarget } from '../../massive-darkness2.db.model';
import { MD2Logic } from '../../massive-darkness2.logic';
import { MD2MobSkill } from '../../massive-darkness2.db.model';
type SkillResolutionResult = { result: boolean; skill: MD2MobSkill | null };
@ -32,7 +31,7 @@ export class SpawnMobDlgComponent extends MD2ComponentBase implements OnInit {
MD2Icon = MD2Icon;
mob: MobInfo;
actionInfoHtml: string;
beenAttackedHero = null as MD2HeroInfo;
beenAttackedHero = [] as MD2HeroInfo[];
attackTarget: string;
otherAttackTarget: string;
constructor(
@ -158,45 +157,50 @@ export class SpawnMobDlgComponent extends MD2ComponentBase implements OnInit {
if (this.mode == MobDlgType.Spawn) {
htmlText = `${this.mob.description} Shows Up`;
} else if (this.mode == MobDlgType.Activating) {
let targetType = null as MobSkillTarget;
let randomAttack = Math.random() * MobSkillTarget.HighestLevel;
const values = Object.values(MobSkillTarget);
let targetType = null as AttackTarget;
let randomAttack = Math.random() * AttackTarget.LowestLevel;
const values = Object.values(AttackTarget);
const stringKeys = Object
.keys(MobSkillTarget)
.keys(AttackTarget)
.filter((v) => isNaN(Number(v)))
for (let i = 0; i < stringKeys.length; i++) {
const element = MobSkillTarget[stringKeys[i]];
const element = AttackTarget[stringKeys[i]];
if (element >= randomAttack) {
targetType = element;
break;
}
}
this.beenAttackedHero = MD2Logic.getTargetHeroByFilter(this.md2Service.heros, targetType);
switch (targetType) {
case MobSkillTarget.LeastHp:
this.otherAttackTarget = 'attacking the other <b>Lowest HP</b> hero.';
case AttackTarget.LeastHp:
let lowestHp = Math.min(...this.md2Service.heros.map(h => h.hp));
this.beenAttackedHero = this.md2Service.heros.filter(h => h.hp == lowestHp);
//this.otherAttackTarget = 'attacking the other <b>Lowest HP</b> hero.';
break;
case MobSkillTarget.HighestHp:
this.otherAttackTarget = 'attacking the other <b>Highest HP</b> hero.';
case AttackTarget.HighestHp:
let highestHp = Math.max(...this.md2Service.heros.map(h => h.hp));
this.beenAttackedHero = this.md2Service.heros.filter(h => h.hp == highestHp);
//this.otherAttackTarget = 'attacking the other <b>Highest HP</b> hero.';
break;
case MobSkillTarget.HighestMp:
this.otherAttackTarget = 'attacking the other <b>Highest Mana</b> hero.';
case AttackTarget.HighestMp:
let highestMp = Math.max(...this.md2Service.heros.map(h => h.mp));
this.beenAttackedHero = this.md2Service.heros.filter(h => h.mp == highestMp);
//this.otherAttackTarget = 'attacking the other <b>Highest Mp</b> hero.';
break;
case MobSkillTarget.LowestLevel:
this.otherAttackTarget = 'attacking the other <b>Lowest Level</b> hero.';
case AttackTarget.LowestLevel:
let lowestLevel = Math.max(...this.md2Service.heros.map(h => h.level));
this.beenAttackedHero = this.md2Service.heros.filter(h => h.level == lowestLevel);
//this.otherAttackTarget = 'attacking the other <b>Lowest Level</b> hero.';
break;
case MobSkillTarget.HighestLevel:
this.otherAttackTarget = 'attacking the other <b>Highest Level</b> hero.';
break;
case MobSkillTarget.Random:
case AttackTarget.Random:
default:
this.otherAttackTarget = 'Just act like normal.';
this.beenAttackedHero = [this.md2Service.heros[Math.round(Math.random() * (this.md2Service.heros.length - 1))]];
//this.otherAttackTarget = 'Just act like normal.';
break;
}
//let attackedHeros = StringUtils.makeCommaSeparatedString(this.beenAttackedHero.map(h => this.md2Service.heroFullName(h)), true, true);
this.attackTarget = `Attacking <b>${this.beenAttackedHero.heroFullName}</b> first if possible, otherwise ${this.otherAttackTarget}`;
let attackedHeros = StringUtils.makeCommaSeparatedString(this.beenAttackedHero.map(h => this.md2Service.heroFullName(h)), true, true);
this.attackTarget = `Attacking <b>${attackedHeros}</b> first if possible.`;
htmlText = `${this.title}`;
} else {
htmlText = `${this.mob.description}`;

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { CrudService } from '../../../services/crudServices/crud.service';
import { MD2MobInfo, MD2MobLevelInfo, MD2MobSkill, BossFightProfile, BossFightPhaseBuff } from '../massive-darkness2.db.model';
import { MD2MobInfo, MD2MobLevelInfo, MD2MobSkill } from '../massive-darkness2.db.model';
import { HttpClient } from '@angular/common/http';
import { MD2HeroProfile } from '../massive-darkness2.model';
@ -47,26 +47,4 @@ export class MD2HeroProfileService extends CrudService<MD2HeroProfile> {
super(http, (action: string = null) => { return `MD2HeroProfile${(action ? `/${action}` : '')}` });
}
}
@Injectable({
providedIn: 'root'
})
export class MD2BossFightProfileService extends CrudService<BossFightProfile> {
constructor(http: HttpClient) {
super(http, (action: string = null) => { return `MD2BossFightProfile${(action ? `/${action}` : '')}` });
}
}
@Injectable({
providedIn: 'root'
})
export class MD2PhaseBuffService extends CrudService<BossFightPhaseBuff> {
constructor(http: HttpClient) {
super(http, (action: string = null) => { return `MD2BossFightPhaseBuff${(action ? `/${action}` : '')}` });
}
}

View File

@ -31,17 +31,16 @@ export class MD2InitService {
this.mobInfoService.getAll().pipe(first()).subscribe(result => {
this.md2Service.mobInfos = result.filter(m => m.type == MobType.Mob);
this.md2Service.roamingMobInfos = result.filter(m => m.type == MobType.RoamingMonster);
this.md2Service.bossInfos = result.filter(m => m.type == MobType.Boss);
for (let i = 0; i < result.length; i++) {
const mobInfo = result[i];
for (let j = 0; j < mobInfo.mobLevelInfos.length; j++) {
const levelInfo = mobInfo.mobLevelInfos[j];
switch (mobInfo.type) {
case MobType.Mob:
this.md2Service.mobDeck.AddItem(new MobInfo({ id: mobInfo.id, name: mobInfo.name, from: mobInfo.from, level: levelInfo.level, drawingWeight: 1 }));
this.md2Service.mobDeck.AddItem(new MobInfo({ name: mobInfo.name, level: levelInfo.level, drawingWeight: 1 }));
break;
case MobType.RoamingMonster:
this.md2Service.roamingMobDeck.AddItem(new MobInfo({ id: mobInfo.id, name: mobInfo.name, from: mobInfo.from, level: levelInfo.level, drawingWeight: 1 }));
this.md2Service.roamingMobDeck.AddItem(new MobInfo({ name: mobInfo.name, level: levelInfo.level, drawingWeight: 1 }));
break;
}
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import { AttackInfo, AttackTarget, CoreGameDarknessPhaseRule, DrawingBag, DrawingItem, HeroClass, IDarknessPhaseRule, MD2EnemyPhaseSpecialInfo, MD2EnemyPhaseSpecialRule, MD2HeroInfo, MD2Icon, MD2Rules, MobInfo, MobType, RoundPhase, TreasureItem, TreasureType } from '../../games/massive-darkness2/massive-darkness2.model';
import { AttackInfo, AttackTarget, CoreGameDarknessPhaseRule, DrawingBag, DrawingItem, HeroClass, IDarknessPhaseRule, MD2HeroInfo, MD2Icon, MD2Rules, MobInfo, MobType, RoundPhase, TreasureItem, TreasureType } from '../../games/massive-darkness2/massive-darkness2.model';
import { first, map, reduce } from "rxjs/operators";
import { NbDialogService, NbThemeService } from '@nebular/theme';
import { Observable, Subject } from 'rxjs';
import { BossFight, MobSkillType } from '../../games/massive-darkness2/massive-darkness2.model.boss';
import { NbDialogService } from '@nebular/theme';
import { Subject } from 'rxjs';
import { BossMicheal, BossReaper, IBossFight } from '../../games/massive-darkness2/massive-darkness2.model.boss';
import { ADIcon, MessageBoxConfig } from '../../ui/alert-dlg/alert-dlg.model';
import { NumberUtils } from '../../utilities/number-utils';
import { StringUtils } from '../../utilities/string-utils';
@ -16,11 +16,8 @@ import { MD2Logic } from '../../games/massive-darkness2/massive-darkness2.logic'
import { MD2InitService } from './md2-init.service';
import { ArrayUtils } from '../../utilities/array-utils';
import { DropDownOption } from '../../entity/dropDownOption';
import { GameBundle, MD2DiceSet, MD2MobInfo, MD2MobSkill, MobSkillTarget } from '../../games/massive-darkness2/massive-darkness2.db.model';
import { GameBundle, MD2DiceSet, MD2MobInfo } from '../../games/massive-darkness2/massive-darkness2.db.model';
import { environment } from '../../../environments/environment';
import { RollingBlackDice } from '../../games/massive-darkness2/massive-darkness2.model.dice';
import { BossActivationComponent } from '../../games/massive-darkness2/boss-fight/boss-activation/boss-activation.component';
import { NbToastrService } from '@nebular/theme';
const MD2_IMG_URL = (id: string = null) => { return `${environment.apiUrl}/Files/Images/MD2/Mobs${(id ? `${encodeURI(id)}` : '')}` };
@ -32,10 +29,9 @@ export class MD2Service {
private _highestPlayerLevel: number = 1;
private _playerAmount: number = 2;
public specialRule: MD2EnemyPhaseSpecialInfo;
public info: MD2GameInfo;
public playerHero: MD2HeroInfo;
public bossInfos: MD2MobInfo[] = [];
public mobInfos: MD2MobInfo[] = [];
public roamingMobInfos: MD2MobInfo[] = [];
public mobDeck: DrawingBag<MobInfo>;
@ -79,16 +75,13 @@ export class MD2Service {
public gameRoomService: GameRoomService,
public loginUserService: LoginUserService,
public signalRService: SignalRService,
public dlgService: NbDialogService,
public themeService: NbThemeService,
public toastrService: NbToastrService
public dlgService: NbDialogService
) {
this.darknessPhaseRule = new CoreGameDarknessPhaseRule();
this.info = new MD2GameInfo();
this.darknessPhaseRule.addTreasureToken.subscribe(treasureType => {
this.addTreasure(treasureType, 1);
});
this.specialRule = new MD2EnemyPhaseSpecialInfo();
}
// #endregion Constructors (1)
@ -135,30 +128,18 @@ export class MD2Service {
this.info.roundPhase = RoundPhase.HeroPhase;
if (!this.info.isBossFight) {
this.info.round++;
if (this.info.round == 5 && this.info.enableHeroBetrayal) {
let betrayalHero = this.getTargetHerosByFilter(MobSkillTarget.LowestLevel, true)[0];
this.sendMsgboxMsg(betrayalHero.playerInfo.signalRClientId,
{
title: 'Dark Lords Invitation', text: 'Child of light… why pretend?<br>' +
'I have seen the hunger within you, the one your allies fear you will one day unleash.<br>' +
'Serve me openly, and the world shall kneel before you.<br>' +
'One act of betrayal—one moment of truth—and you will become the legend the others only pretend to be.', icon: ADIcon.WARNING
});
betrayalHero.uiBetrayal = true;
this.broadcastGameInfo();
}
if (this.darknessPhaseRule.runDarknessPhase()) {
this.msgBoxService.show(`${NumberUtils.Ordinal(this.info.round)} Hero Phase`, { icon: ADIcon.INFO });
}
} else {
this.bossDarknessPhase();
this.msgBoxService.show(`Boss Fight - ${NumberUtils.Ordinal(this.info.bossRound)} Hero Phase`, { icon: ADIcon.INFO });
this.info.boss.darknessPhase();
this.msgBoxService.show(`Boss Fight - ${NumberUtils.Ordinal(this.info.boss.rounds)} Hero Phase`, { icon: ADIcon.INFO });
}
//this.runNextPhase();
}
public spawnMob(isRoamingMonster: boolean, mobId: string = null) {
public spawnMob(isRoamingMonster: boolean, mobName: string = null) {
let mobDeck = null as DrawingBag<MobInfo>;
let level = 1;
if (this.highestPlayerLevel < 3) {
@ -173,17 +154,17 @@ export class MD2Service {
mobDeck = this.mobDeck;
}
if (mobDeck.drawingItems.filter(m => (m as MobInfo).level == level && this.info.enabledBundles.includes((m as MobInfo).from))
if (mobDeck.drawingItems.filter(m => (m as MobInfo).level == level)
.map(d => d.drawingWeight).reduce((a, b) => a + b, 0) == 0) {
mobDeck.RestoreRemoveItems();
}
let newSpawnMob = null as MobInfo;
if (mobId) {
newSpawnMob = new MobInfo(mobDeck.DrawAndRemove(1, m => m.level == level && m.id == mobId)[0]);
if (mobName) {
newSpawnMob = new MobInfo(mobDeck.DrawAndRemove(1, m => m.level == level && m.name == mobName)[0]);
} else {
newSpawnMob = new MobInfo(mobDeck.DrawAndRemove(1, m => m.level == level && this.info.enabledBundles.includes(m.from))[0]);
newSpawnMob = new MobInfo(mobDeck.DrawAndRemove(1, m => m.level == level)[0]);
}
let exitingMob = isRoamingMonster ? this.roamingMonsters.find(m => m.name == newSpawnMob.name) : this.mobs.find(m => m.name == newSpawnMob.name);
@ -238,15 +219,15 @@ export class MD2Service {
mobInfo.skills = mobInfo.skills.sort((a, b) => b.seq - a.seq);
switch (dbMobInfo.type) {
case MobType.Mob:
mobInfo.leaderImgUrl = !!dbMobInfo.leaderImgUrl ? this.imgUrl(dbMobInfo.leaderImgUrl) : MD2_IMG_URL(`/${GameBundle[dbMobInfo.from]}/Mobs/${mobInfo.name}/Leader.png`);
mobInfo.minionImgUrl = !!dbMobInfo.minionImgUrl ? this.imgUrl(dbMobInfo.minionImgUrl) : MD2_IMG_URL(`/${GameBundle[dbMobInfo.from]}/Mobs/${mobInfo.name}/Minion.png`);
mobInfo.leaderImgUrl = mobInfo.leaderImgUrl || MD2_IMG_URL(`/${GameBundle[dbMobInfo.from]}/Mobs/${mobInfo.name}/Leader.png`);
mobInfo.minionImgUrl = mobInfo.minionImgUrl || MD2_IMG_URL(`/${GameBundle[dbMobInfo.from]}/Mobs/${mobInfo.name}/Minion.png`);
break;
case MobType.RoamingMonster:
mobInfo.leaderImgUrl = !!dbMobInfo.leaderImgUrl ? this.imgUrl(dbMobInfo.leaderImgUrl) : MD2_IMG_URL(`/${GameBundle[dbMobInfo.from]}/RoamingMonsters/${mobInfo.name}/Stand.png`);
mobInfo.leaderImgUrl = mobInfo.leaderImgUrl || MD2_IMG_URL(`/${GameBundle[dbMobInfo.from]}/RoamingMonsters/${mobInfo.name}/Stand.png`);
mobInfo.minionImgUrl = null;
break;
case MobType.Boss:
mobInfo.leaderImgUrl = !!dbMobInfo.leaderImgUrl ? this.imgUrl(dbMobInfo.leaderImgUrl) : this.imgUrl(`/Boss/${mobInfo.name}-Stand.png`);
mobInfo.leaderImgUrl = mobInfo.leaderImgUrl || MD2_IMG_URL(`/Boss/${mobInfo.name}-Stand.png`);
mobInfo.minionImgUrl = null;
break;
default:
@ -282,28 +263,10 @@ export class MD2Service {
private getAttackInfo(attackInfo: MD2DiceSet): AttackInfo {
return new AttackInfo(attackInfo.type as unknown as MD2Icon, attackInfo.yellow, attackInfo.orange, attackInfo.red, attackInfo.black);
}
public enemyPhase(triggerSpecialRule: boolean = true) {
public enemyPhase() {
//this.msgBoxService
//Draw a special rule
this.enemyPhaseMobs = this.roamingMonsters.concat(this.mobs);
if (this.info.enableMobSpecialRule && this.mobs.length > 0 && triggerSpecialRule) {
let specialRuleDrawingBag = new DrawingBag<MD2EnemyPhaseSpecialRule>(this.specialRule.specialRules);
let specialRule = specialRuleDrawingBag.Draw(1)[0];
this.specialRule.specialRule = specialRule;
if (specialRule.description) {
this.themeService.changeTheme('dark');
this.msgBoxService.show(specialRule.title, { text: specialRule.description, icon: ADIcon.WARNING })
.pipe(first()).subscribe(result => {
this.enemyPhase(false);
});
return;
} else {
this.specialRule.specialRule = null;
}
}
if (this.enemyPhaseMobs.length > 0) {
this.enemyPhaseMobs = ArrayUtils.Shuffle(this.enemyPhaseMobs);
//this.showEnemyPhaseAction();
@ -314,58 +277,34 @@ export class MD2Service {
}
public enterBossFight() {
this.msgBoxService.showInputbox('Boss Fight', 'Choose the boss', {
inputType: 'dropdown',
dropDownOptions: this.bossInfos.map(b => new DropDownOption(b.id, b.name))
dropDownOptions: [new DropDownOption('The Reaper', 'The Reaper'), new DropDownOption('Michael - The Corrupted Archangel', 'Michael - The Corrupted Archangel')]
}).pipe(first()).subscribe(result => {
if (result) {
this.info.mobs = [];
this.info.roamingMonsters = [];
let bossInfo = this.bossInfos.find(b => b.id == result);
this.info.boss = new MobInfo(bossInfo);
this.info.boss.leaderImgUrl = !!this.info.boss.leaderImgUrl ? this.imgUrl(this.info.boss.leaderImgUrl) : this.imgUrl(`/Boss/${this.info.boss.name}-Stand.png`);
let bossLevelInfo = bossInfo.mobLevelInfos.sort((a, b) => b.level - a.level)
.find(l => l.level <= Math.max(...this.info.heros.map(h => h.level)));
if (bossLevelInfo) {
this.info.boss.hp = bossLevelInfo.hpPerHero * this.info.heros.length;
this.info.boss.unitRemainHp = this.info.boss.hp;
this.info.boss.actions = bossLevelInfo.actions;
this.info.boss.attackInfos = [this.getAttackInfo(bossLevelInfo.attackInfo)];
let altAttackInfo = bossLevelInfo.alterAttackInfo;
if (altAttackInfo
&& (altAttackInfo.black > 0 || altAttackInfo.blue > 0 || altAttackInfo.green > 0 || altAttackInfo.orange > 0 || altAttackInfo.red > 0 || altAttackInfo.yellow > 0)
) {
this.info.boss.attackInfos.push(this.getAttackInfo(altAttackInfo));
}
this.info.boss.defenseInfo = bossLevelInfo.defenceInfo;
if (result == 'The Reaper') {
this.info.boss = new BossReaper(this);
} else {
this.info.boss = new BossMicheal(this);
}
this.info.roamingMonsters = [];
this.info.mobs = [];
this.info.isBossFight = true;
this.info.isBossFight = true;
this.info.boss.info.hp = this.info.boss.info.hpPerHero * this.info.heros.length;
this.info.boss.info.unitRemainHp = this.info.boss.info.hp;
this.refreshUI$.next();
this.prepareForBossFight();
this.info.boss.prepareForBossFight();
this.levelUpPhase(false);
//Reset all heroes
let extraTokenName = this.info.boss.bossFightProfile.extraTokenName;
let extraTokenHtml = this.info.boss.bossFightProfile.extraTokenHtml;
let extraTokenName2 = this.info.boss.bossFightProfile.extraTokenName2;
let extraTokenHtml2 = this.info.boss.bossFightProfile.extraTokenHtml2;
this.heros.forEach(hero => {
hero.hp = hero.hpMaximum;
hero.mp = hero.mpMaximum;
hero.remainActions = 3;
hero.uiActivating = false;
hero.uiBossFight = true;
hero.uiExtraTokenHtml = extraTokenHtml;
hero.uiExtraTokenName = extraTokenName;
hero.uiShowExtraToken = !!extraTokenName;
hero.uiShowExtraToken2 = !!extraTokenName2;
hero.uiExtraTokenHtml2 = extraTokenHtml2;
hero.uiExtraTokenName2 = extraTokenName2;
});
this.broadcastGameInfo();
}
@ -373,6 +312,9 @@ export class MD2Service {
});
//this.sendMsgboxMsg
}
public activateBoss() {
this.info.boss.activating();
}
public fileList(folderPath: string) {
return this.fileService.FileList('Images/MD2/' + folderPath);
}
@ -442,11 +384,10 @@ export class MD2Service {
this.info.roundPhase++;
switch (this.info.roundPhase) {
case RoundPhase.HeroPhase:
//HeroPhase will be handled in the darkness phase
// this.heros.forEach(hero => {
// hero.remainActions = 3;
// this.broadcastHeroInfoToOwner(hero);
// });
this.heros.forEach(hero => {
hero.remainActions = 3;
this.broadcastHeroInfoToOwner(hero);
});
break;
case RoundPhase.EnemyPhase:
this.enemyPhase();
@ -456,7 +397,6 @@ export class MD2Service {
break;
case RoundPhase.DarknessPhase:
this.darknessPhase();
this.themeService.changeTheme('default');
break;
default: break;
}
@ -477,7 +417,7 @@ export class MD2Service {
});
}
public getTargetHerosByFilter(targetType: MobSkillTarget, onlyOne: boolean = false) {
public getTargetHerosByFilter(targetType: AttackTarget, onlyOne: boolean = false) {
return MD2Logic.getTargetHerosByFilter(this.info.heros, targetType, onlyOne);
}
@ -543,39 +483,39 @@ export class MD2Service {
return `<span md2-icon='${String.fromCharCode(65 + icon)}' class='MD2Icon dice ${MD2Icon[icon].replace('Dice', '')} ${cssClass}'></span>`;
} else {
if (!cssClass) {
cssClass = 'MD2IconImg g-height-25 mr-1';
cssClass = 'g-height-25 mr-1';
}
//image based icons
switch (icon) {
case MD2Icon.HP_Color:
return this.imgHtml('HeartIcon.png', cssClass, 'HP');
return this.imgHtml('HeartIcon.png', cssClass);
case MD2Icon.Mana_Color:
return this.imgHtml('ManaIcon.png', cssClass, 'Mana');
return this.imgHtml('ManaIcon.png', cssClass);
case MD2Icon.CorruptToken:
return this.imgHtml('Tokens/CorruptToken.png', cssClass, 'Corruption Token');
return this.imgHtml('Tokens/CorruptToken.png', cssClass);
case MD2Icon.TimeToken:
return this.imgHtml('Tokens/TimeToken.png', cssClass, 'Time Token');
return this.imgHtml('Tokens/TimeToken.png', cssClass);
case MD2Icon.FireToken:
return this.imgHtml('Tokens/FireToken.png', cssClass, 'Fire Token');
return this.imgHtml('Tokens/FireToken.png', cssClass);
case MD2Icon.FrozenToken:
return this.imgHtml('Tokens/FrozenToken.png', cssClass, 'Frozen Token');
return this.imgHtml('Tokens/FrozenToken.png', cssClass);
case MD2Icon.TreasureToken:
return this.imgHtml('TreasureToken/Cover.png', cssClass, 'Treasure Token');
return this.imgHtml('TreasureToken/Cover.png', cssClass);
case MD2Icon.TreasureToken_Common:
return this.imgHtml('TreasureToken/Common.png', cssClass, 'Common Treasure Token');
return this.imgHtml('TreasureToken/Common.png', cssClass);
case MD2Icon.TreasureToken_Rare:
return this.imgHtml('TreasureToken/Rare.png', cssClass, 'Rare Treasure Token');
return this.imgHtml('TreasureToken/Rare.png', cssClass);
case MD2Icon.TreasureToken_Epic:
return this.imgHtml('TreasureToken/Epic.png', cssClass, 'Epic Treasure Token');
return this.imgHtml('TreasureToken/Epic.png', cssClass);
case MD2Icon.TreasureToken_Legendary:
return this.imgHtml('TreasureToken/Legendary.png', cssClass, 'Legendary Treasure Token');
return this.imgHtml('TreasureToken/Legendary.png', cssClass);
}
}
}
public imgHtml(imgPath: string, cssClass = 'g-height-25 mr-1', imgTitle = '') {
return `<img src='${this.imgUrl(imgPath)}' class='${cssClass}' title='${imgTitle}'>`;
public imgHtml(imgPath: string, cssClass = 'g-height-25 mr-1') {
return `<img src='${this.imgUrl(imgPath)}' class='${cssClass}'>`;
}
public imgUrl(imgPath: string) {
@ -646,6 +586,7 @@ export class MD2Service {
actionName: actionName,
parameters: parameters
} as SignalRMessage;
if (sessionId == null) {
message.receiver = { isGroup: true, connectionId: this.gameRoomService.gameRoomId } as SignalRClient
} else {
@ -656,7 +597,14 @@ export class MD2Service {
}
public broadcastGameInfo() {
let parameters = {};
if (this.info.boss) {
this.info.boss.md2Service = undefined;
}
parameters['gameInfo'] = JSON.stringify(this.info);
if (this.info.boss) {
this.info.boss.md2Service = this;
}
this.broadcastMessage('GameRoom', 'update', parameters);
}
broadcastFetchGameInfo() {
@ -680,192 +628,8 @@ export class MD2Service {
}
// #endregion Public Methods (27)
//#region Boss Fight
private bossActivatedTimes: number = 0;
activateBoss(): boolean {
this.bossActivatedTimes = this.info.boss.actions;
this.runBossAction();
return true;
}
runBossAction() {
this.bossAction().pipe(first()).subscribe(result => {
this.bossActivatedTimes--;
if (this.bossActivatedTimes) {
this.runBossAction();
} else {
if (false == this.heros.some(h => h.remainActions > 0)) {
this.darknessPhase();
}
}
});
}
bossAction(): Observable<boolean> {
let actionBlackDice = 2;
let actionResult = new RollingBlackDice().roll(actionBlackDice);
let actionHtml = '';
let beenAttackedHero = [] as MD2HeroInfo[];
let bossAction: MD2MobSkill;
bossAction = new MD2MobSkill(this.info.boss.skills.find(s => s.type == MobSkillType.ActiveSkill && s.skillRoll == actionResult.claws));
bossAction.description = this.skillParse(bossAction.description, bossAction.skillTarget);
return this.dlgService.open(BossActivationComponent, { context: { boss: this.info.boss, bossAction: bossAction, currentAction: this.bossActivatedTimes, allActions: this.info.boss.actions } }).onClose;
}
prepareForBossFight(): void {
this.heros.forEach(hero => {
hero.uiShowExtraToken = !!this.info.boss.bossFightProfile.extraTokenName;
hero.uiShowExtraToken2 = !!this.info.boss.bossFightProfile.extraTokenName2;
});
//init boss first round buff
this.bossDarknessPhase();
let prerequisiteHtml = this.info.boss.bossFightProfile.prerequisite;
//Detect prerequisiteHtml is empty or not,like <p></p>
if (prerequisiteHtml.trim() == '<p></p>') {
prerequisiteHtml = '';
}
if (prerequisiteHtml) {
prerequisiteHtml = this.skillParse(prerequisiteHtml, MobSkillTarget.Random);
this.msgBoxService.show('Prepare Boss Fight', {
text: prerequisiteHtml
});
}
}
bossDarknessPhase(): void {
this.info.bossRound++;
let roundBuff = this.info.boss.bossFightProfile.phaseBuffs.find(p => p.phase == this.info.bossRound);
if (roundBuff) {
this.info.boss.actions += roundBuff.extraAction || 0;
for (let i = 0; i < this.info.boss.attackInfos.length; i++) {
this.info.boss.attackInfos[i].black += roundBuff.extraAttackDice.black || 0;
this.info.boss.attackInfos[i].yellow += roundBuff.extraAttackDice.yellow || 0;
this.info.boss.attackInfos[i].orange += roundBuff.extraAttackDice.orange || 0;
this.info.boss.attackInfos[i].red += roundBuff.extraAttackDice.red || 0;
}
this.info.boss.defenseInfo.black += roundBuff.extraDefenceDice.black || 0;
this.info.boss.defenseInfo.yellow += roundBuff.extraDefenceDice.yellow || 0;
this.info.boss.defenseInfo.orange += roundBuff.extraDefenceDice.orange || 0;
this.info.boss.defenseInfo.red += roundBuff.extraDefenceDice.red || 0;
this.info.boss.hp += roundBuff.extraHp || 0;
this.info.boss.unitRemainHp += roundBuff.extraHp || 0;
this.info.boss.uiExtraTokenCount += roundBuff.extraTokenCount || 0;
this.info.boss.uiExtraTokenCount2 += roundBuff.extraTokenCount2 || 0;
}
}
//#endregion Boss Fight
public skillParse(skillHtmlContent: string, targetRule: MobSkillTarget, onlyOneTarget: boolean = true) {
let targetHeros = MD2Logic.getTargetHerosByFilter(this.info.heros, targetRule, onlyOneTarget);
//if the skillHtmlContent contains ${Expression} replace it with the expression result
//Example: ${1+1} will be replaced with 2
//bossExtraTokenCount will be replaced with this.info.boss.uiExtraTokenCount
//bossExtraTokenCount2 will be replaced with this.info.boss.uiExtraTokenCount2
//targetHero.level will be replaced with the hero level
//targetHero.hp will be replaced with the hero hp
//targetHero.mp will be replaced with the hero mp
//targetHero.ap will be replaced with the hero ap
//targetHero.fireToken will be replaced with the hero fire token
//targetHero.frozenToken will be replaced with the hero frozen token
//targetHero.uiExtraTokenCount will be replaced with the hero extra token count
//targetHero.uiExtraTokenCount2 will be replaced with the hero extra token count2
//For example:
//${targetHero.level+bossExtraTokenCount} will be replaced with the number
//then calculate the result and replace the ${Expression} with the result
//Example: ${1+1} will be replaced with 2
//${3-1}
//${2*3}
//${2/3}
//${2%3}
// No target heroes found, still process boss tokens
let result = skillHtmlContent;
result = result.replace(/\bheroes.length\b/g, String(this.info?.heros?.length || 0));
if (result.includes('boss.') && this.info?.boss) {
// Replace boss tokens even if no heroes
result = result.replace(/\bboss.ExtraTokenCount\b/g, String(this.info?.boss?.uiExtraTokenCount || 0));
result = result.replace(/\bboss.ExtraTokenCount2\b/g, String(this.info?.boss?.uiExtraTokenCount2 || 0));
}
// Evaluate remaining expressions (if any numbers remain)
if (result.includes('targetHero') && targetHeros && targetHeros.length > 0) {
// Use the first hero as targetHero for replacements
const targetHero = targetHeros[0];
// Replace targetHero properties
result = result.replace(/\btargetHero\.name\b/g, String(targetHero?.heroFullName || 0));
result = result.replace(/\btargetHero\.level\b/g, String(targetHero?.level || 0));
result = result.replace(/\btargetHero\.hp\b/g, String(targetHero?.hp || 0));
result = result.replace(/\btargetHero\.mp\b/g, String(targetHero?.mp || 0));
result = result.replace(/\btargetHero\.ap\b/g, String(targetHero?.ap || 0));
result = result.replace(/\btargetHero\.fireToken\b/g, String(targetHero?.fireToken || 0));
result = result.replace(/\btargetHero\.frozenToken\b/g, String(targetHero?.frozenToken || 0));
// Map uiExtraTokenCount to extraToken and uiExtraTokenCount2 to extraToken2
result = result.replace(/\btargetHero\.uiExtraTokenCount\b/g, String(targetHero?.extraToken || 0));
result = result.replace(/\btargetHero\.uiExtraTokenCount2\b/g, String(targetHero?.extraToken2 || 0));
}
// Evaluate expressions in ${...} format
result = this.evaluateExpressions(result);
return result;
}
private evaluateExpressions(content: string): string {
// Match ${expression} pattern
const expressionPattern = /\$\{([^}]+)\}/g;
return content.replace(expressionPattern, (match, expression) => {
try {
const sanitizedExpression = expression.trim();
// After placeholder replacement, expression should only contain numbers, operators, and spaces
// Check if expression still contains letters (variables that weren't replaced)
if (/[a-zA-Z_]/.test(sanitizedExpression)) {
// Variables weren't replaced, return original match
return match.replace('${', '').replace('}', '');
}
// Validate expression contains only safe mathematical characters
// Allow: numbers, decimal points, operators (+, -, *, /, %), parentheses, and spaces
if (!/^[0-9+\-*/().%\s]+$/.test(sanitizedExpression)) {
// Contains unsafe characters, return original
return match;
}
// Evaluate the expression safely using Function constructor
// This is safer than eval() as it runs in strict mode
const result = Function('"use strict"; return (' + sanitizedExpression + ')')();
// Check if result is a valid number
if (typeof result === 'number' && !isNaN(result) && isFinite(result)) {
return String(result);
}
// Invalid result, return original match
return match;
} catch (error) {
// If evaluation fails for any reason, return the original match
return match;
}
});
}
}
export class MD2GameInfo {
@ -880,8 +644,8 @@ export class MD2GameInfo {
this.mobs = this.mobs.map(m => new MobInfo(m));
this.roamingMonsters = this.roamingMonsters.map(m => new MobInfo(m));
if (this.boss) {
this.boss = new MobInfo(this.boss);
if (this.boss && this.boss.info) {
this.boss.info = new MobInfo(this.boss.info);
}
this.heros = this.heros.map(h => new MD2HeroInfo(h));
}
@ -893,12 +657,7 @@ export class MD2GameInfo {
public heros: MD2HeroInfo[] = [];
public disconnectedHeroes: MD2HeroInfo[] = [];
public round = 1;
public bossRound = 0;
public roundPhase: RoundPhase = RoundPhase.HeroPhase;
public showAttackBtn: boolean = false;
public boss: MobInfo;
public enabledBundles: GameBundle[] = [GameBundle.CoreGame];
public enableMobSpecialRule: boolean = false;
public enableHeroBetrayal: boolean = false;
public boss: IBossFight;
}

View File

@ -12,6 +12,6 @@
// background: nb-theme(color-primary-200) !important;
// }
// .label {
// color: #736f6f;
// }
.label {
color: #736f6f;
}

View File

@ -26,10 +26,7 @@ p {
}
button,
a,
input {
touch-action: manipulation;
}
k-editor-content p {
line-height: 1rem;
}