From 46ec236ed5e2e246cf255cad3c020b11b5f3cabe Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Tue, 4 Nov 2025 12:42:10 -0800 Subject: [PATCH] WIP --- src/app/games/games.module.ts | 8 +- .../md2-html-editor.component.html | 2 +- .../md2-html-editor.component.ts | 64 ++++++++-- .../md2-icon-picker-dlg.component.ts | 23 ++-- .../md2-icon/md2-icon.component.html | 2 +- .../md2-icon/md2-icon.component.ts | 11 +- .../md2-mob-info-detail.component.html | 39 ++---- .../md2-mob-info-detail.component.ts | 91 ++++++++------ .../md2-mob-skill-editor.component.html | 64 ++++++++++ .../md2-mob-skill-editor.component.scss | 8 ++ .../md2-mob-skill-editor.component.ts | 115 ++++++++++++++++++ 11 files changed, 335 insertions(+), 92 deletions(-) create mode 100644 src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.html create mode 100644 src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.scss create mode 100644 src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.ts diff --git a/src/app/games/games.module.ts b/src/app/games/games.module.ts index 97fcb3a..3be727c 100644 --- a/src/app/games/games.module.ts +++ b/src/app/games/games.module.ts @@ -43,10 +43,12 @@ import { GridModule } from '@progress/kendo-angular-grid'; import { DialogModule } from '@progress/kendo-angular-dialog'; import { InputsModule } from '@progress/kendo-angular-inputs'; import { DropDownsModule } from '@progress/kendo-angular-dropdowns'; +import { LayoutModule } from '@progress/kendo-angular-layout'; import { ReactiveFormsModule } from '@angular/forms'; import { MD2MobInfoMaintenanceComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-info-maintenance.component'; import { MD2MobInfoEditorComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-info-editor/md2-mob-info-editor.component'; 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'; @NgModule({ @@ -79,7 +81,8 @@ import { MD2MobInfoDetailComponent } from './massive-darkness2/md2-mob-info-main MD2IconPickerDlgComponent, MD2MobInfoMaintenanceComponent, MD2MobInfoEditorComponent, - MD2MobInfoDetailComponent + MD2MobInfoDetailComponent, + MD2MobSkillEditorComponent ], imports: [ CommonModule, @@ -119,7 +122,8 @@ import { MD2MobInfoDetailComponent } from './massive-darkness2/md2-mob-info-main GridModule, DialogModule, InputsModule, - DropDownsModule + DropDownsModule, + LayoutModule ] }) export class GamesModule { } diff --git a/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.html b/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.html index 0712807..281dd7a 100644 --- a/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.html +++ b/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.html @@ -1,5 +1,5 @@ + [schema]="messageTemplateSchema" [iframe]="false" class="h-100"> diff --git a/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts b/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts index fb0939a..5d07069 100644 --- a/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts +++ b/src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts @@ -1,5 +1,5 @@ -import { Component, ElementRef, EventEmitter, Inject, Input, NgZone, Output, PLATFORM_ID, Renderer2, ViewChild, AfterViewInit } from '@angular/core'; -import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors } from '@angular/forms'; +import { Component, ElementRef, EventEmitter, Inject, Input, NgZone, Output, PLATFORM_ID, Renderer2, ViewChild, AfterViewInit, forwardRef, ChangeDetectorRef } from '@angular/core'; +import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors, NG_VALUE_ACCESSOR } from '@angular/forms'; import { EditorComponent, NodeSpec, schema, Schema, FontSizeItem } from '@progress/kendo-angular-editor'; import { MsgBoxService } from '../../../services/msg-box.service'; @@ -10,10 +10,18 @@ import { MD2StateService } from '../../../services/MD2/md2-state.service'; import { MD2IconPickerDlgComponent } from './md2-icon-picker-dlg.component'; import { NbDialogService } from '@nebular/theme'; import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model'; +import { DialogService } from '@progress/kendo-angular-dialog'; @Component({ selector: 'md2-html-editor', templateUrl: './md2-html-editor.component.html', - styleUrl: './md2-html-editor.component.scss' + styleUrl: './md2-html-editor.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MD2HtmlEditorComponent), + multi: true + } + ] }) export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewInit { @ViewChild(EditorComponent) editor: EditorComponent; @@ -52,7 +60,8 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn constructor( private msgBoxService: MsgBoxService, private md2StateService: MD2StateService, - private dialogService: NbDialogService, + private dialogService: DialogService, + private cdr: ChangeDetectorRef, elementRef: ElementRef, ngZone: NgZone, @Inject(PLATFORM_ID) platformId: Object) { } @@ -71,12 +80,38 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn this.fontSizeDropdown.defaultItem = this.defaultFontSize; } } + + // Ensure the editor value is set if writeValue was called before view init + if (this.editor && this.value && this.editor.value !== this.value) { + this.editor.value = this.value; + } }, 0); } // ControlValueAccessor implementation - writeValue(value: string): void { - this.value = value || ''; + writeValue(value: string | null | undefined): void { + const newValue = value || ''; + + // Only update if the value actually changed to avoid unnecessary updates + if (this.value !== newValue) { + this.value = newValue; + + // Angular's [value] binding will handle updating the editor + // But if editor is already initialized and value is out of sync, ensure sync + if (this.editor && this.editor.value !== this.value) { + // Use setTimeout to avoid ExpressionChangedAfterItHasBeenCheckedError + setTimeout(() => { + if (this.editor && this.editor.value !== this.value) { + this.editor.value = this.value; + } + }, 0); + } + + // Trigger change detection to ensure the binding updates + if (!this.cdr['destroyed']) { + this.cdr.markForCheck(); + } + } } registerOnChange(fn: (value: string) => void): void { @@ -92,8 +127,11 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn } onChange(value: string): void { - this.value = value; - this.onChangeFn(value); + // Only update if value actually changed to prevent infinite loops + if (this.value !== value) { + this.value = value || ''; + this.onChangeFn(this.value); + } } onTouched(): void { @@ -101,10 +139,12 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn } showInsertMD2Icon() { - this.dialogService.open(MD2IconPickerDlgComponent, { - closeOnBackdropClick: true, - closeOnEsc: true - }).onClose.pipe(first()).subscribe((html: string) => { + this.dialogService.open({ + title: 'Insert MD2 Icon', + content: MD2IconPickerDlgComponent, + width: '800px', + height: 600 + }).result.subscribe((html: string) => { if (html && this.editor) { this.insertAfterSelection(html, true); return; diff --git a/src/app/games/massive-darkness2/md2-html-editor/md2-icon-picker-dlg.component.ts b/src/app/games/massive-darkness2/md2-html-editor/md2-icon-picker-dlg.component.ts index c41703d..e06775c 100644 --- a/src/app/games/massive-darkness2/md2-html-editor/md2-icon-picker-dlg.component.ts +++ b/src/app/games/massive-darkness2/md2-html-editor/md2-icon-picker-dlg.component.ts @@ -2,16 +2,11 @@ import { Component, OnInit } from '@angular/core'; import { NbDialogRef } from '@nebular/theme'; import { MD2Icon } from '../massive-darkness2.model'; import { MD2StateService } from '../../../services/MD2/md2-state.service'; +import { DialogRef } from '@progress/kendo-angular-dialog'; @Component({ selector: 'md2-icon-picker-dlg', - template: ` - - -
Insert MD2 Icon
-
- -
+ template: `
+ `, styles: [` + nb-card{ + max-width: 800px; + z-index: 1050; + } .md2-icon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); @@ -57,7 +62,7 @@ export class MD2IconPickerDlgComponent implements OnInit { iconList: Array<{ icon: MD2Icon, name: string, html: string }> = []; constructor( - private dlgRef: NbDialogRef, + private dlgRef: DialogRef, private md2StateService: MD2StateService ) { } diff --git a/src/app/games/massive-darkness2/md2-icon/md2-icon.component.html b/src/app/games/massive-darkness2/md2-icon/md2-icon.component.html index ac3e628..dc32e52 100644 --- a/src/app/games/massive-darkness2/md2-icon/md2-icon.component.html +++ b/src/app/games/massive-darkness2/md2-icon/md2-icon.component.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/games/massive-darkness2/md2-icon/md2-icon.component.ts b/src/app/games/massive-darkness2/md2-icon/md2-icon.component.ts index 97a9df8..fa4ff36 100644 --- a/src/app/games/massive-darkness2/md2-icon/md2-icon.component.ts +++ b/src/app/games/massive-darkness2/md2-icon/md2-icon.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { MD2Icon } from '../massive-darkness2.model'; +import { MD2StateService } from '../../../services/MD2/md2-state.service'; @Component({ selector: 'md2-icon', @@ -10,13 +11,17 @@ export class MD2IconComponent implements OnInit { @Input() iconClass: string = 'mr-1'; - + iconHtml: string; private _icon: string | MD2Icon; @Input() public set icon(v: string | MD2Icon) { if (this._icon != v) { this._icon = v; - + //if it's string, convert it to MD2Icon + if (typeof v === 'string') { + v = MD2Icon[v]; + } + this.iconHtml = this.md2StateService.iconHtml(v as MD2Icon); } if (this.isMD2Icon(v)) { this.iconName = MD2Icon[v].toLowerCase(); @@ -30,7 +35,7 @@ export class MD2IconComponent implements OnInit { } @Input() size: string = 'sm'; iconName: string; - constructor() { } + constructor(private md2StateService: MD2StateService) { } ngOnInit(): void { } diff --git a/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.html b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.html index 9ec6d79..526c712 100644 --- a/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.html +++ b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.html @@ -158,12 +158,11 @@ + (remove)="removeSkillHandler($event)" (dataStateChange)="skillsState = $event"> - - + + {{ dataItem.name }} @@ -171,55 +170,37 @@ {{ getSkillTypeName(dataItem.type) }} - - - - {{ getSkillTargetName(dataItem.skillTarget) }} - - - - - - - + + {{ dataItem.clawRoll }} - - - + + {{ dataItem.skillRoll }} - - + +
- + - - - -
diff --git a/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.ts b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.ts index a4b45b5..d2d323e 100644 --- a/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.ts +++ b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; -import { DialogRef, DialogContentBase } from '@progress/kendo-angular-dialog'; +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'; @@ -8,6 +8,7 @@ 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'; @Component({ selector: 'ngx-md2-mob-info-detail', @@ -48,6 +49,7 @@ export class MD2MobInfoDetailComponent extends DialogContentBase implements OnIn constructor( public dialog: DialogRef, + private dialogService: DialogService, private mobLevelInfoService: MD2MobLevelInfoService, private mobSkillService: MD2MobSkillService, private msgBoxService: MsgBoxService @@ -224,45 +226,64 @@ export class MD2MobInfoDetailComponent extends DialogContentBase implements OnIn description: '' }; - if (!this.selectedLevelInfo.skills) { - this.selectedLevelInfo.skills = []; + this.openSkillEditor(newSkill, true); + } + + public editSkillHandler(dataItem: MD2MobSkill): void { + if (!this.selectedLevelInfo) return; + + // Create a copy of the skill for editing + const skillCopy: MD2MobSkill = JSON.parse(JSON.stringify(dataItem)); + this.openSkillEditor(skillCopy, false); + } + + private openSkillEditor(skill: MD2MobSkill, isNew: boolean): void { + if (!this.selectedLevelInfo) return; + + const dialogRef = this.dialogService.open({ + title: isNew ? 'Add New Skill' : 'Edit Skill', + content: MD2MobSkillEditorComponent, + width: '80vw', + height: 700 + }); + + const editor = dialogRef.content.instance; + editor.isAdding = isNew; + editor.data = skill; + editor.mobLevelInfoId = this.selectedLevelInfo.id; + + // Force model re-initialization after data is set + setTimeout(() => { + editor.initializeModel(); + }, 0); + + dialogRef.result.subscribe(result => { + if (result && typeof result === 'object' && 'id' in result) { + this.handleSkillSaved(result as MD2MobSkill, isNew); + } + }); + } + + private handleSkillSaved(result: MD2MobSkill, isNew: boolean): void { + if (!this.selectedLevelInfo) return; + + if (isNew) { + if (!this.selectedLevelInfo.skills) { + this.selectedLevelInfo.skills = []; + } + this.selectedLevelInfo.skills.push(result); + } else { + const index = this.selectedLevelInfo.skills?.findIndex(s => s.id === result.id); + if (index !== undefined && index !== -1 && this.selectedLevelInfo.skills) { + this.selectedLevelInfo.skills[index] = result; + } } - this.selectedLevelInfo.skills.push(newSkill); this.loadSkills(this.selectedLevelInfo); } public saveSkillHandler({ dataItem, isNew }: any): void { - if (isNew) { - dataItem.id = this.generateId(); - dataItem.mobLevelInfoId = this.selectedLevelInfo?.id; - } - - this.isLoading = true; - this.mobSkillService.createOrUpdate(dataItem).pipe(first()).subscribe(result => { - this.isLoading = false; - if (this.selectedLevelInfo) { - if (isNew) { - if (!this.selectedLevelInfo.skills) { - this.selectedLevelInfo.skills = []; - } - const index = this.selectedLevelInfo.skills.findIndex(s => s.id === dataItem.id); - if (index !== -1) { - this.selectedLevelInfo.skills[index] = result; - } else { - this.selectedLevelInfo.skills.push(result); - } - } else { - const index = this.selectedLevelInfo.skills.findIndex(s => s.id === result.id); - if (index !== -1) { - this.selectedLevelInfo.skills[index] = result; - } - } - this.loadSkills(this.selectedLevelInfo); - } - }, error => { - this.isLoading = false; - console.error('Error saving skill:', error); - }); + // This method is no longer used but kept for backward compatibility + // Skills are now edited via dialog } public removeSkillHandler({ dataItem }: { dataItem: MD2MobSkill }): void { diff --git a/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.html b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.html new file mode 100644 index 0000000..06bd472 --- /dev/null +++ b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.html @@ -0,0 +1,64 @@ +
+
+ +
+
+
+ + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + +
+
+
+ + + +
+
+ + + + + \ No newline at end of file diff --git a/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.scss b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.scss new file mode 100644 index 0000000..7028e00 --- /dev/null +++ b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.scss @@ -0,0 +1,8 @@ +.k-label { + font-weight: 500; +} + +md2-html-editor { + width: 100%; + min-height: 300px; +} diff --git a/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.ts b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.ts new file mode 100644 index 0000000..14bdfb8 --- /dev/null +++ b/src/app/games/massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component.ts @@ -0,0 +1,115 @@ +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 { MD2MobSkill, MobSkillTarget } from '../../massive-darkness2.db.model'; +import { MobSkillType } from '../../massive-darkness2.model.boss'; +import { MD2MobSkillService } from '../../service/massive-darkness2.service'; + +@Component({ + selector: 'ngx-md2-mob-skill-editor', + templateUrl: './md2-mob-skill-editor.component.html', + styleUrls: ['./md2-mob-skill-editor.component.scss'] +}) +export class MD2MobSkillEditorComponent extends DialogContentBase implements OnInit { + @Input() public data: MD2MobSkill; + @Input() public mobLevelInfoId: string; + @Input() public isAdding: boolean = false; + @ViewChild('form') form: NgForm; + + public model: MD2MobSkill; + public processing: boolean = false; + public skillTypes: Array<{ value: MobSkillType; text: string }> = []; + public skillTargets: Array<{ value: MobSkillTarget | null; text: string }> = []; + public selectedSkillType: { value: MobSkillType; text: string } | null = null; + public selectedSkillTarget: { value: MobSkillTarget | null; text: string } | null = null; + + constructor( + public dialog: DialogRef, + private mobSkillService: MD2MobSkillService, + private cdr: ChangeDetectorRef + ) { + super(dialog); + this.initializeEnums(); + } + + ngOnInit(): void { + this.initializeModel(); + } + + public initializeModel(): void { + const typeValue = this.data?.type !== undefined && this.data?.type !== null ? this.data.type : MobSkillType.Combat; + const targetValue = this.data?.skillTarget !== undefined ? this.data.skillTarget : null; + + this.model = { + id: this.data?.id || '', + mobLevelInfoId: this.mobLevelInfoId || this.data?.mobLevelInfoId || '', + type: typeValue, + skillTarget: targetValue, + clawRoll: this.data?.clawRoll ?? 0, + skillRoll: this.data?.skillRoll ?? 1, + name: this.data?.name || '', + description: this.data?.description || '' + }; + + // Set selected objects for dropdowns + this.selectedSkillType = this.skillTypes.find(t => t.value === typeValue) || this.skillTypes[0] || null; + this.selectedSkillTarget = this.skillTargets.find(t => t.value === targetValue) || this.skillTargets[0] || null; + + this.cdr.detectChanges(); + } + + private initializeEnums(): void { + // Initialize MobSkillType options + Object.keys(MobSkillType).filter(key => isNaN(Number(key))).forEach(key => { + this.skillTypes.push({ + value: MobSkillType[key] as MobSkillType, + text: key + }); + }); + + // Initialize MobSkillTarget options + this.skillTargets.push({ value: null, text: 'None' }); + Object.keys(MobSkillTarget).filter(key => isNaN(Number(key))).forEach(key => { + this.skillTargets.push({ + value: MobSkillTarget[key] as MobSkillTarget, + text: key + }); + }); + } + + public close(): void { + this.dialog.close(); + } + + public save(): void { + if (!this.processing) { + this.processing = true; + + // Extract enum values from selected objects + const mobSkill: MD2MobSkill = { + ...this.model, + type: this.selectedSkillType?.value ?? MobSkillType.Combat, + skillTarget: this.selectedSkillTarget?.value ?? null, + mobLevelInfoId: this.mobLevelInfoId || this.model.mobLevelInfoId + }; + + this.mobSkillService.createOrUpdate(mobSkill).pipe(first()).subscribe(result => { + this.processing = false; + this.dialog.close(result); + }, error => { + this.processing = false; + console.error('Error saving mob skill:', error); + }); + } + } + + public get isValid(): boolean { + if (!this.model) { + return false; + } + const typeValid = this.selectedSkillType !== null && this.selectedSkillType !== undefined; + return typeValid; + } +} +