Compare commits

..

48 Commits

Author SHA1 Message Date
Chris Chen 3325c63631 Fix reconnect issue. 2025-11-15 15:10:48 -08:00
Chris Chen 3a12b6a4ab WIP 2025-11-14 13:53:44 -08:00
Chris Chen 9ea2278dfb Update boss maint 2025-11-14 13:09:09 -08:00
Chris Chen a4391c84d0 Init Boss Fight 2025-11-14 07:37:48 -08:00
Chris Chen ee6dc58a21 WIP 2025-11-14 07:28:22 -08:00
Chris Chen 0d3995764b Add Hp bar for mob and Boss 2025-11-13 16:09:15 -08:00
Chris Chen f30c41afba Optmize 2025-11-13 15:44:39 -08:00
Chris Chen 2ef9968920 WIP 2025-11-12 18:22:33 -08:00
Chris Chen d8db9f650b WIP 2025-11-06 21:47:04 -08:00
Chris Chen 349510db56 WIP 2025-11-06 16:58:36 -08:00
Chris Chen b44834343a WIP 2025-11-06 07:10:28 -08:00
Chris Chen b41c01e6f7 WIP 2025-11-05 17:42:32 -08:00
Chris Chen 9ad991a70e WIP 2025-11-05 17:28:00 -08:00
Chris Chen 6806eeff8a WIP 2025-11-05 17:06:41 -08:00
Chris Chen 3c3c880a3c WIP 2025-11-05 17:01:51 -08:00
Chris Chen 542f24c12d WIP 2025-11-05 16:43:27 -08:00
Chris Chen 23e6da2808 WIP 2025-11-05 16:39:48 -08:00
Chris Chen 61604355c1 WIP 2025-11-05 16:26:06 -08:00
Chris Chen b4d52283aa WIP 2025-11-05 16:20:08 -08:00
Chris Chen b24753afe7 WIP 2025-11-05 15:49:51 -08:00
Chris Chen 89cb09adb6 WIP 2025-11-05 15:47:14 -08:00
Chris Chen 716e25f0ba WIP 2025-11-05 15:21:42 -08:00
Chris Chen d20f2a37c4 WIP 2025-11-05 08:04:55 -08:00
Chris Chen 701c36112c WIP 2025-11-04 22:14:55 -08:00
Chris Chen e5933104cc WIP 2025-11-04 18:13:43 -08:00
Chris Chen 46ec236ed5 WIP 2025-11-04 12:42:10 -08:00
Chris Chen b8b35645ac WIP 2025-11-04 07:42:24 -08:00
Chris Chen ed3c116d13 WIP 2025-11-03 21:43:24 -08:00
Chris Chen 719108fd6a WIP 2025-11-03 21:11:10 -08:00
Chris Chen f88cd21b33 Update MD2 editor 2025-11-03 15:35:52 -08:00
ChrisChen fd32ae5dcc Update src/app/services/crudServices/crud.service.ts 2025-11-03 17:10:18 +00:00
ChrisChen 9fec45a91f Update src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.html 2025-11-03 16:50:51 +00:00
ChrisChen ba3ad023ad Update src/app/games/massive-darkness2/md2-html-editor/md2-html-editor.component.ts 2025-11-03 16:50:28 +00:00
Chris Chen ed90250876 MD2 editor 2025-11-02 09:44:59 -08:00
Chris Chen cdceaab2fd WIP 2025-11-02 09:37:55 -08:00
Chris Chen 70aa8adbba WIP 2025-11-02 09:13:42 -08:00
Chris Chen cd9021d9c0 WIP 2024-04-08 16:48:41 -07:00
Chris Chen 0ee2e7e545 Update angular.json 2024-04-07 09:59:09 -07:00
Chris Chen 8961490ff8 Update _units.scss 2024-04-07 09:50:56 -07:00
Chris Chen d1039a409b Upgrade to Angular 17 2024-04-06 17:45:52 -07:00
Chris Chen cca0de9812 Update libs 2024-04-06 16:55:21 -07:00
Chris Chen 3b37d7d798 Remove pages 2024-04-06 10:04:16 -07:00
Chris Chen e2f55f0b8b Upgrade to angular 16 2024-04-06 09:47:55 -07:00
Chris Chen 56d2bd17e4 Upgrade to Angular 15 2024-04-06 09:42:44 -07:00
Chris Chen c68d9ba749 Upgrade to angular 14 2024-04-06 09:35:41 -07:00
Chris Chen dc49c0a958 Upgrade to angular 13 2024-04-06 09:30:39 -07:00
Chris Chen 853b7069f9 Remove tslint-language-service 2024-04-06 09:16:06 -07:00
Chris Chen dfc1f269a0 Stable 2024-04-06 07:26:12 -07:00
468 changed files with 68135 additions and 64098 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"mcpServers": {
"kendoai": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@progress/kendo-angular-mcp@latest"],
"env": {
"TELERIK_LICENSE_PATH":"C:/Users/Chris/AppData/Roaming/Telerik/telerik-license.txt"
}
}
}
}
+1
View File
@@ -23,6 +23,7 @@
!.vscode/extensions.json
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
+18
View File
@@ -0,0 +1,18 @@
{
"extends": [
"development"
],
"hints": {
"axe/text-alternatives": [
"default",
{
"image-alt": "off"
}
]
},
"browserslist": [
"defaults",
"not ie 11",
"not ie <= 11"
]
}
+12 -51
View File
@@ -24,8 +24,8 @@
"src/web.config",
{
"glob": "**/*",
"input": "node_modules/leaflet/dist/images",
"output": "/assets/img/markers"
"input": "node_modules/tinymce",
"output": "/tinymce/"
}
],
"styles": [
@@ -38,16 +38,13 @@
"node_modules/nebular-icons/scss/nebular-icons.scss",
"node_modules/pace-js/templates/pace-theme-flash.tmpl.css",
"node_modules/leaflet/dist/leaflet.css",
"node_modules/@progress/kendo-theme-default/dist/all.css",
"src/app/@theme/styles/styles.scss",
"src/assets/styles/site.scss"
],
"scripts": [
"node_modules/pace-js/pace.min.js",
"node_modules/tinymce/tinymce.min.js",
"node_modules/tinymce/themes/modern/theme.min.js",
"node_modules/tinymce/plugins/link/plugin.min.js",
"node_modules/tinymce/plugins/paste/plugin.min.js",
"node_modules/tinymce/plugins/table/plugin.min.js",
"node_modules/echarts/dist/echarts.min.js",
"node_modules/echarts/dist/extension/bmap.min.js",
"node_modules/chart.js/dist/Chart.min.js"
@@ -57,7 +54,10 @@
"echarts",
"lodash",
"zrender/lib/svg/svg",
"zrender/lib/vml/vml"
"zrender/lib/vml/vml",
"file-saver",
"eva-icons",
"rxjs-compat"
],
"vendorChunk": true,
"extractLicenses": false,
@@ -94,18 +94,18 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "ngx-admin-demo:build"
"buildTarget": "ngx-admin-demo:build"
},
"configurations": {
"production": {
"browserTarget": "ngx-admin-demo:build:production"
"buildTarget": "ngx-admin-demo:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "ngx-admin-demo:build"
"buildTarget": "ngx-admin-demo:build"
}
},
"test": {
@@ -118,10 +118,6 @@
"scripts": [
"node_modules/pace-js/pace.min.js",
"node_modules/tinymce/tinymce.min.js",
"node_modules/tinymce/themes/modern/theme.min.js",
"node_modules/tinymce/plugins/link/plugin.min.js",
"node_modules/tinymce/plugins/paste/plugin.min.js",
"node_modules/tinymce/plugins/table/plugin.min.js",
"node_modules/echarts/dist/echarts.min.js",
"node_modules/echarts/dist/extension/bmap.min.js",
"node_modules/chart.js/dist/Chart.min.js"
@@ -144,50 +140,15 @@
"src/web.config",
{
"glob": "**/*",
"input": "node_modules/leaflet/dist/images",
"output": "/assets/img/markers"
"input": "node_modules/tinymce",
"output": "/tinymce/"
}
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"typeCheck": true,
"exclude": []
}
}
}
},
"ngx-admin-demo-e2e": {
"root": "",
"sourceRoot": "",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "ngx-admin-demo:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": []
}
}
}
}
},
"defaultProject": "ngx-admin-demo",
"schematics": {
"@schematics/angular:component": {
"prefix": "ngx",
+60418 -46416
View File
File diff suppressed because it is too large Load Diff
+120 -118
View File
@@ -1,120 +1,122 @@
{
"name": "ngx-admin",
"version": "8.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/akveo/ngx-admin.git"
},
"bugs": {
"url": "https://github.com/akveo/ngx-admin/issues"
},
"scripts": {
"ng": "ng",
"conventional-changelog": "conventional-changelog",
"start": "ng serve",
"build": "ng build --output-path \\\\ArkNAS\\docker\\nginx-proxy\\data\\ChurchAngular --configuration production",
"build:prod": "npm run build -- --configuration production --aot",
"test": "ng test",
"test:coverage": "rimraf coverage && npm run test -- --code-coverage",
"lint": "ng lint",
"lint:fix": "ng lint ngx-admin-demo --fix",
"lint:styles": "stylelint ./src/**/*.scss",
"lint:ci": "npm run lint && npm run lint:styles",
"pree2e": "webdriver-manager update --standalone false --gecko false",
"e2e": "ng e2e",
"docs": "compodoc -p src/tsconfig.app.json -d docs",
"docs:serve": "compodoc -p src/tsconfig.app.json -d docs -s",
"prepush": "npm run lint:ci",
"release:changelog": "npm run conventional-changelog -- -p angular -i CHANGELOG.md -s",
"postinstall": "ngcc --properties es2015 es5 browser module main --first-only --create-ivy-entry-points --tsconfig \"./src/tsconfig.app.json\""
},
"dependencies": {
"@angular/animations": "^12.2.16",
"@angular/cdk": "12.1.0",
"@angular/common": "^12.2.16",
"@angular/compiler": "^12.2.16",
"@angular/core": "^12.2.16",
"@angular/forms": "^12.2.16",
"@angular/google-maps": "^12.2.13",
"@angular/platform-browser": "^12.2.16",
"@angular/platform-browser-dynamic": "^12.2.16",
"@angular/router": "^12.2.16",
"@asymmetrik/ngx-leaflet": "3.0.1",
"@microsoft/signalr": "^6.0.8",
"@nebular/auth": "8.0.0",
"@nebular/date-fns": "^9.0.3",
"@nebular/eva-icons": "8.0.0",
"@nebular/security": "8.0.0",
"@nebular/theme": "8.0.0",
"@swimlane/ngx-charts": "^14.0.0",
"angular2-chartjs": "0.4.1",
"angular2-qrcode": "^2.0.3",
"bootstrap": "4.3.1",
"chart.js": "2.7.1",
"ckeditor": "4.7.3",
"classlist.js": "1.1.20150312",
"core-js": "2.5.1",
"echarts": "^4.9.0",
"eva-icons": "^1.1.3",
"file-saver": "^2.0.5",
"intl": "1.2.5",
"ionicons": "2.0.1",
"leaflet": "1.2.0",
"nebular-icons": "1.1.0",
"ng-in-viewport": "^13.0.1",
"ng2-ckeditor": "~1.2.9",
"ng2-completer": "^9.0.1",
"ng2-smart-table": "^1.6.0",
"ngx-echarts": "^4.2.2",
"ngx-infinite-scroll": "^13.0.2",
"ngx-mask": "^12.0.0",
"node-sass": "^4.14.1",
"normalize.css": "6.0.0",
"pace-js": "1.0.2",
"roboto-fontface": "0.8.0",
"rxjs": "6.6.2",
"rxjs-compat": "6.3.0",
"socicon": "3.0.5",
"style-loader": "^1.3.0",
"tinymce": "4.5.7",
"tslib": "^2.3.1",
"typeface-exo": "0.0.22",
"typeit": "^8.7.0",
"web-animations-js": "^2.3.2",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^12.1.4",
"@angular/cli": "^12.2.17",
"@angular/compiler-cli": "^12.2.16",
"@angular/language-service": "12.1.0",
"@compodoc/compodoc": "1.0.1",
"@fortawesome/fontawesome-free": "^5.2.0",
"@schematics/angular": "^14.1.3",
"@types/d3-color": "1.0.5",
"@types/jasmine": "~3.3.0",
"@types/jasminewd2": "2.0.3",
"@types/leaflet": "1.2.3",
"@types/node": "^12.12.70",
"codelyzer": "^6.0.2",
"conventional-changelog-cli": "1.3.4",
"husky": "0.13.3",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.3.19",
"karma-chrome-launcher": "~3.1.1",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.2",
"karma-jasmine-html-reporter": "^1.7.0",
"npm-run-all": "4.0.2",
"protractor": "~7.0.0",
"rimraf": "2.6.1",
"stylelint": "7.13.0",
"ts-node": "3.2.2",
"tslint": "~6.1.0",
"tslint-language-service": "^0.9.9",
"typescript": "~4.2.3||~4.3.0"
}
"name": "ngx-admin",
"version": "8.0.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/akveo/ngx-admin.git"
},
"bugs": {
"url": "https://github.com/akveo/ngx-admin/issues"
},
"scripts": {
"ng": "ng",
"conventional-changelog": "conventional-changelog",
"start": "ng serve --host=127.0.0.1",
"build": "ng build",
"build:prod": "ng build --output-path \\\\ArkNAS\\docker\\nginx-proxy\\data\\ChurchAngular --configuration production --aot",
"test": "ng test",
"test:coverage": "rimraf coverage && npm run test -- --code-coverage",
"lint": "ng lint",
"lint:fix": "ng lint ngx-admin-demo --fix",
"lint:styles": "stylelint ./src/**/*.scss",
"lint:ci": "npm run lint && npm run lint:styles",
"pree2e": "webdriver-manager update --standalone false --gecko false",
"e2e": "ng e2e",
"docs": "compodoc -p src/tsconfig.app.json -d docs",
"docs:serve": "compodoc -p src/tsconfig.app.json -d docs -s",
"prepush": "echo 'Pre-push hook disabled - style linting skipped'",
"release:changelog": "npm run conventional-changelog -- -p angular -i CHANGELOG.md -s"
},
"dependencies": {
"@angular/animations": "^17.3.3",
"@angular/cdk": "17.3.3",
"@angular/common": "^17.3.3",
"@angular/compiler": "^17.3.3",
"@angular/core": "^17.3.3",
"@angular/forms": "^17.3.3",
"@angular/platform-browser": "^17.3.3",
"@angular/platform-browser-dynamic": "^17.3.3",
"@angular/router": "^17.3.3",
"@asymmetrik/ngx-leaflet": "3.0.1",
"@microsoft/signalr": "^6.0.8",
"@nebular/auth": "13.0.0",
"@nebular/date-fns": "^13.0.0",
"@nebular/eva-icons": "13.0.0",
"@nebular/security": "13.0.0",
"@nebular/theme": "13.0.0",
"@progress/kendo-angular-buttons": "^20.1.1",
"@progress/kendo-angular-dialog": "^20.1.1",
"@progress/kendo-angular-dropdowns": "^20.1.1",
"@progress/kendo-angular-editor": "^20.1.1",
"@progress/kendo-angular-grid": "^20.1.1",
"@progress/kendo-angular-inputs": "^20.1.1",
"@progress/kendo-angular-toolbar": "^20.1.1",
"@progress/kendo-licensing": "^1.7.1",
"@progress/kendo-svg-icons": "^4.5.0",
"@progress/kendo-theme-default": "^12.2.0",
"@tinymce/tinymce-angular": "^7.0.0",
"angular2-chartjs": "0.4.1",
"angular2-qrcode": "^2.0.3",
"bootstrap": "4.3.1",
"chart.js": "2.7.1",
"core-js": "2.5.1",
"echarts": "^4.9.0",
"eva-icons": "^1.1.3",
"file-saver": "^2.0.5",
"intl": "1.2.5",
"ionicons": "2.0.1",
"leaflet": "1.2.0",
"nebular-icons": "1.1.0",
"ng-in-viewport": "^13.0.1",
"ng2-completer": "^9.0.1",
"ngx-echarts": "^4.2.2",
"ngx-infinite-scroll": "^17.0.0",
"ngx-mask": "^12.0.0",
"node-sass": "^4.14.1",
"normalize.css": "6.0.0",
"pace-js": "1.0.2",
"roboto-fontface": "0.8.0",
"rxjs": "6.6.2",
"rxjs-compat": "6.3.0",
"socicon": "3.0.5",
"style-loader": "^1.3.0",
"tinymce": "^7.0.0",
"tslib": "^2.3.1",
"typeface-exo": "0.0.22",
"typeit": "^8.7.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.3",
"@angular/cli": "^17.3.3",
"@angular/compiler-cli": "^17.3.3",
"@angular/language-service": "17.3.3",
"@angular/localize": "^17.3.3",
"@compodoc/compodoc": "1.0.1",
"@fortawesome/fontawesome-free": "^5.2.0",
"@schematics/angular": "^14.1.3",
"@types/d3-color": "1.0.5",
"@types/jasmine": "~3.3.0",
"@types/jasminewd2": "2.0.3",
"@types/leaflet": "1.2.3",
"@types/node": "^18.19.30",
"codelyzer": "^6.0.2",
"conventional-changelog-cli": "1.3.4",
"husky": "0.13.3",
"jasmine-core": "~5.1.2",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.3.19",
"karma-chrome-launcher": "~3.1.1",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"npm-run-all": "4.0.2",
"protractor": "~7.0.0",
"rimraf": "2.6.1",
"stylelint": "7.13.0",
"ts-node": "3.2.2",
"typescript": "~5.4.4"
}
}
@@ -1,6 +1,6 @@
@import '../../styles/themes';
@import '~@nebular/theme/styles/global/breakpoints';
@import '~bootstrap/scss/mixins/breakpoints';
@import "../../styles/themes";
@import "@nebular/theme/styles/global/breakpoints";
@import "bootstrap/scss/mixins/breakpoints";
@include nb-install-component() {
width: 100%;
@@ -1,6 +1,6 @@
@import '~bootstrap/scss/mixins/breakpoints';
@import '~@nebular/theme/styles/global/breakpoints';
@import '../../styles/themes';
@import "bootstrap/scss/mixins/breakpoints";
@import "@nebular/theme/styles/global/breakpoints";
@import "../../styles/themes";
@include nb-install-component() {
display: flex;
@@ -24,7 +24,7 @@
}
::ng-deep nb-search button {
padding: 0!important;
padding: 0 !important;
}
.header-container {
@@ -1,6 +1,6 @@
@import '../../styles/themes';
@import '~bootstrap/scss/mixins/breakpoints';
@import '~@nebular/theme/styles/global/breakpoints';
@import "../../styles/themes";
@import "bootstrap/scss/mixins/breakpoints";
@import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable {
@@ -1,6 +1,6 @@
@import '../../styles/themes';
@import '~bootstrap/scss/mixins/breakpoints';
@import '~@nebular/theme/styles/global/breakpoints';
@import "../../styles/themes";
@import "bootstrap/scss/mixins/breakpoints";
@import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable {
@@ -1,6 +1,6 @@
@import '../../styles/themes';
@import '~bootstrap/scss/mixins/breakpoints';
@import '~@nebular/theme/styles/global/breakpoints';
@import "../../styles/themes";
@import "bootstrap/scss/mixins/breakpoints";
@import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable {
@@ -1,6 +1,6 @@
@import '../../styles/themes';
@import '~bootstrap/scss/mixins/breakpoints';
@import '~@nebular/theme/styles/global/breakpoints';
@import "../../styles/themes";
@import "bootstrap/scss/mixins/breakpoints";
@import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable {
+12 -13
View File
@@ -1,26 +1,25 @@
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap');
@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap");
// themes - our custom or/and out of the box themes
@import 'themes';
@import "themes";
// framework component themes (styles tied to theme variables)
@import '~@nebular/theme/styles/globals';
@import '~@nebular/auth/styles/globals';
@import "@nebular/theme/styles/globals";
@import "@nebular/auth/styles/globals";
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
@import '~bootstrap/scss/mixins';
@import '~bootstrap/scss/grid';
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/grid";
// loading progress bar theme
@import './pace.theme';
@import "./pace.theme";
@import './layout';
@import './overrides';
@import "./layout";
@import "./overrides";
// install the framework and custom global styles
@include nb-install() {
// framework global styles
@include nb-theme-global();
@include nb-auth-global();
@@ -30,4 +29,4 @@
@include ngx-pace-theme();
@include nb-overrides();
};
} ;
+82 -66
View File
@@ -1,88 +1,104 @@
// @nebular theming framework
@import '~@nebular/theme/styles/theming';
@import "@nebular/theme/styles/theming";
// @nebular out of the box themes
@import '~@nebular/theme/styles/themes';
@import "@nebular/theme/styles/themes";
$nb-themes: nb-register-theme((
layout-padding-top: 2.25rem,
$nb-themes: nb-register-theme(
(
layout-padding-top: 2.25rem,
menu-item-icon-margin: 0 0.5rem 0 0,
menu-item-icon-margin: 0 0.5rem 0 0,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
select-min-width: 6rem,
select-min-width: 6rem,
slide-out-background: #f7f9fc,
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
), default, default);
slide-out-background: #f7f9fc,
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
),
default,
default
);
$nb-themes: nb-register-theme((
layout-padding-top: 2.25rem,
$nb-themes: nb-register-theme(
(
layout-padding-top: 2.25rem,
menu-item-icon-margin: 0 0.5rem 0 0,
menu-item-icon-margin: 0 0.5rem 0 0,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
select-min-width: 6rem,
select-min-width: 6rem,
slide-out-background: #252547,
slide-out-shadow-color: 2px 0 3px #29157a,
slide-out-shadow-color-rtl: -2px 0 3px #29157a,
), cosmic, cosmic);
slide-out-background: #252547,
slide-out-shadow-color: 2px 0 3px #29157a,
slide-out-shadow-color-rtl: -2px 0 3px #29157a,
),
cosmic,
cosmic
);
$nb-themes: nb-register-theme((
layout-padding-top: 2.25rem,
$nb-themes: nb-register-theme(
(
layout-padding-top: 2.25rem,
menu-item-icon-margin: 0 0.5rem 0 0,
menu-item-icon-margin: 0 0.5rem 0 0,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
select-min-width: 6rem,
select-min-width: 6rem,
slide-out-background: linear-gradient(270deg, #edf1f7 0%, #e4e9f2 100%),
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
), corporate, corporate);
slide-out-background: linear-gradient(270deg, #edf1f7 0%, #e4e9f2 100%),
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
),
corporate,
corporate
);
$nb-themes: nb-register-theme((
layout-padding-top: 2.25rem,
$nb-themes: nb-register-theme(
(
layout-padding-top: 2.25rem,
menu-item-icon-margin: 0 0.5rem 0 0,
menu-item-icon-margin: 0 0.5rem 0 0,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
card-height-tiny: 13.5rem,
card-height-small: 21.1875rem,
card-height-medium: 28.875rem,
card-height-large: 36.5625rem,
card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem,
select-min-width: 6rem,
select-min-width: 6rem,
slide-out-background: linear-gradient(270deg, #222b45 0%, #151a30 100%),
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
), dark, dark);
slide-out-background: linear-gradient(270deg, #222b45 0%, #151a30 100%),
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
),
dark,
dark
);
-2
View File
@@ -12,7 +12,6 @@ import { FancyTableModule } from '../ui/fancy-table/fancy-table.module';
import { FamilyMembersComponent } from './family-members/family-members.component';
import { FamilyMemberEditorComponent } from './family-members/family-member-editor/family-member-editor.component';
import { DropDownListModule } from '../ui/drop-down-list/drop-down-list.module';
import { NgxMaskModule } from 'ngx-mask';
import { PastoralDomainsComponent } from './pastoral-domains/pastoral-domains.component';
import { PastoralDomainEditorComponent } from './pastoral-domains/pastoral-domain-editor/pastoral-domain-editor.component';
import { AssignMemberCellGroupComponent } from './family-members/assign-member-cell-group/assign-member-cell-group.component';
@@ -61,7 +60,6 @@ import { LineMessagingAccountEditorComponent } from './lines/line-messaging-acco
AlertDlgModule,
FancyTableModule,
DropDownListModule,
NgxMaskModule,
CurrencyInputModule,
MaskDirectiveModule,
DateInputModule
@@ -3,14 +3,12 @@ import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { inherits } from 'util';
import { CellGroupRoutineEvents } from '../../entity/CellGroupRoutineEvents';
import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { StateService } from '../../services/state.service';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { FancyTableComponent } from '../../ui/fancy-table/fancy-table.component';
import { ObjectUtils } from '../../utilities/object-utils';
@Component({
selector: 'ngx-cell-group-routine-events',
@@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { DropDownOption } from '../../../entity/dropDownOption';
import { DomainMemberRelationship, PastoralDomain } from '../../../entity/PastoralDomain';
import { DialogComponent } from '../../../pages/modal-overlays/dialog/dialog.component';
import { ArrayUtils } from '../../../utilities/array-utils';
import { first } from "rxjs/operators"
import { FamilyMember } from '../../../entity/Member';
-5
View File
@@ -17,11 +17,6 @@ export const routes: Routes = [
loadChildren: () => import('./invitation/invitation.module')
.then(m => m.InvitationModule),
},
{
path: 'pages',
loadChildren: () => import('./pages/pages.module')
.then(m => m.PagesModule),
},
{
path: 'auth',
component: NbAuthComponent,
-1
View File
@@ -74,7 +74,6 @@ const socialLinks = [
closeOnBackdropClick: false,
closeOnEsc: false
}),
NgxMaskModule.forRoot(maskConfig),
NbDateFnsDateModule.forRoot({ format: 'MM/dd/yyyy' }),
CoreModule.forRoot(),
ThemeModule.forRoot(),
-3
View File
@@ -1,8 +1,5 @@
import { ActivatedRoute } from "@angular/router";
import { basename } from "path";
import { Observable, Subject } from "rxjs";
import { ScreenBase } from "../ScreenBase";
import { ICrudService } from "../services/crudServices/crud.service";
import { PastoralDomainService } from "../services/crudServices/pastoral-domain.service";
import { StateService } from "../services/state.service";
import { first } from 'rxjs/operators';
@@ -95,7 +95,7 @@ export class PrayerComponent extends MyAppBase {
// message += "\n======= 備註 =======" + "\n" + comment;
// }
// message += "\n請使用方舟晚宴系統新增菜單唷!" + "\n" + "https://happiness.tours/CellGroup/dinner?openExternalBrowser=1"
// message += "\n請使用方舟晚宴系統新增菜單唷!" + "\n" + "https://golife.love/CellGroup/dinner?openExternalBrowser=1"
// this.lineService.pushLineMessage(message);
});
}
@@ -8,15 +8,12 @@ import { ContextMenuModule } from '../../ui/context-menu/context-menu.module';
const components = [RightClickMenuDirective,];
@NgModule({
declarations: [...components],
entryComponents: [
ContextMenuComponent
],
imports: [
CommonModule,
NbDialogModule,
ContextMenuModule
],
exports: [...components],
declarations: [...components],
imports: [
CommonModule,
NbDialogModule,
ContextMenuModule
],
exports: [...components]
})
export class RightClickMenuModule { }
+1 -1
View File
@@ -20,7 +20,7 @@ export interface LoginTokenViewModel {
avatarImage: string;
role: Role;
cellGroup: PastoralDomain;
signalRSessionId;
signalRConnectionId;
sessionTabId: string;
}
+1 -1
View File
@@ -30,7 +30,7 @@ const teamSize: number[][] = [
const fourthQuestNeed2Failed = 7;
const SIGNAL_R_URL = (id: string = null) => { return `${environment.signalRUrl}/${id}Hub` }
//const SIGNAL_R_URL = (id: string = null) => { return `http://localhost:12071/hub` }
//const SIGNAL_R_URL = (id: string = null) => { return `http://happiness.tours:8088/${id}hub` }
//const SIGNAL_R_URL = (id: string = null) => { return `http://golife.love:8088/${id}hub` }
@Component({
selector: 'ngx-avalon',
@@ -26,5 +26,5 @@
<ng-template #WaitingMessage>
<h1>等待遊戲開始中...</h1>
<qr-code *ngIf="isHost" [size]="qrCodeWidth" [value]="'http://happiness.tours/games/avalon'"></qr-code>
<qr-code *ngIf="isHost" [size]="qrCodeWidth" [value]="'http://golife.love/games/avalon'"></qr-code>
</ng-template>
+4
View File
@@ -5,6 +5,8 @@ import { AvalonComponent } from './avalon/avalon.component';
import { GamesComponent } from './games.component';
import { HeroDashboardComponent } from './massive-darkness2/hero-dashboard/hero-dashboard.component';
import { MassiveDarkness2Component } from './massive-darkness2/massive-darkness2.component';
import { MD2MobInfoMaintenanceComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-info-maintenance.component';
import { MD2HeroProfileMaintenanceComponent } from './massive-darkness2/md2-hero-profile-maintenance/md2-hero-profile-maintenance.component';
export class GameRoomMenuConfig {
public static HostMenu: NbMenuItem[] = [
@@ -47,6 +49,8 @@ const routes: Routes = [
{ path: 'avalonHost', component: AvalonComponent },
{ path: 'MD2', component: MassiveDarkness2Component },
{ path: 'MD2_Hero/:roomId', component: HeroDashboardComponent },
{ path: 'MD2MobInfo', component: MD2MobInfoMaintenanceComponent },
{ path: 'MD2HeroProfile', component: MD2HeroProfileMaintenanceComponent },
]
},
+2
View File
@@ -3,4 +3,6 @@
<router-outlet></router-outlet>
</ngx-plain-layout>
<div kendoDialogContainer></div>
<!-- ngx-plain-layout ngx-one-column-layout-->
+2
View File
@@ -5,6 +5,7 @@ export interface IGamePlayer {
isPlayer: boolean;
signalRClientId: string;
tabId: string;
isDisconnected: boolean;
}
export class GamePlayer implements IGamePlayer {
@@ -14,4 +15,5 @@ export class GamePlayer implements IGamePlayer {
isPlayer: boolean;
signalRClientId: string;
tabId: string;
isDisconnected: boolean;
}
+48 -4
View File
@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { QRCodeModule } from 'angular2-qrcode';
import { GamesRoutingModule } from './games-routing.module';
import { GamesComponent } from './games.component';
import { FormsModule } from '@angular/forms';
@@ -33,6 +32,29 @@ import { MobAttackInfoComponent } from './massive-darkness2/mobs/mob-detail-info
import { MobDefInfoComponent } from './massive-darkness2/mobs/mob-detail-info/mob-def-info/mob-def-info.component';
import { MobCombatInfoComponent } from './massive-darkness2/mobs/mob-detail-info/mob-combat-info/mob-combat-info.component';
import { MobStandInfoComponent } from './massive-darkness2/mobs/mob-stand-info/mob-stand-info.component';
import { HtmlEditorModule } from '../ui/html-editor/html-editor.module';
import { EditorModule } from '@tinymce/tinymce-angular';
import { MD2HtmlEditorComponent } from './massive-darkness2/md2-html-editor/md2-html-editor.component';
import { MD2IconPickerDlgComponent } from './massive-darkness2/md2-html-editor/md2-icon-picker-dlg.component';
import { EditorModule as KendoEditorModule } from '@progress/kendo-angular-editor';
import { ToolBarModule } from '@progress/kendo-angular-toolbar';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
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';
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({
@@ -60,12 +82,25 @@ import { MobStandInfoComponent } from './massive-darkness2/mobs/mob-stand-info/m
MobAttackInfoComponent,
MobDefInfoComponent,
MobCombatInfoComponent,
MobStandInfoComponent
MobStandInfoComponent,
MD2HtmlEditorComponent,
MD2IconPickerDlgComponent,
MD2MobInfoMaintenanceComponent,
MD2MobInfoEditorComponent,
MD2MobInfoDetailComponent,
MD2MobSkillEditorComponent,
MD2MobLevelEditorComponent,
MD2BossFightEditorComponent,
MD2PhaseBuffEditorComponent,
MD2HeroProfileMaintenanceComponent,
MD2HeroProfileEditorComponent,
GameInitDlgComponent
],
imports: [
CommonModule,
GamesRoutingModule,
FormsModule,
ReactiveFormsModule,
AdminRoutingModule,
ThemeModule,
NbMenuModule,
@@ -90,8 +125,17 @@ import { MobStandInfoComponent } from './massive-darkness2/mobs/mob-stand-info/m
CurrencyInputModule,
NbDialogModule.forRoot(),
AlertDlgModule,
QRCodeModule,
DropDownListModule
DropDownListModule,
HtmlEditorModule,
EditorModule,
KendoEditorModule,
ToolBarModule,
ButtonsModule,
GridModule,
DialogModule,
InputsModule,
DropDownsModule,
LayoutModule
]
})
export class GamesModule { }
+109 -43
View File
@@ -7,6 +7,8 @@ import { SignalRMessage } from "../../services/signal-r.service";
import { StateService } from "../../services/state.service";
import { ADIcon, MessageBoxConfig } from "../../ui/alert-dlg/alert-dlg.model";
import { MD2HeroInfo, MD2Icon, MobInfo, RoundPhase } from "./massive-darkness2.model";
import { LoginUserService } from "../../services/login-user.service";
import { GamePlayer } from "../games.model";
@Injectable()
export abstract class MD2Base {
@@ -58,14 +60,14 @@ export abstract class MD2Base {
}
imgUrl(imgPath: string) {
return this.md2Service.stateService.imgUrl(imgPath);
return this.md2Service.imgUrl(imgPath);
}
fileList(folderPath: string) {
return this.md2Service.fileList(folderPath);
}
iconHtml(icon: MD2Icon, cssClass = '') {
return this.md2Service.stateService.iconHtml(icon, cssClass);
return this.md2Service.iconHtml(icon, cssClass);
}
imgHtml(imgFile: string, cssClass = '') {
@@ -81,12 +83,18 @@ export abstract class MD2Base {
}
abstract refreshUI();
handleSignalRCallback(message: SignalRMessage): void {
if (message.from.isGroup) {
if (!this.isHeroDashboard) return;
} else {
if (this.isHeroDashboard && this.md2Service.playerHero.playerInfo.signalRClientId == message.from.sessionId) return;
console.log('handleSignalRCallback', message);
if (message.from) {
if (message.from.isGroup) {
if (!this.isHeroDashboard) return;
} else {
if (this.isHeroDashboard && this.md2Service.playerHero?.playerInfo?.signalRClientId == message.from.connectionId) return;
}
}
if (!this.isHeroDashboard) {
}
switch (message.actionType) {
case 'hero':
let heroInfo = new MD2HeroInfo(JSON.parse(message.parameters['hero']));
@@ -95,43 +103,12 @@ export abstract class MD2Base {
this.md2Service.heros.push(heroInfo);
break;
case 'update':
let exitingHero = this.md2Service.heros.find(h => h.playerInfo.signalRClientId == heroInfo.playerInfo.signalRClientId);
if (exitingHero) {
let activateBoss = exitingHero.uiActivating && !heroInfo.uiActivating;
this.md2Service.heros[this.md2Service.heros.indexOf(exitingHero)] = heroInfo;
if (this.isHeroDashboard && this.md2Service.stateService.playerHero.playerInfo.tabId == heroInfo.playerInfo.tabId) {
this.md2Service.stateService.playerHero = heroInfo;
}
if (!this.isHeroDashboard && this.md2Service.info.isBossFight && activateBoss) {
this.md2Service.activateBoss();
}
} else {
this.md2Service.heros.push(heroInfo);
}
if (!this.isHeroDashboard) {
if (this.gameInfo.roundPhase == RoundPhase.HeroPhase) {
if (!this.md2Service.heros.some(h => h.remainActions > 0) && !this.md2Service.heros.some(h => h.uiActivating)) {
if (!this.md2Service.info.isBossFight) {
if (this.md2Service.mobs.length > 0 || this.md2Service.roamingMonsters.length > 0) {
this.md2Service.msgBoxService.show('Enemy Phase', { icon: ADIcon.WARNING }).pipe(first()).subscribe(result => {
this.md2Service.runNextPhase();
});
} else {
this.md2Service.runNextPhase();
}
}
}
}
}
this.updateHeroInfo(heroInfo);
//Object.assign(heroInfo, exitingHero);
break;
case 'updateMyHero':
if (this.isHeroDashboard) {
this.md2Service.stateService.playerHero = heroInfo;
this.md2Service.playerHero = heroInfo;
}
break;
@@ -140,15 +117,54 @@ export abstract class MD2Base {
}
this.detectChanges();
break;
case 'heroes':
switch (message.actionName) {
case 'updateAll':
if (this.isHeroDashboard) {
let allHeroes = (JSON.parse(message.parameters['heros']) as MD2HeroInfo[]).map(h => new MD2HeroInfo(h));
//Remove heroes that are not in the list
this.md2Service.info.heros = this.md2Service.heros.filter(h => allHeroes.some(h2 => h2.playerInfo.tabId == h.playerInfo.tabId));
allHeroes.forEach(heroInfo => {
this.updateHeroInfo(heroInfo);
});
this.detectChanges();
}
break;
}
break;
case 'GameRoom':
switch (message.actionName) {
case 'Leaving':
this.md2Service.heros.splice(this.md2Service.heros.findIndex(h => h.playerInfo.signalRClientId == message.from.sessionId));
let leavingPlayerInfo = message.value as GamePlayer;
let leavingHero = this.md2Service.heros.find(h => h.playerInfo.tabId == leavingPlayerInfo.tabId);
if (leavingHero) {
leavingHero.playerInfo.isDisconnected = true;
}
//var disconnectHero = this.md2Service.heros.splice(this.md2Service.heros.findIndex(h => h.playerInfo.signalRClientId == leavingPlayerInfo.signalRClientId));
//this.md2Service.info.disconnectedHeroes.push(...disconnectHero);
this.detectChanges();
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) {
playerHero.playerInfo = this.md2Service.gameRoomService.currentPlayer();
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();
}
break;
@@ -158,6 +174,21 @@ export abstract class MD2Base {
this.detectChanges();
}
break;
case 'getGameInfo':
if (!this.isHeroDashboard) {
this.md2Service.broadcastGameInfo();
}
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();
}
break;
default:
break;
}
@@ -206,7 +237,7 @@ export abstract class MD2Base {
break;
case 'tradeAction':
this.md2Service.msgBoxService.show('Trade and Equip', {
text: `every one in the <b>same zone with <b>${this.md2Service.heroFullName(this.md2Service.currentActivateHero)}</b> may freely trade and
text: `every one in the <b>same zone with ${this.md2Service.heroFullName(this.md2Service.currentActivateHero)}</b> may freely trade and
equip items!`,
icon: ADIcon.INFO
});
@@ -223,6 +254,41 @@ export abstract class MD2Base {
break;
}
}
updateHeroInfo(heroInfo: MD2HeroInfo) {
let exitingHero = this.md2Service.heros.find(h => h.playerInfo.tabId == heroInfo.playerInfo.tabId);
if (exitingHero) {
//For boss fight, if the hero finished activating, activate the boss
let activateBoss = exitingHero.uiActivating && !heroInfo.uiActivating;
this.md2Service.heros[this.md2Service.heros.indexOf(exitingHero)] = heroInfo;
//My hero update
if (this.isHeroDashboard && this.md2Service.loginUserService.sessionTabId == heroInfo.playerInfo.tabId) {
this.md2Service.playerHero = heroInfo;
}
if (!this.isHeroDashboard && this.md2Service.info.isBossFight && activateBoss) {
this.md2Service.activateBoss();
}
} else {
this.md2Service.heros.push(heroInfo);
}
if (!this.isHeroDashboard) {
if (this.gameInfo.roundPhase == RoundPhase.HeroPhase) {
if (!this.md2Service.heros.some(h => h.remainActions > 0) && !this.md2Service.heros.some(h => h.uiActivating)) {
if (!this.md2Service.info.isBossFight) {
if (this.md2Service.mobs.length > 0 || this.md2Service.roamingMonsters.length > 0) {
this.md2Service.msgBoxService.show('Enemy Phase', { icon: ADIcon.WARNING }).pipe(first()).subscribe(result => {
this.md2Service.runNextPhase();
});
} else {
this.md2Service.runNextPhase();
}
}
}
}
}
}
abstract heroAction(hero: MD2HeroInfo, action: string);
}
@@ -254,14 +320,14 @@ export abstract class MD2ComponentBase {
this.destroy$.complete();
}
imgUrl(imgPath: string) {
return this.md2Service.stateService.imgUrl(imgPath);
return this.md2Service.imgUrl(imgPath);
}
fileList(folderPath: string) {
return this.md2Service.fileList(folderPath);
}
iconHtml(icon: MD2Icon, cssClass = '') {
return this.md2Service.stateService.iconHtml(icon, cssClass);
return this.md2Service.iconHtml(icon, cssClass);
}
detectChanges() {
if (!this.cdRef['destroyed']) {
@@ -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.info" [mode]="mode"></md2-mob-stand-info>
<md2-mob-stand-info [mob]="boss" [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.info"></md2-mob-attack-info>
<md2-mob-attack-info [mob]="boss"></md2-mob-attack-info>
</div>
<div class="col-md-8 MD2IconContainer-lg">
<md2-mob-combat-info [mob]="boss.info"></md2-mob-combat-info>
<md2-mob-combat-info [mob]="boss"></md2-mob-combat-info>
</div>
</div>
@@ -6,10 +6,11 @@ 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 } from '../../massive-darkness2.model';
import { MobDlgType, MD2Icon, MD2HeroInfo, RoundPhase, MobInfo } 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',
@@ -17,8 +18,8 @@ import { SpawnMobDlgComponent } from '../../mobs/spawn-mob-dlg/spawn-mob-dlg.com
styleUrls: ['./boss-activation.component.scss']
})
export class BossActivationComponent implements OnInit {
boss: IBossFight;
bossAction: MobSkill;
boss: MobInfo;
bossAction: MD2MobSkill;
currentAction: number;
allActions: number;
MobDlgType = MobDlgType;
@@ -3,34 +3,50 @@
{{boss.name}}
<button nbButton hero status="primary" (click)="activate()">Action</button>
</nb-card-header>
<nb-card-body>
<nb-card-body class="g-overflow-hidden">
<div class="row">
<div class="col-md-5">
<img src="{{boss.standUrl}}" class="w-100 g-max-height-80vh">
<!-- <img src="{{boss.standUrl}}" class="w-100 bossStandImg"> -->
<md2-mob-stand-info [mob]="boss" [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>
</div>
<div class="stat-progress-bar-overlay">
<div class="stat-progress-fill-overlay hp-fill-overlay"
[style.width.%]="(boss.unitRemainHp / boss.hp) * 100">
</div>
</div>
</div>
</div>
</div>
<div class="col-md-7">
<div class="row">
<div class="col-md">
<adj-number-input name="mob{{boss.info.name}}" [(ngModel)]="boss.info.unitRemainHp" minimum="0"
<adj-number-input name="mob{{boss.name}}" [(ngModel)]="boss.unitRemainHp" minimum="0"
class="mb-3" title="Boss HP" (hitMinimum)="WIN()">
</adj-number-input>
<md2-mob-attack-info [mob]="boss.info">
<md2-mob-attack-info [mob]="boss">
</md2-mob-attack-info>
<md2-mob-def-info [mob]="boss.info"></md2-mob-def-info>
<md2-mob-def-info [mob]="boss"></md2-mob-def-info>
</div>
<div class="col-md-9 h6" *ngIf="boss.extraRules">
<div [innerHtml]="boss.extraRules"></div>
<div class="col-md-9 bossSpecialRules" *ngIf="boss.bossFightProfile.specialRules">
<div [innerHtml]="boss.bossFightProfile.specialRules"></div>
</div>
</div>
<md2-mob-combat-info [mob]="boss.info"></md2-mob-combat-info>
<md2-mob-combat-info [mob]="boss"></md2-mob-combat-info>
<!--
<button nbButton hero status="danger" size="small" (click)="attack(boss.info)">Attack It</button> -->
<button nbButton hero status="danger" size="small" (click)="attack(boss)">Attack It</button> -->
<!-- <label class="MD2Text mt-3" [innerHtml]="boss.info.combatSkill.skillName">
<!-- <label class="MD2Text mt-3" [innerHtml]="boss.combatSkill.skillName">
</label>
<label class="MD2Text" [innerHtml]="boss.info.combatInfo.skillDescription">
<label class="MD2Text" [innerHtml]="boss.combatInfo.skillDescription">
</label> -->
</div>
@@ -2,3 +2,136 @@ nb-card {
height: 80vh;
//width: 80vw;
}
.bossStandImg {
max-height: 67vh;
object-fit: contain;
}
::ng-deep .bossSpecialRules {
.MD2Icon {
font-size: 30px;
}
}
// HP and Mana Bars Overlay
.hero-stats-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 0.5rem;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.5) 70%, transparent 100%);
border-radius: 0 0 8px 8px;
z-index: 2;
width: 95%;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.35rem;
}
}
.stat-bar-overlay {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
@media (max-height: 450px) and (orientation: landscape) {
margin-bottom: 0.35rem;
}
}
.stat-bar-label-overlay {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.25rem;
gap: 0.5rem;
md2-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
}
.stat-value-overlay {
color: white;
font-size: 0.75rem;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
flex-shrink: 0;
}
@media (max-height: 450px) and (orientation: landscape) {
margin-bottom: 0.15rem;
gap: 0.3rem;
md2-icon {
width: 14px;
height: 14px;
}
.stat-value-overlay {
font-size: 0.65rem;
}
}
}
.stat-progress-bar-overlay {
width: 100%;
height: 20px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.3);
@media (max-height: 450px) and (orientation: landscape) {
height: 16px;
border-radius: 8px;
}
}
.stat-progress-fill-overlay {
height: 100%;
border-radius: 10px;
transition: width 0.5s ease-out;
position: relative;
overflow: hidden;
@media (max-height: 450px) and (orientation: landscape) {
border-radius: 8px;
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: shimmer 2s infinite;
}
&.full-stat {
width: 100%;
}
}
.hp-fill-overlay {
background: linear-gradient(90deg, #ff6b6b, #ee5a6f);
box-shadow: 0 0 8px rgba(238, 90, 111, 0.6);
}
.mp-fill-overlay {
background: linear-gradient(90deg, #4ecdc4, #44a08d);
box-shadow: 0 0 8px rgba(68, 160, 141, 0.6);
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@@ -17,8 +17,8 @@ import { SpawnMobDlgComponent } from '../mobs/spawn-mob-dlg/spawn-mob-dlg.compon
styleUrls: ['./boss-fight.component.scss']
})
export class BossFightComponent extends MD2ComponentBase {
MobDlgType = MobDlgType;
MD2Icon = MD2Icon;
public get boss() {
return this.md2Service.info.boss;
}
@@ -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.info);
this.attack(this.boss);
}
});
}
@@ -49,10 +49,14 @@ export class BossFightComponent extends MD2ComponentBase {
this.destroy$.complete();
}
activate() {
this.boss.activating();
this.md2Service.activateBoss();
}
WIN() {
this.msgBoxService.show('Win', { text: 'You Win the Boss Fight', icon: ADIcon.INFO });
this.md2Service.info.isBossFight = false;
this.md2Service.info.boss = undefined;
this.md2Service.heros.forEach(h => h.uiBossFight = false);
this.md2Service.broadcastGameInfo();
}
attack(mob: MobInfo) {
@@ -61,7 +65,10 @@ export class BossFightComponent extends MD2ComponentBase {
if (mobResult) {
let attackDamage = mobResult.uiWounds;
if (attackDamage) {
this.boss.info.hp -= attackDamage;
this.boss.unitRemainHp -= attackDamage;
if (this.boss.unitRemainHp <= 0) {
this.WIN();
}
this.cdRef.detectChanges();
}
}
@@ -8,6 +8,7 @@ export class MD2Clone {
let cloneObj = null;
switch (type) {
case "TreasureItem":
//let copy = structuredClone(obj);
return new TreasureItem(obj['type'], 1);
break;
case "MobInfo":
@@ -1,222 +0,0 @@
import { environment } from "../../../../../environments/environment";
import { DefenseInfo, IMobFactory, MD2Icon, MobInfo } from "../../massive-darkness2.model";
import { MobSkill, MobSkillType } from "../../massive-darkness2.model.boss";
const MD2_IMG_URL = (id: string = null) => { return `${environment.apiUrl}/Files/Images/MD2/Mobs${(id ? `${encodeURI(id)}` : '')}` }
const CORE_GAME_MOB_LEVEL = [
{ name: 'Gargoyles', level: 1, hp: 2, rewardTokens: 1, defBlue: 1 },
{ name: 'Gargoyles', level: 3, hp: 3, rewardTokens: 1, defBlue: 2 },
{ name: 'Gargoyles', level: 5, hp: 6, rewardTokens: 2, defBlue: 3 },
{ name: 'Demons', level: 1, hp: 3, rewardTokens: 1, defBlue: 1 },
{ name: 'Demons', level: 3, hp: 4, rewardTokens: 1, defBlue: 2 },
{ name: 'Demons', level: 5, hp: 6, rewardTokens: 2, defBlue: 4 },
{ name: 'Undead', level: 1, hp: 4, rewardTokens: 1, defBlue: 1 },
{ name: 'Undead', level: 3, hp: 5, rewardTokens: 1, defBlue: 1 },
{ name: 'Undead', level: 5, hp: 8, rewardTokens: 2, defBlue: 1 },
{ name: 'Fire Entities', level: 1, hp: 3, rewardTokens: 1, defBlue: 1 },
{ name: 'Fire Entities', level: 3, hp: 4, rewardTokens: 1, defBlue: 2 },
{ name: 'Fire Entities', level: 5, hp: 7, rewardTokens: 2, defBlue: 3 },
{ name: 'Fallen Angels', level: 1, hp: 2, rewardTokens: 1, defBlue: 2 },
{ name: 'Fallen Angels', level: 3, hp: 3, rewardTokens: 1, defBlue: 3 },
{ name: 'Fallen Angels', level: 5, hp: 5, rewardTokens: 2, defBlue: 5 },
{ name: 'Infernal Imps', level: 1, hp: 3, rewardTokens: 1, defBlue: 1 },
{ name: 'Infernal Imps', level: 3, hp: 4, rewardTokens: 1, defBlue: 1 },
{ name: 'Infernal Imps', level: 5, hp: 5, rewardTokens: 2, defBlue: 3 },
{ name: 'Skeletons', level: 1, hp: 2, rewardTokens: 1, defBlue: 1 },
{ name: 'Skeletons', level: 3, hp: 3, rewardTokens: 1, defBlue: 2 },
{ name: 'Skeletons', level: 5, hp: 5, rewardTokens: 2, defBlue: 4 },
{ name: 'Satyrs', level: 1, hp: 3, rewardTokens: 1, defBlue: 1 },
{ name: 'Satyrs', level: 3, hp: 4, rewardTokens: 1, defBlue: 2 },
{ name: 'Satyrs', level: 5, hp: 6, rewardTokens: 2, defBlue: 4 }]
export abstract class MobFactory implements IMobFactory {
abstract mobName: string;
abstract generate(level: number): MobInfo
protected mob: MobInfo;
protected loadLevelInfo(mobName: string, level: number) {
let levelInfo = CORE_GAME_MOB_LEVEL.find(m => m.name == mobName && level >= m.level);
this.mob = new MobInfo({
name: mobName, hp: levelInfo.hp, level: level, rewardTokens: levelInfo.rewardTokens,
defenseInfo: new DefenseInfo(levelInfo.defBlue)
});
this.mob.leaderImgUrl = MD2_IMG_URL(`/CoreGame/Mobs/${this.mob.name}/Leader.png`);
this.mob.minionImgUrl = MD2_IMG_URL(`/CoreGame/Mobs/${this.mob.name}/Minion.png`);
}
iconHtml(icon: MD2Icon, cssClass = 'g-font-size-24') {
if (icon == MD2Icon.Fire) {
cssClass += ' g-color-google-plus ';
}
if (icon == MD2Icon.Frost || icon == MD2Icon.Mana) {
cssClass += ' g-color-aqua ';
}
if (icon < MD2Icon.RedDice) {
return `<span class='MD2Icon ${cssClass}'>${String.fromCharCode(65 + icon)}</span>`
}
else {
return `<span class='MD2Icon dice ${MD2Icon[icon].replace('Dice', '')} ${cssClass}'></span>`;
}
}
}
export class MobDemonsFactory extends MobFactory {
mobName: string = 'Demons';
generate(level: number): MobInfo {
this.loadLevelInfo('Demons', level);
this.mob.combatSkill = new MobSkill(
{
description: `Attacking or defending Hero discards 1 ${this.iconHtml(MD2Icon.Mana)}`
}
)
return this.mob;
}
}
export class MobFallenAngelFactory extends MobFactory {
mobName: string = 'Fallen Angels';
generate(level: number): MobInfo {
this.loadLevelInfo('Fallen Angels', level);
this.mob.combatSkill = new MobSkill(
{
description: `Defender -${level == 1 ? 1 : 2} ${this.iconHtml(MD2Icon.Defense)}`,
type: MobSkillType.Attack
}
)
return this.mob;
}
}
export class MobFireEntitiesFactory extends MobFactory {
mobName: string = 'Fire Entities';
generate(level: number): MobInfo {
this.loadLevelInfo('Fire Entities', level);
this.mob.combatSkill = new MobSkill(
{
description: `Add 1 ${this.iconHtml(MD2Icon.Fire)} to the attacking or defending Hero.`,
type: MobSkillType.Combat
}
)
return this.mob;
}
}
export class MobGargoylesFactory extends MobFactory {
mobName: string = 'Gargoyles';
generate(level: number): MobInfo {
this.loadLevelInfo('Gargoyles', level);
this.mob.combatSkill = new MobSkill(
{
description: `+ ${level < 5 ? 1 : 2} ${this.iconHtml(MD2Icon.Defense)}`,
type: MobSkillType.Defense
}
)
return this.mob;
}
}
export class MobInfernalImpsFactory extends MobFactory {
mobName: string = 'Infernal Imps';
generate(level: number): MobInfo {
this.loadLevelInfo('Infernal Imps', level);
let damage = 1;
switch (level) {
case 1:
case 2:
damage = 1;
break;
case 3:
case 4:
damage = 2;
break;
case 5:
damage = 3;
break;
default:
damage = 1;
break;
}
this.mob.combatSkill = new MobSkill(
{
description: `Kill 1 Imp, then deal ${damage} Wound to each Hero in the attacker's Zone(once per roll).`,
type: MobSkillType.Defense
}
)
return this.mob;
}
}
export class MobSatyrsFactory extends MobFactory {
mobName: string = 'Satyrs';
generate(level: number): MobInfo {
this.loadLevelInfo('Satyrs', level);
this.mob.combatSkill = new MobSkill(
{
description: `+ ${level < 3 ? 1 : 2} ${this.iconHtml(MD2Icon.Attack)}`,
type: MobSkillType.Attack
}
)
return this.mob;
}
}
export class MobSkeletonsFactory extends MobFactory {
mobName: string = 'Skeletons';
generate(level: number): MobInfo {
this.loadLevelInfo('Skeletons', level);
this.mob.combatSkill = new MobSkill(
{
description: `Add 1 minion to this Mob(if possible) unless the Hero discards ${level < 5 ? 1 : 2} ${this.iconHtml(MD2Icon.Mana)}.`,
type: MobSkillType.Defense,
skillRoll: 2
}
)
return this.mob;
}
}
export class MobUndeadFactory extends MobFactory {
mobName: string = 'Undead';
generate(level: number): MobInfo {
this.loadLevelInfo('Undead', level);
let skillDesc = '';
if (level < 3) {
skillDesc = `+1 ${this.iconHtml(MD2Icon.YellowDice)}`;
} else if (level < 5) {
skillDesc = `+2 ${this.iconHtml(MD2Icon.YellowDice)}`;
} else {
skillDesc = `+1 ${this.iconHtml(MD2Icon.YellowDice)} 1 ${this.iconHtml(MD2Icon.OrangeDice)}`;
}
skillDesc += ' and this Mob takes 2 wounds';
this.mob.combatSkill = new MobSkill(
{
description: skillDesc,
type: MobSkillType.Attack
}
)
this.mob.drawingWeight = 1;
return this.mob;
}
}
export const CoreGameMobFactories = [
new MobDemonsFactory(),
new MobFallenAngelFactory(),
new MobFireEntitiesFactory(),
new MobGargoylesFactory(),
new MobInfernalImpsFactory(),
new MobSatyrsFactory(),
new MobSkeletonsFactory(),
new MobUndeadFactory(),
];
@@ -3,80 +3,11 @@ import { first, map } from "rxjs/operators";
import { environment } from "../../../../../environments/environment";
import { ADButtons, ADIcon } from "../../../../ui/alert-dlg/alert-dlg.model";
import { MD2Logic } from "../../massive-darkness2.logic";
import { AttackInfo, AttackTarget, DefenseInfo, IMobFactory, MD2Icon, MobInfo, MobType, TreasureItem, TreasureType } from "../../massive-darkness2.model";
import { MobSkill, MobSkillType } from "../../massive-darkness2.model.boss";
import { AttackInfo, AttackTarget, IMobFactory, MD2Icon, MobInfo, MobType, TreasureItem, TreasureType } from "../../massive-darkness2.model";
import { MobSkillType } from "../../massive-darkness2.model.boss";
import { MD2DiceSet, MD2MobSkill } from "../../massive-darkness2.db.model";
const MD2_IMG_URL = (id: string = null) => { return `${environment.apiUrl}/Files/Images/MD2/Mobs${(id ? `${encodeURI(id)}` : '')}` }
const CORE_GAME_MOB_LEVEL = [
new MobInfo({
name: 'Andra', level: 1, hp: 5,
attackInfos: [new AttackInfo(MD2Icon.Melee, 1, 0, 0, 1), new AttackInfo(MD2Icon.Range, 1, 0, 0, 1)],
defenseInfo: new DefenseInfo(2, 1),
}),
new MobInfo({
name: 'Andra', level: 3, hp: 7,
attackInfos: [new AttackInfo(MD2Icon.Melee, 1, 1, 0, 1), new AttackInfo(MD2Icon.Range, 1, 1, 0, 1)],
defenseInfo: new DefenseInfo(3, 1),
}),
new MobInfo({
name: 'Andra', level: 5, hp: 5,
attackInfos: [new AttackInfo(MD2Icon.Melee, 1, 2, 0, 1), new AttackInfo(MD2Icon.Range, 1, 2, 0, 1)],
defenseInfo: new DefenseInfo(5, 1),
}),
new MobInfo({
name: 'Ytheria, Undead Queen', level: 1, hp: 4,
attackInfos: [new AttackInfo(MD2Icon.Melee, 1), new AttackInfo(MD2Icon.Range, 2, 0, 0, 1)],
defenseInfo: new DefenseInfo(1, 1),
}),
new MobInfo({
name: 'Ytheria, Undead Queen', level: 3, hp: 6,
attackInfos: [new AttackInfo(MD2Icon.Melee, 0, 1), new AttackInfo(MD2Icon.Range, 1, 1, 0, 1)],
defenseInfo: new DefenseInfo(2, 1),
}),
new MobInfo({
name: 'Ytheria, Undead Queen', level: 5, hp: 8,
attackInfos: [new AttackInfo(MD2Icon.Melee, 2, 1), new AttackInfo(MD2Icon.Range, 2, 1, 0, 1)],
defenseInfo: new DefenseInfo(4, 1),
}),
new MobInfo({
name: 'Lyidan, Incubus Lord', level: 1, hp: 7,
attackInfos: [new AttackInfo(MD2Icon.Melee, 0, 1, 0, 2)],
defenseInfo: new DefenseInfo(2, 1),
}),
new MobInfo({
name: 'Lyidan, Incubus Lord', level: 3, hp: 10,
attackInfos: [new AttackInfo(MD2Icon.Melee, 0, 2, 0, 1)],
defenseInfo: new DefenseInfo(2, 1),
}),
new MobInfo({
name: 'Lyidan, Incubus Lord', level: 5, hp: 12,
attackInfos: [new AttackInfo(MD2Icon.Melee, 2, 2, 0, 1)],
defenseInfo: new DefenseInfo(4, 1),
}),
new MobInfo({
name: 'The Ghoul', level: 1, hp: 5,
attackInfos: [new AttackInfo(MD2Icon.Melee, 0, 1, 0, 1)],
defenseInfo: new DefenseInfo(2, 1),
}),
new MobInfo({
name: 'The Ghoul', level: 3, hp: 8,
attackInfos: [new AttackInfo(MD2Icon.Melee, 0, 2, 0, 2)],
defenseInfo: new DefenseInfo(3, 1),
}),
new MobInfo({
name: 'The Ghoul', level: 5, hp: 10,
attackInfos: [new AttackInfo(MD2Icon.Melee, 0, 3, 0, 3)],
defenseInfo: new DefenseInfo(4, 1),
}),
]
export abstract class CoreGameRMFactory implements IMobFactory {
abstract mobName: string;
@@ -168,15 +99,16 @@ export class RMUndeadQueenFactory extends CoreGameRMFactory {
}
});
}
});;
});
}
this.mob.combatSkill = new MobSkill(
this.mob.skills = [
{
description: `Add 1 Minion to each Mob in the Dungeon, if possible.`,
type: MobSkillType.Attack
}
)
type: MobSkillType.Attack,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
@@ -200,9 +132,6 @@ export class RMAndraFactory extends CoreGameRMFactory {
}).pipe(first()).subscribe(result => {
if (result) {
mob.actions = 0;
mob.actionSubject.next(
`Undead Queen attacks each Hero in LoS(resolve each attack separately).`
);
msgBoxService.show('Is Any Hero in the LoS of Andra?', {
icon: ADIcon.QUESTION,
@@ -229,12 +158,12 @@ export class RMAndraFactory extends CoreGameRMFactory {
});
}
this.mob.combatSkill = new MobSkill(
this.mob.skills = [
{
description: `Deal ${damage} wound to another Hero with the lowest HP in LoS`,
type: MobSkillType.Combat
}
)
type: MobSkillType.Combat,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
@@ -273,12 +202,12 @@ export class RMTheGhoulFactory extends CoreGameRMFactory {
});
}
this.mob.combatSkill = new MobSkill(
this.mob.skills = [
{
description: `Move the closest <b>Mob with minion</b> 1 Zone toward The Ghoul.`,
type: MobSkillType.Combat
}
)
type: MobSkillType.Combat,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
@@ -300,7 +229,7 @@ export class RMLyidanIncubusLordFactory extends CoreGameRMFactory {
`The Incubus Lord got 3 Wounds, Move it to the closest Shadow Zone.`
);
} else {
msgBoxService.show('Is there a Herp up to 3 Zones away(regardless of LoS) from The Incubus Lord?', {
msgBoxService.show('Is there a Hero up to 3 Zones away(regardless of LoS) from The Incubus Lord?', {
icon: ADIcon.QUESTION,
buttons: ADButtons.YesNo
}).pipe(first()).subscribe(result => {
@@ -320,12 +249,48 @@ export class RMLyidanIncubusLordFactory extends CoreGameRMFactory {
});
}
this.mob.combatSkill = new MobSkill(
this.mob.skills = [
{
description: `After combat, resolve all ${this.iconHtml(MD2Icon.Fire)} on the defending Hero(once per combat).`,
type: MobSkillType.Attack
type: MobSkillType.Attack,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
export class RMBalrogFactory extends CoreGameRMFactory {
mobName: string = 'Balrog';
generate(level: number): MobInfo {
this.loadLevelInfo('Balrog', level);
this.mob.extraRule = `When Balrog is in the Dungeon, ${this.iconHtml(MD2Icon.Fire)} on Heros can't be removed, Heros still suffer its effects when activating.`
this.mob.activateFunction = (mob, msgBoxService, heros) => {
let actionResult = '';
mob.actions = 0;
let noFireHeros = heros.filter(h => h.fireToken == 0);
if (noFireHeros.length == 0) {
mob.actions = 3;
mob.actionSubject.next(
`The Balrog gains 3 Actions`
);
} else {
let fireTokens = Math.round(Math.random() * 2) + 1;
mob.actionSubject.next(
`Balrog ,moves 2 Zones toward to ${MD2Logic.getTargetHerosHtml(noFireHeros)} and Each Hero in ${this.iconHtml(MD2Icon.Magic)} range takes ${fireTokens} ${this.iconHtml(MD2Icon.Fire)}`
);
}
)
}
this.mob.skills = [
{
description: `The Hero takes 1 ${this.iconHtml(MD2Icon.Fire)}`,
type: MobSkillType.Combat,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
@@ -334,4 +299,5 @@ export const CoreGameRMFactories = [
new RMAndraFactory(),
new RMTheGhoulFactory(),
new RMLyidanIncubusLordFactory(),
new RMBalrogFactory()
];
@@ -0,0 +1,40 @@
<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>
@@ -0,0 +1,12 @@
nb-card {
min-width: 400px;
}
.form-group {
margin-bottom: 1rem;
}
nb-checkbox {
display: block;
margin-bottom: 0.5rem;
}
@@ -0,0 +1,73 @@
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();
}
}
@@ -1,24 +1,203 @@
<nb-card *ngIf="!hero">
<nb-card-body>
<button nbButton hero status="primary" fullWidth (click)="initHero()">Choose Hero</button>
<!-- Hero Selection Screen - Initial -->
<nb-card *ngIf="!hero && !isSelectingHero" class="hero-selection-card">
<nb-card-body class="hero-selection-body">
<div class="hero-selection-content">
<h2 class="hero-selection-title">Choose Your Hero</h2>
<p class="hero-selection-subtitle">Begin your epic adventure</p>
<button nbButton hero status="primary" size="large" class="hero-selection-btn" (click)="initHero()">
<nb-icon icon="star-outline" class="mr-2"></nb-icon>
Select Hero
</button>
</div>
</nb-card-body>
</nb-card>
<!-- Hero Selection Panel -->
<div *ngIf="!hero && isSelectingHero && currentSelectingHero" class="hero-selection-panel">
<div class="row no-gutters hero-selection-row">
<div class="col-12 hero-selection-left">
<div class="hero-selection-card-wrapper">
<div class="hero-selection-header">
<div class="hero-selection-title-bar">
<h3 class="hero-selection-hero-name">{{currentSelectingHero.name}}</h3>
<span class="hero-selection-class">{{HeroClass[selectedHeroClass]}}</span>
<span class="hero-selection-counter">({{currentHeroIndex + 1}} / {{heros.length}})</span>
</div>
</div>
<div class="hero-selection-content-area">
<div class="row no-gutters h-100">
<div class="col-6 hero-select-image-col">
<div class="hero-select-image-wrapper"
[style.background-image]="'url(' + imgUrl('/Mobs/BG.png') + ')'">
<img src="{{imgUrl('Heros/'+className+'.png')}}" class="hero-select-image"
alt="{{currentSelectingHero.name}}">
<!-- HP and Mana Bars -->
<div class="hero-select-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">{{currentSelectingHero.hpMaximum}}</span>
</div>
<div class="stat-progress-bar-overlay">
<div class="stat-progress-fill-overlay hp-fill-overlay full-stat"></div>
</div>
</div>
<div class="stat-bar-overlay mp-bar-overlay">
<div class="stat-bar-label-overlay">
<md2-icon [icon]="MD2Icon.Mana_Color" size="sm"></md2-icon>
<span class="stat-value-overlay">{{currentSelectingHero.mpMaximum}}</span>
</div>
<div class="stat-progress-bar-overlay">
<div class="stat-progress-fill-overlay mp-fill-overlay full-stat"></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-6 hero-select-skills-col">
<div class="hero-select-skills">
<div class="skills-title">Abilities</div>
<div class="skill-content" [innerHTML]="currentSelectingHero.skillHtml"></div>
<div class="skills-title shadow-skills-title">Shadow Abilities</div>
<div class="skill-content shadow-skill-content"
[innerHTML]="currentSelectingHero.shadowSkillHtml"></div>
</div>
</div>
</div>
</div>
<div class="hero-selection-actions">
<button nbButton hero status="basic" class="nav-hero-btn" (click)="previousHero()">
<nb-icon icon="chevron-back-outline" class="mr-1"></nb-icon>
Previous
</button>
<button nbButton hero status="primary" class="select-hero-btn" (click)="selectCurrentHero()">
<nb-icon icon="checkmark-circle-outline" class="mr-2"></nb-icon>
It's Me!
</button>
<button nbButton hero status="basic" class="nav-hero-btn" (click)="nextHero()">
Next
<nb-icon icon="chevron-forward-outline" class="ml-1"></nb-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="hero">
<div class="row no-gutters">
<div class="col-12 col-sm-7">
<div class="tp-wrapper mb-2">
<div class="tp-box g-height-300 g-height-350--sm g-height-500--md" (click)="toggleFlip()"
[@flipState]="flip">
<div class="tp-box" [@flipState]="flip">
<div class="tp-box__side tp-box__front ">
<img class="MD2HeroCard " src="{{hero.imgUrl}}">
<div class="hero-card-content">
<div class="row no-gutters h-100">
<div class="col-6 hero-image-col">
<div class="hero-image-wrapper"
[style.background-image]="'url(' + imgUrl('/Mobs/BG.png') + ')'">
<img src="{{imgUrl('Heros/'+className+'.png')}}" class="hero-image"
(click)="toggleFlip()" alt="{{hero.name}}">
<!-- 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">{{hero.hp}}/{{hero.hpMaximum}}</span>
</div>
<div class="stat-progress-bar-overlay">
<div class="stat-progress-fill-overlay hp-fill-overlay"
[style.width.%]="(hero.hp / hero.hpMaximum) * 100"></div>
</div>
</div>
<div class="stat-bar-overlay mp-bar-overlay">
<div class="stat-bar-label-overlay">
<md2-icon [icon]="MD2Icon.Mana_Color" size="sm"></md2-icon>
<span
class="stat-value-overlay">{{hero.mp}}/{{hero.mpMaximum}}</span>
</div>
<div class="stat-progress-bar-overlay">
<div class="stat-progress-fill-overlay mp-fill-overlay"
[style.width.%]="(hero.mp / hero.mpMaximum) * 100"></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-6 hero-skills-col">
<div class="hero-skills">
<div class="skills-title" (click)="showSkills('abilities')">Abilities</div>
<div class="skill-content" [innerHTML]="hero.skillHtml"></div>
<div class="skills-title shadow-skills-title" (click)="showSkills('shadow')">
Shadow Abilities</div>
<div class="skill-content shadow-skill-content"
[innerHTML]="hero.shadowSkillHtml"></div>
</div>
</div>
</div>
</div>
<!-- <img class="MD2HeroCard " src="{{imgUrl('Heros/'+className+'.jpg')}}" (click)="toggleFlip()"> -->
<!-- Action Buttons (Desktop/Landscape) -->
<div class="hero-actions d-block">
<div class="action-buttons-group" *ngIf="hero.uiActivating && hero.remainActions > 0">
<button nbButton hero class="action-btn" status="info" (click)="moveAction()"
*ngIf="!showMoveAction">
<nb-icon icon="arrow-forward-outline" class="mr-1"></nb-icon>
Move
</button>
<button nbButton hero class="action-btn" status="info" (click)="moveActionEnd()"
*ngIf="showMoveAction">
<nb-icon icon="checkmark-outline" class="mr-1"></nb-icon>
End Move
</button>
<button nbButton hero class="action-btn" status="danger"
(click)="action('attackAction')" *ngIf="!showMoveAction && allowAttack">
<nb-icon icon="flash-outline" class="mr-1"></nb-icon>
Attack!
</button>
<button nbButton hero class="action-btn" status="info" (click)="action('tradeAction')"
*ngIf="!showMoveAction">
<nb-icon icon="swap-outline" class="mr-1"></nb-icon>
Trade
</button>
<button nbButton hero class="action-btn" status="success"
(click)="action('recoveryAction')" *ngIf="!showMoveAction">
<nb-icon icon="heart-outline" class="mr-1"></nb-icon>
Recovery
</button>
</div>
<button nbButton hero class="action-btn special-action-btn" status="info" fullWidth
(click)="openDoor()" *ngIf="showMoveAction">
<nb-icon icon="grid-outline" class="mr-1"></nb-icon>
Open Door
</button>
<button nbButton hero fullWidth status="info" class="start-activation-btn"
*ngIf="allowStartAction" (click)="startActivation()">
<nb-icon icon="play-circle-outline" class="mr-2"></nb-icon>
Start Activation
</button>
<button nbButton hero fullWidth status="warning" class="end-activation-btn"
*ngIf="hero.uiActivating" (click)="endActivation()">
<nb-icon icon="stop-circle-outline" class="mr-2"></nb-icon>
End Activation
</button>
</div>
</div>
<div class="tp-box__side tp-box__back">
<div class="row no-gutters">
<div class="col-6">
<img class="MD2HeroCard " src="{{imgUrl('Heros/Guide/'+className+'.jpg')}}"
(click)="toggleFlip()">
</div>
<div class="col-6">
<img class="MD2HeroCard " src="{{imgUrl('Sets/Shadowbane/'+className+'.png')}}"
(click)="toggleFlip()">
</div>
</div>
<img class="MD2HeroCard " src="{{imgUrl('Heros/Guide/'+className+'.jpg')}}">
<img class="MD2HeroCard " src="{{imgUrl('Sets/Shadowbane/'+className+'.png')}}">
</div>
</div>
@@ -33,6 +212,7 @@
<img class="MD2HeroCard" src="{{hero.imgUrl}}">
<img class="MD2HeroCard HpMpBar" src="{{imgUrl('/Heros/Template/Border.png')}}">
</div> -->
</div>
<div class="col-12 col-sm-5">
@@ -41,78 +221,79 @@
<div class="row no-gutters">
<div class="col-6">
<!-- <adj-number-input name="heroHP" [(ngModel)]="hero.hp"
[maximum]="hero.hpMaximum" minimum="0"
title="{{iconHtml(MD2Icon.HP,'g-color-google-plus mr-1 g-font-size-18')}}HP" showMaximum
(blur)="heroUpdateDebounceTimer.resetTimer()" (hitDecreasing)="increaseRage()">
</adj-number-input> -->
<adj-number-input name="heroHP" [(ngModel)]="hero.hp" [maximum]="hero.hpMaximum" minimum="0"
title="{{imgHtml('HpIcon.png','g-height-25 mr-1')}}HP" showMaximum
title="{{imgHtml('HpIcon.png','g-height-25')}}" showMaximum
(blur)="heroUpdateDebounceTimer.resetTimer()" (hitDecreasing)="increaseRage()">
</adj-number-input>
</div>
<div class="col-6">
<adj-number-input name="heroLevel" [(ngModel)]="hero.level" minimum="1" maximum="5"
title="Level" (blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
</div>
<div class="col-6">
<adj-number-input name="heroMana" [(ngModel)]="hero.mp" [maximum]="hero.mpMaximum"
minimum="0" title="{{imgHtml('HeroIcon.png','g-height-25 mr-1')}}Mana" showMaximum
minimum="0" title="{{imgHtml('HeroIcon.png','g-height-25')}}" showMaximum
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
</div>
<div class="col-6">
<adj-number-input name="heroExp" [(ngModel)]="hero.exp" minimum="0" title="Exp"
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
</div>
<div class="col-6">
<adj-number-input name="heroFire" [(ngModel)]="hero.fireToken" minimum="0"
title="{{iconHtml(MD2Icon.Fire,'g-color-google-plus mr-1 g-font-size-18')}}Fire Token"
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
<adj-number-input name="heroFire" [(ngModel)]="hero.frozenToken" minimum="0"
</div>
<div class="col-6">
<adj-number-input name="heroFrozen" [(ngModel)]="hero.frozenToken" minimum="0"
title="{{iconHtml(MD2Icon.Frost,'g-color-aqua mr-1 g-font-size-18')}}Frozen Token"
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
</div>
<div class="col-6">
<adj-number-input name="remainActions" [(ngModel)]="hero.remainActions" minimum="0"
title="Remain Actions" (blur)="heroUpdateDebounceTimer.resetTimer()" hideIncreaseBtn
*ngIf="hero.uiActivating">
</adj-number-input>
<adj-number-input name="heroLevel" [(ngModel)]="hero.level" minimum="1" maximum="5"
title="Level" (blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
<adj-number-input name="heroExp" [(ngModel)]="hero.exp" minimum="0" title="Exp"
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
</div>
<div class="col-6" *ngIf="hero.class==HeroClass.Berserker">
<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()" *ngIf="hero.class==HeroClass.Berserker">
</adj-number-input>
<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">
(blur)="heroUpdateDebounceTimer.resetTimer()">
</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>
</div>
</div>
<div *ngIf="md2Service.info.isBossFight"></div>
<div *ngIf="hero.uiActivating&&hero.remainActions>0">
<button nbButton hero class="mr-2" status="info" (click)="moveAction()"
*ngIf="!showMoveAction">Move</button>
<button nbButton hero class="mr-2" status="info" (click)="moveActionEnd()"
*ngIf="showMoveAction">Move End</button>
<button nbButton hero class="mr-2" status="danger" (click)="action('attackAction')"
*ngIf="!showMoveAction&&allowAttack">Attack!</button>
<button nbButton hero class="mr-2" status="info" (click)="action('tradeAction')"
*ngIf="!showMoveAction">Trade</button>
<button nbButton hero status="success" (click)="action('recoveryAction')"
*ngIf="!showMoveAction">Recovery</button>
</div>
<button nbButton hero fullWidth status="info" *ngIf="allowStartAction"
(click)="startActivation()">Start Activation</button>
<button nbButton hero status="info" class="mt-2" (click)="openDoor()" *ngIf="showMoveAction">Open
Door</button>
<button nbButton hero fullWidth status="warning" class="mt-3" *ngIf="hero.uiActivating"
(click)="endActivation()">End
Activation</button>
</nb-card-body>
</nb-card>
@@ -1,3 +1,367 @@
// Hero Selection Screen
.hero-selection-card {
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
overflow: hidden;
}
.hero-selection-body {
padding: 3rem 2rem;
text-align: center;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.9) 0%, rgba(118, 75, 162, 0.9) 100%);
@media (max-height: 450px) and (orientation: landscape) {
padding: 2rem 1.5rem;
}
}
.hero-selection-content {
max-width: 500px;
margin: 0 auto;
}
.hero-selection-title {
color: white;
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.75rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
@media (max-height: 450px) and (orientation: landscape) {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
}
.hero-selection-subtitle {
color: rgba(255, 255, 255, 0.9);
font-size: 1rem;
margin-bottom: 1.5rem;
@media (max-height: 450px) and (orientation: landscape) {
font-size: 0.85rem;
margin-bottom: 1rem;
}
}
.hero-selection-btn {
padding: 0.75rem 2rem;
font-size: 1rem;
font-weight: 600;
border-radius: 50px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.6rem 1.5rem;
font-size: 0.9rem;
}
}
// Hero Selection Panel
.hero-selection-panel {
height: 85vh;
max-height: 85vh;
padding: 0.5rem;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
overflow: hidden;
display: flex;
flex-direction: column;
@media (orientation: landscape) {
height: 85vh;
max-height: 85vh;
padding: 0.25rem;
}
@media (orientation: portrait) {
height: 85vh;
max-height: 85vh;
padding: 0.5rem;
}
@media (orientation: portrait) and (max-height: 667px) {
height: 85vh;
max-height: 85vh;
padding: 0.25rem;
}
@media (max-height: 450px) and (orientation: landscape) {
height: 85vh;
max-height: 85vh;
padding: 0.15rem;
}
}
.hero-selection-row {
height: 100%;
margin: 0;
display: flex;
flex-direction: column;
}
.hero-selection-left {
padding: 0;
height: 100%;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0;
}
}
.hero-selection-card-wrapper {
height: 100%;
background: white;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
@media (max-height: 450px) and (orientation: landscape) {
border-radius: 8px;
}
}
.hero-selection-header {
padding: 0.75rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
flex-shrink: 0;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.5rem 0.75rem;
border-bottom-width: 1px;
}
}
.hero-selection-title-bar {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
@media (max-height: 450px) and (orientation: landscape) {
gap: 0.5rem;
}
@media (orientation: portrait) and (max-width: 767px) {
gap: 0.5rem;
}
}
.hero-selection-hero-name {
font-size: 1.2rem;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
@media (max-height: 450px) and (orientation: landscape) {
font-size: 0.95rem;
}
@media (orientation: portrait) and (max-width: 767px) {
font-size: 1rem;
}
}
.hero-selection-class {
font-size: 0.85rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
@media (max-height: 450px) and (orientation: landscape) {
font-size: 0.7rem;
}
@media (orientation: portrait) and (max-width: 767px) {
font-size: 0.75rem;
}
}
.hero-selection-counter {
font-size: 0.8rem;
opacity: 0.8;
margin-left: auto;
@media (max-height: 450px) and (orientation: landscape) {
font-size: 0.65rem;
}
@media (orientation: portrait) and (max-width: 767px) {
font-size: 0.7rem;
}
}
.hero-selection-content-area {
flex: 1;
padding: 0.75rem;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.4rem;
}
@media (orientation: portrait) and (max-width: 767px) {
padding: 0.5rem;
}
}
.hero-select-image-col,
.hero-select-skills-col {
padding: 0.4rem;
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.2rem;
}
@media (orientation: portrait) and (max-width: 767px) {
padding: 0.3rem;
}
}
.hero-select-image-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 8px;
background: #f8f9fa;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.hero-select-image {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.hero-select-stats-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.5) 70%, transparent 100%);
border-radius: 0 0 8px 8px;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.35rem;
}
}
.hero-select-skills {
width: 100%;
height: 100%;
background: #f8f9fa;
border-radius: 8px;
padding: 0.75rem;
overflow-y: auto;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
min-height: 0;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.4rem;
}
@media (orientation: portrait) and (max-width: 767px) {
padding: 0.5rem;
}
}
.hero-selection-actions {
padding: 0.75rem;
border-top: 2px solid #e9ecef;
display: flex;
gap: 0.75rem;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.5rem;
gap: 0.35rem;
border-top-width: 1px;
}
@media (orientation: portrait) and (max-width: 767px) {
padding: 0.5rem;
gap: 0.5rem;
}
}
.nav-hero-btn {
flex: 1;
min-height: 40px;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.3s;
padding: 0.5rem;
@media (max-height: 450px) and (orientation: landscape) {
min-height: 32px;
font-size: 0.75rem;
padding: 0.35rem 0.5rem;
}
@media (orientation: portrait) and (max-width: 767px) {
min-height: 38px;
font-size: 0.8rem;
}
}
.select-hero-btn {
flex: 2;
min-height: 40px;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
transition: all 0.3s;
padding: 0.5rem;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
}
@media (max-height: 450px) and (orientation: landscape) {
min-height: 32px;
font-size: 0.8rem;
padding: 0.35rem 0.5rem;
}
@media (orientation: portrait) and (max-width: 767px) {
min-height: 38px;
font-size: 0.85rem;
}
}
.MD2Hp {
font-size: xx-large;
position: fixed;
@@ -36,45 +400,416 @@
.tp-wrapper {
-webkit-perspective: 800px;
perspective: 800px;
height: 40vh; // Default for portrait
@media (orientation: landscape) {
height: 85vh;
}
@media (orientation: portrait) and (max-height: 667px) {
height: 50vh;
}
}
.tp-box {
position: relative;
//width: 200px;
//height: 100px;
//margin: 3rem auto;
width: 100%;
height: 100%;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
-webkit-transform: transform 1s;
-ms-transform: transform 1s;
transform: transform 1s;
}
.tp-box__side {
width: 100%;
height: 100%;
position: absolute;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
color: #fff;
text-align: center;
line-height: 100px;
font-size: 24px;
font-weight: 700;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
display: flex;
flex-direction: column;
overflow: hidden;
}
// Hero Card Content Section
.hero-card-content {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
padding: 0.5rem;
overflow: hidden;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.25rem;
}
}
.hero-image-col {
padding: 0.25rem;
display: flex;
flex-direction: column;
height: 100%;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.15rem;
}
}
.hero-image-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 8px;
background: #f8f9fa;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.hero-image {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
cursor: pointer;
transition: transform 0.3s;
&:hover {
transform: scale(1.02);
}
}
// HP and Mana Bars Overlay
.hero-stats-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0.5rem;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.5) 70%, transparent 100%);
border-radius: 0 0 8px 8px;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.35rem;
}
}
.stat-bar-overlay {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
@media (max-height: 450px) and (orientation: landscape) {
margin-bottom: 0.35rem;
}
}
.stat-bar-label-overlay {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.25rem;
gap: 0.5rem;
md2-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
}
.stat-value-overlay {
color: white;
font-size: 0.75rem;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
flex-shrink: 0;
}
@media (max-height: 450px) and (orientation: landscape) {
margin-bottom: 0.15rem;
gap: 0.3rem;
md2-icon {
width: 14px;
height: 14px;
}
.stat-value-overlay {
font-size: 0.65rem;
}
}
}
.stat-progress-bar-overlay {
width: 100%;
height: 20px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.3);
@media (max-height: 450px) and (orientation: landscape) {
height: 16px;
border-radius: 8px;
}
}
.stat-progress-fill-overlay {
height: 100%;
border-radius: 10px;
transition: width 0.5s ease-out;
position: relative;
overflow: hidden;
@media (max-height: 450px) and (orientation: landscape) {
border-radius: 8px;
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: shimmer 2s infinite;
}
&.full-stat {
width: 100%;
}
}
.hp-fill-overlay {
background: linear-gradient(90deg, #ff6b6b, #ee5a6f);
box-shadow: 0 0 8px rgba(238, 90, 111, 0.6);
}
.mp-fill-overlay {
background: linear-gradient(90deg, #4ecdc4, #44a08d);
box-shadow: 0 0 8px rgba(68, 160, 141, 0.6);
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.hero-skills-col {
padding: 0.25rem;
display: flex;
flex-direction: column;
height: 100%;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.15rem;
}
}
.hero-skills {
width: 100%;
height: 100%;
background: #f8f9fa;
border-radius: 8px;
padding: 0.75rem;
overflow-y: auto;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.5rem;
}
}
.skills-title {
font-size: 0.95rem;
font-weight: 700;
color: #667eea;
margin-bottom: 0.5rem;
padding-bottom: 0.25rem;
border-bottom: 2px solid #667eea;
@media (max-height: 450px) and (orientation: landscape) {
font-size: 0.75rem;
margin-bottom: 0.3rem;
padding-bottom: 0.15rem;
}
}
.shadow-skills-title {
margin-top: 1rem;
color: #764ba2;
border-bottom-color: #764ba2;
@media (max-height: 450px) and (orientation: landscape) {
margin-top: 0.5rem;
}
}
.skill-content {
font-size: 0.85rem;
line-height: 1.5;
color: #495057;
@media (max-height: 450px) and (orientation: landscape) {
font-size: 0.7rem;
line-height: 1.3;
}
}
.shadow-skill-content {
color: #6c757d;
}
// Action Buttons Section
.hero-actions {
padding: 0.75rem;
border-top: 2px solid #e9ecef;
margin-top: auto;
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.5rem;
border-top-width: 1px;
}
}
.action-buttons-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.75rem;
@media (max-height: 450px) and (orientation: landscape) {
gap: 0.25rem;
margin-bottom: 0.5rem;
flex-wrap: nowrap;
}
}
.action-btn {
flex: 1;
min-width: 100px;
min-height: 40px;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
@media (max-height: 450px) and (orientation: landscape) {
min-width: 70px;
min-height: 36px;
font-size: 0.75rem;
padding: 0.35rem 0.5rem;
}
}
.special-action-btn {
margin-bottom: 0.5rem;
border-radius: 8px;
font-weight: 600;
min-height: 40px;
font-size: 0.875rem;
transition: all 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
@media (max-height: 450px) and (orientation: landscape) {
margin-bottom: 0.35rem;
min-height: 32px;
font-size: 0.75rem;
padding: 0.35rem 0.5rem;
}
}
.start-activation-btn {
border-radius: 8px;
font-weight: 600;
padding: 0.75rem;
min-height: 44px;
font-size: 0.95rem;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.5rem;
min-height: 36px;
font-size: 0.8rem;
}
}
.end-activation-btn {
border-radius: 8px;
font-weight: 600;
padding: 0.75rem;
min-height: 44px;
font-size: 0.95rem;
transition: all 0.3s;
box-shadow: 0 2px 8px rgba(255, 193, 7, 0.3);
margin-top: 0.5rem;
background: linear-gradient(135deg, #ffc107 0%, #ff9800 100%);
border: none;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.4);
}
@media (max-height: 450px) and (orientation: landscape) {
padding: 0.5rem;
min-height: 36px;
font-size: 0.85rem;
margin-top: 0.35rem;
}
}
.tp-box__front {
-webkit-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
transform: rotateY(0deg);
background: white;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.tp-box__back {
-webkit-transform: rotateY(-180deg);
-ms-transform: rotateY(-180deg);
transform: rotateY(-180deg);
}
::ng-deep .skill-content .MD2Icon {
font-size: 30px;
}
@@ -11,8 +11,11 @@ import { ADButtonColor, ADButtons } from '../../../ui/alert-dlg/alert-dlg.model'
import { ArrayUtils } from '../../../utilities/array-utils';
import { StringUtils } from '../../../utilities/string-utils';
import { DebounceTimer } from '../../../utilities/timer-utils';
import { HeroClass, MD2HeroInfo } from '../massive-darkness2.model';
import { HeroClass, MD2HeroInfo, MD2HeroProfile, MD2Icon } from '../massive-darkness2.model';
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',
@@ -33,6 +36,7 @@ import { MD2Base } from '../MD2Base';
]
})
export class HeroDashboardComponent extends MD2Base implements OnInit {
MD2Icon = MD2Icon;
heroAction(hero: MD2HeroInfo, action: string) {
throw new Error('Method not implemented.');
}
@@ -43,7 +47,9 @@ 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'),
@@ -53,36 +59,65 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
new DropDownOption(HeroClass.Wizard, 'Wizard'),
new DropDownOption(HeroClass.Shaman, 'Shaman'),
new DropDownOption(HeroClass.Druid, 'Druid'),
new DropDownOption(HeroClass.Necromancer, 'Necromancer'),
new DropDownOption(HeroClass.Monk, 'Monk'),
new DropDownOption(HeroClass.Thinker, 'Thinker'),
new DropDownOption(HeroClass.Bard, 'Bard'),
];
heros = [] as MD2HeroInfo[];
wizards: MD2HeroInfo[] = [
new MD2HeroInfo({ name: 'Ajax', mpMaximum: 6, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Baldric', mpMaximum: 5, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Ego', mpMaximum: 5, hpMaximum: 6, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Elias', mpMaximum: 6, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Megan', mpMaximum: 5, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Moira', mpMaximum: 6, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Myriam', mpMaximum: 7, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Valdis', mpMaximum: 6, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' })
]
heroProfiles: MD2HeroProfile[] = [];
currentHeroIndex: number = 0;
isSelectingHero: boolean = false;
selectedHeroClass: HeroClass;
public get hero() {
return this.md2Service.playerHero;
}
public get className() {
if (this.md2Service.playerHero) {
return HeroClass[this.md2Service.playerHero.class];
}
if (this.selectedHeroClass) {
return HeroClass[this.selectedHeroClass];
}
return '';
}
public get currentSelectingHero(): MD2HeroInfo {
return this.heros[this.currentHeroIndex];
}
constructor(
private gameRoomService: GameRoomService,
public md2Service: MD2Service,
private heroProfileService: MD2HeroProfileService,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
private msgBoxService: MsgBoxService,
private signalRService: SignalRService,
private toastrService: NbToastrService
) {
super(md2Service, stateService, route, cdRef);
this.isHeroDashboard = true;
}
public get allowAttack(): boolean {
return this.md2Service.playerHero.uiBossFight || (!!this.md2Service.mobs && this.md2Service.mobs.length > 0) || (!!this.md2Service.roamingMonsters && this.md2Service.roamingMonsters.length > 0);
return this.hero.uiBossFight || this.md2Service.mobs?.length > 0 || this.md2Service.roamingMonsters?.length > 0;
}
ngOnInit(): void {
super.ngOnInit();
this.gameRoomService.gameRoomId = this.roomId;
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();
// }
});
}
override signalRInitialized() {
@@ -93,9 +128,8 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
// }
}
initHero() {
this.gameRoomService.gameRoomId = this.roomId;
this.gameRoomService.joinGameRoom(this.roomId);
if (!this.md2Service.heros.some(h => h.playerInfo.signalRClientId == this.stateService.loginUserService.userAccess.signalRSessionId)) {
if (!this.md2Service.heros.some(h => h.playerInfo.signalRClientId == this.stateService.loginUserService.userAccess.signalRConnectionId)) {
this.msgBoxService.showInputbox('Select Hero Class', '', { dropDownOptions: this.classOptions, inputType: 'dropdown' })
.pipe(first()).subscribe(heroClass => {
if (heroClass != null) {
@@ -116,50 +150,75 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
});
}
}
className: string;
initClassHeroList(heroClass: HeroClass) {
this.heros = [];
this.className = HeroClass[heroClass];
this.fileList(`Heros/${this.className}`).pipe(first()).subscribe(fileNames => {
for (let i = 0; i < fileNames.length; i++) {
const heroNames = fileNames[i].split('.')[0].split('-');
this.selectedHeroClass = heroClass;
this.heroProfileService.getAll().pipe(first()).subscribe(result => {
this.heros.push(new MD2HeroInfo({
name: heroNames[0].replace('/', ''),
mpMaximum: Number.parseInt(heroNames[1]),
hpMaximum: Number.parseInt(heroNames[2]),
imgUrl: this.imgUrl(`Heros/${this.className}/${fileNames[i]}`),
this.heroProfiles = result.filter(h => h.heroClass == heroClass);
for (let i = 0; i < this.heroProfiles.length; i++) {
const heroProfile = this.heroProfiles[i];
const heroInfo = new MD2HeroInfo({
name: heroProfile.title,
mpMaximum: heroProfile.mana,
hpMaximum: heroProfile.hp,
hp: heroProfile.hp,
mp: heroProfile.mana,
skillHtml: heroProfile.skillHtml,
shadowSkillHtml: heroProfile.shadowSkillHtml.replace("<p>", '<p>' + this.iconHtml(MD2Icon.Shadow) + ' : '),
class: heroClass
}))
});
heroInfo.imgUrl = this.imgUrl('Heros/' + HeroClass[heroClass] + '.jpg');
this.heros.push(heroInfo);
}
this.heros = ArrayUtils.Shuffle(this.heros);//.sort((a, b) => StringUtils.compareSemVer(a.name, b.name));
this.showHeroList(heroClass, 0);
this.currentHeroIndex = 0;
this.isSelectingHero = true;
this.detectChanges();
});
}
showHeroList(heroClass: HeroClass, index: number) {
let className = HeroClass[heroClass];
let heroInfo = this.heros[index];
this.msgBoxService.show(`${className}(${index + 1}/${this.heros.length})`, {
text: `<img src='${heroInfo.imgUrl}' class="g-width-50vw-md g-width-80vw">`,
buttons: ADButtons.YesNo,
cardWidthClass: '',
confirmButtonText: 'It\'s Me!',
cancelButtonText: 'Next',
cancelButtonColor: ADButtonColor.INFO
}).pipe(first()).subscribe(result => {
if (result) {
this.md2Service.playerJoin(heroInfo);
this.detectChanges();
} else {
index++;
if (index == this.heros.length) index = 0;
this.showHeroList(heroClass, index);
}
});
selectCurrentHero() {
if (this.currentSelectingHero) {
this.md2Service.playerJoin(this.currentSelectingHero);
this.md2Service.broadcastMyHeroInfo();
this.isSelectingHero = false;
this.detectChanges();
}
}
showSkills(type: string) {
if (type == 'abilities') {
this.msgBoxService.show('Abilities', { text: this.currentSelectingHero.skillHtml });
} else {
this.msgBoxService.show('Shadow Abilities', { text: this.currentSelectingHero.shadowSkillHtml });
}
}
nextHero() {
this.currentHeroIndex++;
if (this.currentHeroIndex >= this.heros.length) {
this.currentHeroIndex = 0;
}
this.detectChanges();
}
previousHero() {
this.currentHeroIndex--;
if (this.currentHeroIndex < 0) {
this.currentHeroIndex = this.heros.length - 1;
}
this.detectChanges();
}
fetchGameInfo() {
this.md2Service.broadcastFetchGameInfo();
}
broadcastHeroInfo() {
this.md2Service.broadcastService.broadcastMyHeroInfo();
this.md2Service.broadcastMyHeroInfo();
this.heroUpdateDebounceTimer.clearOut();
}
increaseRage() {
@@ -168,8 +227,8 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
}
}
openDoor() {
this.md2Service.broadcastService.broadcastHeroAction('openDoor');
this.showMoveAction = false;
this.md2Service.broadcastHeroAction('openDoor');
//this.showMoveAction = false;
this.detectChanges();
}
moveAction() {
@@ -193,7 +252,7 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
default:
break;
}
this.md2Service.broadcastService.broadcastHeroAction(action);
this.md2Service.broadcastHeroAction(action);
this.reduceAction();
}
reduceAction() {
@@ -210,12 +269,25 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
get allowStartAction() {
return !this.md2Service.heros.some(h => h.uiActivating) && !this.hero.uiActivating && this.hero.remainActions > 0;
}
public get hero() {
return this.md2Service.playerHero;
}
startActivation() {
this.hero.uiActivating = true;
//this.hero.remainActions = 3;
if (this.hero.fireToken > 0) {
this.msgBoxService.show(`You Are On ${this.iconHtml(MD2Icon.Fire)}!`, {
text: `Roll ${this.iconHtml(MD2Icon.YellowDice)} ${this.hero.fireToken} times.`
});
}
if (this.hero.frozenToken > 0) {
let loseActions = Math.min(this.hero.frozenToken, this.hero.remainActions);
this.hero.remainActions -= loseActions;
this.hero.frozenToken -= loseActions;
this.msgBoxService.show(`It's So Cold ${this.iconHtml(MD2Icon.Frost)}!`, {
text: `Lose ${loseActions} actions.`
});
}
if (this.hero.remainActions == 0) {
this.hero.uiActivating = false;
}
this.broadcastHeroInfo();
}
endActivation() {
@@ -10,17 +10,24 @@
</nb-accordion-item-body>
</nb-accordion-item>
</nb-accordion>
</div> -->
</div>
<div class="col-12">
</div>
-->
<div class="col-12 col-md-5">
<nb-card>
<nb-card-header>
<img src="{{imgUrl('HeroIcon.png')}}" width="40px"> Game Info
<img src="{{imgUrl('HeroIcon.png')}}" width="40px">
<span class="ml-2 g-font-size-17 MD2text" [innerHtml]="round"></span>
<button nbButton hero status="info" size="small" (click)="showQrCode()"
class="float-right">Invite</button>
<button nbButton hero status="info" size="small" [disabled]="anyHeroRemainAction"
(click)="md2Service.runNextPhase()" class="float-right mr-2">Next Phase</button>
<button nbButton hero status="info" size="small" (click)="broadcastHeros()"
class="float-right mr-2">Broadcast</button>
</nb-card-header>
<nb-card-body>
@@ -41,8 +48,7 @@
</div>
<div class="row" *ngIf="md2Service.heros.length>0">
<div class="col-12 g-font-size-17" [innerHtml]="roundPhase"></div>
<!-- <div class="col-12 g-font-size-17" [innerHtml]="roundPhase"></div> -->
<!-- <div class="col-6">
<label for='playerAmount' class='label'>Hero Amount ({{md2Service.playerAmount}})</label>
</div>
@@ -51,20 +57,44 @@
({{md2Service.highestPlayerLevel}})</label>
</div> -->
<div class="col-12" *ngFor="let hero of md2Service.heros">
<label class='label mr-1'>{{hero.playerInfo.name}} ({{heroClassName(hero)}} -
<label class='label mr-1'
(click)="adjustHeroValue(hero,'remainActions')">{{hero.playerInfo.name}}
({{heroClassName(hero)}} -
{{hero.name}})</label>
<span class="badge badge-primary mr-1">Lv.:{{hero.level}}</span>
<span class="badge badge-primary mr-1">HP: {{hero.hp}}/{{hero.hpMaximum}}</span>
<span class="badge badge-primary mr-1">Mana: {{hero.mp}}/{{hero.mpMaximum}}</span>
<span class="badge badge-success mr-1">Exp: {{hero.exp}}</span>
<span class="badge badge-danger mr-1" *ngIf="hero.fireToken">Fire:{{hero.fireToken}}</span>
<span class="badge badge-info mr-1" *ngIf="hero.frozenToken">Frozen:{{hero.frozenToken}}</span>
<span class="badge badge-success mr-1" *ngIf="hero.remainActions>0">Remain
Actions: {{hero.remainActions}}</span>
<span class="badge badge-light mr-1" *ngIf=" !hero.uiActivating">Inactive</span>
<span class="badge badge-primary mr-1"
(click)="adjustHeroValue(hero,'level')">Lv.:{{hero.level}}</span>
<span class="badge badge-primary mr-1" (click)="adjustHeroValue(hero,'hp')">HP:
{{hero.hp}}/{{hero.hpMaximum}}</span>
<span class="badge badge-primary mr-1" (click)="adjustHeroValue(hero,'mp')">Mana:
{{hero.mp}}/{{hero.mpMaximum}}</span>
<span class="badge badge-success mr-1" (click)="adjustHeroValue(hero,'exp')">Exp:
{{hero.exp}}</span>
<span class="badge mr-1" *ngIf="hero.fireToken">
<md2-icon [icon]="MD2Icon.FireToken" size="sm"></md2-icon> {{hero.fireToken}}
</span>
<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>
<span class="badge badge-light mr-1" *ngIf=" !hero.uiActivating"
(click)="activatingHero(hero)">Inactive</span>
<span class="badge badge-primary mr-1" *ngIf="hero.uiActivating">Activating</span>
<span class="badge badge-warning mr-1"
*ngIf="hero.playerInfo.isDisconnected">Disconnected</span>
<!-- <span class="badge badge-success mr-1">{{hero.playerInfo.signalRClientId}}</span> -->
<span class="badge badge-danger mr-1" (click)="removeHero(hero)">X
</span>
</div>
</div>
@@ -77,10 +107,38 @@
<div class="col-12 col-md-3">
<div class="row">
<div class="form-group col-12">
<!-- <div class="form-group col-4">
<button nbButton hero fullWidth status="primary" (click)="enterBossFight()">Enter Boss Fight</button>
</div>
<div class="form-group col-4">
<button nbButton hero fullWidth status="primary" (click)="enterBossFight()">Enter Boss Fight</button>
</div>
<div class="form-group col-4">
openGreatTreasureChest
<button nbButton hero fullWidth status="success" (click)="accessHealFountain()">Access Heal Fountain</button>
</div> -->
<ng-container *ngIf="md2Service.currentActivateHero">
<div class="form-group col-12">
<button nbButton hero fullWidth status="info" (click)="md2Service.openTreasureChest()">Open
Treasure Chest</button>
</div>
<div class="form-group col-12">
<button nbButton hero fullWidth status="primary" (click)="md2Service.openGreatTreasureChest()">Open
Great
Treasure Chest</button>
</div>
<div class="form-group col-12">
<button nbButton hero fullWidth status="success" (click)="accessHealFountain()">Access Heal
Fountain</button>
</div>
</ng-container>
<div class="form-group col-12">
<button nbButton hero fullWidth status="danger" (click)="enterBossFight()">Enter Boss Fight</button>
</div>
</div>
@@ -0,0 +1,3 @@
.badge {
cursor: pointer;
}
@@ -5,7 +5,7 @@ import { MsgBoxService } from '../../services/msg-box.service';
import { ArrayUtils } from '../../utilities/array-utils';
import { ObjectUtils } from '../../utilities/object-utils';
import { first, map, take, takeUntil } from 'rxjs/operators';
import { TreasureType, DrawingBag, DrawingItem, HeroClass, MD2HeroInfo, RoundPhase, MobInfo, MobDlgType } from './massive-darkness2.model';
import { TreasureType, DrawingBag, DrawingItem, HeroClass, MD2HeroInfo, RoundPhase, MobInfo, MobDlgType, MD2Icon } from './massive-darkness2.model';
import { MD2Service } from '../../services/MD2/md2.service';
import { GameRoomService } from '../../services/game-room.service';
import { MD2Base } from './MD2Base';
@@ -16,6 +16,9 @@ import { StringUtils } from '../../utilities/string-utils';
import { SpawnMobDlgComponent } from './mobs/spawn-mob-dlg/spawn-mob-dlg.component';
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',
@@ -24,7 +27,8 @@ import { MD2InitService } from '../../services/MD2/md2-init.service';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MassiveDarkness2Component extends MD2Base implements OnInit {
HeroClass: HeroClass
MD2Icon = MD2Icon;
HeroClass = HeroClass;
constructor(
private fileService: FileService,
private initService: MD2InitService,
@@ -41,20 +45,100 @@ 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();
}
override signalRInitialized() {
}
adjustHeroValue(hero: MD2HeroInfo, value: string) {
this.msgBoxService.showInputbox(`Adjust ${value} for ${hero.playerInfo.name}`, `Enter the new value for ${value}`, {
inputType: 'number',
inputValue: hero[value].toString()
}).pipe(first()).subscribe(result => {
if (![false, null, undefined].includes(result)) {
hero[value] = Number.parseInt(result);
this.md2Service.broadcastHeroInfoToAll(hero, true);
this.detectChanges();
}
});
}
activatingHero(hero: MD2HeroInfo) {
if (hero.remainActions > 0) {
this.msgBoxService.show('Activating', { text: `Are you sure you want to activate ${hero.playerInfo.name}?` }).pipe(first()).subscribe(result => {
if (result) {
this.md2Service.heros.forEach(h => h.uiActivating = false);
hero.uiActivating = true;
this.md2Service.broadcastAllHeroInfoToAll();
this.detectChanges();
}
});
}
}
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>` });
}
@@ -137,4 +221,31 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
this.md2Service.enterBossFight();
}
accessHealFountain() {
this.md2Service.drawingHealFountain();
}
broadcastHeros() {
this.md2Service.heros.forEach(hero => {
hero.uiShowAttackBtn = this.md2Service.mobs.length > 0 || this.md2Service.roamingMonsters.length > 0 || this.md2Service.info.isBossFight;
});
this.md2Service.broadcastAllHeroInfoToAll();
}
removeHero(hero) {
this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(result => {
if (result) {
this.md2Service.info.heros.splice(this.md2Service.info.heros.indexOf(hero));
this.cdRef.detectChanges();
}
});
}
public get round(): string {
if (this.md2Service.info.isBossFight) {
return `Boss Fight ${NumberUtils.Ordinal(this.md2Service.info.bossRound)} Round`;
} else {
return NumberUtils.Ordinal(this.md2Service.info.round) + ' Round';
}
}
}
@@ -0,0 +1,105 @@
import { MobType } from "./massive-darkness2.model";
import { MobSkillType } from "./massive-darkness2.model.boss";
export enum MobSkillTarget {
Random = 40,
LeastHp = 50,
LeastMp = 60,
HighestHp = 70,
HighestMp = 80,
LowestLevel = 90,
HighestLevel = 100,
MostExtraToken = 200,
LeastExtraToken,
MostExtraToken2,
LeastExtraToken2
}
export enum GameBundle {
CoreGame,
HeavenFallen,
Zombicide,
ZombicideWhiteDeath,
DarkBringerPack
}
export interface MD2MobInfo {
id: string;
type: MobType;
from: GameBundle;
name: string;
leaderImgUrl: string;
minionImgUrl: string;
mobLevelInfos: MD2MobLevelInfo[];
skills: MD2MobSkill[];
bossFightProfile?: BossFightProfile;
}
export interface MD2MobLevelInfo {
id: string;
level: number;
mobInfoId: string;
rewardTokens: number;
fixedRareTreasure: number;
fixedEpicTreasure: number;
fixedLegendTreasure: number;
fixedHp: number;
hpPerHero: number;
actions: number;
attackInfo: MD2DiceSet;
alterAttackInfo?: MD2DiceSet;
defenceInfo: MD2DiceSet;
}
export class MD2MobSkill {
constructor(config: Partial<MD2MobSkill> = {}) {
Object.assign(this, config);
}
id: string;
seq: number;
level: number;
mobInfoId: string;
type: MobSkillType;
skillTarget: MobSkillTarget | null;
clawRoll: number;
skillRoll: number;
name: string;
skillCondition: string;
description: string;
uiDisplay?: boolean;
}
export interface MD2DiceSet {
type: MobSkillType;
yellow: number | null;
orange: number | null;
red: number | null;
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;
}
@@ -6,47 +6,65 @@ 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 getTargetHerosByFilter(heros: MD2HeroInfo[], targetType: AttackTarget, onlyOne: boolean = false) {
public static getTargetHeroByFilter(heros: MD2HeroInfo[], targetType: MobSkillTarget) {
return this.getTargetHerosByFilter(heros, targetType, true)[0];
}
public static getTargetHerosByFilter(heros: MD2HeroInfo[], targetType: MobSkillTarget, onlyOne: boolean = false) {
let beenAttackedHero = [] as MD2HeroInfo[];
switch (targetType) {
case AttackTarget.LeastHp:
case MobSkillTarget.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 AttackTarget.LeastMp:
case MobSkillTarget.LeastMp:
let lowestMp = Math.min(...heros.map(h => h.mp));
beenAttackedHero = heros.filter(h => h.hp == lowestMp);
beenAttackedHero = heros.filter(h => h.mp == lowestMp);
//this.otherAttackTarget = 'attacking the other <b>Lowest HP</b> hero.';
break;
case AttackTarget.HighestHp:
case MobSkillTarget.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 AttackTarget.HighestMp:
case MobSkillTarget.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 AttackTarget.LowestLevel:
let lowestLevel = Math.max(...heros.map(h => h.level));
case MobSkillTarget.LowestLevel:
let lowestLevel = Math.min(...heros.map(h => h.level));
beenAttackedHero = heros.filter(h => h.level == lowestLevel);
//this.otherAttackTarget = 'attacking the other <b>Lowest Level</b> hero.';
break;
case AttackTarget.LeastCorruption:
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:
let leastCor = Math.min(...heros.map(h => h.corruptionToken));
beenAttackedHero = heros.filter(h => h.corruptionToken == leastCor);
let leastExtraToken = Math.min(...heros.map(h => h.extraToken));
beenAttackedHero = heros.filter(h => h.extraToken == leastExtraToken);
break;
case AttackTarget.MostCorruption:
let mostCor = Math.max(...heros.map(h => h.corruptionToken));
beenAttackedHero = heros.filter(h => h.corruptionToken == mostCor);
case MobSkillTarget.MostExtraToken:
let mostExtraToken = Math.max(...heros.map(h => h.extraToken));
beenAttackedHero = heros.filter(h => h.extraToken == mostExtraToken);
break;
case AttackTarget.Random:
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:
default:
beenAttackedHero = [heros[Math.round(Math.random() * (heros.length - 1))]];
//this.otherAttackTarget = 'Just act like normal.';
@@ -1,17 +1,25 @@
import { stringify } from "querystring"
import { Observable, Subject, Subscription } from "rxjs"
import { first } from "rxjs/operators"
import { MD2Service } from "../../services/MD2/md2.service"
import { StringUtils } from "../../utilities/string-utils"
import { BossActivationComponent } from "./boss-fight/boss-activation/boss-activation.component"
import { TreasureType, AttackInfo, DefenseInfo, AttackType, MD2Icon, MD2HeroInfo, AttackTarget, MobInfo, MobType } from "./massive-darkness2.model"
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"
export enum MobSkillType {
Attack,
Defense,
Combat
Combat,
Passive,
ConditionalSkill,
OtherWiseSkill,
ActiveSkill,
MeleeAttack = 15,
RangeAttack,
MagicAttack,
}
export class MobSkill {
constructor(config: Partial<MobSkill> = {}) {
@@ -45,6 +53,7 @@ export interface IBossFight {
imgUrl: string
standUrl: string
extraRules: string
md2Service: MD2Service
activating(): boolean
prepareForBossFight(): void
darknessPhase(): void
@@ -65,7 +74,7 @@ export abstract class BossFight implements IBossFight {
extraRules: string
protected subscription: Subscription
constructor(protected md2Service: MD2Service) {
constructor(public md2Service: MD2Service) {
this.rounds = 1;
}
activating(): boolean {
@@ -97,35 +106,39 @@ export abstract class BossFight implements IBossFight {
}
export class BossMicheal extends BossFight {
constructor(protected md2Service: MD2Service) {
constructor(public md2Service: MD2Service) {
super(md2Service);
this.corruptionTokenHtml = this.md2Service.stateService.imgHtml('Tokens/CorruptToken.png');
this.corruptionTokenHtml = this.md2Service.imgHtml('Tokens/CorruptToken.png');
this.name = 'Michael - The Corrupted Archangel';
this.imgUrl = md2Service.stateService.imgUrl('/Boss/Michael - The Corrupted Archangel.jpg');
this.standUrl = md2Service.stateService.imgUrl('/Boss/Michael.png');
this.imgUrl = md2Service.imgUrl('/Boss/Michael - The Corrupted Archangel.jpg');
this.standUrl = md2Service.imgUrl('/Boss/Michael.png');
this.info = new MobInfo({
description: this.name,
type: MobType.Boss,
hpPerHero: 15,
level: 10,
combatSkill: new MobSkill(
{
name: `Combat 1 ${this.md2Service.stateService.iconHtml(MD2Icon.EnemySkill)}`,
description: `Deal 1 Wound for each ${this.corruptionTokenHtml} on the attacking or defending Hero. Discard the tokens afterwards(once per combat).`
}),
imageUrl: md2Service.stateService.imgUrl('/Boss/Michael.png')
imageUrl: md2Service.imgUrl('/Boss/Michael.png')
});
this.info.defenseInfo = new DefenseInfo(5, 1);
if (!this.info.skills) {
this.info.skills = [];
}
this.info.skills.push({
name: `Combat 1 ${this.md2Service.iconHtml(MD2Icon.EnemySkill)}`,
description: `Deal 1 Wound for each ${this.corruptionTokenHtml} on the attacking or defending Hero. Discard the tokens afterwards(once per combat).`,
type: MobSkillType.Combat,
skillRoll: 1
} as MD2MobSkill);
this.info.defenseInfo = { blue: 5, black: 1 } as MD2DiceSet;
this.info.attackInfos = [new AttackInfo(MD2Icon.Melee, 2, 2, 0, 1)];
this.actions = 1;
this.actionBlackDice = 2;
this.extraRules = `Archangel Michael cant be the target of any attack, skill, ability or take Wounds until there are no Corruption tokens in the whole Tile.<br><br>` +
`Any Hero on a Zone with a ${this.corruptionTokenHtml} may spend 1 action to remove it. Each time a Hero removes a ${this.corruptionTokenHtml} from a Zone they must roll 1 ${this.md2Service.stateService.iconHtml(MD2Icon.BlackDice)}.` +
`If ${this.md2Service.stateService.iconHtml(MD2Icon.EnemyClaw)} the Hero takes 1 Wound.<br>If ${this.md2Service.stateService.iconHtml(MD2Icon.EnemySkill)} place 1 ${this.corruptionTokenHtml} on their Dashboard.<br>` +
`If ${this.md2Service.stateService.iconHtml(MD2Icon.EnemyClaw)}/${this.md2Service.stateService.iconHtml(MD2Icon.EnemySkill)} the Hero takes 1 Wound and places 1 ${this.corruptionTokenHtml} on their Dashboard.`
`Any Hero on a Zone with a ${this.corruptionTokenHtml} may spend 1 action to remove it. Each time a Hero removes a ${this.corruptionTokenHtml} from a Zone they must roll 1 ${this.md2Service.iconHtml(MD2Icon.BlackDice)}.` +
`If ${this.md2Service.iconHtml(MD2Icon.EnemyClaw)} the Hero takes 1 Wound.<br>If ${this.md2Service.iconHtml(MD2Icon.EnemySkill)} place 1 ${this.corruptionTokenHtml} on their Dashboard.<br>` +
`If ${this.md2Service.iconHtml(MD2Icon.EnemyClaw)}/${this.md2Service.iconHtml(MD2Icon.EnemySkill)} the Hero takes 1 Wound and places 1 ${this.corruptionTokenHtml} on their Dashboard.`
}
activatedTimes: number
acted: number
@@ -143,14 +156,14 @@ export class BossMicheal extends BossFight {
bossAction(): Observable<boolean> {
let actionResult = new RollingBlackDice().roll(this.actionBlackDice);
let actionResult = new RollingBlackDice().roll(2);
let actionHtml = '';
let beenAttackedHero = [] as MD2HeroInfo[];
let bossAction: MobSkill;
switch (actionResult.claws) {
case 0:
//Justice From Above
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.MostCorruption, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.MostExtraToken, true);
bossAction = new MobSkill(
{
name: 'Justice From Above',
@@ -160,7 +173,7 @@ export class BossMicheal extends BossFight {
break;
case 1:
//Lance Dash
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastCorruption, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastExtraToken, true);
bossAction = new MobSkill({
name: 'Lance Dash',
description:
@@ -180,12 +193,13 @@ export class BossMicheal extends BossFight {
default:
break;
}
return this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction, currentAction: this.activatedTimes, allActions: this.actions } }).onClose;
return null;
//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.uiShowCorruptionToken = true;
hero.uiShowExtraToken = true;
});
this.md2Service.msgBoxService.show('Prepare Boss Fight', {
text: `<h6>Place ${this.md2Service.heros.length * 2} ${this.corruptionTokenHtml} on the Corruption Stone Zones (Shadow
@@ -220,28 +234,30 @@ export class BossMicheal extends BossFight {
export class BossReaper extends BossFight {
constructor(protected md2Service: MD2Service) {
constructor(public md2Service: MD2Service) {
super(md2Service);
this.timeTokenHtml = this.md2Service.stateService.imgHtml('Tokens/TimeToken.png');
this.timeTokenHtml = this.md2Service.imgHtml('Tokens/TimeToken.png');
this.name = 'The Reaper';
this.imgUrl = md2Service.stateService.imgUrl('/Boss/The Reaper.jpg');
this.standUrl = md2Service.stateService.imgUrl('/Boss/The Reaper-Stand.png');
this.imgUrl = md2Service.imgUrl('/Boss/The Reaper.jpg');
this.standUrl = md2Service.imgUrl('/Boss/The Reaper-Stand.png');
this.info = new MobInfo({
description: this.name,
type: MobType.Boss,
hpPerHero: 25,
level: 10,
combatSkill: new MobSkill(
{
description: `If the Hero has no ${this.md2Service.stateService.iconHtml(MD2Icon.Mana)}, they take 1 ${this.md2Service.stateService.iconHtml(MD2Icon.Frost)}`,
type: MobSkillType.Attack,
}),
imageUrl: md2Service.stateService.imgUrl('/Boss/The Reaper-Stand.png')
imageUrl: md2Service.imgUrl('/Boss/The Reaper-Stand.png')
});
this.info.defenseInfo = new DefenseInfo(4, 3);
if (!this.info.skills) {
this.info.skills = [];
}
this.info.skills.push({
description: `If the Hero has no ${this.md2Service.iconHtml(MD2Icon.Mana_Color)}, they take 1 ${this.md2Service.iconHtml(MD2Icon.FrozenToken)}`,
type: MobSkillType.Attack,
skillRoll: 1
} as MD2MobSkill);
this.info.defenseInfo = { blue: 4, black: 3 } as MD2DiceSet;
this.info.attackInfos = [new AttackInfo(MD2Icon.Melee, 1, 2, 0, 3)];
this.actions = 1;
this.actionBlackDice = 2;
@@ -267,7 +283,7 @@ export class BossReaper extends BossFight {
switch (actionResult.claws) {
case 0:
//Justice From Above
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastMp, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastMp, true);
bossAction = new MobSkill(
{
name: 'Soul Drain',
@@ -277,7 +293,7 @@ export class BossReaper extends BossFight {
break;
case 1:
//Lance Dash
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastCorruption, true);
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastExtraToken, true);
bossAction = new MobSkill({
name: 'Time Ticking',
description:
@@ -290,15 +306,16 @@ export class BossReaper extends BossFight {
name: 'Death Is Coming',
description:
`Place The Reaper in the central Zone.<br>` +
`Roll 1 ${this.md2Service.stateService.iconHtml(MD2Icon.YellowDice)}. Remove ${this.timeTokenHtml} equal to ${this.md2Service.stateService.iconHtml(MD2Icon.Melee)} rolled from both <b>Hourglass Zone</b>.<br>` +
`Each Hero discards ${this.md2Service.stateService.iconHtml(MD2Icon.MP)} equal to ${this.md2Service.stateService.iconHtml(MD2Icon.Melee)} rolled.`
`Roll 1 ${this.md2Service.iconHtml(MD2Icon.YellowDice)}. Remove ${this.timeTokenHtml} equal to ${this.md2Service.iconHtml(MD2Icon.Melee)} rolled from both <b>Hourglass Zone</b>.<br>` +
`Each Hero discards ${this.md2Service.iconHtml(MD2Icon.MP)} equal to ${this.md2Service.iconHtml(MD2Icon.Melee)} rolled.`
});
break;
default:
break;
}
return this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction, currentAction: this.activatedTimes, allActions: this.actions } }).onClose;
return null;
//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', {
@@ -5,6 +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";
const MD2_IMG_URL = (id: string = null) => { return `${environment.apiUrl}/Files/Images/MD2/${(id ? `${encodeURI(id)}` : '')}` }
export enum MobDlgType {
@@ -22,6 +23,7 @@ export enum RoundPhase {
BossActivation
}
export enum TreasureType {
Cover,
Common,
Rare,
Epic,
@@ -35,6 +37,10 @@ export enum HeroClass {
Shaman,
Paladin,
Druid,
Necromancer,
Monk,
Thinker,
Bard
}
export enum MobType {
Mob,
@@ -71,9 +77,23 @@ export enum MD2Icon {
Rage,
RedDice,
BlueDice,
GreenDice,
YellowDice,
OrangeDice,
BlackDice
BlackDice,
//Below are image based icons
TreasureToken = 300,
TreasureToken_Common,
TreasureToken_Rare,
TreasureToken_Epic,
TreasureToken_Legendary,
HP_Color,
Mana_Color,
CorruptToken,
TimeToken,
FireToken,
FrozenToken
}
export enum AttackTarget {
Random = 40,
@@ -110,14 +130,7 @@ export class AttackInfo {
yellow: number
black: number
}
export class DefenseInfo {
constructor(blue: number, black: number = 0) {
this.blue = blue
this.black = black
}
blue: number
black: number
}
export class MD2LevelUpReward {
constructor(config: Partial<MD2LevelUpReward>) {
Object.assign(this, config);
@@ -294,6 +307,8 @@ export class MobInfo implements IDrawingItem {
this.drawingWeight = 1;
this.unitRemainHp = config.hp
}
id: string;
from: GameBundle;
type: MobType = MobType.Mob;
imageUrl: string
standUrl: string
@@ -311,20 +326,24 @@ export class MobInfo implements IDrawingItem {
fixedCarriedTreasure: TreasureItem[];
unitRemainHp: number;
attackInfos: AttackInfo[];
defenseInfo: DefenseInfo;
defenseInfo: MD2DiceSet;
skills: MD2MobSkill[];
actions: number = 0;
activateDescription: string;
combatSkill: MobSkill
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}`;
}
@@ -367,10 +386,17 @@ export class MobInfo implements IDrawingItem {
return 0;
}
}
public actionSubject: Subject<string>;
public activateFunction?: (mob: MobInfo, msgBoxService: MsgBoxService, heros: MD2HeroInfo[]) => void;
}
export interface MD2HeroProfile {
id: string;
heroClass: HeroClass;
title: string;
hp: number;
mana: number;
skillHtml: string;
shadowSkillHtml: string;
}
export class MD2HeroInfo {
constructor(
config: Partial<MD2HeroInfo> = {}
@@ -388,7 +414,8 @@ export class MD2HeroInfo {
level: number = 1;
fireToken: number = 0;
frozenToken: number = 0;
corruptionToken: number = 0;
extraToken: number = 0;
extraToken2: number = 0;
playerInfo: GamePlayer;
imgUrl: string;
skillHtml: string;
@@ -396,9 +423,15 @@ export class MD2HeroInfo {
remainActions: number = 3;
rage: number = 0;
uiActivating = false;
uiShowCorruptionToken = false;
uiExtraTokenHtml: string = '';
uiExtraTokenHtml2: string = '';
uiExtraTokenName: string = '';
uiExtraTokenName2: string = '';
uiShowExtraToken = false;
uiShowExtraToken2 = false;
uiBossFight = false;
uiShowAttackBtn = false;
uiBetrayal = false;
public get heroFullName(): string {
return `${this.playerInfo.name} (${HeroClass[this.class]} - ${this.name})`
}
@@ -488,7 +521,7 @@ export class CoreGameDarknessPhaseRule implements IDarknessPhaseRule {
switch (this.round) {
case 3:
case 9:
this.spawnMob.next();
this.spawnRoamingMonster.next();
return false;
break;
case 4:
@@ -496,7 +529,7 @@ export class CoreGameDarknessPhaseRule implements IDarknessPhaseRule {
break;
case 5:
case 7:
this.spawnRoamingMonster.next();
this.spawnMob.next();
return false;
break;
case 6:
@@ -0,0 +1,55 @@
<div class="k-dialog-content">
<form #form="ngForm">
<div class="row form-group">
<div class="col-md-4">
<label class="k-label">Hero Class *</label>
<kendo-dropdownlist [(ngModel)]="selectedHeroClass" name="heroClass" [data]="heroClasses"
[valueField]="'value'" [textField]="'text'"
[defaultItem]="{ value: null, text: 'Select hero class...' }">
</kendo-dropdownlist>
</div>
<div class="col-md-4">
<label class="k-label">Title</label>
<input kendoTextBox [(ngModel)]="model.title" name="title" class="k-input"
placeholder="Enter hero profile title" />
</div>
<div class="col-md-2">
<label class="k-label">Mana</label>
<kendo-numerictextbox [(ngModel)]="model.mana" name="mana" [format]="'n0'" [min]="0" [max]="100"
class="k-input" placeholder="Enter mana value">
</kendo-numerictextbox>
</div>
<div class="col-md-2">
<label class="k-label">HP</label>
<kendo-numerictextbox [(ngModel)]="model.hp" name="hP" [format]="'n0'" [min]="0" [max]="100"
class="k-input" placeholder="Enter HP value">
</kendo-numerictextbox>
</div>
</div>
<div class="form-group">
<label class="k-label">Skill HTML</label>
<md2-html-editor [(ngModel)]="model.skillHtml" name="skillHtml" class="htmlEditor"></md2-html-editor>
</div>
<div class="form-group">
<label class="k-label">Shadow Skill HTML</label>
<md2-html-editor [(ngModel)]="model.shadowSkillHtml" name="shadowSkillHtml"
class="htmlEditor"></md2-html-editor>
</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>
@@ -0,0 +1,25 @@
:host {
display: block;
}
.k-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.k-form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.k-label {
font-weight: 500;
}
.k-form-error {
color: #f31700;
font-size: 0.875rem;
}
@@ -0,0 +1,98 @@
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 { MD2HeroProfile, HeroClass } from '../../massive-darkness2.model';
import { MD2HeroProfileService } from '../../service/massive-darkness2.service';
@Component({
selector: 'ngx-md2-hero-profile-editor',
templateUrl: './md2-hero-profile-editor.component.html',
styleUrls: ['./md2-hero-profile-editor.component.scss']
})
export class MD2HeroProfileEditorComponent extends DialogContentBase implements OnInit {
@Input() public data: MD2HeroProfile;
@Input() public isAdding: boolean = false;
@ViewChild('form') form: NgForm;
public model: MD2HeroProfile;
public processing: boolean = false;
public heroClasses: Array<{ value: HeroClass; text: string }> = [];
public selectedHeroClass: { value: HeroClass; text: string } | null = null;
constructor(
public dialog: DialogRef,
private heroProfileService: MD2HeroProfileService,
private cdr: ChangeDetectorRef
) {
super(dialog);
this.initializeEnums();
}
ngOnInit(): void {
this.initializeModel();
}
public initializeModel(): void {
const classValue = this.data?.heroClass !== undefined && this.data?.heroClass !== null ? this.data.heroClass : HeroClass.Berserker;
this.model = {
id: this.data?.id || '',
heroClass: classValue,
title: this.data?.title || '',
hp: this.data?.hp || 0,
mana: this.data?.mana || 0,
skillHtml: this.data?.skillHtml || '',
shadowSkillHtml: this.data?.shadowSkillHtml || ''
};
// Set selected object for dropdown
this.selectedHeroClass = this.heroClasses.find(c => c.value === classValue) || this.heroClasses[0] || null;
this.cdr.detectChanges();
}
private initializeEnums(): void {
// Initialize HeroClass options
Object.keys(HeroClass).filter(key => isNaN(Number(key))).forEach(key => {
this.heroClasses.push({
value: HeroClass[key] as HeroClass,
text: key
});
});
}
public close(): void {
this.dialog.close();
}
public save(): void {
if (this.model.title && !this.processing) {
this.processing = true;
// Extract enum value from selected object
const heroProfile: MD2HeroProfile = {
...this.model,
heroClass: this.selectedHeroClass?.value ?? HeroClass.Berserker
};
this.heroProfileService.createOrUpdate(heroProfile).pipe(first()).subscribe(result => {
this.processing = false;
this.dialog.close(result);
}, error => {
this.processing = false;
console.error('Error saving hero profile:', error);
});
}
}
public get isValid(): boolean {
if (!this.model) {
return false;
}
const titleValid = true;// this.model.title && this.model.title.trim().length > 0;
const classValid = this.selectedHeroClass !== null && this.selectedHeroClass !== undefined;
return titleValid && classValid;
}
}
@@ -0,0 +1,57 @@
<nb-card>
<nb-card-header>
<h4>MD2 Hero Profile Maintenance</h4>
<button class="float-right" kendoButton (click)="addHandler()" [primary]="true">
<span class="k-icon k-i-plus"></span> Add New
</button>
</nb-card-header>
<nb-card-body>
<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()">
<kendo-grid-toolbar>
<button kendoGridAddCommand>Add new</button>
</kendo-grid-toolbar>
<kendo-grid-column field="title" title="Title" [width]="150">
</kendo-grid-column>
<kendo-grid-column field="heroClass" title="Hero Class" [width]="60">
<ng-template kendoGridCellTemplate let-dataItem>
{{ getHeroClassName(dataItem.heroClass) }}
</ng-template>
<ng-template kendoGridGroupHeaderTemplate let-value="value">
{{ getHeroClassName(value) }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="hP" title="HP" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
<md2-icon [icon]="MD2Icon.Mana_Color" size="sm"></md2-icon> {{ dataItem.mana }}
<md2-icon [icon]="MD2Icon.HP_Color" size="sm"></md2-icon> {{ dataItem.hp }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="skill" title="Skill" [width]="500">
<ng-template kendoGridCellTemplate let-dataItem>
<div [innerHTML]="dataItem.skillHtml"></div>
<div>
<md2-icon [icon]="MD2Icon.Shadow" size="sm"></md2-icon> <b>Shadow:</b>
<div [innerHTML]="dataItem.shadowSkillHtml"></div>
</div>
</ng-template>
</kendo-grid-column>
<kendo-grid-command-column title="Actions" [width]="140">
<ng-template kendoGridCellTemplate let-isNew="isNew" let-dataItem="dataItem" let-rowIndex="rowIndex">
<button kendoGridEditCommand [primary]="true">Edit</button>
<button kendoGridRemoveCommand>Remove</button>
</ng-template>
</kendo-grid-command-column>
</kendo-grid>
</nb-card-body>
</nb-card>
@@ -0,0 +1,11 @@
:host {
display: block;
}
.float-right {
float: right;
}
kendo-grid {
height: 77vh;
}
@@ -0,0 +1,150 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { GridComponent, GridDataResult } from '@progress/kendo-angular-grid';
import { State, process } from '@progress/kendo-data-query';
import { first } from 'rxjs/operators';
import { MD2HeroProfile, HeroClass, MD2Icon } from '../massive-darkness2.model';
import { MD2HeroProfileService } from '../service/massive-darkness2.service';
import { MD2HeroProfileEditorComponent } from './md2-hero-profile-editor/md2-hero-profile-editor.component';
import { DialogService } from '@progress/kendo-angular-dialog';
import { MsgBoxService } from '../../../services/msg-box.service';
@Component({
selector: 'ngx-md2-hero-profile-maintenance',
templateUrl: './md2-hero-profile-maintenance.component.html',
styleUrls: ['./md2-hero-profile-maintenance.component.scss']
})
export class MD2HeroProfileMaintenanceComponent implements OnInit {
@ViewChild('grid') grid: GridComponent;
MD2Icon = MD2Icon;
public gridData: GridDataResult = { data: [], total: 0 };
private allData: MD2HeroProfile[] = [];
private lastSelectedHero: MD2HeroProfile;
private lastSelectedHeroClass: HeroClass;
public gridState: State = {
skip: 0,
take: 1000,
sort: [{
field: 'title',
dir: 'asc'
}],
filter: {
logic: 'and',
filters: []
},
group: [{
field: 'heroClass',
dir: 'asc'
}]
};
public isLoading: boolean = false;
constructor(
private heroProfileService: MD2HeroProfileService,
private dialogService: DialogService,
private msgBoxService: MsgBoxService
) {
}
ngOnInit(): void {
this.loadData();
}
public loadData(): void {
this.isLoading = true;
this.heroProfileService.getAll().pipe(first()).subscribe(result => {
this.allData = result;
this.processGridData();
this.isLoading = false;
});
}
public processGridData(): void {
// Normalize filter state to handle null/undefined/empty filters
let normalizedFilter: { logic: 'and' | 'or'; filters: any[] } = { logic: 'and', filters: [] };
if (this.gridState.filter) {
const filters = this.gridState.filter.filters || [];
if (filters.length > 0) {
normalizedFilter = this.gridState.filter;
}
}
const normalizedState: State = {
...this.gridState,
filter: normalizedFilter
};
this.gridData = process(this.allData, normalizedState);
}
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,
width: '90vw',
height: 600
});
const editor = dialogRef.content.instance;
editor.isAdding = true;
editor.data = editorData;
// Force model re-initialization after data is set
setTimeout(() => {
editor.initializeModel();
}, 0);
dialogRef.result.subscribe((result: MD2HeroProfile) => {
if (result) {
this.lastSelectedHero = result;
this.lastSelectedHeroClass = result.heroClass;
this.loadData();
}
});
}
public editHandler({ dataItem }: { dataItem: MD2HeroProfile }): void {
const dialogRef = this.dialogService.open({
title: 'Edit Hero Profile',
content: MD2HeroProfileEditorComponent,
width: '90vw',
height: 600
});
this.lastSelectedHero = dataItem;
const editor = dialogRef.content.instance;
editor.isAdding = false;
editor.data = JSON.parse(JSON.stringify(dataItem));
// Force model re-initialization after data is set
setTimeout(() => {
editor.initializeModel();
}, 0);
dialogRef.result.subscribe(result => {
if (result) {
this.loadData();
}
});
}
public removeHandler({ dataItem }: { dataItem: MD2HeroProfile }): void {
this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(answer => {
if (answer === true) {
this.isLoading = true;
this.heroProfileService.delete(dataItem.id).pipe(first()).subscribe(result => {
this.loadData();
});
}
});
}
public getHeroClassName(heroClass: HeroClass): string {
return HeroClass[heroClass] || '';
}
}
@@ -27,7 +27,7 @@ export class MD2HeroSelectComponent implements ControlValueAccessor, Validator {
readonly: boolean = false;
isRequired: boolean = false;
heroOptions: DropDownOption[];
@Input() id?= '';
@Input() id? = '';
@Input() name = '';
@Input() data: MD2HeroInfo;
@@ -0,0 +1,40 @@
<kendo-editor [value]="value" (valueChange)="onChange($event)" (blur)="onTouched()" [disabled]="disabled"
[schema]="messageTemplateSchema" [iframe]="false" class="h-100">
<kendo-toolbar>
<!-- Custom MD2 Icon button -->
<kendo-toolbar-button text="Insert Icon" (click)="showInsertMD2Icon()"></kendo-toolbar-button>
<!-- Standard editing tools -->
<kendo-toolbar-buttongroup>
<kendo-toolbar-button kendoEditorBoldButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorItalicButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorUnderlineButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorStrikethroughButton></kendo-toolbar-button>
</kendo-toolbar-buttongroup>
<kendo-toolbar-buttongroup>
<kendo-toolbar-button kendoEditorAlignLeftButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorAlignCenterButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorAlignRightButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorAlignJustifyButton></kendo-toolbar-button>
</kendo-toolbar-buttongroup>
<kendo-toolbar-dropdownlist kendoEditorFormat></kendo-toolbar-dropdownlist>
<kendo-toolbar-dropdownlist kendoEditorFontSize #fontSizeDropdown
[data]="fontSizeData"></kendo-toolbar-dropdownlist>
<kendo-toolbar-dropdownlist kendoEditorFontFamily></kendo-toolbar-dropdownlist>
<kendo-toolbar-colorpicker kendoEditorForeColor></kendo-toolbar-colorpicker>
<kendo-toolbar-colorpicker kendoEditorBackColor view="gradient"></kendo-toolbar-colorpicker>
<kendo-toolbar-buttongroup>
<kendo-toolbar-button kendoEditorInsertUnorderedListButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorInsertOrderedListButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorIndentButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorOutdentButton></kendo-toolbar-button>
</kendo-toolbar-buttongroup>
<kendo-toolbar-buttongroup>
<kendo-toolbar-button kendoEditorUndoButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorRedoButton></kendo-toolbar-button>
</kendo-toolbar-buttongroup>
<kendo-toolbar-button kendoEditorInsertFileButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorInsertImageButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorViewSourceButton></kendo-toolbar-button>
<kendo-toolbar-button kendoEditorCleanFormattingButton></kendo-toolbar-button>
</kendo-toolbar>
</kendo-editor>
@@ -0,0 +1,3 @@
:host ::ng-deep .k-editor-content .MD2Icon {
font-size: 30px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MD2HtmlEditorComponent } from './md2-html-editor.component';
import { GamesModule } from '../../games.module';
describe('MD2HtmlEditorComponent', () => {
let component: MD2HtmlEditorComponent;
let fixture: ComponentFixture<MD2HtmlEditorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GamesModule]
})
.compileComponents();
fixture = TestBed.createComponent(MD2HtmlEditorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,443 @@
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';
import { DropDownOption } from '../../../entity/dropDownOption';
import { MD2Icon } from '../massive-darkness2.model';
import { first } from 'rxjs/operators';
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',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MD2HtmlEditorComponent),
multi: true
}
]
})
export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewInit {
@ViewChild(EditorComponent) editor: EditorComponent;
@ViewChild('fontSizeDropdown') fontSizeDropdown: any;
value: string = '';
disabled: boolean = false;
messageTemplateSchema = this.createCustomSchema();
customCssClass = '.MD2Icon{font-family: "Massive Darkness 2", sans-serif !important; font-size: 40px; margin-left:5px} body{font-size: 30px; }';
// Default font size: 30px
fontSizeData: FontSizeItem[] = [
{ size: 30, text: '30px' },
{ size: 8, text: '8px' },
{ size: 10, text: '10px' },
{ size: 12, text: '12px' },
{ size: 14, text: '14px' },
{ size: 16, text: '16px' },
{ size: 18, text: '18px' },
{ size: 20, text: '20px' },
{ size: 24, text: '24px' },
{ size: 30, text: '30px' },
{ size: 36, text: '36px' },
{ size: 48, text: '48px' },
{ size: 60, text: '60px' },
{ size: 72, text: '72px' }
];
defaultFontSize: FontSizeItem = { size: 30, text: '30px' };
// ControlValueAccessor interface
private onChangeFn = (value: string) => { };
private onTouchedFn = () => { };
constructor(
private msgBoxService: MsgBoxService,
private dialogService: DialogService,
private cdr: ChangeDetectorRef,
elementRef: ElementRef, ngZone: NgZone, @Inject(PLATFORM_ID) platformId: Object) {
}
ngAfterViewInit(): void {
// Set default font size after view initialization
// The fontSizeDropdown is the EditorFontSizeComponent instance
setTimeout(() => {
if (this.fontSizeDropdown) {
// Access the fontSizeDropDownList component which has the defaultItem property
if (this.fontSizeDropdown.fontSizeDropDownList) {
this.fontSizeDropdown.fontSizeDropDownList.defaultItem = this.defaultFontSize;
}
// Also try setting it directly on the component if it has the property
if (this.fontSizeDropdown.defaultItem !== undefined) {
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 | 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 {
this.onChangeFn = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouchedFn = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onChange(value: string): void {
// Only update if value actually changed to prevent infinite loops
if (this.value !== value) {
this.value = value || '';
this.onChangeFn(this.value);
}
}
onTouched(): void {
this.onTouchedFn();
}
showInsertMD2Icon() {
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);
}
});
}
// Text manipulation methods for Kendo Editor
/**
* Parses HTML string to ProseMirror nodes
* @param htmlContent - HTML string to parse
* @returns ProseMirror Fragment
*/
private parseHtmlContent(htmlContent: string): any {
if (!this.editor || !this.editor.view) return null;
const view = this.editor.view;
const element = document.createElement('div');
element.innerHTML = htmlContent;
// Use ProseMirror's DOMParser to parse HTML into nodes
const parser = ProseMirrorDOMParser.fromSchema(view.state.schema);
const slice = parser.parseSlice(element);
return slice.content;
}
/**
* Inserts HTML content at the beginning of the editor
* @param content - HTML string to insert
* @param asHtml - If true, parses as HTML; if false, inserts as plain text
*/
public insertAtBeginning(content: string, asHtml: boolean = false): void {
if (!this.editor || !this.editor.view) return;
const view = this.editor.view;
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
if (contentNode) {
const transaction = view.state.tr.insert(0, contentNode);
view.dispatch(transaction);
this.onChange(this.editor.value);
}
}
/**
* Inserts HTML content at the end of the editor
* @param content - HTML string to insert
* @param asHtml - If true, parses as HTML; if false, inserts as plain text
*/
public insertAtEnd(content: string, asHtml: boolean = false): void {
if (!this.editor || !this.editor.view) return;
const view = this.editor.view;
const endPos = view.state.doc.content.size;
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
if (contentNode) {
const transaction = view.state.tr.insert(endPos, contentNode);
view.dispatch(transaction);
this.onChange(this.editor.value);
}
}
/**
* Inserts HTML content before the current selection
* @param content - HTML string to insert
* @param asHtml - If true, parses as HTML; if false, inserts as plain text
*/
public insertBeforeSelection(content: string, asHtml: boolean = false): void {
if (!this.editor || !this.editor.view) return;
const view = this.editor.view;
const { from } = view.state.selection;
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
if (contentNode) {
const transaction = view.state.tr.insert(from, contentNode);
view.dispatch(transaction);
this.onChange(this.editor.value);
}
}
/**
* Inserts HTML content after the current selection
* @param content - HTML string to insert
* @param asHtml - If true, parses as HTML; if false, inserts as plain text
*/
public insertAfterSelection(content: string, asHtml: boolean = false): void {
if (!this.editor || !this.editor.view) return;
const view = this.editor.view;
const { to } = view.state.selection;
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
if (contentNode) {
const transaction = view.state.tr.insert(to, contentNode);
view.dispatch(transaction);
this.onChange(this.editor.value);
}
}
/**
* Replaces the currently selected text with new content
* @param content - HTML string to replace selection with
* @param asHtml - If true, parses as HTML; if false, inserts as plain text
*/
public replaceSelectedText(content: string, asHtml: boolean = false): void {
if (!this.editor || !this.editor.view) return;
const view = this.editor.view;
const { from, to } = view.state.selection;
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
if (!contentNode) return;
// If there's no selection, just insert at cursor position
if (from === to) {
const transaction = view.state.tr.insert(from, contentNode);
view.dispatch(transaction);
} else {
// Replace the selected content
const transaction = view.state.tr.replaceWith(from, to, contentNode);
view.dispatch(transaction);
}
this.onChange(this.editor.value);
}
/**
* Gets the currently selected text in the editor
* @returns The selected text as a string
*/
public getSelectedText(): string {
if (!this.editor) return '';
return this.editor.selectionText || '';
}
public getSelectionTextOrWholeText(): string {
if (!this.editor) return '';
// If there's selected text, return it
if (this.editor.selectionText && this.editor.selectionText.trim().length > 0) {
return this.editor.selectionText;
}
// Otherwise, get the whole text content from the editor's document
if (this.editor.view && this.editor.view.state) {
return this.editor.view.state.doc.textContent;
}
// Fallback: strip HTML tags and return plain text
const div = document.createElement('div');
div.innerHTML = this.editor.value;
return div.textContent || div.innerText || '';
}
/**
* Gets the current cursor position or selection range
* @returns Object with 'from' and 'to' positions
*/
public getSelectionRange(): { from: number; to: number } | null {
if (!this.editor || !this.editor.view) return null;
const { from, to } = this.editor.view.state.selection;
return { from, to };
}
/**
* Inserts HTML content at a specific position
* @param content - HTML string to insert
* @param position - Position to insert at (0 = beginning)
* @param asHtml - If true, parses as HTML; if false, inserts as plain text
*/
public insertAtPosition(content: string, position: number, asHtml: boolean = false): void {
if (!this.editor || !this.editor.view) return;
const view = this.editor.view;
const maxPos = view.state.doc.content.size;
const safePos = Math.min(Math.max(0, position), maxPos);
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
if (contentNode) {
const transaction = view.state.tr.insert(safePos, contentNode);
view.dispatch(transaction);
this.onChange(this.editor.value);
}
}
/**
* Replaces text in a specific range
* @param content - HTML string to insert
* @param from - Start position
* @param to - End position
* @param asHtml - If true, parses as HTML; if false, inserts as plain text
*/
public replaceRange(content: string, from: number, to: number, asHtml: boolean = false): void {
if (!this.editor || !this.editor.view) return;
const view = this.editor.view;
const maxPos = view.state.doc.content.size;
const safeFrom = Math.min(Math.max(0, from), maxPos);
const safeTo = Math.min(Math.max(safeFrom, to), maxPos);
const contentNode = asHtml ? this.parseHtmlContent(content) : view.state.schema.text(content);
if (contentNode) {
const transaction = view.state.tr.replaceWith(safeFrom, safeTo, contentNode);
view.dispatch(transaction);
this.onChange(this.editor.value);
}
}
// showVariablePicker() {
// this.easyEditorService.openTableMultiPicker(this.variables, this.variableTableSettings, "Please select a variable").pipe(first()).subscribe(result => {
// if (result) {
// result.forEach(c => {
// this.insertAfterSelection(`<${RbjTagNode} class="rbj-tag" ${RbjTagIdAttribute}="${c.name}" ${RbjTagValueAttribute}="${c.name}">${c.name}</${RbjTagNode}>`, true);
// });
// }
// });
// }
// Private methods
private createCustomSchema(): Schema {
let nodes = schema.spec.nodes.addBefore("div", "rbjTag", rbjTagNodeSpec);
let marks = schema.spec.marks;
//marks = marks.addToStart("rbjSpanTag", rbjTagMarkSpec);
return new Schema({
nodes: nodes,
marks: marks
});
}
}
// Define the custom node specification for the rbj-tag element.
export const rbjTagNodeSpec: NodeSpec = {
// Define the node attributes for the tag
attrs: {
"md2-icon": { default: "" },
"class": { default: "" },
// "tag-value": { default: "" },
// "tag-preview": { default: "" }
},
// Specify that this node should be treated as an inline element
inline: true,
// Allow the node to be part of inline content
group: "inline",
// Make it atomic (non-editable, treated as a single unit)
atom: true,
// Define how the node should be rendered in the DOM
toDOM: (node) => {
let md2IconText = node.attrs["md2-icon"] as string;
let classValue = node.attrs["class"] as string;
if (classValue.includes('dice')) {
md2IconText = '';
}
// let displayValue = tagPreview == 'true' ? node.attrs["tag-value"] : node.attrs["rbj-tag-id"];
return [
"span",
{
class: classValue,
"md2-icon": md2IconText,
// "tag-marker": node.attrs["tag-marker"],
// "tag-value": node.attrs["tag-value"],
// "tag-preview": node.attrs["tag-preview"],
contenteditable: "false",
//spellcheck: "false", style="font-size: 36px;"
style: "display: inline;"
},
//node.attrs["tag-marker"] + node.attrs["tag-value"] // Display the tag content directly
md2IconText
];
},
// Define how to parse the node from existing DOM elements
parseDOM: [
{
// Look for span elements with class rbj-tag (higher priority)
tag: "span[md2-icon]",
priority: 51, // Higher priority to catch before other parsers
// Extract attributes from the DOM element
getAttrs: (dom) => {
const element = dom as HTMLElement;
// Must have rbj-tag-id attribute to be valid
if (!element.hasAttribute("md2-icon")) {
return false;
}
return {
"md2-icon": element.getAttribute("md2-icon") || "",
"class": element.className || "",
// "tag-preview": element.getAttribute("tag-preview") || "false",
// "tag-value": element.getAttribute("tag-value") || element.getAttribute("rbj-tag-id")
};
}
}
]
};
@@ -0,0 +1,88 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { MD2Icon } from '../massive-darkness2.model';
import { DialogRef } from '@progress/kendo-angular-dialog';
import { MD2Service } from '../../../services/MD2/md2.service';
@Component({
selector: 'md2-icon-picker-dlg',
template: ` <div class="md2-icon-grid">
<div
*ngFor="let iconData of iconList"
(click)="selectIcon(iconData)"
class="icon-item" title="{{iconData.name}}"
[innerHTML]="iconData.html">
</div>
</div>
<!-- <nb-card>
<nb-card-header>
<h5>Insert MD2 Icon</h5>
</nb-card-header>
<nb-card-body>
</nb-card-body>
<nb-card-footer>
<button nbButton status="primary" (click)="cancel()">Cancel</button>
</nb-card-footer>
</nb-card> -->
`,
styles: [`
nb-card{
max-width: 800px;
z-index: 1050;
}
.md2-icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 10px;
max-height: 400px;
overflow-y: auto;
//width:400px;
}
.icon-item {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
transition: all 0.2s;
font-size: 30px;
}
.icon-item:hover {
background-color: #f0f0f0;
border-color: #007bff;
//transform: scale(1.1);
}
`]
})
export class MD2IconPickerDlgComponent implements OnInit {
iconList: Array<{ icon: MD2Icon, name: string, html: string }> = [];
constructor(
private dlgRef: DialogRef,
private md2Service: MD2Service
) { }
ngOnInit(): void {
// Get all icons
for (let icon of Object.values(MD2Icon).filter(val => typeof val === 'number') as MD2Icon[]) {
this.iconList.push({
icon: icon,
name: MD2Icon[icon],
html: this.md2Service.iconHtml(icon)
});
}
}
selectIcon(iconData: { icon: MD2Icon, name: string, html: string }) {
this.dlgRef.close(iconData.html);
}
cancel() {
this.dlgRef.close(null);
}
}
@@ -1 +1,5 @@
<span class="MD2Icon {{iconName}} {{iconClass}} {{sizeClass}}"></span>
@if(isImageIcon) {
<span [innerHTML]="imgUrl" class="{{sizeClass}} mx-2"></span>
} @else {
<span [innerHtml]="iconHtml" class="{{sizeClass}} mx-2"></span>
}
@@ -1,5 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { MD2Icon } from '../massive-darkness2.model';
import { MD2Icon, TreasureType } from '../massive-darkness2.model';
import { MD2Service } from '../../../services/MD2/md2.service';
@Component({
selector: 'md2-icon',
@@ -9,20 +10,33 @@ import { MD2Icon } from '../massive-darkness2.model';
export class MD2IconComponent implements OnInit {
@Input() iconClass: string = 'mr-1';
isImageIcon: boolean = false;
imgUrl: string;
iconHtml: string;
private _icon: string | MD2Icon;
@Input() public set icon(v: string | MD2Icon) {
if (this._icon != v) {
this._icon = v;
if (v !== undefined) {
if (this._icon != v) {
this._icon = v;
//if it's string, convert it to MD2Icon
if (typeof v === 'string') {
const key = Object.keys(MD2Icon).find(
k => k.toLowerCase() === v.toString().toLowerCase()
);
if (key) {
v = MD2Icon[key as keyof typeof MD2Icon];
}
}
this.initIcon(v as MD2Icon);
}
if (this.isMD2Icon(v)) {
this.iconName = MD2Icon[v].toLowerCase();
} else {
this.iconName = v;
}
}
}
if (this.isMD2Icon(v)) {
this.iconName = MD2Icon[v].toLowerCase();
} else {
this.iconName = v;
}
}
isMD2Icon(icon: MD2Icon | string): icon is MD2Icon {
@@ -30,25 +44,36 @@ export class MD2IconComponent implements OnInit {
}
@Input() size: string = 'sm';
iconName: string;
constructor() { }
constructor(private md2Service: MD2Service) { }
ngOnInit(): void {
}
private initIcon(icon: MD2Icon): void {
if (icon < MD2Icon.TreasureToken) {
this.isImageIcon = false;
this.iconHtml = this.md2Service.iconHtml(icon);
} else {
this.isImageIcon = true;
this.imgUrl = this.md2Service.iconHtml(icon);
}
}
public get sizeClass(): string {
switch (this.size) {
case 'sm':
return 'g-font-size-18'
return this.isImageIcon ? 'g-width-25 img-fluid' : 'g-font-size-18'
break;
case 'med':
return 'g-font-size-30'
return this.isImageIcon ? 'g-width-35 img-fluid' : 'g-font-size-30'
break;
case 'lg':
return 'g-font-size-50'
return this.isImageIcon ? 'g-width-50 img-fluid' : 'g-font-size-50'
break;
default:
return 'g-font-size-' + this.size;
return this.isImageIcon ? 'g-width-20 img-fluid' : 'g-font-size-' + this.size;
break;
}
}
@@ -0,0 +1,135 @@
<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>
@@ -0,0 +1,10 @@
// Boss Fight Editor styles
.tokenIconDiv {
margin-left: 5px;
font-size: 30px;
img {
width: 30px;
height: 30px;
object-fit: contain;
}
}
@@ -0,0 +1,216 @@
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 !== '';
}
}
@@ -0,0 +1,188 @@
<div class="k-dialog-content detail-container">
<div class="mob-info-section">
<h3>{{ mobInfo?.name }}</h3>
<div class="info-grid">
<div class="info-item">
<label>Type:</label>
<span>{{ getMobTypeName(mobInfo?.type) }}</span>
</div>
<div class="info-item">
<label>Game Bundle:</label>
<span>{{ getGameBundleName(mobInfo?.from) }}</span>
</div>
<div class="info-item">
<label>Leader Image:</label>
<span>{{ mobInfo?.leaderImgUrl || 'N/A' }}</span>
</div>
<div class="info-item">
<label>Minion Image:</label>
<span>{{ mobInfo?.minionImgUrl || 'N/A' }}</span>
</div>
</div>
</div>
<kendo-tabstrip tabPosition="top">
<kendo-tabstrip-tab title="Levels" [selected]="true">
<ng-template kendoTabContent>
<div class="levels-toolbar">
<button kendoButton (click)="addLevelHandler()" [primary]="true">
<span class="k-icon k-i-plus"></span> Add Level
</button>
</div>
<kendo-grid #levelsGrid [data]="mobLevelInfos" [loading]="isLoading" [pageSize]="levelsState.take"
[skip]="levelsState.skip" [sortable]="true" [filterable]="true" [pageable]="true" [height]="300"
(remove)="removeLevelHandler($event)" (dataStateChange)="levelsState = $event">
<kendo-grid-column field="level" title="Level" [width]="50">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.level }}
</ng-template>
</kendo-grid-column>
<!-- <kendo-grid-column field="fixedHp" title="HP" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.fixedHp }}
</ng-template>
<ng-template kendoGridEditTemplate let-dataItem="dataItem" let-rowIndex="rowIndex">
<kendo-numerictextbox [(ngModel)]="dataItem.fixedHp" [name]="'fixedHp_' + rowIndex" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</ng-template>
</kendo-grid-column> -->
<kendo-grid-column field="hpPerHero" title="HP/Hero" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.hpPerHero }}
</ng-template>
</kendo-grid-column>
<!-- <kendo-grid-column field="actions" title="Actions" [width]="100">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.actions }}
</ng-template>
<ng-template kendoGridEditTemplate let-dataItem="dataItem" let-rowIndex="rowIndex">
<kendo-numerictextbox [(ngModel)]="dataItem.actions" [name]="'actions_' + rowIndex" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</ng-template>
</kendo-grid-column> -->
<kendo-grid-column field="rewardTokens" title="Reward Tokens" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.rewardTokens }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="fixedRareTreasure" title="Rare Treasure" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.fixedRareTreasure }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="fixedEpicTreasure" title="Epic Treasure" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.fixedEpicTreasure }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="fixedLegendTreasure" title="Legend Treasure" [width]="60">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.fixedLegendTreasure }}
</ng-template>
</kendo-grid-column>
<!-- result.defenceInfo.blue
result.defenceInfo.green -->
<kendo-grid-column field="defenceInfo.blue" title="Blue Dice" [width]="60">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.defenceInfo?.blue }}
</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)="editLevelHandler(dataItem)">Edit</button>
<button kendoGridRemoveCommand>Remove</button>
</ng-template>
</kendo-grid-command-column>
</kendo-grid>
</ng-template>
</kendo-tabstrip-tab>
<kendo-tabstrip-tab title="Skills">
<ng-template kendoTabContent>
<div class="skills-toolbar">
<button kendoButton (click)="addSkillHandler()" [primary]="true">
<span class="k-icon k-i-plus"></span> Add Skill
</button>
</div>
<kendo-grid #skillsGrid [data]="skillsData" [loading]="isLoading" [pageSize]="skillsState.take"
[skip]="skillsState.skip" [sortable]="true" [filterable]="true" [pageable]="true" [height]="400"
(remove)="removeSkillHandler($event)" (dataStateChange)="skillsState = $event">
<kendo-grid-column field="seq" title="Seq" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.seq }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="level" title="Level" [width]="80">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.level }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="name" title="Name" [width]="150">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.name }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="type" title="Type" [width]="120">
<ng-template kendoGridCellTemplate let-dataItem>
{{ getSkillTypeName(dataItem.type) }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="skillTarget" title="Target" [width]="150">
<ng-template kendoGridCellTemplate let-dataItem>
{{ getSkillTargetName(dataItem.skillTarget) }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="skillRoll" title="Skill Roll" [width]="100">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.skillRoll }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="clawRoll" title="Claw Roll" [width]="100">
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.clawRoll }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="description" title="Description" [width]="300">
<ng-template kendoGridCellTemplate let-dataItem>
<div *ngIf="dataItem.type == MobSkillType.ConditionalSkill" [innerHTML]="dataItem.skillCondition"></div>
<div [innerHTML]="dataItem.description"></div>
</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)="editSkillHandler(dataItem)">Edit</button>
<button kendoGridRemoveCommand>Remove</button>
</ng-template>
</kendo-grid-command-column>
</kendo-grid>
</ng-template>
</kendo-tabstrip-tab>
</kendo-tabstrip>
</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>
@@ -0,0 +1,56 @@
.k-dialog-content {
padding: 20px;
max-height: 80vh;
//overflow-y: auto;
}
.detail-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.mob-info-section {
border-bottom: 1px solid #e0e0e0;
padding-bottom: 15px;
h3 {
margin: 0 0 15px 0;
}
}
.info-grid {
display: grd;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.info-item {
display: flex;
gap: 10px;
label {
font-weight: 500;
min-width: 120px;
}
}
.levels-section {
h4 {
margin: 0 0 10px 0;
}
}
.levels-toolbar {
margin-bottom: 10px;
}
.skills-section {
h4 {
margin: 0 0 10px 0;
}
}
.skills-toolbar {
margin-bottom: 10px;
}
@@ -0,0 +1,492 @@
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 { MD2MobInfo, MD2MobLevelInfo, MD2MobSkill, MobSkillTarget, GameBundle, BossFightProfile } 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',
templateUrl: './md2-mob-info-detail.component.html',
styleUrls: ['./md2-mob-info-detail.component.scss']
})
export class MD2MobInfoDetailComponent extends DialogContentBase implements OnInit {
MobSkillType = MobSkillType;
MobType = MobType;
@Input() public mobInfo: MD2MobInfo;
@ViewChild('levelsGrid') levelsGrid: GridComponent;
@ViewChild('skillsGrid') skillsGrid: GridComponent;
public mobLevelInfos: MD2MobLevelInfo[] = [];
public levelsData: GridDataResult = { data: [], total: 0 };
public levelsState: State = {
skip: 0,
take: 10,
sort: [],
filter: {
logic: 'and',
filters: []
}
};
public selectedLevelInfo: MD2MobLevelInfo | null = null;
public skillsData: GridDataResult = { data: [], total: 0 };
public skillsState: State = {
skip: 0,
take: 10,
sort: [],
filter: {
logic: 'and',
filters: []
}
};
public isLoading: boolean = false;
public skillTypes: Array<{ value: MobSkillType; text: string }> = [];
public skillTargets: Array<{ value: MobSkillTarget | null; text: string }> = [];
constructor(
public dialog: DialogRef,
private dialogService: DialogService,
private mobLevelInfoService: MD2MobLevelInfoService,
private mobSkillService: MD2MobSkillService,
private mobInfoService: MD2MobInfoService,
private msgBoxService: MsgBoxService
) {
super(dialog);
}
ngOnInit(): void {
if (this.mobInfo) {
this.mobLevelInfos = this.mobInfo.mobLevelInfos || [];
if (this.mobLevelInfos.length > 0) {
this.selectLevelInfo(this.mobLevelInfos[0]);
}
this.loadSkills();
}
this.initializeEnums();
}
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 selectLevelInfo(levelInfo: MD2MobLevelInfo): void {
if (levelInfo.defenceInfo == null) {
levelInfo.defenceInfo = { type: MobSkillType.Defense, blue: 0, green: 0, yellow: 0, orange: 0, red: 0, black: 0 };
}
if (levelInfo.attackInfo == null) {
levelInfo.attackInfo = { type: MobSkillType.Attack, blue: 0, green: 0, yellow: 0, orange: 0, red: 0, black: 0 };
}
this.selectedLevelInfo = levelInfo;
this.loadSkills();
}
public editLevelHandler(dataItem: MD2MobLevelInfo): void {
if (!this.mobInfo) return;
// Create a copy of the level info for editing
const levelCopy: MD2MobLevelInfo = JSON.parse(JSON.stringify(dataItem));
this.openLevelEditor(levelCopy, false);
}
public addLevelHandler(): void {
if (!this.mobInfo) return;
// Get the last level info (highest level) if any exists
const lastLevel = this.mobLevelInfos.length > 0
? this.mobLevelInfos.reduce((prev, current) => (prev.level > current.level) ? prev : current)
: null;
// Calculate the next level number
const nextLevel = lastLevel ? lastLevel.level + 2 : 1;
let rewardTokens = 0;
let actions = 1;
let newLevel: MD2MobLevelInfo;
if (lastLevel) {
// Copy the last level's info as initial values
newLevel = {
id: this.generateLevelId(),
level: nextLevel,
mobInfoId: this.mobInfo.id,
rewardTokens: rewardTokens,
fixedRareTreasure: lastLevel.fixedRareTreasure ?? 0,
fixedEpicTreasure: lastLevel.fixedEpicTreasure ?? 0,
fixedLegendTreasure: lastLevel.fixedLegendTreasure ?? 0,
fixedHp: lastLevel.fixedHp ?? 0,
hpPerHero: lastLevel.hpPerHero ?? 0,
actions: lastLevel.actions ?? actions,
attackInfo: lastLevel.attackInfo
? { ...lastLevel.attackInfo }
: { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
alterAttackInfo: lastLevel.alterAttackInfo
? { ...lastLevel.alterAttackInfo }
: null,
defenceInfo: lastLevel.defenceInfo
? { ...lastLevel.defenceInfo }
: { type: MobSkillType.Defense, yellow: null, orange: null, red: null, blue: null, green: null, black: null }
};
} else {
// Default values if no levels exist
newLevel = {
id: this.generateLevelId(),
level: nextLevel,
mobInfoId: this.mobInfo.id,
rewardTokens: rewardTokens,
fixedRareTreasure: 0,
fixedEpicTreasure: 0,
fixedLegendTreasure: 0,
fixedHp: 0,
hpPerHero: 0,
actions: actions,
attackInfo: { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
alterAttackInfo: { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
defenceInfo: { type: MobSkillType.Defense, yellow: null, orange: null, red: null, blue: null, green: null, black: null }
};
}
switch (this.mobInfo.type) {
case MobType.Mob:
newLevel.actions = 2;
switch (nextLevel) {
case 1:
case 3:
newLevel.rewardTokens = 1;
break;
case 5:
newLevel.rewardTokens = 2;
break;
}
break;
case MobType.Boss:
newLevel.actions = 1;
break;
case MobType.RoamingMonster:
newLevel.actions = 1;
switch (nextLevel) {
case 1:
newLevel.rewardTokens = 2;
newLevel.fixedRareTreasure = 1;
newLevel.fixedEpicTreasure = 0;
newLevel.fixedLegendTreasure = 0;
break;
case 3:
newLevel.rewardTokens = 2;
newLevel.fixedRareTreasure = 0;
newLevel.fixedEpicTreasure = 1;
newLevel.fixedLegendTreasure = 0;
break;
case 5:
newLevel.rewardTokens = 0;
newLevel.fixedRareTreasure = 0;
newLevel.fixedEpicTreasure = 3;
newLevel.fixedLegendTreasure = 0;
break;
}
break;
}
this.openLevelEditor(newLevel, true);
}
private openLevelEditor(level: MD2MobLevelInfo, isNew: boolean): void {
if (!this.mobInfo) return;
const dialogRef = this.dialogService.open({
title: isNew ? 'Add New Level' : 'Edit Level',
content: MD2MobLevelEditorComponent,
width: '80vw',
height: 700
});
const editor = dialogRef.content.instance as MD2MobLevelEditorComponent;
editor.isAdding = isNew;
editor.data = level;
editor.mobInfoId = this.mobInfo.id;
editor.mobType = this.mobInfo.type;
// 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.handleLevelSaved(result as MD2MobLevelInfo, isNew);
}
});
}
private handleLevelSaved(result: MD2MobLevelInfo, isNew: boolean): void {
if (!this.mobInfo) return;
if (isNew) {
if (!this.mobInfo.mobLevelInfos) {
this.mobInfo.mobLevelInfos = [];
}
result.attackInfo.black = 0;
this.mobLevelInfos.push(result);
} else {
const index = this.mobLevelInfos.findIndex(l => l.id === result.id);
if (index !== -1) {
this.mobLevelInfos[index] = result;
// Update selected level if it's the one being edited
if (this.selectedLevelInfo?.id === result.id) {
this.selectedLevelInfo = result;
this.loadSkills();
}
}
}
}
public removeLevelHandler({ dataItem }: { dataItem: MD2MobLevelInfo }): void {
this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(answer => {
if (answer === true) {
this.isLoading = true;
this.mobLevelInfoService.delete(dataItem.id).pipe(first()).subscribe(result => {
const index = this.mobLevelInfos.findIndex(l => l.id === dataItem.id);
if (index !== -1) {
this.mobLevelInfos.splice(index, 1);
// Clear selection if deleted level was selected
if (this.selectedLevelInfo?.id === dataItem.id) {
this.selectedLevelInfo = null;
this.loadSkills();
}
}
this.isLoading = false;
});
}
});
}
public loadSkills(): void {
if (this.mobInfo && this.mobInfo.skills) {
// Filter skills by selected level if a level is selected
let filteredSkills = this.mobInfo.skills.sort((a, b) => a.seq - b.seq);
// if (this.selectedLevelInfo) {
// filteredSkills = this.mobInfo.skills.filter(s => s.level === this.selectedLevelInfo.level);
// }
this.skillsData = {
data: filteredSkills,
total: filteredSkills.length
};
} else {
this.skillsData = { data: [], total: 0 };
}
}
public addSkillHandler(): void {
if (!this.mobInfo) return;
// Get the last level info (highest level) if any exists
const lastLevel = this.mobInfo.skills.length > 0
? this.mobInfo.skills.reduce((prev, current) => (prev.seq > current.seq) ? prev : current)
: null;
let seq = 0;
let level = 1;
let type = MobSkillType.Combat;
if (lastLevel) {
seq = lastLevel.seq + 1;
level = lastLevel.level + 2;
type = lastLevel.type;
}
const newSkill: MD2MobSkill = {
id: this.generateId(),
seq: seq,
level: level,
mobInfoId: this.mobInfo.id,
type: type,
skillTarget: lastLevel?.skillTarget || MobSkillTarget.Random,
clawRoll: lastLevel?.clawRoll || 0,
skillRoll: lastLevel?.skillRoll || 1,
name: lastLevel?.name || 'Combat Skill',
skillCondition: lastLevel?.skillCondition || '',
description: lastLevel?.description || ''
};
this.openSkillEditor(newSkill, true);
}
public editSkillHandler(dataItem: MD2MobSkill): void {
if (!this.mobInfo) 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.mobInfo) 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.mobInfoId = this.mobInfo.id;
editor.selectedLevel = this.selectedLevelInfo?.level || 1;
// 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.mobInfo) return;
if (isNew) {
if (!this.mobInfo.skills) {
this.mobInfo.skills = [];
}
this.mobInfo.skills.push(result);
} else {
const index = this.mobInfo.skills?.findIndex(s => s.id === result.id);
if (index !== undefined && index !== -1 && this.mobInfo.skills) {
this.mobInfo.skills[index] = result;
}
}
this.loadSkills();
}
public saveSkillHandler({ dataItem, isNew }: any): void {
// This method is no longer used but kept for backward compatibility
// Skills are now edited via dialog
}
public removeSkillHandler({ dataItem }: { dataItem: MD2MobSkill }): void {
if (!this.mobInfo) return;
this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(answer => {
if (answer === true) {
this.isLoading = true;
this.mobSkillService.delete(dataItem.id).pipe(first()).subscribe(result => {
if (this.mobInfo.skills) {
const index = this.mobInfo.skills.findIndex(s => s.id === dataItem.id);
if (index !== -1) {
this.mobInfo.skills.splice(index, 1);
}
this.loadSkills();
}
this.isLoading = false;
});
}
});
}
private generateId(): string {
return 'skill_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
private generateLevelId(): string {
return 'level_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
public getSkillTypeName(type: MobSkillType): string {
return MobSkillType[type] || '';
}
public getSkillTargetName(target: MobSkillTarget | null): string {
if (target === null) return 'None';
return MobSkillTarget[target] || '';
}
public getMobTypeName(type: MobType): string {
return MobType[type] || '';
}
public getGameBundleName(bundle: GameBundle): string {
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);
}
}
@@ -0,0 +1,45 @@
<div class="k-dialog-content">
<form #form="ngForm" class="k-form">
<div class="k-form-field">
<label class="k-label">Name *</label>
<input kendoTextBox [(ngModel)]="model.name" name="name" required class="k-input"
placeholder="Enter mob name" />
<span class="k-form-error" *ngIf="form.controls['name']?.invalid && form.controls['name']?.touched">
Name is required
</span>
</div>
<div class="k-form-field">
<label class="k-label">Type *</label>
<kendo-dropdownlist [(ngModel)]="selectedMobType" name="type" [data]="mobTypes" [valueField]="'value'"
[textField]="'text'" [defaultItem]="{ value: null, text: 'Select type...' }">
</kendo-dropdownlist>
</div>
<div class="k-form-field">
<label class="k-label">Game Bundle *</label>
<kendo-dropdownlist [(ngModel)]="selectedGameBundle" name="from" [data]="gameBundles" [valueField]="'value'"
[textField]="'text'" [defaultItem]="{ value: null, text: 'Select bundle...' }">
</kendo-dropdownlist>
</div>
<div class="k-form-field">
<label class="k-label">Leader Image URL</label>
<input kendoTextBox [(ngModel)]="model.leaderImgUrl" name="leaderImgUrl" class="k-input"
placeholder="Enter leader image URL" />
</div>
<div class="k-form-field">
<label class="k-label">Minion Image URL</label>
<input kendoTextBox [(ngModel)]="model.minionImgUrl" name="minionImgUrl" class="k-input"
placeholder="Enter minion image URL" />
</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>
@@ -0,0 +1,29 @@
.k-dialog-content {
padding: 20px;
}
.k-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.k-form-field {
display: flex;
flex-direction: column;
gap: 5px;
}
.k-label {
font-weight: 500;
}
.k-input {
width: 100%;
}
.k-form-error {
color: red;
font-size: 12px;
}
@@ -0,0 +1,149 @@
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 { MD2MobInfo, GameBundle, BossFightProfile } from '../../massive-darkness2.db.model';
import { MobType } from '../../massive-darkness2.model';
import { MD2MobInfoService } from '../../service/massive-darkness2.service';
@Component({
selector: 'ngx-md2-mob-info-editor',
templateUrl: './md2-mob-info-editor.component.html',
styleUrls: ['./md2-mob-info-editor.component.scss']
})
export class MD2MobInfoEditorComponent extends DialogContentBase implements OnInit {
@Input() public data: MD2MobInfo;
@Input() public isAdding: boolean = false;
@ViewChild('form') form: NgForm;
public model: MD2MobInfo;
public processing: boolean = false;
public mobTypes: Array<{ value: MobType; text: string }> = [];
public gameBundles: Array<{ value: GameBundle; text: string }> = [];
public selectedMobType: { value: MobType; text: string } | null = null;
public selectedGameBundle: { value: GameBundle; text: string } | null = null;
constructor(
public dialog: DialogRef,
private mobInfoService: MD2MobInfoService,
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 : MobType.Mob;
const fromValue = this.data?.from !== undefined && this.data?.from !== null ? this.data.from : GameBundle.CoreGame;
this.model = {
id: this.data?.id || '',
name: this.data?.name || '',
type: typeValue,
from: fromValue,
leaderImgUrl: this.data?.leaderImgUrl || '',
minionImgUrl: this.data?.minionImgUrl || '',
mobLevelInfos: this.data?.mobLevelInfos || [],
skills: this.data?.skills || [],
bossFightProfile: this.data?.bossFightProfile
};
// 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;
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
Object.keys(MobType).filter(key => isNaN(Number(key))).forEach(key => {
this.mobTypes.push({
value: MobType[key] as MobType,
text: key
});
});
// Initialize GameBundle options
Object.keys(GameBundle).filter(key => isNaN(Number(key))).forEach(key => {
this.gameBundles.push({
value: GameBundle[key] as GameBundle,
text: key
});
});
}
public close(): void {
this.dialog.close();
}
public save(): void {
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,
from: this.selectedGameBundle?.value ?? GameBundle.CoreGame,
mobLevelInfos: this.data?.mobLevelInfos || [],
skills: this.data?.skills || [],
bossFightProfile: bossFightProfile
};
this.mobInfoService.createOrUpdate(mobInfo).pipe(first()).subscribe(result => {
this.processing = false;
this.dialog.close(result);
}, error => {
this.processing = false;
console.error('Error saving mob info:', error);
});
}
}
public get isValid(): boolean {
if (!this.model) {
return false;
}
const nameValid = this.model.name && this.model.name.trim().length > 0;
const typeValid = this.selectedMobType !== null && this.selectedMobType !== undefined;
const fromValid = this.selectedGameBundle !== null && this.selectedGameBundle !== undefined;
return nameValid && typeValid && fromValid;
}
}
@@ -0,0 +1,58 @@
<nb-card>
<nb-card-header>
<h4>MD2 Mob Info Maintenance</h4>
<div class="float-right">
<button kendoButton (click)="addHandler()" [primary]="true">
<span class="k-icon k-i-plus"></span> Add New
</button>
</div>
</nb-card-header>
<nb-card-body>
<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()">
<kendo-grid-toolbar>
<button kendoGridAddCommand>Add new</button>
</kendo-grid-toolbar>
<kendo-grid-column field="name" title="Name" [width]="200">
</kendo-grid-column>
<kendo-grid-column field="type" title="Type" [width]="1">
<ng-template kendoGridCellTemplate let-dataItem>
<ng-template kendoGridCellTemplate let-dataItem>
{{ getMobTypeName(dataItem.type) }}
</ng-template>
<ng-template kendoGridGroupHeaderTemplate let-value="value">
{{ getMobTypeName(value) }}
</ng-template>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="from" title="Game Bundle" [width]="150">
<ng-template kendoGridCellTemplate let-dataItem>
{{ getGameBundleName(dataItem.from) }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="leaderImgUrl" title="Leader Image" [width]="200">
</kendo-grid-column>
<kendo-grid-column field="minionImgUrl" title="Minion Image" [width]="200">
</kendo-grid-column>
<kendo-grid-command-column title="Actions" [width]="250">
<ng-template kendoGridCellTemplate let-isNew="isNew" let-dataItem="dataItem" let-rowIndex="rowIndex">
<button kendoGridEditCommand [primary]="true">Edit</button>
<button kendoGridRemoveCommand>Remove</button>
<button kendoButton (click)="viewDetailHandler({ dataItem })" [look]="'flat'">
View Details
</button>
</ng-template>
</kendo-grid-command-column>
</kendo-grid>
</nb-card-body>
</nb-card>
@@ -0,0 +1,12 @@
:host {
display: block;
}
.float-right {
float: right;
}
kendo-grid {
height: 600px;
}
@@ -0,0 +1,162 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { GridComponent, GridDataResult } from '@progress/kendo-angular-grid';
import { State, process } from '@progress/kendo-data-query';
import { first } from 'rxjs/operators';
import { MD2MobInfo, GameBundle } from '../massive-darkness2.db.model';
import { MobType } from '../massive-darkness2.model';
import { MD2MobInfoService } from '../service/massive-darkness2.service';
import { MD2MobInfoDetailComponent } from './md2-mob-info-detail/md2-mob-info-detail.component';
import { MD2MobInfoEditorComponent } from './md2-mob-info-editor/md2-mob-info-editor.component';
import { DialogService } from '@progress/kendo-angular-dialog';
import { MsgBoxService } from '../../../services/msg-box.service';
@Component({
selector: 'ngx-md2-mob-info-maintenance',
templateUrl: './md2-mob-info-maintenance.component.html',
styleUrls: ['./md2-mob-info-maintenance.component.scss']
})
export class MD2MobInfoMaintenanceComponent implements OnInit {
@ViewChild('grid') grid: GridComponent;
public gridData: GridDataResult = { data: [], total: 0 };
private allData: MD2MobInfo[] = [];
public gridState: State = {
skip: 0,
take: 10,
sort: [{
field: 'name',
dir: 'asc'
}],
filter: {
logic: 'and',
filters: []
},
group: [{
field: 'type',
dir: 'asc'
}]
};
public isLoading: boolean = false;
constructor(
private mobInfoService: MD2MobInfoService,
private dialogService: DialogService,
private msgBoxService: MsgBoxService
) {
}
ngOnInit(): void {
this.loadData();
}
public loadData(): void {
this.isLoading = true;
this.mobInfoService.getAll().pipe(first()).subscribe(result => {
this.allData = result;
this.processGridData();
this.isLoading = false;
});
}
public processGridData(): void {
// Normalize filter state to handle null/undefined/empty filters
let normalizedFilter: { logic: 'and' | 'or'; filters: any[] } = { logic: 'and', filters: [] };
if (this.gridState.filter) {
const filters = this.gridState.filter.filters || [];
if (filters.length > 0) {
normalizedFilter = this.gridState.filter;
}
}
const normalizedState: State = {
...this.gridState,
filter: normalizedFilter
};
this.gridData = process(this.allData, normalizedState);
}
public addHandler(): void {
const editorData = {} as MD2MobInfo;
const dialogRef = this.dialogService.open({
title: 'Add New Mob Info',
content: MD2MobInfoEditorComponent,
width: '90vw',
height: 600
});
const editor = dialogRef.content.instance;
editor.isAdding = true;
editor.data = editorData;
// Force model re-initialization after data is set
setTimeout(() => {
editor.initializeModel();
}, 0);
dialogRef.result.subscribe(result => {
if (result) {
this.loadData();
}
});
}
public editHandler({ dataItem }: { dataItem: MD2MobInfo }): void {
const dialogRef = this.dialogService.open({
title: 'Edit Mob Info',
content: MD2MobInfoEditorComponent,
width: '90vw',
height: 600
});
const editor = dialogRef.content.instance;
editor.isAdding = false;
editor.data = JSON.parse(JSON.stringify(dataItem));
dialogRef.result.subscribe(result => {
if (result) {
this.loadData();
}
});
}
public removeHandler({ dataItem }: { dataItem: MD2MobInfo }): void {
this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(answer => {
if (answer === true) {
this.isLoading = true;
this.mobInfoService.delete(dataItem.id).pipe(first()).subscribe(result => {
this.loadData();
});
}
});
}
public viewDetailHandler({ dataItem }: { dataItem: MD2MobInfo }): void {
this.mobInfoService.getById(dataItem.id).pipe(first()).subscribe(mobInfo => {
const dialogRef = this.dialogService.open({
title: `Mob Info: ${mobInfo.name}`,
content: MD2MobInfoDetailComponent,
width: '90vw',
height: 800
});
const detail = dialogRef.content.instance;
detail.mobInfo = mobInfo;
dialogRef.result.subscribe(result => {
if (result) {
this.loadData();
}
});
});
}
public getMobTypeName(type: MobType): string {
return MobType[type] || '';
}
public getGameBundleName(bundle: GameBundle): string {
return GameBundle[bundle] || '';
}
}
@@ -0,0 +1,241 @@
<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">Level *</label>
<kendo-numerictextbox [(ngModel)]="model.level" name="level" [min]="1" [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>
{{mobType==MobType.Mob?'HP/Unit':'HP/Hero'}}
</label>
<kendo-numerictextbox [(ngModel)]="model.hpPerHero" name="hpPerHero" [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.TreasureToken"></md2-icon>Reward Tokens</label>
<kendo-numerictextbox [(ngModel)]="model.rewardTokens" name="rewardTokens" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Fixed HP</label>
<kendo-numerictextbox [(ngModel)]="model.fixedHp" name="fixedHp" [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.TreasureToken_Rare"></md2-icon>Rare Treasure</label>
<kendo-numerictextbox [(ngModel)]="model.fixedRareTreasure" name="fixedRareTreasure" [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.TreasureToken_Epic"></md2-icon>Epic Treasure</label>
<kendo-numerictextbox [(ngModel)]="model.fixedEpicTreasure" name="fixedEpicTreasure" [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.TreasureToken_Legendary"></md2-icon>Legend Treasure</label>
<kendo-numerictextbox [(ngModel)]="model.fixedLegendTreasure" name="fixedLegendTreasure" [min]="0"
[decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Actions</label>
<kendo-numerictextbox [(ngModel)]="model.actions" name="actions" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h5>Defense Info</h5>
</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.defenceInfo.blue" name="defenceInfo.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.defenceInfo.green" name="defenceInfo.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.defenceInfo.black" name="defenceInfo.black" [min]="0"
[decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
</div>
<div class="row" *ngIf="mobType!=MobType.Mob">
<div class="col-md-12">
<h5>Attack Info</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>
<!-- Dropdown list for attack type , the text is html string-->
<kendo-dropdownlist [(ngModel)]="selectedAttackType" name="attackInfo.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.attackInfo.yellow" name="attackInfo.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.attackInfo.orange" name="attackInfo.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.attackInfo.red" name="attackInfo.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.attackInfo.black" name="attackInfo.black" [min]="0"
[decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
</div>
<div class="row" *ngIf="mobType!=MobType.Mob">
<div class="col-md-12">
<h5>Alter Attack Info</h5>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">
<md2-icon [icon]="MD2Icon.Attack"></md2-icon>Alter Attack Type</label>
<!-- Dropdown list for attack type , the text is html string-->
<kendo-dropdownlist [(ngModel)]="selectedAlterAttackType" name="alterAttackInfo.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.alterAttackInfo.yellow" name="alterAttackInfo.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.alterAttackInfo.orange" name="alterAttackInfo.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.alterAttackInfo.red" name="alterAttackInfo.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.alterAttackInfo.black" name="alterAttackInfo.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>
@@ -0,0 +1,22 @@
.k-dialog-content {
padding: 20px;
}
.k-form {
.form-group {
margin-bottom: 15px;
}
.k-label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
h5 {
margin-top: 20px;
margin-bottom: 15px;
font-weight: 600;
}
}
@@ -0,0 +1,115 @@
import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core';
import { DialogRef, DialogContentBase } from '@progress/kendo-angular-dialog';
import { first } from 'rxjs/operators';
import { MD2MobLevelInfo } from '../../massive-darkness2.db.model';
import { MobSkillType } from '../../massive-darkness2.model.boss';
import { MD2MobLevelInfoService } from '../../service/massive-darkness2.service';
import { MD2Icon, MobType } from '../../massive-darkness2.model';
import { MD2Service } from '../../../../services/MD2/md2.service';
@Component({
selector: 'ngx-md2-mob-level-editor',
templateUrl: './md2-mob-level-editor.component.html',
styleUrls: ['./md2-mob-level-editor.component.scss']
})
export class MD2MobLevelEditorComponent extends DialogContentBase implements OnInit {
MobType = MobType;
MD2Icon = MD2Icon;
MobSkillType = MobSkillType;
@Input() public data: MD2MobLevelInfo;
@Input() public mobType: MobType;
@Input() public mobInfoId: string;
@Input() public isAdding: boolean = false;
public selectedAttackType: { value: MobSkillType; text: string } | null = null;
public model: MD2MobLevelInfo;
public processing: boolean = false;
public attackTypes: Array<{ value: MobSkillType; text: string }> = [];
public selectedAlterAttackType: { value: MobSkillType; text: string } | null = null;
constructor(
public dialog: DialogRef,
private mobLevelInfoService: MD2MobLevelInfoService,
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' },
];
this.selectedAttackType = this.attackTypes.find(t => t.value === this.model.attackInfo.type) || this.attackTypes[0] || null;
this.selectedAlterAttackType = this.attackTypes.find(t => t.value === this.model.alterAttackInfo?.type) || this.attackTypes[0] || null;
}
public initializeModel(): void {
this.model = {
id: this.data?.id || '',
level: this.data?.level ?? 1,
mobInfoId: this.mobInfoId || this.data?.mobInfoId || '',
rewardTokens: this.data?.rewardTokens ?? 0,
fixedRareTreasure: this.data?.fixedRareTreasure ?? 0,
fixedEpicTreasure: this.data?.fixedEpicTreasure ?? 0,
fixedLegendTreasure: this.data?.fixedLegendTreasure ?? 0,
fixedHp: this.data?.fixedHp ?? 1,
hpPerHero: this.data?.hpPerHero ?? 0,
actions: this.data?.actions ?? 1,
attackInfo: this.data?.attackInfo
? { ...this.data.attackInfo }
: { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
alterAttackInfo: this.data?.alterAttackInfo
? { ...this.data.alterAttackInfo }
: { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null },
defenceInfo: this.data?.defenceInfo
? { ...this.data.defenceInfo }
: { type: MobSkillType.Defense, yellow: null, orange: null, red: null, blue: null, green: null, black: null }
};
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.attackInfo) {
this.model.attackInfo = { type: MobSkillType.Attack, yellow: null, orange: null, red: null, blue: null, green: null, black: null };
}
if (!this.model.defenceInfo) {
this.model.defenceInfo = { type: MobSkillType.Defense, yellow: null, orange: null, red: null, blue: null, green: null, black: null };
}
this.model.attackInfo.type = this.selectedAttackType?.value ?? MobSkillType.Attack;
if (this.model.alterAttackInfo) {
this.model.alterAttackInfo.type = this.selectedAlterAttackType?.value ?? MobSkillType.Attack;
}
this.mobLevelInfoService.createOrUpdate(this.model).pipe(first()).subscribe(result => {
this.processing = false;
this.dialog.close(result);
}, error => {
this.processing = false;
console.error('Error saving mob level info:', error);
});
}
}
public get isValid(): boolean {
if (!this.model) {
return false;
}
return this.model.level > 0 && this.model.mobInfoId !== '';
}
}
@@ -0,0 +1,87 @@
<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">Sequence</label>
<kendo-numerictextbox [(ngModel)]="model.seq" name="seq" [min]="0" [decimals]="0" [format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Level</label>
<kendo-numerictextbox [(ngModel)]="model.level" name="level" [min]="1" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="k-label">Name</label>
<input kendoTextBox [(ngModel)]="model.name" name="name" class="k-input"
placeholder="Enter skill name" />
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Type *</label>
<kendo-dropdownlist [(ngModel)]="selectedSkillType" name="type" [data]="skillTypes"
[valueField]="'value'" [textField]="'text'"
[defaultItem]="{ value: null, text: 'Select type...' }">
</kendo-dropdownlist>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Target</label>
<kendo-dropdownlist [(ngModel)]="selectedSkillTarget" name="skillTarget" [data]="skillTargets"
[valueField]="'value'" [textField]="'text'">
</kendo-dropdownlist>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label"><md2-icon icon="EnemySkill"></md2-icon>Skill Roll</label>
<kendo-numerictextbox [(ngModel)]="model.skillRoll" name="skillRoll" [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="EnemyClaw"></md2-icon>Claw Roll</label>
<kendo-numerictextbox [(ngModel)]="model.clawRoll" name="clawRoll" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-12" *ngIf="selectedSkillType.value == MobSkillType.ConditionalSkill">
<div class="form-group">
<label class="k-label">Skill Condition</label>
<md2-html-editor [(ngModel)]="model.skillCondition" name="skillCondition"
class="htmlEditor"></md2-html-editor>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label class="k-label">Description</label>
<md2-html-editor [(ngModel)]="model.description" name="description"
class="htmlEditor"></md2-html-editor>
</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>
@@ -0,0 +1,8 @@
.k-label {
font-weight: 500;
}
md2-html-editor {
width: 100%;
min-height: 300px;
}
@@ -0,0 +1,121 @@
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 {
MobSkillType = MobSkillType;
@Input() public data: MD2MobSkill;
@Input() public mobInfoId: string;
@Input() public selectedLevel: number = 1;
@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 || '',
seq: this.data?.seq ?? 0,
level: this.data?.level ?? this.selectedLevel,
mobInfoId: this.mobInfoId || this.data?.mobInfoId || '',
type: typeValue,
skillTarget: targetValue,
clawRoll: this.data?.clawRoll ?? 0,
skillRoll: this.data?.skillRoll ?? 1,
name: this.data?.name || '',
skillCondition: this.data?.skillCondition || '',
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,
mobInfoId: this.mobInfoId || this.model.mobInfoId,
level: this.model.level || this.selectedLevel
};
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;
}
}
@@ -0,0 +1,180 @@
<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>
@@ -0,0 +1,114 @@
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 !== '';
}
}
@@ -20,9 +20,9 @@
<span class="MD2text diceAmount">x{{info.red}}</span>
</span>
</div>
<div *ngIf="mob.defenseInfo.black" class="g-height-45 mt-1">
<div *ngIf="info.black" class="g-height-45 mt-1">
<span class="MD2Icon Black dice g-font-size-50">
<span class="MD2text diceAmount">x{{mob.defenseInfo.black}}</span>
<span class="MD2text diceAmount">x{{info.black}}</span>
</span>
</div>
</div>
@@ -15,10 +15,17 @@
</span>
</div>
</div>
<div class='form-group' *ngIf="showSkill">
<label for='' class='MD2text g-font-size-22 label mb-2'>
<div class='form-group'>
<!-- <label for='' class='MD2text g-font-size-22 label mb-2'>
<md2-icon icon="blackDice" size="lg"></md2-icon> {{skillTriggerHtml}} <md2-icon icon="enemySkill" size="md">
</md2-icon>
</label>
<div class='g-font-size-20 skillDesc MD2text' [innerHtml]="mob.combatSkill.description"></div>
</label> -->
<ng-container *ngFor="let skill of mob.skills">
<div *ngIf="skill.uiDisplay" class=" g-brd-bottom--dashed g-brd-gray-light-v2">
<label for='' class='MD2text g-font-size-22 label mb-2'>
{{MobSkillType[skill.type]}} {{skill.skillRoll}} <md2-icon icon="enemySkill" size="md"></md2-icon>
</label>
<div class='g-font-size-20 skillDesc MD2text' [innerHtml]="skill.description"></div>
</div>
</ng-container>
</div>
@@ -23,6 +23,9 @@
font-size: 45px;
}
}
.skillDesc .MD2Icon {
font-size: 45px;
//override the style of the skillDesc class and sub elements
:host ::ng-deep .skillDesc .MD2Icon {
font-size: 30px;
}
@@ -8,7 +8,7 @@ import { MobSkillType } from '../../../massive-darkness2.model.boss';
styleUrls: ['./mob-combat-info.component.scss']
})
export class MobCombatInfoComponent implements OnInit {
MobSkillType = MobSkillType;
MD2Icon = MD2Icon;
private _mob: MobInfo;
public get mob(): MobInfo {
@@ -22,29 +22,31 @@ export class MobCombatInfoComponent implements OnInit {
}
}
@Input() mode: MobDlgType = MobDlgType.PreView;
showSkill: boolean = false;
showAllSkill: boolean = false;
showBlackDice: boolean
skillTriggerHtml: string = '';
constructor() { }
ngOnInit(): void {
if (this.mob.combatSkill) {
switch (this.mode) {
case MobDlgType.Activating:
this.showSkill = [MobSkillType.Combat, MobSkillType.Attack].includes(this.mob.combatSkill.type);
break;
case MobDlgType.BeenAttacked:
this.showSkill = [MobSkillType.Combat, MobSkillType.Defense].includes(this.mob.combatSkill.type);
break;
case MobDlgType.PreView:
this.showSkill = true;
break;
case MobDlgType.Spawn:
default:
this.showSkill = false;
break;
}
this.skillTriggerHtml = `${MobSkillType[this.mob.combatSkill.type]} ${this.mob.combatSkill.skillRoll} `
this.showAllSkill = [MobDlgType.PreView, MobDlgType.Dashboard].includes(this.mode);
if (this.mob.skills && this.mob.skills.length > 0) {
this.mob.skills.forEach(element => {
switch (this.mode) {
case MobDlgType.Activating:
element.uiDisplay = [MobSkillType.Combat, MobSkillType.Attack].includes(element.type);
break;
case MobDlgType.BeenAttacked:
element.uiDisplay = [MobSkillType.Combat, MobSkillType.Defense].includes(element.type);
break;
case MobDlgType.PreView:
case MobDlgType.Dashboard:
element.uiDisplay = [MobSkillType.Combat, MobSkillType.Attack, MobSkillType.Defense].includes(element.type);
break;
case MobDlgType.Spawn:
element.uiDisplay = false;
break;
}
});
}
this.showBlackDice = this.mob.type == MobType.Mob && (this.mode == MobDlgType.Activating || this.mode == MobDlgType.BeenAttacked) && this.mob.minionAmount > 0;;
@@ -5,12 +5,12 @@
<md2-icon icon="defense" size="lg"></md2-icon>
</div>
<div class="col-md-8">
<div *ngIf="mob.defenseInfo.blue" class="g-height-45">
<div *ngIf="mob.defenseInfo?.blue" class="g-height-45">
<span class="MD2Icon Blue dice g-font-size-50">
<span class="MD2text diceAmount">x{{mob.defenseInfo.blue}}</span>
</span>
</div>
<div *ngIf="mob.defenseInfo.black" class="g-height-45 mt-1">
<div *ngIf="mob.defenseInfo?.black" class="g-height-45 mt-1">
<span class="MD2Icon Black dice g-font-size-50">
<span class="MD2text diceAmount">x{{mob.defenseInfo.black}}</span>
</span>
@@ -38,6 +38,9 @@ export class MobDefInfoComponent implements OnInit {
this.display = false;
break;
}
if (!this.mob.defenseInfo || this.mob.defenseInfo.blue == 0 && this.mob.defenseInfo.black == 0) {
this.display = false;
}
}
}
@@ -8,8 +8,8 @@
<div class="pl-2 col-md-6" *ngIf="mob.mobAmount">
<ng-container>
<label class='label g-text-nowrap'>Alive Units <b
class="MD2text g-font-size-18">{{mob.mobAmount}}</b></label><br>
<label class='label g-text-nowrap'>Minions <b
class="MD2text g-font-size-18">{{mob.mobAmount-1}}</b></label><br>
</ng-container>
</div>
@@ -34,11 +34,21 @@
<ng-container *ngIf="showAdjustment">
<div class="row">
<div class="col-md-6">
<adj-number-input name="mob{{mob.name}}" [ngModel]="mob.unitRemainHp" [maximum]="mob.hp" minimum="1"
title="Target Unit HP" (hitChange)="adjustMobHp($event)" (hitMinimum)="adjustMobHp(-1)"
(hitMaximum)="adjustMobHp(1)">
</adj-number-input>
<adj-number-input name="mob{{mob.name}}" [ngModel]="mob.unitRemainHp" [maximum]="mob.hp" minimum="1"
title="Target Unit HP" (hitChange)="adjustMobHp($event)" (hitMinimum)="adjustMobHp(-1)"
(hitMaximum)="adjustMobHp(1)">
</adj-number-input>
</div>
<div class="col-md-6" *ngIf="mode==MobDlgType.Activating">
<adj-number-input name="mob{{mob.name}}Actions" [(ngModel)]="mob.actions" title="Remaining Actions"
hideIncreaseBtn>
</adj-number-input>
</div>
</div>
</ng-container>
@@ -50,4 +60,8 @@
<md2-mob-attack-info [mob]="mob" [mode]="mode">
</md2-mob-attack-info>
<md2-mob-def-info [mob]="mob" [mode]="mode"></md2-mob-def-info>
<md2-mob-combat-info [mob]="mob" [mode]="mode"></md2-mob-combat-info>
<md2-mob-combat-info *ngIf="!hideCombatInfo" [mob]="mob" [mode]="mode"></md2-mob-combat-info>
<div *ngIf="!hideCombatInfo && mob.extraRule">
<div class="alert alert-warning" role="alert" [innerHtml]="mob.extraRule">
</div>
</div>
@@ -12,7 +12,7 @@ import { MD2ComponentBase } from '../../MD2Base';
styleUrls: ['./mob-detail-info.component.scss']
})
export class MobDetailInfoComponent extends MD2ComponentBase implements OnInit {
MobDlgType = MobDlgType;
MD2Icon = MD2Icon;
private _mob: MobInfo;
public get mob(): MobInfo {
@@ -33,6 +33,10 @@ export class MobDetailInfoComponent extends MD2ComponentBase implements OnInit {
return this.mode == MobDlgType.Spawn;
}
public get hideCombatInfo(): boolean {
return this.mode == MobDlgType.Dashboard;
}
showAttackingInfo: boolean = false;
@Input("showAttackingInfo")
@@ -1,7 +1,22 @@
<img class="g-width-95x img-thumbnail mobBg" src="{{imgUrl('/Mobs/BG.png')}}" />
<img class="mobImg roamingMonster" src="{{getMobImageUrl(mob)}}" (click)="showMobImage(mob)" *ngIf="!isMob" />
<div *ngIf="isMob">
<img class="mobImg mobLeader" src="{{mob.leaderImgUrl}}" (click)="showMobImage(mob)" />
<img class="mobImg mobMinion" src="{{mob.minionImgUrl}}" (click)="showMobImage(mob)" />
<img class="mobImg mobLeader" src="{{mob.leaderImgUrl}}" (click)="showMobImage(mob)"
[ngClass]="{'noMinions': mob.mobAmount==1}" />
<img class="mobImg mobMinion" src="{{mob.minionImgUrl}}" (click)="showMobImage(mob)" *ngIf="mob.mobAmount>1" />
</div>
<!-- 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">{{mob.unitRemainHp}}/{{mob.hp}}</span>
</div>
<div class="stat-progress-bar-overlay">
<div class="stat-progress-fill-overlay hp-fill-overlay" [style.width.%]="(mob.unitRemainHp / mob.hp) * 100">
</div>
</div>
</div>
</div>
@@ -1,23 +1,153 @@
.mobImg {
position: absolute;
z-index: 2;
object-fit: contain;
&.roamingMonster {
width: 95%;
max-height: 80%;
left: 0;
}
&.mobLeader {
z-index: 3;
width: 70%;
max-height: 80%;
top: 40px;
left: 0;
&.noMinions {
width: 95%;
max-height: 90%;
}
}
&.mobMinion {
width: 60%;
max-height: 80%;
right: 0;
top: 0;
}
}
.mobBg {
position: absolute;
position: relative;
z-index: 1;
}
// HP and Mana Bars Overlay
.hero-stats-overlay {
position: relative;
bottom: 60px;
left: 0;
right: 0;
padding: 0.5rem;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.5) 70%, transparent 100%);
border-radius: 0 0 8px 8px;
z-index: 1;
width: 95%;
}
.stat-bar-overlay {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
@media (max-height: 450px) and (orientation: landscape) {
margin-bottom: 0.35rem;
}
}
.stat-bar-label-overlay {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.25rem;
gap: 0.5rem;
md2-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
}
.stat-value-overlay {
color: white;
font-size: 0.75rem;
font-weight: 600;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
flex-shrink: 0;
}
@media (max-height: 450px) and (orientation: landscape) {
margin-bottom: 0.15rem;
gap: 0.3rem;
md2-icon {
width: 14px;
height: 14px;
}
.stat-value-overlay {
font-size: 0.65rem;
}
}
}
.stat-progress-bar-overlay {
width: 100%;
height: 20px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.3);
@media (max-height: 450px) and (orientation: landscape) {
height: 16px;
border-radius: 8px;
}
}
.stat-progress-fill-overlay {
height: 100%;
border-radius: 10px;
transition: width 0.5s ease-out;
position: relative;
overflow: hidden;
@media (max-height: 450px) and (orientation: landscape) {
border-radius: 8px;
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: shimmer 2s infinite;
}
&.full-stat {
width: 100%;
}
}
.hp-fill-overlay {
background: linear-gradient(90deg, #ff6b6b, #ee5a6f);
box-shadow: 0 0 8px rgba(238, 90, 111, 0.6);
}
.mp-fill-overlay {
background: linear-gradient(90deg, #4ecdc4, #44a08d);
box-shadow: 0 0 8px rgba(68, 160, 141, 0.6);
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@@ -3,10 +3,10 @@
<img src="{{imgUrl('Mobs/MobToken.png')}}" width="40px"> {{(isRoamingMonster?'Roaming Monsters':'Mobs')}}
<!-- <button nbButton hero status="warning" size="small" (click)="initMobDecks()" class="float-right">Reset
Mobs</button> -->
<button nbButton hero status="danger" size="small" (click)="spawnMob()" class="float-right"
<button nbButton hero status="danger" size="small" (click)="spawnSpecificMob()" class="float-right"
*ngIf="isRoamingMonster">Spawn Roaming
Monster</button>
<button nbButton hero status="warning" size="small" (click)="spawnMob()" class="float-right mr-2"
<button nbButton hero status="warning" size="small" (click)="spawnSpecificMob()" class="float-right mr-2"
*ngIf="!isRoamingMonster">Spawn
Mob</button>
<!-- <button nbButton hero status="warning" size="tiny" (click)="resetTreasureBag()"
@@ -29,6 +29,12 @@
<div class="row no-gutters">
<div class="col-12 col-md-8">
<md2-mob-stand-info [mob]="mob"></md2-mob-stand-info>
<md2-mob-combat-info [mob]="mob" [mode]="MobDlgType.Dashboard"></md2-mob-combat-info>
<div *ngIf="mob.extraRule">
<div class="alert alert-warning" role="alert" [innerHtml]="mob.extraRule">
</div>
</div>
</div>
<div class=" col-12 col-md-4">
@@ -1,19 +1,20 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme';
import { stringify } from 'querystring';
import { first } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption';
import { FileService } from '../../../services/file.service';
import { MD2MobService } from '../../../services/MD2/md2-mob.service';
import { MD2Service } from '../../../services/MD2/md2.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { StateService } from '../../../services/state.service';
import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
import { ADButtons, ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
import { NumberUtils } from '../../../utilities/number-utils';
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',
@@ -86,9 +87,44 @@ export class MobsComponent extends MD2ComponentBase implements OnInit {
});
this.cdRef.detectChanges();
}
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));
this.msgBoxService.showInputbox('Spawn', '',
{
inputType: 'dropdown', dropDownOptions: mobOptions,
buttons: ADButtons.YesNoCancel,
confirmButtonText: 'Spawn',
cancelButtonText: 'Random'
}).pipe(first()).subscribe(mobId => {
if (mobId || mobId === false) {
if (!mobId) { mobId = null; }
let result = this.md2Service.spawnMob(this.isRoamingMonster, mobId);
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;
this.dlgService.open(SpawnMobDlgComponent, { context: { title: titleText, mode: actType, mob: mob } })
.onClose.pipe(first()).subscribe(result => {
this.afterSpawn();
});
this.cdRef.detectChanges();
}
});
}
afterSpawn() {
this.cdRef.detectChanges();
this.md2Service.broadcastService.broadcastMobsInfo();
this.md2Service.broadcastMobsInfo();
if (this.showRoundMessage) {
this.msgBoxService.show(`${NumberUtils.Ordinal(this.md2Service.info.round)} Hero Phase`, { icon: ADIcon.INFO });
}
@@ -119,7 +155,7 @@ export class MobsComponent extends MD2ComponentBase implements OnInit {
if (attacker) {
attacker.exp += 1;
this.msgBoxService.show(`${attacker.heroFullName} Gain 1 Exp`, { icon: ADIcon.INFO }).pipe(first()).subscribe(result => {
this.md2Service.broadcastService.broadcastHeroInfoToOwner(attacker);
this.md2Service.broadcastHeroInfoToOwner(attacker);
});
}
@@ -5,20 +5,33 @@
</nb-card-header>
<nb-card-body>
<div class="row no-gutters">
<div class="col-md-7 g-height-90vh">
<div class="col-md-7">
<!-- <img src="{{mob.imageUrl}}" class="g-width-90x"> -->
<md2-mob-stand-info [mob]="mob" [mode]="mode"></md2-mob-stand-info>
<div *ngIf="activeSkill">
<!-- <div class="alert alert-warning" role="alert">
<h4>{{activeSkill.name}}</h4>
<div [innerHtml]="activeSkill.description"></div>
</div> -->
<details class="skill-card" open>
<summary>
<h3 class="skill-name">{{ activeSkill.name }}</h3>
<!-- <span class="rarity" [ngClass]="rarity.toLowerCase()">{{ rarity }}</span> -->
</summary>
<div class="skill-body" [innerHtml]="activeSkill.description">
<!-- <p>{{ description }}</p>
<ul>
<li *ngFor="let s of stats"><strong>{{ s.label }}:</strong> {{ s.value }}</li>
</ul> -->
</div>
</details>
</div>
</div>
<div class="col-md-5">
<md2-mob-detail-info [mob]="mob" [mode]="mode">
</md2-mob-detail-info>
<div *ngIf="actionInfoHtml">
<div class="alert alert-warning" role="alert" [innerHtml]="actionInfoHtml">
</div>
</div>
<ng-container *ngIf="mode==MobDlgType.Spawn&&mob.type==MobType.Mob">
<div class="row form-group mt-2">
@@ -2,3 +2,122 @@
width: 885px;
height: 95vh !important;
}
.skill-card {
border-radius: 16px;
overflow: hidden;
position: relative;
border: 1px solid #252a34;
background: radial-gradient(120% 180% at 50% -40%, rgba(217, 143, 43, 0.08), transparent 60%),
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01)), #14161b;
transition:
transform 0.12s ease,
box-shadow 0.25s ease,
filter 0.2s ease;
margin-top: -130px;
z-index: 1;
width: 96%;
}
.skill-card::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: 16px;
padding: 1px;
background: linear-gradient(
180deg,
rgba(246, 230, 185, 0.6),
rgba(176, 136, 46, 0.35) 55%,
rgba(246, 230, 185, 0.6)
);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.skill-card[open] {
box-shadow:
0 0 0 2px rgba(217, 143, 43, 0.25),
0 12px 28px rgba(0, 0, 0, 0.6);
}
summary {
list-style: none;
cursor: pointer;
position: relative;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 0.75rem;
padding: 6px 1rem;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0));
}
summary::-webkit-details-marker {
display: none;
}
summary:hover {
filter: saturate(1.08);
box-shadow:
0 0 0 2px rgba(155, 28, 28, 0.25),
0 0 18px rgba(217, 143, 43, 0.18) inset;
}
summary:focus-visible {
outline: 2px solid #d98f2b;
outline-offset: 2px;
box-shadow: 0 0 18px rgba(217, 143, 43, 0.35);
}
.skill-name {
margin: 0;
font-size: 2rem;
letter-spacing: 0.04em;
background: linear-gradient(180deg, #f6e6b9, #b0882e 50%, #f6e6b9);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-family: "DwarvenAxeBBW00-Regular", sans-serif !important;
}
.rarity {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.12em;
padding: 0.25rem 0.5rem;
border-radius: 999px;
border: 1px solid rgba(240, 217, 154, 0.45);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
color: #e8e5de;
}
.rarity.legendary {
border-color: rgba(217, 143, 43, 0.6);
box-shadow: inset 0 0 10px rgba(217, 143, 43, 0.25);
}
.rarity.epic {
border-color: rgba(155, 28, 28, 0.6);
box-shadow: inset 0 0 10px rgba(155, 28, 28, 0.3);
}
.rarity.rare {
border-color: rgba(148, 164, 58, 0.6);
box-shadow: inset 0 0 10px rgba(148, 164, 58, 0.28);
}
::ng-deep {
.skill-body {
padding: 0 1rem 1rem;
color: #cfcab7;
}
.skill-body p {
margin: 0.6rem 0 0.4rem;
color: #ddd6bf;
}
.skill-body ul {
margin: 0.25rem 0 0;
padding-left: 1.1rem;
}
.skill-body li {
margin: 0.25rem 0;
color: #a7a196;
}
.skill-body strong {
color: #efe7cf;
}
}

Some files were not shown because too many files have changed in this diff Show More