Compare commits

...

50 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
Chris Chen d486fe9594 Update boss fight 2024-03-29 08:04:07 -07:00
Chris Chen 6a031ca478 WIP 2024-03-22 11:06:42 -07:00
477 changed files with 70002 additions and 64338 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 !.vscode/extensions.json
# misc # misc
/.angular/cache
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /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", "src/web.config",
{ {
"glob": "**/*", "glob": "**/*",
"input": "node_modules/leaflet/dist/images", "input": "node_modules/tinymce",
"output": "/assets/img/markers" "output": "/tinymce/"
} }
], ],
"styles": [ "styles": [
@@ -38,16 +38,13 @@
"node_modules/nebular-icons/scss/nebular-icons.scss", "node_modules/nebular-icons/scss/nebular-icons.scss",
"node_modules/pace-js/templates/pace-theme-flash.tmpl.css", "node_modules/pace-js/templates/pace-theme-flash.tmpl.css",
"node_modules/leaflet/dist/leaflet.css", "node_modules/leaflet/dist/leaflet.css",
"node_modules/@progress/kendo-theme-default/dist/all.css",
"src/app/@theme/styles/styles.scss", "src/app/@theme/styles/styles.scss",
"src/assets/styles/site.scss" "src/assets/styles/site.scss"
], ],
"scripts": [ "scripts": [
"node_modules/pace-js/pace.min.js", "node_modules/pace-js/pace.min.js",
"node_modules/tinymce/tinymce.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/echarts.min.js",
"node_modules/echarts/dist/extension/bmap.min.js", "node_modules/echarts/dist/extension/bmap.min.js",
"node_modules/chart.js/dist/Chart.min.js" "node_modules/chart.js/dist/Chart.min.js"
@@ -57,7 +54,10 @@
"echarts", "echarts",
"lodash", "lodash",
"zrender/lib/svg/svg", "zrender/lib/svg/svg",
"zrender/lib/vml/vml" "zrender/lib/vml/vml",
"file-saver",
"eva-icons",
"rxjs-compat"
], ],
"vendorChunk": true, "vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
@@ -94,18 +94,18 @@
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"browserTarget": "ngx-admin-demo:build" "buildTarget": "ngx-admin-demo:build"
}, },
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "ngx-admin-demo:build:production" "buildTarget": "ngx-admin-demo:build:production"
} }
} }
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "ngx-admin-demo:build" "buildTarget": "ngx-admin-demo:build"
} }
}, },
"test": { "test": {
@@ -118,10 +118,6 @@
"scripts": [ "scripts": [
"node_modules/pace-js/pace.min.js", "node_modules/pace-js/pace.min.js",
"node_modules/tinymce/tinymce.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/echarts.min.js",
"node_modules/echarts/dist/extension/bmap.min.js", "node_modules/echarts/dist/extension/bmap.min.js",
"node_modules/chart.js/dist/Chart.min.js" "node_modules/chart.js/dist/Chart.min.js"
@@ -144,50 +140,15 @@
"src/web.config", "src/web.config",
{ {
"glob": "**/*", "glob": "**/*",
"input": "node_modules/leaflet/dist/images", "input": "node_modules/tinymce",
"output": "/assets/img/markers" "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": {
"@schematics/angular:component": { "@schematics/angular:component": {
"prefix": "ngx", "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", "name": "ngx-admin",
"version": "8.0.0", "version": "8.0.0",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/akveo/ngx-admin.git" "url": "git+https://github.com/akveo/ngx-admin.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/akveo/ngx-admin/issues" "url": "https://github.com/akveo/ngx-admin/issues"
}, },
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"conventional-changelog": "conventional-changelog", "conventional-changelog": "conventional-changelog",
"start": "ng serve", "start": "ng serve --host=127.0.0.1",
"build": "ng build --output-path \\\\ArkNAS\\docker\\nginx-proxy\\data\\ChurchAngular --configuration production", "build": "ng build",
"build:prod": "npm run build -- --configuration production --aot", "build:prod": "ng build --output-path \\\\ArkNAS\\docker\\nginx-proxy\\data\\ChurchAngular --configuration production --aot",
"test": "ng test", "test": "ng test",
"test:coverage": "rimraf coverage && npm run test -- --code-coverage", "test:coverage": "rimraf coverage && npm run test -- --code-coverage",
"lint": "ng lint", "lint": "ng lint",
"lint:fix": "ng lint ngx-admin-demo --fix", "lint:fix": "ng lint ngx-admin-demo --fix",
"lint:styles": "stylelint ./src/**/*.scss", "lint:styles": "stylelint ./src/**/*.scss",
"lint:ci": "npm run lint && npm run lint:styles", "lint:ci": "npm run lint && npm run lint:styles",
"pree2e": "webdriver-manager update --standalone false --gecko false", "pree2e": "webdriver-manager update --standalone false --gecko false",
"e2e": "ng e2e", "e2e": "ng e2e",
"docs": "compodoc -p src/tsconfig.app.json -d docs", "docs": "compodoc -p src/tsconfig.app.json -d docs",
"docs:serve": "compodoc -p src/tsconfig.app.json -d docs -s", "docs:serve": "compodoc -p src/tsconfig.app.json -d docs -s",
"prepush": "npm run lint:ci", "prepush": "echo 'Pre-push hook disabled - style linting skipped'",
"release:changelog": "npm run conventional-changelog -- -p angular -i CHANGELOG.md -s", "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": {
"dependencies": { "@angular/animations": "^17.3.3",
"@angular/animations": "^12.2.16", "@angular/cdk": "17.3.3",
"@angular/cdk": "12.1.0", "@angular/common": "^17.3.3",
"@angular/common": "^12.2.16", "@angular/compiler": "^17.3.3",
"@angular/compiler": "^12.2.16", "@angular/core": "^17.3.3",
"@angular/core": "^12.2.16", "@angular/forms": "^17.3.3",
"@angular/forms": "^12.2.16", "@angular/platform-browser": "^17.3.3",
"@angular/google-maps": "^12.2.13", "@angular/platform-browser-dynamic": "^17.3.3",
"@angular/platform-browser": "^12.2.16", "@angular/router": "^17.3.3",
"@angular/platform-browser-dynamic": "^12.2.16", "@asymmetrik/ngx-leaflet": "3.0.1",
"@angular/router": "^12.2.16", "@microsoft/signalr": "^6.0.8",
"@asymmetrik/ngx-leaflet": "3.0.1", "@nebular/auth": "13.0.0",
"@microsoft/signalr": "^6.0.8", "@nebular/date-fns": "^13.0.0",
"@nebular/auth": "8.0.0", "@nebular/eva-icons": "13.0.0",
"@nebular/date-fns": "^9.0.3", "@nebular/security": "13.0.0",
"@nebular/eva-icons": "8.0.0", "@nebular/theme": "13.0.0",
"@nebular/security": "8.0.0", "@progress/kendo-angular-buttons": "^20.1.1",
"@nebular/theme": "8.0.0", "@progress/kendo-angular-dialog": "^20.1.1",
"@swimlane/ngx-charts": "^14.0.0", "@progress/kendo-angular-dropdowns": "^20.1.1",
"angular2-chartjs": "0.4.1", "@progress/kendo-angular-editor": "^20.1.1",
"angular2-qrcode": "^2.0.3", "@progress/kendo-angular-grid": "^20.1.1",
"bootstrap": "4.3.1", "@progress/kendo-angular-inputs": "^20.1.1",
"chart.js": "2.7.1", "@progress/kendo-angular-toolbar": "^20.1.1",
"ckeditor": "4.7.3", "@progress/kendo-licensing": "^1.7.1",
"classlist.js": "1.1.20150312", "@progress/kendo-svg-icons": "^4.5.0",
"core-js": "2.5.1", "@progress/kendo-theme-default": "^12.2.0",
"echarts": "^4.9.0", "@tinymce/tinymce-angular": "^7.0.0",
"eva-icons": "^1.1.3", "angular2-chartjs": "0.4.1",
"file-saver": "^2.0.5", "angular2-qrcode": "^2.0.3",
"intl": "1.2.5", "bootstrap": "4.3.1",
"ionicons": "2.0.1", "chart.js": "2.7.1",
"leaflet": "1.2.0", "core-js": "2.5.1",
"nebular-icons": "1.1.0", "echarts": "^4.9.0",
"ng-in-viewport": "^13.0.1", "eva-icons": "^1.1.3",
"ng2-ckeditor": "~1.2.9", "file-saver": "^2.0.5",
"ng2-completer": "^9.0.1", "intl": "1.2.5",
"ng2-smart-table": "^1.6.0", "ionicons": "2.0.1",
"ngx-echarts": "^4.2.2", "leaflet": "1.2.0",
"ngx-infinite-scroll": "^13.0.2", "nebular-icons": "1.1.0",
"ngx-mask": "^12.0.0", "ng-in-viewport": "^13.0.1",
"node-sass": "^4.14.1", "ng2-completer": "^9.0.1",
"normalize.css": "6.0.0", "ngx-echarts": "^4.2.2",
"pace-js": "1.0.2", "ngx-infinite-scroll": "^17.0.0",
"roboto-fontface": "0.8.0", "ngx-mask": "^12.0.0",
"rxjs": "6.6.2", "node-sass": "^4.14.1",
"rxjs-compat": "6.3.0", "normalize.css": "6.0.0",
"socicon": "3.0.5", "pace-js": "1.0.2",
"style-loader": "^1.3.0", "roboto-fontface": "0.8.0",
"tinymce": "4.5.7", "rxjs": "6.6.2",
"tslib": "^2.3.1", "rxjs-compat": "6.3.0",
"typeface-exo": "0.0.22", "socicon": "3.0.5",
"typeit": "^8.7.0", "style-loader": "^1.3.0",
"web-animations-js": "^2.3.2", "tinymce": "^7.0.0",
"zone.js": "~0.11.4" "tslib": "^2.3.1",
}, "typeface-exo": "0.0.22",
"devDependencies": { "typeit": "^8.7.0",
"@angular-devkit/build-angular": "^12.1.4", "zone.js": "~0.14.4"
"@angular/cli": "^12.2.17", },
"@angular/compiler-cli": "^12.2.16", "devDependencies": {
"@angular/language-service": "12.1.0", "@angular-devkit/build-angular": "^17.3.3",
"@compodoc/compodoc": "1.0.1", "@angular/cli": "^17.3.3",
"@fortawesome/fontawesome-free": "^5.2.0", "@angular/compiler-cli": "^17.3.3",
"@schematics/angular": "^14.1.3", "@angular/language-service": "17.3.3",
"@types/d3-color": "1.0.5", "@angular/localize": "^17.3.3",
"@types/jasmine": "~3.3.0", "@compodoc/compodoc": "1.0.1",
"@types/jasminewd2": "2.0.3", "@fortawesome/fontawesome-free": "^5.2.0",
"@types/leaflet": "1.2.3", "@schematics/angular": "^14.1.3",
"@types/node": "^12.12.70", "@types/d3-color": "1.0.5",
"codelyzer": "^6.0.2", "@types/jasmine": "~3.3.0",
"conventional-changelog-cli": "1.3.4", "@types/jasminewd2": "2.0.3",
"husky": "0.13.3", "@types/leaflet": "1.2.3",
"jasmine-core": "~3.6.0", "@types/node": "^18.19.30",
"jasmine-spec-reporter": "~5.0.0", "codelyzer": "^6.0.2",
"karma": "~6.3.19", "conventional-changelog-cli": "1.3.4",
"karma-chrome-launcher": "~3.1.1", "husky": "0.13.3",
"karma-cli": "1.0.1", "jasmine-core": "~5.1.2",
"karma-coverage-istanbul-reporter": "~3.0.2", "jasmine-spec-reporter": "~5.0.0",
"karma-jasmine": "~4.0.2", "karma": "~6.3.19",
"karma-jasmine-html-reporter": "^1.7.0", "karma-chrome-launcher": "~3.1.1",
"npm-run-all": "4.0.2", "karma-cli": "1.0.1",
"protractor": "~7.0.0", "karma-coverage-istanbul-reporter": "~3.0.2",
"rimraf": "2.6.1", "karma-jasmine": "~5.1.0",
"stylelint": "7.13.0", "karma-jasmine-html-reporter": "^2.1.0",
"ts-node": "3.2.2", "npm-run-all": "4.0.2",
"tslint": "~6.1.0", "protractor": "~7.0.0",
"tslint-language-service": "^0.9.9", "rimraf": "2.6.1",
"typescript": "~4.2.3||~4.3.0" "stylelint": "7.13.0",
} "ts-node": "3.2.2",
"typescript": "~5.4.4"
}
} }
@@ -1,6 +1,6 @@
@import '../../styles/themes'; @import "../../styles/themes";
@import '~@nebular/theme/styles/global/breakpoints'; @import "@nebular/theme/styles/global/breakpoints";
@import '~bootstrap/scss/mixins/breakpoints'; @import "bootstrap/scss/mixins/breakpoints";
@include nb-install-component() { @include nb-install-component() {
width: 100%; width: 100%;
@@ -1,6 +1,6 @@
@import '~bootstrap/scss/mixins/breakpoints'; @import "bootstrap/scss/mixins/breakpoints";
@import '~@nebular/theme/styles/global/breakpoints'; @import "@nebular/theme/styles/global/breakpoints";
@import '../../styles/themes'; @import "../../styles/themes";
@include nb-install-component() { @include nb-install-component() {
display: flex; display: flex;
@@ -24,7 +24,7 @@
} }
::ng-deep nb-search button { ::ng-deep nb-search button {
padding: 0!important; padding: 0 !important;
} }
.header-container { .header-container {
@@ -1,6 +1,6 @@
@import '../../styles/themes'; @import "../../styles/themes";
@import '~bootstrap/scss/mixins/breakpoints'; @import "bootstrap/scss/mixins/breakpoints";
@import '~@nebular/theme/styles/global/breakpoints'; @import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() { @include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable { .menu-sidebar ::ng-deep .scrollable {
@@ -1,6 +1,6 @@
@import '../../styles/themes'; @import "../../styles/themes";
@import '~bootstrap/scss/mixins/breakpoints'; @import "bootstrap/scss/mixins/breakpoints";
@import '~@nebular/theme/styles/global/breakpoints'; @import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() { @include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable { .menu-sidebar ::ng-deep .scrollable {
@@ -1,6 +1,6 @@
@import '../../styles/themes'; @import "../../styles/themes";
@import '~bootstrap/scss/mixins/breakpoints'; @import "bootstrap/scss/mixins/breakpoints";
@import '~@nebular/theme/styles/global/breakpoints'; @import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() { @include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable { .menu-sidebar ::ng-deep .scrollable {
@@ -1,6 +1,6 @@
@import '../../styles/themes'; @import "../../styles/themes";
@import '~bootstrap/scss/mixins/breakpoints'; @import "bootstrap/scss/mixins/breakpoints";
@import '~@nebular/theme/styles/global/breakpoints'; @import "@nebular/theme/styles/global/breakpoints";
@include nb-install-component() { @include nb-install-component() {
.menu-sidebar ::ng-deep .scrollable { .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 // themes - our custom or/and out of the box themes
@import 'themes'; @import "themes";
// framework component themes (styles tied to theme variables) // framework component themes (styles tied to theme variables)
@import '~@nebular/theme/styles/globals'; @import "@nebular/theme/styles/globals";
@import '~@nebular/auth/styles/globals'; @import "@nebular/auth/styles/globals";
@import '~bootstrap/scss/functions'; @import "bootstrap/scss/functions";
@import '~bootstrap/scss/variables'; @import "bootstrap/scss/variables";
@import '~bootstrap/scss/mixins'; @import "bootstrap/scss/mixins";
@import '~bootstrap/scss/grid'; @import "bootstrap/scss/grid";
// loading progress bar theme // loading progress bar theme
@import './pace.theme'; @import "./pace.theme";
@import './layout'; @import "./layout";
@import './overrides'; @import "./overrides";
// install the framework and custom global styles // install the framework and custom global styles
@include nb-install() { @include nb-install() {
// framework global styles // framework global styles
@include nb-theme-global(); @include nb-theme-global();
@include nb-auth-global(); @include nb-auth-global();
@@ -30,4 +29,4 @@
@include ngx-pace-theme(); @include ngx-pace-theme();
@include nb-overrides(); @include nb-overrides();
}; } ;
+82 -66
View File
@@ -1,88 +1,104 @@
// @nebular theming framework // @nebular theming framework
@import '~@nebular/theme/styles/theming'; @import "@nebular/theme/styles/theming";
// @nebular out of the box themes // @nebular out of the box themes
@import '~@nebular/theme/styles/themes'; @import "@nebular/theme/styles/themes";
$nb-themes: nb-register-theme(( $nb-themes: nb-register-theme(
layout-padding-top: 2.25rem, (
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-tiny: 13.5rem,
card-height-small: 21.1875rem, card-height-small: 21.1875rem,
card-height-medium: 28.875rem, card-height-medium: 28.875rem,
card-height-large: 36.5625rem, card-height-large: 36.5625rem,
card-height-giant: 44.25rem, card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem, card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem, card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem, card-header-with-select-padding-bottom: 0.5rem,
select-min-width: 6rem, select-min-width: 6rem,
slide-out-background: #f7f9fc, slide-out-background: #f7f9fc,
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3, slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3, slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
), default, default); ),
default,
default
);
$nb-themes: nb-register-theme(( $nb-themes: nb-register-theme(
layout-padding-top: 2.25rem, (
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-tiny: 13.5rem,
card-height-small: 21.1875rem, card-height-small: 21.1875rem,
card-height-medium: 28.875rem, card-height-medium: 28.875rem,
card-height-large: 36.5625rem, card-height-large: 36.5625rem,
card-height-giant: 44.25rem, card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem, card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem, card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 0.5rem, card-header-with-select-padding-bottom: 0.5rem,
select-min-width: 6rem, select-min-width: 6rem,
slide-out-background: #252547, slide-out-background: #252547,
slide-out-shadow-color: 2px 0 3px #29157a, slide-out-shadow-color: 2px 0 3px #29157a,
slide-out-shadow-color-rtl: -2px 0 3px #29157a, slide-out-shadow-color-rtl: -2px 0 3px #29157a,
), cosmic, cosmic); ),
cosmic,
cosmic
);
$nb-themes: nb-register-theme(( $nb-themes: nb-register-theme(
layout-padding-top: 2.25rem, (
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-tiny: 13.5rem,
card-height-small: 21.1875rem, card-height-small: 21.1875rem,
card-height-medium: 28.875rem, card-height-medium: 28.875rem,
card-height-large: 36.5625rem, card-height-large: 36.5625rem,
card-height-giant: 44.25rem, card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem, card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem, card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 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-background: linear-gradient(270deg, #edf1f7 0%, #e4e9f2 100%),
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3, slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3, slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
), corporate, corporate); ),
corporate,
corporate
);
$nb-themes: nb-register-theme(( $nb-themes: nb-register-theme(
layout-padding-top: 2.25rem, (
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-tiny: 13.5rem,
card-height-small: 21.1875rem, card-height-small: 21.1875rem,
card-height-medium: 28.875rem, card-height-medium: 28.875rem,
card-height-large: 36.5625rem, card-height-large: 36.5625rem,
card-height-giant: 44.25rem, card-height-giant: 44.25rem,
card-margin-bottom: 1.875rem, card-margin-bottom: 1.875rem,
card-header-with-select-padding-top: 0.5rem, card-header-with-select-padding-top: 0.5rem,
card-header-with-select-padding-bottom: 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-background: linear-gradient(270deg, #222b45 0%, #151a30 100%),
slide-out-shadow-color: 0 4px 14px 0 #8f9bb3, slide-out-shadow-color: 0 4px 14px 0 #8f9bb3,
slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3, slide-out-shadow-color-rtl: 0 4px 14px 0 #8f9bb3,
), dark, dark); ),
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 { FamilyMembersComponent } from './family-members/family-members.component';
import { FamilyMemberEditorComponent } from './family-members/family-member-editor/family-member-editor.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 { DropDownListModule } from '../ui/drop-down-list/drop-down-list.module';
import { NgxMaskModule } from 'ngx-mask';
import { PastoralDomainsComponent } from './pastoral-domains/pastoral-domains.component'; import { PastoralDomainsComponent } from './pastoral-domains/pastoral-domains.component';
import { PastoralDomainEditorComponent } from './pastoral-domains/pastoral-domain-editor/pastoral-domain-editor.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'; 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, AlertDlgModule,
FancyTableModule, FancyTableModule,
DropDownListModule, DropDownListModule,
NgxMaskModule,
CurrencyInputModule, CurrencyInputModule,
MaskDirectiveModule, MaskDirectiveModule,
DateInputModule DateInputModule
@@ -3,14 +3,12 @@ import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme'; import { NbDialogService } from '@nebular/theme';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { inherits } from 'util';
import { CellGroupRoutineEvents } from '../../entity/CellGroupRoutineEvents'; import { CellGroupRoutineEvents } from '../../entity/CellGroupRoutineEvents';
import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service'; import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service';
import { MsgBoxService } from '../../services/msg-box.service'; import { MsgBoxService } from '../../services/msg-box.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model'; import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { FancyTableComponent } from '../../ui/fancy-table/fancy-table.component'; import { FancyTableComponent } from '../../ui/fancy-table/fancy-table.component';
import { ObjectUtils } from '../../utilities/object-utils';
@Component({ @Component({
selector: 'ngx-cell-group-routine-events', selector: 'ngx-cell-group-routine-events',
@@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme'; import { NbDialogRef } from '@nebular/theme';
import { DropDownOption } from '../../../entity/dropDownOption'; import { DropDownOption } from '../../../entity/dropDownOption';
import { DomainMemberRelationship, PastoralDomain } from '../../../entity/PastoralDomain'; import { DomainMemberRelationship, PastoralDomain } from '../../../entity/PastoralDomain';
import { DialogComponent } from '../../../pages/modal-overlays/dialog/dialog.component';
import { ArrayUtils } from '../../../utilities/array-utils'; import { ArrayUtils } from '../../../utilities/array-utils';
import { first } from "rxjs/operators" import { first } from "rxjs/operators"
import { FamilyMember } from '../../../entity/Member'; import { FamilyMember } from '../../../entity/Member';
-5
View File
@@ -17,11 +17,6 @@ export const routes: Routes = [
loadChildren: () => import('./invitation/invitation.module') loadChildren: () => import('./invitation/invitation.module')
.then(m => m.InvitationModule), .then(m => m.InvitationModule),
}, },
{
path: 'pages',
loadChildren: () => import('./pages/pages.module')
.then(m => m.PagesModule),
},
{ {
path: 'auth', path: 'auth',
component: NbAuthComponent, component: NbAuthComponent,
+4 -1
View File
@@ -70,7 +70,10 @@ const socialLinks = [
NbChatModule.forRoot({ NbChatModule.forRoot({
messageGoogleMapKey: 'AIzaSyA_wNuCzia92MAmdLRemailRGvCF7wCZPY', messageGoogleMapKey: 'AIzaSyA_wNuCzia92MAmdLRemailRGvCF7wCZPY',
}), }),
NgxMaskModule.forRoot(maskConfig), NbDialogModule.forRoot({
closeOnBackdropClick: false,
closeOnEsc: false
}),
NbDateFnsDateModule.forRoot({ format: 'MM/dd/yyyy' }), NbDateFnsDateModule.forRoot({ format: 'MM/dd/yyyy' }),
CoreModule.forRoot(), CoreModule.forRoot(),
ThemeModule.forRoot(), ThemeModule.forRoot(),
-3
View File
@@ -1,8 +1,5 @@
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { basename } from "path";
import { Observable, Subject } from "rxjs"; 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 { PastoralDomainService } from "../services/crudServices/pastoral-domain.service";
import { StateService } from "../services/state.service"; import { StateService } from "../services/state.service";
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
@@ -95,7 +95,7 @@ export class PrayerComponent extends MyAppBase {
// message += "\n======= 備註 =======" + "\n" + comment; // 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); // this.lineService.pushLineMessage(message);
}); });
} }
@@ -8,15 +8,12 @@ import { ContextMenuModule } from '../../ui/context-menu/context-menu.module';
const components = [RightClickMenuDirective,]; const components = [RightClickMenuDirective,];
@NgModule({ @NgModule({
declarations: [...components], declarations: [...components],
entryComponents: [ imports: [
ContextMenuComponent CommonModule,
], NbDialogModule,
imports: [ ContextMenuModule
CommonModule, ],
NbDialogModule, exports: [...components]
ContextMenuModule
],
exports: [...components],
}) })
export class RightClickMenuModule { } export class RightClickMenuModule { }
+1 -1
View File
@@ -20,7 +20,7 @@ export interface LoginTokenViewModel {
avatarImage: string; avatarImage: string;
role: Role; role: Role;
cellGroup: PastoralDomain; cellGroup: PastoralDomain;
signalRSessionId; signalRConnectionId;
sessionTabId: string; sessionTabId: string;
} }
+1 -1
View File
@@ -30,7 +30,7 @@ const teamSize: number[][] = [
const fourthQuestNeed2Failed = 7; const fourthQuestNeed2Failed = 7;
const SIGNAL_R_URL = (id: string = null) => { return `${environment.signalRUrl}/${id}Hub` } 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://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({ @Component({
selector: 'ngx-avalon', selector: 'ngx-avalon',
@@ -26,5 +26,5 @@
<ng-template #WaitingMessage> <ng-template #WaitingMessage>
<h1>等待遊戲開始中...</h1> <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> </ng-template>
+4
View File
@@ -5,6 +5,8 @@ import { AvalonComponent } from './avalon/avalon.component';
import { GamesComponent } from './games.component'; import { GamesComponent } from './games.component';
import { HeroDashboardComponent } from './massive-darkness2/hero-dashboard/hero-dashboard.component'; import { HeroDashboardComponent } from './massive-darkness2/hero-dashboard/hero-dashboard.component';
import { MassiveDarkness2Component } from './massive-darkness2/massive-darkness2.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 { export class GameRoomMenuConfig {
public static HostMenu: NbMenuItem[] = [ public static HostMenu: NbMenuItem[] = [
@@ -47,6 +49,8 @@ const routes: Routes = [
{ path: 'avalonHost', component: AvalonComponent }, { path: 'avalonHost', component: AvalonComponent },
{ path: 'MD2', component: MassiveDarkness2Component }, { path: 'MD2', component: MassiveDarkness2Component },
{ path: 'MD2_Hero/:roomId', component: HeroDashboardComponent }, { 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> <router-outlet></router-outlet>
</ngx-plain-layout> </ngx-plain-layout>
<div kendoDialogContainer></div>
<!-- ngx-plain-layout ngx-one-column-layout--> <!-- ngx-plain-layout ngx-one-column-layout-->
+2
View File
@@ -5,6 +5,7 @@ export interface IGamePlayer {
isPlayer: boolean; isPlayer: boolean;
signalRClientId: string; signalRClientId: string;
tabId: string; tabId: string;
isDisconnected: boolean;
} }
export class GamePlayer implements IGamePlayer { export class GamePlayer implements IGamePlayer {
@@ -14,4 +15,5 @@ export class GamePlayer implements IGamePlayer {
isPlayer: boolean; isPlayer: boolean;
signalRClientId: string; signalRClientId: string;
tabId: string; tabId: string;
isDisconnected: boolean;
} }
+50 -4
View File
@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { QRCodeModule } from 'angular2-qrcode';
import { GamesRoutingModule } from './games-routing.module'; import { GamesRoutingModule } from './games-routing.module';
import { GamesComponent } from './games.component'; import { GamesComponent } from './games.component';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@@ -32,6 +31,30 @@ import { BossActivationComponent } from './massive-darkness2/boss-fight/boss-act
import { MobAttackInfoComponent } from './massive-darkness2/mobs/mob-detail-info/mob-attack-info/mob-attack-info.component'; import { MobAttackInfoComponent } from './massive-darkness2/mobs/mob-detail-info/mob-attack-info/mob-attack-info.component';
import { MobDefInfoComponent } from './massive-darkness2/mobs/mob-detail-info/mob-def-info/mob-def-info.component'; 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 { 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({ @NgModule({
@@ -58,12 +81,26 @@ import { MobCombatInfoComponent } from './massive-darkness2/mobs/mob-detail-info
BossActivationComponent, BossActivationComponent,
MobAttackInfoComponent, MobAttackInfoComponent,
MobDefInfoComponent, MobDefInfoComponent,
MobCombatInfoComponent MobCombatInfoComponent,
MobStandInfoComponent,
MD2HtmlEditorComponent,
MD2IconPickerDlgComponent,
MD2MobInfoMaintenanceComponent,
MD2MobInfoEditorComponent,
MD2MobInfoDetailComponent,
MD2MobSkillEditorComponent,
MD2MobLevelEditorComponent,
MD2BossFightEditorComponent,
MD2PhaseBuffEditorComponent,
MD2HeroProfileMaintenanceComponent,
MD2HeroProfileEditorComponent,
GameInitDlgComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
GamesRoutingModule, GamesRoutingModule,
FormsModule, FormsModule,
ReactiveFormsModule,
AdminRoutingModule, AdminRoutingModule,
ThemeModule, ThemeModule,
NbMenuModule, NbMenuModule,
@@ -88,8 +125,17 @@ import { MobCombatInfoComponent } from './massive-darkness2/mobs/mob-detail-info
CurrencyInputModule, CurrencyInputModule,
NbDialogModule.forRoot(), NbDialogModule.forRoot(),
AlertDlgModule, AlertDlgModule,
QRCodeModule, DropDownListModule,
DropDownListModule HtmlEditorModule,
EditorModule,
KendoEditorModule,
ToolBarModule,
ButtonsModule,
GridModule,
DialogModule,
InputsModule,
DropDownsModule,
LayoutModule
] ]
}) })
export class GamesModule { } export class GamesModule { }
+141 -28
View File
@@ -2,11 +2,13 @@ import { ChangeDetectorRef, Injectable } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { first, takeUntil } from "rxjs/operators"; import { first, takeUntil } from "rxjs/operators";
import { MD2GameInfo, MD2Service } from "../../services/md2.service"; import { MD2GameInfo, MD2Service } from "../../services/MD2/md2.service";
import { SignalRMessage } from "../../services/signal-r.service"; import { SignalRMessage } from "../../services/signal-r.service";
import { StateService } from "../../services/state.service"; import { StateService } from "../../services/state.service";
import { ADIcon, MessageBoxConfig } from "../../ui/alert-dlg/alert-dlg.model"; import { ADIcon, MessageBoxConfig } from "../../ui/alert-dlg/alert-dlg.model";
import { MD2HeroInfo, MD2Icon, MobInfo, RoundPhase } from "./massive-darkness2.model"; import { MD2HeroInfo, MD2Icon, MobInfo, RoundPhase } from "./massive-darkness2.model";
import { LoginUserService } from "../../services/login-user.service";
import { GamePlayer } from "../games.model";
@Injectable() @Injectable()
export abstract class MD2Base { export abstract class MD2Base {
@@ -37,6 +39,9 @@ export abstract class MD2Base {
} }
});
this.md2Service.refreshUI$.pipe(takeUntil(this.destroy$)).subscribe(result => {
this.cdRef.detectChanges();
}); });
this.stateService.loginUserService.signalRInitialized.pipe(first()).subscribe(result => { this.stateService.loginUserService.signalRInitialized.pipe(first()).subscribe(result => {
console.log('signalRInitialized'); console.log('signalRInitialized');
@@ -78,54 +83,112 @@ export abstract class MD2Base {
} }
abstract refreshUI(); abstract refreshUI();
handleSignalRCallback(message: SignalRMessage): void { handleSignalRCallback(message: SignalRMessage): void {
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) { switch (message.actionType) {
case 'hero': case 'hero':
let heroInfo = JSON.parse(message.parameters['hero']) as MD2HeroInfo; let heroInfo = new MD2HeroInfo(JSON.parse(message.parameters['hero']));
switch (message.actionName) { switch (message.actionName) {
case 'join': case 'join':
this.md2Service.heros.push(heroInfo); this.md2Service.heros.push(heroInfo);
break; break;
case 'update': case 'update':
let exitingHero = this.md2Service.heros.find(h => h.playerInfo.signalRClientId == heroInfo.playerInfo.signalRClientId); this.updateHeroInfo(heroInfo);
if (exitingHero) {
Object.keys(heroInfo).forEach(key => exitingHero[key] = heroInfo[key]);
} else {
this.md2Service.heros.push(heroInfo);
}
if (!this.isHeroDashboard) {
if (this.gameInfo.roundPhase == RoundPhase.HeroPhase) {
if (!this.md2Service.heros.some(h => h.remainActions > 0)) {
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();
}
}
}
}
//Object.assign(heroInfo, exitingHero); //Object.assign(heroInfo, exitingHero);
break;
case 'updateMyHero':
if (this.isHeroDashboard) {
this.md2Service.playerHero = heroInfo;
}
break; break;
default: default:
break; break;
} }
this.detectChanges(); this.detectChanges();
break; 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': case 'GameRoom':
switch (message.actionName) { switch (message.actionName) {
case 'Leaving': 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(); this.detectChanges();
break; break;
case 'update': case 'update':
if (this.isHeroDashboard) { 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); 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(); this.detectChanges();
} }
break; break;
case 'phase':
if (this.isHeroDashboard) {
this.md2Service.info.roundPhase = JSON.parse(message.parameters['phase']);
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: default:
break; break;
} }
@@ -160,17 +223,21 @@ export abstract class MD2Base {
break; break;
case 'heroAction': case 'heroAction':
if (!this.isHeroDashboard) { if (!this.isHeroDashboard) {
this.gameInfo.currentActivateHero = this.md2Service.heros.find(h => h.playerInfo.tabId == message.parameters['tabId']); //this.md2Service.currentActivateHero = this.md2Service.heros.find(h => h.playerInfo.tabId == message.parameters['tabId']);
switch (message.actionName) { switch (message.actionName) {
case 'attackAction': case 'attackAction':
this.gameInfo.showAttackBtn = true; if (this.gameInfo.isBossFight) {
this.md2Service.heroAttackingSubject.next(this.md2Service.currentActivateHero);
} else {
this.gameInfo.showAttackBtn = true;
}
break; break;
case 'openDoor': case 'openDoor':
//Door component listen for it //Door component listen for it
break; break;
case 'tradeAction': case 'tradeAction':
this.md2Service.msgBoxService.show('Trade and Equip', { this.md2Service.msgBoxService.show('Trade and Equip', {
text: `every one in the <b>same zone</b> with ${this.md2Service.heroFullName(this.gameInfo.currentActivateHero)} 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!`, equip items!`,
icon: ADIcon.INFO icon: ADIcon.INFO
}); });
@@ -179,7 +246,7 @@ export abstract class MD2Base {
//this.md2Service.roundPhase = Number.parseInt(message.parameters['phase']); //this.md2Service.roundPhase = Number.parseInt(message.parameters['phase']);
break; break;
} }
this.heroAction(this.gameInfo.currentActivateHero, message.actionName); this.heroAction(this.md2Service.currentActivateHero, message.actionName);
this.detectChanges(); this.detectChanges();
} }
break; break;
@@ -187,6 +254,41 @@ export abstract class MD2Base {
break; 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); abstract heroAction(hero: MD2HeroInfo, action: string);
} }
@@ -227,4 +329,15 @@ export abstract class MD2ComponentBase {
iconHtml(icon: MD2Icon, cssClass = '') { iconHtml(icon: MD2Icon, cssClass = '') {
return this.md2Service.iconHtml(icon, cssClass); return this.md2Service.iconHtml(icon, cssClass);
} }
detectChanges() {
if (!this.cdRef['destroyed']) {
this.cdRef.detectChanges();
this.refreshUI();
this.md2Service.refreshUI$.next();
}
}
refreshUI() {
}
} }
@@ -1,21 +1,31 @@
<nb-card> <nb-card>
<nb-card-body> <nb-card-body class="g-overflow-hidden">
<div class="row form-group" style=" <div class="row form-group">
height: 53vh; <div class="col-md-5 g-height-700px">
overflow: auto; <md2-mob-stand-info [mob]="boss" [mode]="mode"></md2-mob-stand-info>
">
<div class="col-md-5">
<img src="{{boss.standUrl}}" class="img-fluid">
</div> </div>
<div class="col-md-7"> <div class="col-md-7">
<label class="MD2text g-font-size-40 mt-5" [innerHtml]="bossAction.skillName"> <label class="MD2text g-font-size-40 mt-4" [innerHtml]="bossAction.name">
</label> </label>
<label class="g-font-size-20 mt-3" [innerHtml]="bossAction.skillDescription"> <label class="mt-2 g-font-size-20 my-3 MD2IconContainer-lg" [innerHtml]="bossAction.description">
</label> </label>
<hr>
<div class="row">
<div class="col-md-4">
<md2-mob-attack-info [mob]="boss"></md2-mob-attack-info>
</div>
<div class="col-md-8 MD2IconContainer-lg">
<md2-mob-combat-info [mob]="boss"></md2-mob-combat-info>
</div>
</div>
</div> </div>
</div> </div>
</nb-card-body> </nb-card-body>
@@ -1,25 +1,29 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { NbDialogRef } from '@nebular/theme'; import { NbDialogRef } from '@nebular/theme';
import { MD2Service } from '../../../../services/md2.service'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { MD2Service } from '../../../../services/MD2/md2.service';
import { MsgBoxService } from '../../../../services/msg-box.service'; import { MsgBoxService } from '../../../../services/msg-box.service';
import { StateService } from '../../../../services/state.service'; import { StateService } from '../../../../services/state.service';
import { MobDlgType, MD2Icon, MD2HeroInfo } from '../../massive-darkness2.model'; import { MobDlgType, MD2Icon, MD2HeroInfo, RoundPhase, MobInfo } from '../../massive-darkness2.model';
import { MobSkill, IBossFight } from '../../massive-darkness2.model.boss'; import { MobSkill, IBossFight } from '../../massive-darkness2.model.boss';
import { MD2ComponentBase } from '../../MD2Base'; import { MD2ComponentBase } from '../../MD2Base';
import { SpawnMobDlgComponent } from '../../mobs/spawn-mob-dlg/spawn-mob-dlg.component'; import { SpawnMobDlgComponent } from '../../mobs/spawn-mob-dlg/spawn-mob-dlg.component';
import { MD2MobInfo, MD2MobSkill } from '../../massive-darkness2.db.model';
@Component({ @Component({
selector: 'ngx-boss-activation', selector: 'ngx-boss-activation',
templateUrl: './boss-activation.component.html', templateUrl: './boss-activation.component.html',
styleUrls: ['./boss-activation.component.scss'] styleUrls: ['./boss-activation.component.scss']
}) })
export class BossActivationComponent extends MD2ComponentBase implements OnInit { export class BossActivationComponent implements OnInit {
boss: IBossFight; boss: MobInfo;
bossAction: MobSkill; bossAction: MD2MobSkill;
currentAction: number;
allActions: number;
MobDlgType = MobDlgType; MobDlgType = MobDlgType;
mode: MobDlgType; mode: MobDlgType = MobDlgType.Activating;
title: string; title: string;
titleHtml: string; titleHtml: string;
@@ -31,16 +35,29 @@ export class BossActivationComponent extends MD2ComponentBase implements OnInit
otherAttackTarget: string; otherAttackTarget: string;
constructor( constructor(
private dlgRef: NbDialogRef<SpawnMobDlgComponent>, private dlgRef: NbDialogRef<SpawnMobDlgComponent>,
private msgBoxService: MsgBoxService,
public md2Service: MD2Service, public md2Service: MD2Service,
protected stateService: StateService, protected stateService: StateService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef, protected cdRef: ChangeDetectorRef,
) { ) {
super(md2Service, stateService, route, cdRef); this.md2Service.refreshUI$.pipe(takeUntil(this.destroy$)).subscribe(result => {
if (!this.cdRef['destroyed']) {
this.cdRef.detectChanges();
}
});
}
private destroy$: Subject<void> = new Subject<void>();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngOnInit(): void {
} }
close() { close() {
this.boss.standUrl //this.boss.standUrl
this.md2Service.info.roundPhase = RoundPhase.HeroPhase;
this.dlgRef.close(); this.dlgRef.close();
} }
@@ -1,26 +1,53 @@
<nb-card> <nb-card>
<nb-card-header> <nb-card-header class="MD2text g-font-size-28">
{{boss.name}} {{boss.name}}
<button nbButton hero status="primary" (click)="activate()">Action</button> <button nbButton hero status="primary" (click)="activate()">Action</button>
</nb-card-header> </nb-card-header>
<nb-card-body> <nb-card-body class="g-overflow-hidden">
<div class="row"> <div class="row">
<div class="col-md-5"> <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>
<div class="col-md-7"> <div class="col-md-7">
<md2-mob-detail-info [mob]="boss.info"> <div class="row">
</md2-mob-detail-info> <div class="col-md">
<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">
</md2-mob-attack-info>
<md2-mob-def-info [mob]="boss"></md2-mob-def-info>
</div>
<div class="col-md-9 bossSpecialRules" *ngIf="boss.bossFightProfile.specialRules">
<div [innerHtml]="boss.bossFightProfile.specialRules"></div>
</div>
<adj-number-input name="mob{{boss.info.name}}" [(ngModel)]="boss.info.unitRemainHp" minimum="0" </div>
title="Boss HP" (hitMinimum)="WIN()">
</adj-number-input>
<button nbButton hero status="danger" size="small" (click)="attack(boss.info)">Attack It</button>
<label class="MD2Text mt-3" [innerHtml]="boss.combatInfo.skillName"> <md2-mob-combat-info [mob]="boss"></md2-mob-combat-info>
<!--
<button nbButton hero status="danger" size="small" (click)="attack(boss)">Attack It</button> -->
<!-- <label class="MD2Text mt-3" [innerHtml]="boss.combatSkill.skillName">
</label> </label>
<label class="MD2Text" [innerHtml]="boss.combatInfo.skillDescription"> <label class="MD2Text" [innerHtml]="boss.combatInfo.skillDescription">
</label> </label> -->
</div> </div>
</div> </div>
@@ -2,3 +2,136 @@ nb-card {
height: 80vh; height: 80vh;
//width: 80vw; //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%);
}
}
@@ -2,8 +2,8 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme'; import { NbDialogService } from '@nebular/theme';
import { Subject, Observable } from 'rxjs'; import { Subject, Observable } from 'rxjs';
import { first } from 'rxjs/operators'; import { first, takeUntil } from 'rxjs/operators';
import { MD2Service } from '../../../services/md2.service'; import { MD2Service } from '../../../services/MD2/md2.service';
import { MsgBoxService } from '../../../services/msg-box.service'; import { MsgBoxService } from '../../../services/msg-box.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.model'; import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
@@ -17,8 +17,8 @@ import { SpawnMobDlgComponent } from '../mobs/spawn-mob-dlg/spawn-mob-dlg.compon
styleUrls: ['./boss-fight.component.scss'] styleUrls: ['./boss-fight.component.scss']
}) })
export class BossFightComponent extends MD2ComponentBase { export class BossFightComponent extends MD2ComponentBase {
MobDlgType = MobDlgType;
MD2Icon = MD2Icon;
public get boss() { public get boss() {
return this.md2Service.info.boss; return this.md2Service.info.boss;
} }
@@ -37,13 +37,26 @@ export class BossFightComponent extends MD2ComponentBase {
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.md2Service.heroAttackingSubject.pipe(takeUntil(this.destroy$)).subscribe(result => {
if (this.md2Service.info.isBossFight) {
this.attack(this.boss);
}
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
} }
activate() { activate() {
this.boss.activating(); this.md2Service.activateBoss();
} }
WIN() { 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) { attack(mob: MobInfo) {
@@ -52,7 +65,10 @@ export class BossFightComponent extends MD2ComponentBase {
if (mobResult) { if (mobResult) {
let attackDamage = mobResult.uiWounds; let attackDamage = mobResult.uiWounds;
if (attackDamage) { if (attackDamage) {
this.boss.info.hp -= attackDamage; this.boss.unitRemainHp -= attackDamage;
if (this.boss.unitRemainHp <= 0) {
this.WIN();
}
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
} }
@@ -5,7 +5,7 @@ import { ADButtons } from '../../../ui/alert-dlg/alert-dlg.model';
import { DrawingBag, DrawingItem } from '../massive-darkness2.model'; import { DrawingBag, DrawingItem } from '../massive-darkness2.model';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { MD2Service } from '../../../services/md2.service'; import { MD2Service } from '../../../services/MD2/md2.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { MD2Base, MD2ComponentBase } from '../MD2Base'; import { MD2Base, MD2ComponentBase } from '../MD2Base';
import { SignalRMessage } from '../../../services/signal-r.service'; import { SignalRMessage } from '../../../services/signal-r.service';
@@ -58,7 +58,7 @@ export class DoorEventsComponent extends MD2ComponentBase implements OnInit {
this.msgBoxService.show('', { text: `<img src="${door.imageUrl}" class="g-height-70vh g-max-width-80vw">`, buttons: ADButtons.YesNo, confirmButtonText: 'Keep It', cancelButtonText: 'Discard' }) this.msgBoxService.show('', { text: `<img src="${door.imageUrl}" class="g-height-70vh g-max-width-80vw">`, buttons: ADButtons.YesNo, confirmButtonText: 'Keep It', cancelButtonText: 'Discard' })
.pipe(first()).subscribe(result => { .pipe(first()).subscribe(result => {
if (result) { if (result) {
door.name = this.md2Service.heroFullName(this.md2Service.info.currentActivateHero); door.name = this.md2Service.heroFullName(this.md2Service.currentActivateHero);
this.drawDoorEvents.push(door); this.drawDoorEvents.push(door);
} }
this.cdRef.detectChanges(); this.cdRef.detectChanges();
@@ -0,0 +1,21 @@
import { ObjectUtils } from "../../../utilities/object-utils";
import { IDrawingItem, MobInfo, TreasureItem } from "../massive-darkness2.model";
export class MD2Clone {
public static CloneDrawingItem(obj: IDrawingItem) {
let type = obj.constructor.name;
let cloneObj = null;
switch (type) {
case "TreasureItem":
//let copy = structuredClone(obj);
return new TreasureItem(obj['type'], 1);
break;
case "MobInfo":
return new MobInfo(obj);
break;
default: break;
}
return ObjectUtils.CloneValue(obj);
}
}
@@ -0,0 +1,303 @@
import { Subject } from "rxjs";
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, 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)}` : '')}` }
export abstract class CoreGameRMFactory 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({
type: MobType.RoamingMonster,
name: mobName, hpPerHero: levelInfo.hp, level: level, rewardTokens: levelInfo.rewardTokens,
attackInfos: levelInfo.attackInfos,
defenseInfo: levelInfo.defenseInfo,
actionSubject: new Subject<string>()
});
this.mob.leaderImgUrl = MD2_IMG_URL(`/CoreGame/RoamingMonsters/${this.mob.name}/Stand.png`);
//this.mob.minionImgUrl = MD2_IMG_URL(`CoreGame/Mobs/${encodeURI(this.mob.name)}/Minion.png`);
if (level < 3) {
this.mob.rewardTokens = 2;
this.mob.fixedCarriedTreasure = [new TreasureItem(TreasureType.Rare)];
} else if (level < 5) {
this.mob.rewardTokens = 2;
this.mob.fixedCarriedTreasure = [new TreasureItem(TreasureType.Epic)];
} else {
this.mob.rewardTokens = 0;
this.mob.fixedCarriedTreasure = [new TreasureItem(TreasureType.Epic, 3)];
}
}
iconHtml(icon: MD2Icon, cssClass = '') {
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 RMUndeadQueenFactory extends CoreGameRMFactory {
mobName: string = 'Ytheria, Undead Queen';
generate(level: number): MobInfo {
this.loadLevelInfo('Ytheria, Undead Queen', level);
this.mob.activateFunction = (mob, msgBoxService, heros) => {
let actionResult = '';
msgBoxService.show('Is There more than 1 Hero in LoS of Undead Queen?', {
icon: ADIcon.QUESTION,
buttons: ADButtons.YesNo
}).pipe(first()).subscribe(result => {
if (result) {
mob.actions = 0;
mob.actionSubject.next(
`Undead Queen attacks each Hero in LoS(resolve each attack separately).`
);
} else {
msgBoxService.show('Is There any Hero in LoS of Undead Queen?', {
icon: ADIcon.QUESTION,
buttons: ADButtons.YesNo
}).pipe(first()).subscribe(result => {
if (result) {
if (level < 3) {
actionResult = `Undead Queen +1 ${this.iconHtml(MD2Icon.YellowDice)} when attack`;
} else if (level < 5) {
actionResult = `Undead Queen +1 ${this.iconHtml(MD2Icon.YellowDice)} 1 ${this.iconHtml(MD2Icon.OrangeDice)} when attack`;
} else {
actionResult = `Undead Queen +2 ${this.iconHtml(MD2Icon.OrangeDice)} when attack`;
}
mob.actions = 0;
mob.actionSubject.next(
actionResult
);
} else {
mob.actions = 2;
mob.actionSubject.next(
`Undead Queen Gains 2 Actions`
);
}
});
}
});
}
this.mob.skills = [
{
description: `Add 1 Minion to each Mob in the Dungeon, if possible.`,
type: MobSkillType.Attack,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
export class RMAndraFactory extends CoreGameRMFactory {
mobName: string = 'Andra';
generate(level: number): MobInfo {
this.loadLevelInfo('Andra', level);
let damage = 2;
if (level < 3) {
damage = 1;
}
this.mob.activateFunction = (mob, msgBoxService, heros) => {
let actionResult = '';
mob.actions = 0;
msgBoxService.show('Is Andra in the Dungeon?', {
icon: ADIcon.QUESTION,
buttons: ADButtons.YesNo
}).pipe(first()).subscribe(result => {
if (result) {
mob.actions = 0;
msgBoxService.show('Is Any Hero in the LoS of Andra?', {
icon: ADIcon.QUESTION,
buttons: ADButtons.YesNo
}).pipe(first()).subscribe(result => {
if (result) {
mob.actionSubject.next(
`Andra attack the Hero with the lowest HP in LoS.<br>then Put Andra to out side of Dungeon.(Hero is not reachable but not dead)`
);
} else {
mob.actions = 0;
mob.actionSubject.next(
`Put Andra to out side of Dungeon.(Hero is not reachable but not dead)`
);
}
});
} else {
let beenAttackHero = MD2Logic.getTargetHerosByFilter(heros, AttackTarget.LeastHp, true)[0];
mob.actionSubject.next(
`Place Andra in the same zone of ${MD2Logic.heroFullName(beenAttackHero)} and attack that Hero.`
);
}
});
}
this.mob.skills = [
{
description: `Deal ${damage} wound to another Hero with the lowest HP in LoS`,
type: MobSkillType.Combat,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
export class RMTheGhoulFactory extends CoreGameRMFactory {
mobName: string = 'The Ghoul';
generate(level: number): MobInfo {
this.loadLevelInfo('The Ghoul', level);
let health = 2;
if (level < 3) {
health = 5;
} else if (level < 5) {
health = 8;
} else {
health = 10;
}
this.mob.activateFunction = (mob, msgBoxService, heros) => {
let actionResult = '';
mob.actions = 0;
msgBoxService.show('Is there any <b>Mob with minion</b> in The Ghoul zone?', {
icon: ADIcon.QUESTION,
buttons: ADButtons.YesNo
}).pipe(first()).subscribe(result => {
if (result) {
mob.unitRemainHp += health;
mob.actionSubject.next(
`Kill 1 minion in The Ghoul zone(player choose), The Ghoul heals ${health}.`
);
} else {
mob.actionSubject.next(
`The Ghoul moves 3 Zones toward the closest Hero and attack him/her, if possible.`
);
}
});
}
this.mob.skills = [
{
description: `Move the closest <b>Mob with minion</b> 1 Zone toward The Ghoul.`,
type: MobSkillType.Combat,
skillRoll: 1
} as MD2MobSkill];
return this.mob;
}
}
export class RMLyidanIncubusLordFactory extends CoreGameRMFactory {
mobName: string = 'Lyidan, Incubus Lord';
generate(level: number): MobInfo {
this.loadLevelInfo('Lyidan, Incubus Lord', level);
this.mob.activateFunction = (mob, msgBoxService, heros) => {
let actionResult = '';
mob.actions = 0;
msgBoxService.show('Is Incubus Lord in a Light Zone?', {
icon: ADIcon.QUESTION,
buttons: ADButtons.YesNo
}).pipe(first()).subscribe(result => {
if (result) {
mob.unitRemainHp -= 3;
mob.actionSubject.next(
`The Incubus Lord got 3 Wounds, Move it to the closest Shadow Zone.`
);
} else {
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 => {
if (result) {
mob.actionSubject.next(
`Place The Incubus Lord in the zone of furthest Hero up to 3 Zones away.<br>` +
`Add 1 ${this.iconHtml(MD2Icon.Fire)} to that Hero and attack him/her.`
);
} else {
mob.actions = 2;
mob.actionSubject.next(
`The Incubus Lord 2 Actions`
);
}
});
}
});
}
this.mob.skills = [
{
description: `After combat, resolve all ${this.iconHtml(MD2Icon.Fire)} on the defending Hero(once per combat).`,
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;
}
}
export const CoreGameRMFactories = [
new RMUndeadQueenFactory(),
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,36 +1,218 @@
<nb-card *ngIf="!md2Service.playerHero"> <!-- Hero Selection Screen - Initial -->
<nb-card-body> <nb-card *ngIf="!hero && !isSelectingHero" class="hero-selection-card">
<button nbButton hero status="primary" fullWidth (click)="initHero()">Choose Hero</button> <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-body>
</nb-card> </nb-card>
<div *ngIf="md2Service.playerHero">
<!-- 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="row no-gutters">
<div class="col-12 col-sm-7"> <div class="col-12 col-sm-7">
<div class="tp-wrapper mb-2"> <div class="tp-wrapper mb-2">
<div class="tp-box g-height-300 g-height-350--sm g-height-500--md" (click)="toggleFlip()" <div class="tp-box" [@flipState]="flip">
[@flipState]="flip">
<div class="tp-box__side tp-box__front "> <div class="tp-box__side tp-box__front ">
<img class="MD2HeroCard " src="{{md2Service.playerHero.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>
<div class="tp-box__side tp-box__back"> <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')}}">
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="g-max-height-80vh mb-2"> <!-- <div class="g-max-height-80vh mb-2">
<img class="MD2HeroCard" src="{{md2Service.playerHero.imgUrl}}"> <img class="MD2HeroCard" src="{{hero.imgUrl}}">
<div class="MD2HeroCard"> <div class="MD2HeroCard">
<span class="MD2text MD2Name">{{md2Service.playerHero.name}}</span> <span class="MD2text MD2Name">{{hero.name}}</span>
<span class="MD2text MD2Hp">{{md2Service.playerHero.hpMaximum}}</span> <span class="MD2text MD2Hp">{{hero.hpMaximum}}</span>
<span class="MD2text MD2Mp">{{md2Service.playerHero.mpMaximum}}</span> <span class="MD2text MD2Mp">{{hero.mpMaximum}}</span>
</div> </div>
<img class="MD2HeroCard" src="{{md2Service.playerHero.imgUrl}}"> <img class="MD2HeroCard" src="{{hero.imgUrl}}">
<img class="MD2HeroCard HpMpBar" src="{{imgUrl('/Heros/Template/Border.png')}}"> <img class="MD2HeroCard HpMpBar" src="{{imgUrl('/Heros/Template/Border.png')}}">
</div> --> </div> -->
</div> </div>
<div class="col-12 col-sm-5"> <div class="col-12 col-sm-5">
@@ -39,78 +221,79 @@
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-6"> <div class="col-6">
<!-- <adj-number-input name="heroHP" [(ngModel)]="md2Service.playerHero.hp" <adj-number-input name="heroHP" [(ngModel)]="hero.hp" [maximum]="hero.hpMaximum" minimum="0"
[maximum]="md2Service.playerHero.hpMaximum" minimum="0" title="{{imgHtml('HpIcon.png','g-height-25')}}" showMaximum
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)]="md2Service.playerHero.hp"
[maximum]="md2Service.playerHero.hpMaximum" minimum="0"
title="{{imgHtml('HpIcon.png','g-height-25 mr-1')}}HP" showMaximum
(blur)="heroUpdateDebounceTimer.resetTimer()" (hitDecreasing)="increaseRage()"> (blur)="heroUpdateDebounceTimer.resetTimer()" (hitDecreasing)="increaseRage()">
</adj-number-input> </adj-number-input>
<adj-number-input name="heroMana" [(ngModel)]="md2Service.playerHero.mp" </div>
[maximum]="md2Service.playerHero.mpMaximum" minimum="0" <div class="col-6">
title="{{imgHtml('HeroIcon.png','g-height-25 mr-1')}}Mana" showMaximum
<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')}}" showMaximum
(blur)="heroUpdateDebounceTimer.resetTimer()"> (blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input> </adj-number-input>
<adj-number-input name="heroFire" [(ngModel)]="md2Service.playerHero.fireToken" minimum="0" </div>
title="{{iconHtml(MD2Icon.Fire,'g-color-google-plus mr-1 g-font-size-18')}}Fire Token" <div class="col-6">
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input> <adj-number-input name="heroExp" [(ngModel)]="hero.exp" minimum="0" title="Exp"
<adj-number-input name="heroFire" [(ngModel)]="md2Service.playerHero.frozenToken"
minimum="0"
title="{{iconHtml(MD2Icon.Frost,'g-color-aqua mr-1 g-font-size-18')}}Frozen Token"
(blur)="heroUpdateDebounceTimer.resetTimer()"> (blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input> </adj-number-input>
</div> </div>
<div class="col-6"> <div class="col-6">
<adj-number-input name="remainActions" [(ngModel)]="md2Service.playerHero.remainActions" <adj-number-input name="heroFire" [(ngModel)]="hero.fireToken" minimum="0"
minimum="0" title="Remain Actions" (blur)="heroUpdateDebounceTimer.resetTimer()" title="{{iconHtml(MD2Icon.Fire,'g-color-google-plus mr-1 g-font-size-18')}}Fire Token"
hideIncreaseBtn> (blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input> </adj-number-input>
<adj-number-input name="heroLevel" [(ngModel)]="md2Service.playerHero.level" minimum="1" </div>
maximum="5" title="Level" (blur)="heroUpdateDebounceTimer.resetTimer()"> <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> </adj-number-input>
<adj-number-input name="heroExp" [(ngModel)]="md2Service.playerHero.exp" minimum="0" </div>
title="Exp" (blur)="heroUpdateDebounceTimer.resetTimer()">
<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>
<adj-number-input name="heroRage" [(ngModel)]="md2Service.playerHero.rage" minimum="0" </div>
maximum="7" <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" title="{{iconHtml(MD2Icon.Rage,'g-color-google-plus mr-1 g-font-size-18')}}Rage"
(blur)="heroUpdateDebounceTimer.resetTimer()" (blur)="heroUpdateDebounceTimer.resetTimer()">
*ngIf="md2Service.playerHero.class==HeroClass.Berserker">
</adj-number-input>
<adj-number-input name="heroCorruption" [(ngModel)]="md2Service.playerHero.corruptionToken"
minimum="0" title="{{imgHtml('Tokens/CorruptToken.png','g-height-18')}} Corruption"
(blur)="heroUpdateDebounceTimer.resetTimer()"
*ngIf="md2Service.playerHero.corruptionToken>0">
</adj-number-input> </adj-number-input>
</div> </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>
<div *ngIf="md2Service.info.isBossFight"></div> <div *ngIf="md2Service.info.isBossFight"></div>
<div *ngIf="md2Service.playerHero.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 status="info" class="mt-2" (click)="openDoor()" *ngIf="showMoveAction">Open
Door</button>
</nb-card-body> </nb-card-body>
</nb-card> </nb-card>
@@ -121,7 +304,7 @@
</div> </div>
</div> </div>
<!-- <nb-flip-card *ngIf="md2Service.playerHero"> <!-- <nb-flip-card *ngIf="hero">
<nb-card-front> <nb-card-front>
<nb-card> <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 { .MD2Hp {
font-size: xx-large; font-size: xx-large;
position: fixed; position: fixed;
@@ -36,45 +400,416 @@
.tp-wrapper { .tp-wrapper {
-webkit-perspective: 800px; -webkit-perspective: 800px;
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 { .tp-box {
position: relative; position: relative;
//width: 200px; width: 100%;
//height: 100px; height: 100%;
//margin: 3rem auto;
-webkit-transform-style: preserve-3d; -webkit-transform-style: preserve-3d;
transform-style: preserve-3d; transform-style: preserve-3d;
-webkit-transform: transform 1s; -webkit-transform: transform 1s;
-ms-transform: transform 1s; -ms-transform: transform 1s;
transform: transform 1s; transform: transform 1s;
} }
.tp-box__side { .tp-box__side {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: absolute; position: absolute;
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
backface-visibility: hidden; backface-visibility: hidden;
color: #fff;
text-align: center;
line-height: 100px;
font-size: 24px;
font-weight: 700;
cursor: pointer; cursor: pointer;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
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 { .tp-box__front {
-webkit-transform: rotateY(0deg); -webkit-transform: rotateY(0deg);
-ms-transform: rotateY(0deg); -ms-transform: rotateY(0deg);
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 { .tp-box__back {
-webkit-transform: rotateY(-180deg); -webkit-transform: rotateY(-180deg);
-ms-transform: rotateY(-180deg); -ms-transform: rotateY(-180deg);
transform: rotateY(-180deg); transform: rotateY(-180deg);
} }
::ng-deep .skill-content .MD2Icon {
font-size: 30px;
}
@@ -4,14 +4,18 @@ import { ActivatedRoute } from '@angular/router';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption'; import { DropDownOption } from '../../../entity/dropDownOption';
import { GameRoomService } from '../../../services/game-room.service'; import { GameRoomService } from '../../../services/game-room.service';
import { MD2Service } from '../../../services/md2.service'; import { MD2Service } from '../../../services/MD2/md2.service';
import { MsgBoxService } from '../../../services/msg-box.service'; import { MsgBoxService } from '../../../services/msg-box.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { ADButtonColor, ADButtons } from '../../../ui/alert-dlg/alert-dlg.model'; import { ADButtonColor, ADButtons } from '../../../ui/alert-dlg/alert-dlg.model';
import { ArrayUtils } from '../../../utilities/array-utils';
import { StringUtils } from '../../../utilities/string-utils'; import { StringUtils } from '../../../utilities/string-utils';
import { DebounceTimer } from '../../../utilities/timer-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 { MD2Base } from '../MD2Base';
import { MD2HeroProfileService } from '../service/massive-darkness2.service';
import { SignalRService } from '../../../services/signal-r.service';
import { NbToastrService } from '@nebular/theme';
@Component({ @Component({
selector: 'ngx-hero-dashboard', selector: 'ngx-hero-dashboard',
@@ -32,6 +36,7 @@ import { MD2Base } from '../MD2Base';
] ]
}) })
export class HeroDashboardComponent extends MD2Base implements OnInit { export class HeroDashboardComponent extends MD2Base implements OnInit {
MD2Icon = MD2Icon;
heroAction(hero: MD2HeroInfo, action: string) { heroAction(hero: MD2HeroInfo, action: string) {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
@@ -42,7 +47,9 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
} }
heroUpdateDebounceTimer = new DebounceTimer(1000, () => { this.broadcastHeroInfo(); }) heroUpdateDebounceTimer = new DebounceTimer(1000, () => {
this.broadcastHeroInfo();
})
classOptions: DropDownOption[] = [ classOptions: DropDownOption[] = [
new DropDownOption(HeroClass.Berserker, 'Berserker'), new DropDownOption(HeroClass.Berserker, 'Berserker'),
@@ -51,36 +58,66 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
new DropDownOption(HeroClass.Rogue, 'Rogue'), new DropDownOption(HeroClass.Rogue, 'Rogue'),
new DropDownOption(HeroClass.Wizard, 'Wizard'), new DropDownOption(HeroClass.Wizard, 'Wizard'),
new DropDownOption(HeroClass.Shaman, 'Shaman'), 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[]; heros = [] as MD2HeroInfo[];
wizards: MD2HeroInfo[] = [ heroProfiles: MD2HeroProfile[] = [];
new MD2HeroInfo({ name: 'Ajax', mpMaximum: 6, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }), currentHeroIndex: number = 0;
new MD2HeroInfo({ name: 'Baldric', mpMaximum: 5, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }), isSelectingHero: boolean = false;
new MD2HeroInfo({ name: 'Ego', mpMaximum: 5, hpMaximum: 6, skillHtml: '', shadowSkillHtml: '' }), selectedHeroClass: HeroClass;
new MD2HeroInfo({ name: 'Elias', mpMaximum: 6, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Megan', mpMaximum: 5, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }), public get hero() {
new MD2HeroInfo({ name: 'Moira', mpMaximum: 6, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }), return this.md2Service.playerHero;
new MD2HeroInfo({ name: 'Myriam', mpMaximum: 7, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }), }
new MD2HeroInfo({ name: 'Valdis', mpMaximum: 6, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' })
] 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( constructor(
private gameRoomService: GameRoomService, private gameRoomService: GameRoomService,
public md2Service: MD2Service, public md2Service: MD2Service,
private heroProfileService: MD2HeroProfileService,
protected stateService: StateService, protected stateService: StateService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef, protected cdRef: ChangeDetectorRef,
private msgBoxService: MsgBoxService, private msgBoxService: MsgBoxService,
private signalRService: SignalRService,
private toastrService: NbToastrService
) { ) {
super(md2Service, stateService, route, cdRef); super(md2Service, stateService, route, cdRef);
this.isHeroDashboard = true; this.isHeroDashboard = true;
} }
public get allowAttack(): boolean { public get allowAttack(): boolean {
return (!!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 { ngOnInit(): void {
super.ngOnInit(); 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() { override signalRInitialized() {
@@ -91,9 +128,8 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
// } // }
} }
initHero() { initHero() {
this.gameRoomService.gameRoomId = this.roomId; if (!this.md2Service.heros.some(h => h.playerInfo.signalRClientId == this.stateService.loginUserService.userAccess.signalRConnectionId)) {
this.gameRoomService.joinGameRoom(this.roomId);
if (!this.md2Service.heros.some(h => h.playerInfo.signalRClientId == this.stateService.loginUserService.userAccess.signalRSessionId)) {
this.msgBoxService.showInputbox('Select Hero Class', '', { dropDownOptions: this.classOptions, inputType: 'dropdown' }) this.msgBoxService.showInputbox('Select Hero Class', '', { dropDownOptions: this.classOptions, inputType: 'dropdown' })
.pipe(first()).subscribe(heroClass => { .pipe(first()).subscribe(heroClass => {
if (heroClass != null) { if (heroClass != null) {
@@ -114,46 +150,71 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
}); });
} }
} }
className: string;
initClassHeroList(heroClass: HeroClass) { initClassHeroList(heroClass: HeroClass) {
this.heros = []; this.heros = [];
this.className = HeroClass[heroClass]; this.selectedHeroClass = heroClass;
this.fileList(`Heros/${this.className}`).pipe(first()).subscribe(fileNames => { this.heroProfileService.getAll().pipe(first()).subscribe(result => {
for (let i = 0; i < fileNames.length; i++) {
const heroNames = fileNames[i].split('.')[0].split('-');
this.heros.push(new MD2HeroInfo({ this.heroProfiles = result.filter(h => h.heroClass == heroClass);
name: heroNames[0].replace('/', ''), for (let i = 0; i < this.heroProfiles.length; i++) {
mpMaximum: Number.parseInt(heroNames[1]), const heroProfile = this.heroProfiles[i];
hpMaximum: Number.parseInt(heroNames[2]), const heroInfo = new MD2HeroInfo({
imgUrl: this.imgUrl(`Heros/${this.className}/${fileNames[i]}`), 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 class: heroClass
})) });
heroInfo.imgUrl = this.imgUrl('Heros/' + HeroClass[heroClass] + '.jpg');
this.heros.push(heroInfo);
} }
this.heros = this.heros.sort((a, b) => StringUtils.compareSemVer(a.name, b.name)); 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]; selectCurrentHero() {
let heroInfo = this.heros[index]; if (this.currentSelectingHero) {
this.msgBoxService.show(`${className}(${index + 1}/${this.heros.length})`, { this.md2Service.playerJoin(this.currentSelectingHero);
text: `<img src='${heroInfo.imgUrl}' class="g-width-50vw-md g-width-80vw">`, this.md2Service.broadcastMyHeroInfo();
buttons: ADButtons.YesNo, this.isSelectingHero = false;
cardWidthClass: '', this.detectChanges();
confirmButtonText: 'It\'s Me!', }
cancelButtonText: 'Next', }
cancelButtonColor: ADButtonColor.INFO showSkills(type: string) {
}).pipe(first()).subscribe(result => { if (type == 'abilities') {
if (result) { this.msgBoxService.show('Abilities', { text: this.currentSelectingHero.skillHtml });
this.md2Service.playerJoin(heroInfo); } else {
this.detectChanges(); this.msgBoxService.show('Shadow Abilities', { text: this.currentSelectingHero.shadowSkillHtml });
} else { }
index++; }
if (index == this.heros.length) index = 0; nextHero() {
this.showHeroList(heroClass, index); 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() { broadcastHeroInfo() {
@@ -161,13 +222,13 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
this.heroUpdateDebounceTimer.clearOut(); this.heroUpdateDebounceTimer.clearOut();
} }
increaseRage() { increaseRage() {
if (this.md2Service.playerHero.rage < 7) { if (this.hero.rage < 7) {
this.md2Service.playerHero.rage++; this.hero.rage++;
} }
} }
openDoor() { openDoor() {
this.md2Service.broadcastHeroAction('openDoor'); this.md2Service.broadcastHeroAction('openDoor');
this.showMoveAction = false; //this.showMoveAction = false;
this.detectChanges(); this.detectChanges();
} }
moveAction() { moveAction() {
@@ -183,9 +244,11 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
this.showMoveAction = false; this.showMoveAction = false;
switch (action) { switch (action) {
case 'recoveryAction': case 'recoveryAction':
this.msgBoxService.show('Recovery', { text: 'takes the Recover action may gain up to 2 Health or Mana in any combination (either 2 Health, 2 Mana, or 1 of each).' }) this.msgBoxService.show('Recovery', { text: 'takes the Recover action may gain up to 2 Health or Mana in any combination (either 2 Health, 2 Mana, or 1 of each).' });
break;
case 'attackAction':
this.msgBoxService.show('Attacking', { text: 'Please process attacking action in Dashboard.' });
break; break;
default: default:
break; break;
} }
@@ -193,7 +256,7 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
this.reduceAction(); this.reduceAction();
} }
reduceAction() { reduceAction() {
this.md2Service.playerHero.remainActions -= 1; this.hero.remainActions -= 1;
this.detectChanges(); this.detectChanges();
this.broadcastHeroInfo(); this.broadcastHeroInfo();
} }
@@ -202,4 +265,43 @@ export class HeroDashboardComponent extends MD2Base implements OnInit {
toggleFlip() { toggleFlip() {
this.flip = (this.flip == 'inactive') ? 'active' : 'inactive'; this.flip = (this.flip == 'inactive') ? 'active' : 'inactive';
} }
get allowStartAction() {
return !this.md2Service.heros.some(h => h.uiActivating) && !this.hero.uiActivating && this.hero.remainActions > 0;
}
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() {
if (this.hero.remainActions > 0) {
this.msgBoxService.show('Are you sure?', { text: `End Activation will lose ${this.hero.remainActions} remaining actions.`, buttons: ADButtons.YesNo }).pipe(first()).subscribe(result => {
if (result) {
this.hero.remainActions = 0;
this.endActivation();
}
});
} else {
this.hero.uiActivating = false;
this.broadcastHeroInfo();
this.detectChanges();
}
}
} }
@@ -10,21 +10,28 @@
</nb-accordion-item-body> </nb-accordion-item-body>
</nb-accordion-item> </nb-accordion-item>
</nb-accordion> </nb-accordion>
</div> --> </div>
<div class="col-12">
</div>
-->
<div class="col-12 col-md-5"> <div class="col-12 col-md-5">
<nb-card> <nb-card>
<nb-card-header> <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()" <button nbButton hero status="info" size="small" (click)="showQrCode()"
class="float-right">Invite</button> class="float-right">Invite</button>
<button nbButton hero status="info" size="small" [disabled]="anyHeroRemainAction" <button nbButton hero status="info" size="small" [disabled]="anyHeroRemainAction"
(click)="md2Service.runNextPhase()" class="float-right mr-2">Next Phase</button> (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-header>
<nb-card-body> <nb-card-body>
<div class="row" *ngIf="md2Service.heros.length==0"> <div class="row" *ngIf="md2Service.heros.length==0&& false">
<div class="col-6"> <div class="col-6">
<adj-number-input name="heroLevel" [(ngModel)]="md2Service.playerAmount" [maximum]="6" <adj-number-input name="heroLevel" [(ngModel)]="md2Service.playerAmount" [maximum]="6"
@@ -41,8 +48,7 @@
</div> </div>
<div class="row" *ngIf="md2Service.heros.length>0"> <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"> <!-- <div class="col-6">
<label for='playerAmount' class='label'>Hero Amount ({{md2Service.playerAmount}})</label> <label for='playerAmount' class='label'>Hero Amount ({{md2Service.playerAmount}})</label>
</div> </div>
@@ -51,19 +57,44 @@
({{md2Service.highestPlayerLevel}})</label> ({{md2Service.highestPlayerLevel}})</label>
</div> --> </div> -->
<div class="col-12" *ngFor="let hero of md2Service.heros"> <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> {{hero.name}})</label>
<span class="badge badge-primary mr-1">Lv.:{{hero.level}}</span> <span class="badge badge-primary mr-1"
<span class="badge badge-primary mr-1">HP: {{hero.hp}}/{{hero.hpMaximum}}</span> (click)="adjustHeroValue(hero,'level')">Lv.:{{hero.level}}</span>
<span class="badge badge-primary mr-1">Mana: {{hero.mp}}/{{hero.mpMaximum}}</span> <span class="badge badge-primary mr-1" (click)="adjustHeroValue(hero,'hp')">HP:
<span class="badge badge-success mr-1">Exp: {{hero.exp}}</span> {{hero.hp}}/{{hero.hpMaximum}}</span>
<span class="badge badge-danger mr-1" *ngIf="hero.fireToken">Fire:{{hero.fireToken}}</span> <span class="badge badge-primary mr-1" (click)="adjustHeroValue(hero,'mp')">Mana:
<span class="badge badge-info mr-1" *ngIf="hero.frozenToken">Frozen:{{hero.frozenToken}}</span> {{hero.mp}}/{{hero.mpMaximum}}</span>
<span class="badge badge-light mr-1" *ngIf="hero.remainActions==0">Inactive</span> <span class="badge badge-success mr-1" (click)="adjustHeroValue(hero,'exp')">Exp:
<span class="badge badge-success mr-1" *ngIf="hero.remainActions>0">Remain {{hero.exp}}</span>
Actions: {{hero.remainActions}}</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-success mr-1">{{hero.playerInfo.signalRClientId}}</span> -->
<span class="badge badge-danger mr-1" (click)="removeHero(hero)">X
</span>
</div> </div>
</div> </div>
@@ -76,10 +107,38 @@
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<div class="row"> <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> <button nbButton hero fullWidth status="primary" (click)="enterBossFight()">Enter Boss Fight</button>
</div> </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> </div>
@@ -0,0 +1,3 @@
.badge {
cursor: pointer;
}
@@ -5,8 +5,8 @@ import { MsgBoxService } from '../../services/msg-box.service';
import { ArrayUtils } from '../../utilities/array-utils'; import { ArrayUtils } from '../../utilities/array-utils';
import { ObjectUtils } from '../../utilities/object-utils'; import { ObjectUtils } from '../../utilities/object-utils';
import { first, map, take, takeUntil } from 'rxjs/operators'; 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.service'; import { MD2Service } from '../../services/MD2/md2.service';
import { GameRoomService } from '../../services/game-room.service'; import { GameRoomService } from '../../services/game-room.service';
import { MD2Base } from './MD2Base'; import { MD2Base } from './MD2Base';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@@ -15,6 +15,10 @@ import { QRCodeService } from '../../services/qrcode.service';
import { StringUtils } from '../../utilities/string-utils'; import { StringUtils } from '../../utilities/string-utils';
import { SpawnMobDlgComponent } from './mobs/spawn-mob-dlg/spawn-mob-dlg.component'; import { SpawnMobDlgComponent } from './mobs/spawn-mob-dlg/spawn-mob-dlg.component';
import { BossMicheal } from './massive-darkness2.model.boss'; 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({ @Component({
selector: 'ngx-massive-darkness2', selector: 'ngx-massive-darkness2',
@@ -23,9 +27,11 @@ import { BossMicheal } from './massive-darkness2.model.boss';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class MassiveDarkness2Component extends MD2Base implements OnInit { export class MassiveDarkness2Component extends MD2Base implements OnInit {
HeroClass: HeroClass MD2Icon = MD2Icon;
HeroClass = HeroClass;
constructor( constructor(
private fileService: FileService, private fileService: FileService,
private initService: MD2InitService,
private msgBoxService: MsgBoxService, private msgBoxService: MsgBoxService,
private qrCodeService: QRCodeService, private qrCodeService: QRCodeService,
public gameRoomService: GameRoomService, public gameRoomService: GameRoomService,
@@ -39,19 +45,100 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.md2Service.enemyPhaseSubject.pipe(takeUntil(this.destroy$)).subscribe(result => { this.md2Service.enemyPhaseSubject.pipe(takeUntil(this.destroy$)).subscribe(result => {
this.showEnemyPhaseAction(0); this.showEnemyPhaseAction(0);
}); });
this.showGameInitDialog();
} }
override signalRInitialized() {
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
});
}
} }
showQrCode() {
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) { if (this.md2Service.initialized == false) {
this.gameRoomService.createGameRoom('MD2'); this.gameRoomService.createGameRoom('MD2');
this.md2Service.initialized = true; 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}`; 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>` }); this.msgBoxService.show("Scan To Join", { text: `<img src='${this.qrCodeService.QRCodeUrl(initUrl, 5)}'><br><a href='${initUrl}' target='_blank'>Link</a>` });
} }
@@ -64,7 +151,7 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
} }
showEnemyPhaseAction(index: number) { showEnemyPhaseAction(index: number) {
let mob = new MobInfo(this.md2Service.enemyPhaseMobs[index]); let mob = this.md2Service.enemyPhaseMobs[index];
let enemyInfo = `<img src="${mob.imageUrl}" class='g-height-70vh'><br>`; let enemyInfo = `<img src="${mob.imageUrl}" class='g-height-70vh'><br>`;
let extraRule = ''; let extraRule = '';
@@ -85,7 +172,13 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
// break; // break;
// } // }
this.msgBoxService.dlgService.open(SpawnMobDlgComponent, { context: { title: `Enemy Phase(${(index + 1)}/${this.md2Service.enemyPhaseMobs.length})`, mode: MobDlgType.Activating, mob: mob } }) this.msgBoxService.dlgService.open(SpawnMobDlgComponent, {
context: {
title: `Enemy Phase(${(index + 1)}/${this.md2Service.enemyPhaseMobs.length})`,
mode: MobDlgType.Activating,
mob: mob
}
})
.onClose.pipe(first()).subscribe(result => { .onClose.pipe(first()).subscribe(result => {
index++; index++;
if (index < this.md2Service.enemyPhaseMobs.length) { if (index < this.md2Service.enemyPhaseMobs.length) {
@@ -125,10 +218,34 @@ export class MassiveDarkness2Component extends MD2Base implements OnInit {
} }
enterBossFight() { enterBossFight() {
this.msgBoxService.showInputbox('Boss Fight', 'Choose the boss').pipe(first()).subscribe(result => { this.md2Service.enterBossFight();
this.md2Service.info.isBossFight = true;
this.md2Service.info.boss = new BossMicheal(this.md2Service); }
this.detectChanges(); 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;
}
@@ -0,0 +1,87 @@
import { Observable, Subject } from "rxjs";
import { environment } from "../../../environments/environment";
import { MsgBoxService } from "../../services/msg-box.service";
import { ObjectUtils } from "../../utilities/object-utils";
import { StringUtils } from "../../utilities/string-utils";
import { GamePlayer } from "../games.model";
import { MD2HeroInfo, AttackTarget, HeroClass } from "./massive-darkness2.model";
import { MobSkill } from "./massive-darkness2.model.boss";
import { MobSkillTarget } from "./massive-darkness2.db.model";
export class MD2Logic {
public static getTargetHeroByFilter(heros: MD2HeroInfo[], targetType: MobSkillTarget) {
return this.getTargetHerosByFilter(heros, targetType, true)[0];
}
public static getTargetHerosByFilter(heros: MD2HeroInfo[], targetType: MobSkillTarget, onlyOne: boolean = false) {
let beenAttackedHero = [] as MD2HeroInfo[];
switch (targetType) {
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 MobSkillTarget.LeastMp:
let lowestMp = Math.min(...heros.map(h => h.mp));
beenAttackedHero = heros.filter(h => h.mp == lowestMp);
//this.otherAttackTarget = 'attacking the other <b>Lowest HP</b> hero.';
break;
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 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 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 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 leastExtraToken = Math.min(...heros.map(h => h.extraToken));
beenAttackedHero = heros.filter(h => h.extraToken == leastExtraToken);
break;
case MobSkillTarget.MostExtraToken:
let mostExtraToken = Math.max(...heros.map(h => h.extraToken));
beenAttackedHero = heros.filter(h => h.extraToken == mostExtraToken);
break;
case MobSkillTarget.LeastExtraToken2:
let leastExtraToken2 = Math.min(...heros.map(h => h.extraToken2));
beenAttackedHero = heros.filter(h => h.extraToken2 == leastExtraToken2);
break;
case MobSkillTarget.MostExtraToken2:
let mostExtraToken2 = Math.max(...heros.map(h => h.extraToken2));
beenAttackedHero = heros.filter(h => h.extraToken2 == mostExtraToken2);
break;
case MobSkillTarget.Random:
default:
beenAttackedHero = [heros[Math.round(Math.random() * (heros.length - 1))]];
//this.otherAttackTarget = 'Just act like normal.';
break;
}
if (onlyOne && beenAttackedHero.length > 1) {
beenAttackedHero = [beenAttackedHero[Math.round(Math.random() * (beenAttackedHero.length - 1))]];
}
return beenAttackedHero;
}
public static getTargetHerosHtml(beenAttackedHero: MD2HeroInfo[]) {
return `<b>${StringUtils.makeCommaSeparatedString(beenAttackedHero.map(h => this.heroFullName(h)), false, true)}</b>`;
}
public static heroFullName(hero: MD2HeroInfo) {
if (!hero) return '';
return `${hero.playerInfo.name} (${HeroClass[hero.class]} - ${hero.name})`
}
}
@@ -1,12 +1,45 @@
import { Subject } from "rxjs"
import { Observable, Subject, Subscription } from "rxjs"
import { first } from "rxjs/operators" import { first } from "rxjs/operators"
import { MD2Service } from "../../services/md2.service" import { MD2Service } from "../../services/MD2/md2.service"
import { StringUtils } from "../../utilities/string-utils" import { StringUtils } from "../../utilities/string-utils"
import { BossActivationComponent } from "./boss-fight/boss-activation/boss-activation.component" import { BossActivationComponent } from "./boss-fight/boss-activation/boss-activation.component"
import { TreasureType, AttackInfo, DefenseInfo, AttackType, MD2Icon, MD2HeroInfo, AttackTarget, MobInfo } 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 { RollingBlackDice } from "./massive-darkness2.model.dice"
import { MD2DiceSet, MD2MobSkill, MobSkillTarget } from "./massive-darkness2.db.model"
export enum MobSkillType {
Attack,
Defense,
Combat,
Passive,
ConditionalSkill,
OtherWiseSkill,
ActiveSkill,
MeleeAttack = 15,
RangeAttack,
MagicAttack,
}
export class MobSkill {
constructor(config: Partial<MobSkill> = {}) {
let defaultConfig = {
type: MobSkillType.Combat,
skillRoll: 1
} as Partial<MobSkill>;
Object.assign(defaultConfig, config);
Object.assign(this, defaultConfig);
// if (StringUtils.isNullOrWhitespace(this.name)) {
// this.name=`${MobSkillType[this.type]} ${this.skillRoll} ${}`
// }
}
type: MobSkillType;
clawRoll: number;
skillRoll: number;
name: string
description: string
targetHeros: MD2HeroInfo[]
}
export interface IBossFight { export interface IBossFight {
name: string name: string
addTreasureToken: Subject<TreasureType> addTreasureToken: Subject<TreasureType>
@@ -14,93 +47,168 @@ export interface IBossFight {
spawnRoamingMonster: Subject<void> spawnRoamingMonster: Subject<void>
rounds: number rounds: number
actions: number actions: number
hpPerHero: number activatedTimes: number
info: MobInfo info: MobInfo
actionBlackDice: number actionBlackDice: number
imgUrl: string imgUrl: string
standUrl: string standUrl: string
combatInfo: MobSkill extraRules: string
md2Service: MD2Service
activating(): boolean activating(): boolean
prepareForBossFight(): void prepareForBossFight(): void
nextRound(): void darknessPhase(): void
} }
export class BossMicheal implements IBossFight {
constructor(private md2Service: MD2Service) { export abstract class BossFight implements IBossFight {
this.name = 'Michael - The Corrupted Archangel';
this.imgUrl = md2Service.imgUrl('/Boss/Michael - The Corrupted Archangel.jpg');
this.standUrl = md2Service.imgUrl('/Boss/Michael.png');
this.hpPerHero = 15;
this.info = new MobInfo({
isRoamingMonster: true,
hp: this.md2Service.heros.length * this.hpPerHero,
level: 10
});
this.info.defenseInfos = new DefenseInfo(5, 1);
this.info.attackInfos = [new AttackInfo(MD2Icon.Melee, 2, 2, 0, 1)];
this.actions = 1;
this.rounds = 0;
this.actionBlackDice = 2;
this.corruptionTokenHtml = this.md2Service.imgHtml('Tokens/CorruptToken.png');
this.combatInfo = new MobSkill(`Combat 1${this.md2Service.iconHtml(MD2Icon.EnemySkill)}`,
`Deal 1 Wound for each ${this.corruptionTokenHtml} on the attacking or defending Hero. Discard the tokens afterwards(once per combat).`);;
}
name: string name: string
addTreasureToken: Subject<TreasureType> addTreasureToken: Subject<TreasureType>
spawnMob: Subject<void> spawnMob: Subject<void>
spawnRoamingMonster: Subject<void> spawnRoamingMonster: Subject<void>
rounds: number rounds: number
actions: number actions: number
hpPerHero: number activatedTimes: number
info: MobInfo info: MobInfo
actionBlackDice: number actionBlackDice: number
imgUrl: string imgUrl: string
standUrl: string standUrl: string
combatInfo: MobSkill extraRules: string
corruptionTokenHtml: string protected subscription: Subscription
activating(): boolean {
let actionResult = new RollingBlackDice().roll(this.actionBlackDice); constructor(public md2Service: MD2Service) {
this.rounds = 1;
}
activating(): boolean {
this.activatedTimes = this.actions;
this.runAction();
return true;
}
runAction() {
this.bossAction().pipe(first()).subscribe(result => {
this.activatedTimes--;
if (this.activatedTimes) {
this.runAction();
} else {
if (false == this.md2Service.heros.some(h => h.remainActions > 0)) {
this.md2Service.darknessPhase();
}
}
});
}
abstract bossAction(): Observable<boolean>;
protected actionEnd
prepareForBossFight(): void {
throw new Error("Method not implemented.")
}
darknessPhase(): void {
throw new Error("Method not implemented.")
}
}
export class BossMicheal extends BossFight {
constructor(public md2Service: MD2Service) {
super(md2Service);
this.corruptionTokenHtml = this.md2Service.imgHtml('Tokens/CorruptToken.png');
this.name = 'Michael - The Corrupted Archangel';
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,
imageUrl: md2Service.imgUrl('/Boss/Michael.png')
});
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.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
name: string
addTreasureToken: Subject<TreasureType>
spawnMob: Subject<void>
spawnRoamingMonster: Subject<void>
rounds: number
actions: number
info: MobInfo
actionBlackDice: number
imgUrl: string
standUrl: string
corruptionTokenHtml: string
bossAction(): Observable<boolean> {
let actionResult = new RollingBlackDice().roll(2);
let actionHtml = ''; let actionHtml = '';
let beenAttackedHero = [] as MD2HeroInfo[]; let beenAttackedHero = [] as MD2HeroInfo[];
let bossAction: MobSkill; let bossAction: MobSkill;
switch (actionResult.claws) { switch (actionResult.claws) {
case 0: case 0:
//Justice From Above //Justice From Above
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.MostCorruption, true); beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.MostExtraToken, true);
bossAction = new MobSkill('Justice From Above', bossAction = new MobSkill(
`Place Michael in the Zone at ${this.md2Service.getTargetHerosHtml(beenAttackedHero)} and attack Him/Her.`, beenAttackedHero); {
name: 'Justice From Above',
description: `Place Michael in the Zone at ${this.md2Service.getTargetHerosHtml(beenAttackedHero)} and attack Him/Her.`
});
break; break;
case 1: case 1:
//Lance Dash //Lance Dash
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastCorruption, true); beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastExtraToken, true);
bossAction = new MobSkill('Lance Dash', bossAction = new MobSkill({
`Move Michael and Place 1 ${this.corruptionTokenHtml} in the Zone at ${this.md2Service.getTargetHerosHtml(beenAttackedHero)} and attack Him/Her.`, beenAttackedHero); name: 'Lance Dash',
description:
`Move Michael and Place 1 ${this.corruptionTokenHtml} in the Zone at ${this.md2Service.getTargetHerosHtml(beenAttackedHero)} and attack Him/Her.`
});
break; break;
case 2: case 2:
//Dark Blessing //Dark Blessing
bossAction = new MobSkill('Dark Blessing', bossAction = new MobSkill({
`Place Michael ion the central Zone and add 1 ${this.corruptionTokenHtml} to the Corruption Stone Zone with the least amount of ${this.corruptionTokenHtml}.<br>` + name: 'Dark Blessing',
`Deal <b>${this.darkBlessingCorruptionAmt}</b> Wounds per ${this.corruptionTokenHtml} to all Heros in each Tiles <b>distributed as they wish</b>.`, beenAttackedHero); description:
`Place Michael on the central Zone and add 1 ${this.corruptionTokenHtml} to the Corruption Stone Zone with the least amount of ${this.corruptionTokenHtml}.<br>` +
`Deal <b>${this.darkBlessingCorruptionAmt}</b> Wounds per ${this.corruptionTokenHtml} to all Heros in each Tiles <b>distributed as they wish</b>.`
});
break; break;
default: default:
break; break;
} }
this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction } }).onClose return null;
.pipe(first()).subscribe(result => { //return this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction, currentAction: this.activatedTimes, allActions: this.actions } }).onClose;
});
return true;
} }
prepareForBossFight(): void { prepareForBossFight(): void {
this.md2Service.heros.forEach(hero => {
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
Zones).<br>Players choose the Zones, but must distribute
the tokens as equally as possible among them.</h6>`
});
} }
darkBlessingCorruptionAmt: number = 1; darkBlessingCorruptionAmt: number = 1;
nextRound(): void { darknessPhase(): void {
this.rounds++; this.rounds++;
switch (this.rounds) { switch (this.rounds) {
case 3: case 3:
@@ -109,7 +217,7 @@ export class BossMicheal implements IBossFight {
break; break;
case 2: case 2:
case 4: case 4:
this.info.defenseInfos[0].black += 1; this.info.defenseInfo.black += 1;
this.info.attackInfos[0].black += 1; this.info.attackInfos[0].black += 1;
break; break;
// case 4: // case 4:
@@ -123,13 +231,104 @@ export class BossMicheal implements IBossFight {
} }
} }
export class MobSkill {
constructor(skillName: string, skillDescription: string, targetHeros: MD2HeroInfo[] = []) { export class BossReaper extends BossFight {
this.skillName = skillName
this.skillDescription = skillDescription constructor(public md2Service: MD2Service) {
this.targetHeros = targetHeros super(md2Service);
this.timeTokenHtml = this.md2Service.imgHtml('Tokens/TimeToken.png');
this.name = 'The Reaper';
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,
imageUrl: md2Service.imgUrl('/Boss/The Reaper-Stand.png')
});
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;
this.extraRules = `A Hero standing in the Hourglass Zone may spend 1 action to add 1 ${this.timeTokenHtml} in each Hourglass Zone.`;
}
name: string
addTreasureToken: Subject<TreasureType>
spawnMob: Subject<void>
spawnRoamingMonster: Subject<void>
rounds: number
actions: number
info: MobInfo
actionBlackDice: number
imgUrl: string
standUrl: string
timeTokenHtml: string
bossAction() {
let actionResult = new RollingBlackDice().roll(this.actionBlackDice);
let actionHtml = '';
let beenAttackedHero = [] as MD2HeroInfo[];
let bossAction: MobSkill;
switch (actionResult.claws) {
case 0:
//Justice From Above
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastMp, true);
bossAction = new MobSkill(
{
name: 'Soul Drain',
description: `Place The Reaper in the Zone at ${this.md2Service.getTargetHerosHtml(beenAttackedHero)} and attack Him/Her.`
});
break;
case 1:
//Lance Dash
beenAttackedHero = this.md2Service.getTargetHerosByFilter(MobSkillTarget.LeastExtraToken, true);
bossAction = new MobSkill({
name: 'Time Ticking',
description:
`Place The Reaper in the <b>Hourglass Zone</b> withe the least ${this.timeTokenHtml} and remove 1 ${this.timeTokenHtml} from <b>The OtherHourglass Zone</b>.`
});
break;
case 2:
//Dark Blessing
bossAction = new MobSkill({
name: 'Death Is Coming',
description:
`Place The Reaper in the central Zone.<br>` +
`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 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', {
text: `<h6>Place 2 ${this.timeTokenHtml} in each Hourglass Zone.</h6>`
})
}
darkBlessingCorruptionAmt: number = 1;
darknessPhase(): void {
this.rounds++;
if (this.rounds > 4) {
this.actions = 3;
} else if (this.rounds > 1) {
this.actions = 2;
}
} }
skillName: string
skillDescription: string
targetHeros: MD2HeroInfo[]
} }
@@ -1,13 +1,19 @@
import { Subject } from "rxjs"; import { Observable, Subject } from "rxjs";
import { environment } from "../../../environments/environment";
import { MsgBoxService } from "../../services/msg-box.service";
import { ObjectUtils } from "../../utilities/object-utils"; import { ObjectUtils } from "../../utilities/object-utils";
import { GamePlayer } from "../games.model"; import { GamePlayer } from "../games.model";
import { MD2Clone } from "./factorys/md2-clone";
import { MobSkill } from "./massive-darkness2.model.boss"; 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 { export enum MobDlgType {
Spawn, Spawn,
Activating, Activating,
BeenAttacked, BeenAttacked,
PreView PreView,
Dashboard
} }
export enum RoundPhase { export enum RoundPhase {
HeroPhase, HeroPhase,
@@ -17,6 +23,7 @@ export enum RoundPhase {
BossActivation BossActivation
} }
export enum TreasureType { export enum TreasureType {
Cover,
Common, Common,
Rare, Rare,
Epic, Epic,
@@ -29,7 +36,18 @@ export enum HeroClass {
Ranger, Ranger,
Shaman, Shaman,
Paladin, Paladin,
Druid,
Necromancer,
Monk,
Thinker,
Bard
} }
export enum MobType {
Mob,
RoamingMonster,
Boss
}
export enum MD2Icon { export enum MD2Icon {
Attack, Attack,
Defense, Defense,
@@ -59,15 +77,31 @@ export enum MD2Icon {
Rage, Rage,
RedDice, RedDice,
BlueDice, BlueDice,
GreenDice,
YellowDice, YellowDice,
OrangeDice OrangeDice,
BlackDice,
//Below are image based icons
TreasureToken = 300,
TreasureToken_Common,
TreasureToken_Rare,
TreasureToken_Epic,
TreasureToken_Legendary,
HP_Color,
Mana_Color,
CorruptToken,
TimeToken,
FireToken,
FrozenToken
} }
export enum AttackTarget { export enum AttackTarget {
Random = 40, Random = 40,
LowestHp = 50, LeastHp = 50,
HighestHp = 60, LeastMp = 60,
HighestMp = 70, HighestHp = 70,
LowestLevel = 80, HighestMp = 80,
LowestLevel = 90,
MostCorruption = 200, MostCorruption = 200,
LeastCorruption = 201 LeastCorruption = 201
} }
@@ -95,17 +129,8 @@ export class AttackInfo {
red: number red: number
yellow: number yellow: number
black: number black: number
attackSkill: MobSkill
}
export class DefenseInfo {
constructor(blue: number, black: number = 0) {
this.blue = blue
this.black = black
}
blue: number
black: number
defenseSkill: MobSkill
} }
export class MD2LevelUpReward { export class MD2LevelUpReward {
constructor(config: Partial<MD2LevelUpReward>) { constructor(config: Partial<MD2LevelUpReward>) {
Object.assign(this, config); Object.assign(this, config);
@@ -126,8 +151,12 @@ export class DrawingBag<T extends IDrawingItem> {
} }
drawingItems: IDrawingItem[] drawingItems: IDrawingItem[]
removedItems: IDrawingItem[] removedItems: IDrawingItem[]
public bagIsEmpty() { public bagIsEmpty(predicate: (value: T) => boolean = undefined) {
return this.drawingItems.reduce((sum, current) => sum + current.drawingWeight, 0) == 0; if (predicate) {
return this.drawingItems.filter(predicate).reduce((sum, current) => sum + current.drawingWeight, 0) == 0;
} else {
return this.drawingItems.reduce((sum, current) => sum + current.drawingWeight, 0) == 0;
}
} }
public Draw(amount: number): T[] { public Draw(amount: number): T[] {
@@ -138,7 +167,7 @@ export class DrawingBag<T extends IDrawingItem> {
public DrawAndRemove(amount: number = 1, predicate: (value: T) => boolean = undefined): T[] { public DrawAndRemove(amount: number = 1, predicate: (value: T) => boolean = undefined): T[] {
let drawItems: T[] = []; let drawItems: T[] = [];
for (let i = 0; i < amount; i++) { for (let i = 0; i < amount; i++) {
if (!this.bagIsEmpty()) { if (!this.bagIsEmpty(predicate)) {
let drawItem = null as T; let drawItem = null as T;
let drawingPool = [] as T[]; let drawingPool = [] as T[];
if (predicate) { if (predicate) {
@@ -153,7 +182,7 @@ export class DrawingBag<T extends IDrawingItem> {
const item = drawingPool[i]; const item = drawingPool[i];
drawCalc += item.drawingWeight; drawCalc += item.drawingWeight;
if (drawCalc >= drawIndex) { if (drawCalc >= drawIndex) {
drawItem = ObjectUtils.CloneValue(item); drawItem = MD2Clone.CloneDrawingItem(item);
drawItem.drawingWeight = 1; drawItem.drawingWeight = 1;
break; break;
} }
@@ -177,7 +206,7 @@ export class DrawingBag<T extends IDrawingItem> {
this.removedItems = []; this.removedItems = [];
} }
public AddItem(item: IDrawingItem) { public AddItem(item: IDrawingItem) {
let existingItem = this.drawingItems.find(i => i.name == item.name); let existingItem = this.drawingItems.find(i => i.identifyName == item.identifyName);
if (existingItem) { if (existingItem) {
existingItem.drawingWeight += item.drawingWeight; existingItem.drawingWeight += item.drawingWeight;
} else { } else {
@@ -187,17 +216,34 @@ export class DrawingBag<T extends IDrawingItem> {
public RemoveItem(item: IDrawingItem) { public RemoveItem(item: IDrawingItem) {
if (item) { if (item) {
let existingItem = this.drawingItems.find(i => i.name == item.name);
if (existingItem) {
existingItem.drawingWeight -= item.drawingWeight;
let removedItem = this.removedItems.find(i => i.name == item.name); if (item.identifyName) {
if (removedItem) { let existingItem = this.drawingItems.find(i => i.identifyName == item.identifyName);
removedItem.drawingWeight += item.drawingWeight; if (existingItem) {
} else { existingItem.drawingWeight -= item.drawingWeight;
this.removedItems.push(item);
let removedItem = this.removedItems.find(i => i.identifyName == item.identifyName);
if (removedItem) {
removedItem.drawingWeight += item.drawingWeight;
} else {
this.removedItems.push(item);
}
} }
} else {
let existingItem = this.drawingItems.find(i => i.name == item.name);
if (existingItem) {
existingItem.drawingWeight -= item.drawingWeight;
let removedItem = this.removedItems.find(i => i.name == item.name);
if (removedItem) {
removedItem.drawingWeight += item.drawingWeight;
} else {
this.removedItems.push(item);
}
}
} }
} }
} }
@@ -207,10 +253,14 @@ export class DrawingBag<T extends IDrawingItem> {
this.removedItems = []; this.removedItems = [];
} }
} }
export interface IMobFactory {
mobName: string;
generate(level: number): MobInfo;
}
export interface IDrawingItem { export interface IDrawingItem {
imageUrl: string imageUrl: string
name: string name: string
get identifyName(): string
description: string description: string
drawingWeight: number drawingWeight: number
} }
@@ -226,12 +276,28 @@ export class DrawingItem implements IDrawingItem {
this.description = description this.description = description
this.drawingWeight = drawingWeight this.drawingWeight = drawingWeight
} }
get identifyName(): string {
return this.name;
}
imageUrl: string imageUrl: string
name: string name: string
description: string description: string
drawingWeight: number drawingWeight: number
} }
export class TreasureItem extends DrawingItem {
constructor(type: TreasureType, itemAmount: number = 1) {
super(`${TreasureType[type]} Treasure`,
`It's a ${TreasureType[type]} Treasure!`,
MD2_IMG_URL(`TreasureToken/${TreasureType[type]}.png`), itemAmount);
this.type = type;
this.itemAmount = itemAmount;
}
type: TreasureType;
itemAmount: number;
get identifyName(): string {
return this.name;
}
}
export class MobInfo implements IDrawingItem { export class MobInfo implements IDrawingItem {
constructor( constructor(
config: Partial<MobInfo> = {} config: Partial<MobInfo> = {}
@@ -241,73 +307,96 @@ export class MobInfo implements IDrawingItem {
this.drawingWeight = 1; this.drawingWeight = 1;
this.unitRemainHp = config.hp this.unitRemainHp = config.hp
} }
id: string;
from: GameBundle;
type: MobType = MobType.Mob;
imageUrl: string imageUrl: string
standUrl: string standUrl: string
leaderImgUrl: string
minionImgUrl: string
name: string name: string
description: string description: string
drawingWeight: number drawingWeight: number
level: number; level: number;
rewardTokens: number; rewardTokens: number;
hp: number; hp: number;
hpPerHero: number;
mobAmount: number; mobAmount: number;
carriedTreasure: DrawingItem[]; carriedTreasure: TreasureItem[];
fixedCarriedTreasure: DrawingItem[]; fixedCarriedTreasure: TreasureItem[];
unitRemainHp: number; unitRemainHp: number;
isRoamingMonster: boolean = false;
attackInfos: AttackInfo[]; attackInfos: AttackInfo[];
defenseInfos: DefenseInfo; defenseInfo: MD2DiceSet;
skills: MD2MobSkill[];
actions: number = 0;
activateDescription: string;
fireToken: number = 0; fireToken: number = 0;
frozenToken: number = 0; frozenToken: number = 0;
corruptionToken: number = 0; corruptionToken: number = 0;
uiExtraTokenCount: number = 0;
uiExtraTokenCount2: number = 0;
uiWounds: number; uiWounds: number;
uiFireTokens: number; uiFireTokens: number;
uiFrozenTokens: number; uiFrozenTokens: number;
uiCorruptionTokens: number; uiCorruptionTokens: number;
uiAttackedBy: string; uiAttackedBy: string;
extraRule: string;
bossFightProfile?: BossFightProfile;
get identifyName(): string {
return `${this.name}_${this.level}`;
}
public get carriedTreasureHtml(): string { public get carriedTreasureHtml(): string {
if (!this.carriedTreasure) return ''; if (!this.carriedTreasure) return '';
return this.carriedTreasure.map(i => `<img src="${i.imageUrl}" class='mr-1' width="40px">`) return this.carriedTreasure.map(i => `<img src="${i.imageUrl}" class='mr-1' width="40px">`)
.concat(this.fixedCarriedTreasure?.map(i => `<img src="${i.imageUrl}" class='mr-1' width="40px">`)).join(); .concat(this.fixedCarriedTreasure?.map(i => `<img src="${i.imageUrl}" class='mr-1' width="40px">`.repeat(i.drawingWeight))).join();
} }
public get totalHp(): number { public get totalHp(): number {
return this.isRoamingMonster ? this.unitRemainHp : (this.mobAmount - 1) * this.hp + this.unitRemainHp; switch (this.type) {
case MobType.Mob:
return (this.mobAmount - 1) * this.hp + this.unitRemainHp;
case MobType.RoamingMonster:
return this.unitRemainHp;
case MobType.Boss:
default:
return this.unitRemainHp;
}
} }
public get minionAmount(): number { public get minionAmount(): number {
return (this.mobAmount - 1); switch (this.type) {
case MobType.Mob:
return (this.mobAmount - 1);
case MobType.RoamingMonster:
case MobType.Boss:
default:
return 0;
}
} }
public get leaderExp(): number { public get leaderExp(): number {
return this.isRoamingMonster ? 4 : 2; switch (this.type) {
} case MobType.Mob:
return 2;
public get mobInfoHtml(): string { case MobType.RoamingMonster:
let html = `<img src="${this.imageUrl}" class="g-height-50vh">` return 4;
+ `<br>Target Unit HP:${this.unitRemainHp}`; case MobType.Boss:
default:
if (this.isRoamingMonster) { return 0;
html += `<br><label class="label">Alive Units:${this.mobAmount}`;
} else {
html += `<br>Total HP:${this.totalHp}`;
} }
return html;
}
public getCssClass(): string {
let levelString = '';
if (this.level < 3) {
levelString = '-lv1-2';
} else if (this.level < 5) {
levelString = '-lv3-4';
} else {
levelString = '-lv5';
}
return `${this.name.replace(' ', '')}${levelString}`;
} }
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 { export class MD2HeroInfo {
constructor( constructor(
config: Partial<MD2HeroInfo> = {} config: Partial<MD2HeroInfo> = {}
@@ -325,17 +414,26 @@ export class MD2HeroInfo {
level: number = 1; level: number = 1;
fireToken: number = 0; fireToken: number = 0;
frozenToken: number = 0; frozenToken: number = 0;
corruptionToken: number = 0; extraToken: number = 0;
extraToken2: number = 0;
playerInfo: GamePlayer; playerInfo: GamePlayer;
imgUrl: string; imgUrl: string;
skillHtml: string; skillHtml: string;
shadowSkillHtml: string; shadowSkillHtml: string;
remainActions: number = 3; remainActions: number = 3;
rage: number = 0; rage: number = 0;
uiActivating = false;
uiExtraTokenHtml: string = '';
uiExtraTokenHtml2: string = '';
uiExtraTokenName: string = '';
uiExtraTokenName2: string = '';
uiShowExtraToken = false;
uiShowExtraToken2 = false;
uiBossFight = false;
uiShowAttackBtn = false;
uiBetrayal = false;
public get heroFullName(): string { public get heroFullName(): string {
return `${this.playerInfo.name} (${HeroClass[this.class]} - ${this.name}` return `${this.playerInfo.name} (${HeroClass[this.class]} - ${this.name})`
} }
} }
@@ -344,7 +442,7 @@ export class MD2Rules {
public static CoreGameLevelBoard = [ public static CoreGameLevelBoard = [
new MD2LevelUpReward({ level: 2, needExp: 5, extraHp: 1, extraMp: 0, extraRareToken: 1 }), new MD2LevelUpReward({ level: 2, needExp: 5, extraHp: 1, extraMp: 0, extraRareToken: 1 }),
new MD2LevelUpReward({ level: 3, needExp: 10, extraHp: 1, extraMp: 1, extraEpicToken: 1 }), new MD2LevelUpReward({ level: 3, needExp: 10, extraHp: 1, extraMp: 1, extraEpicToken: 1 }),
new MD2LevelUpReward({ level: 4, needExp: 12, extraHp: 2, extraMp: 2, extraEpicToken: 1 }), new MD2LevelUpReward({ level: 4, needExp: 12, extraHp: 2, extraMp: 1, extraEpicToken: 1 }),
new MD2LevelUpReward({ level: 5, needExp: 18, extraHp: 2, extraMp: 2, extraEpicToken: 1 }), new MD2LevelUpReward({ level: 5, needExp: 18, extraHp: 2, extraMp: 2, extraEpicToken: 1 }),
]; ];
public static checkCoreGameLevelup(currentLevel: number, currentExp: number): MD2LevelUpReward { public static checkCoreGameLevelup(currentLevel: number, currentExp: number): MD2LevelUpReward {
@@ -381,6 +479,9 @@ export class MD2EnemyPhaseSpecialRule implements IDrawingItem {
this.title = title this.title = title
this.description = description this.description = description
} }
get identifyName(): string {
return this.name;
}
imageUrl: string; imageUrl: string;
name: string; name: string;
drawingWeight: number; drawingWeight: number;
@@ -420,7 +521,7 @@ export class CoreGameDarknessPhaseRule implements IDarknessPhaseRule {
switch (this.round) { switch (this.round) {
case 3: case 3:
case 9: case 9:
this.spawnMob.next(); this.spawnRoamingMonster.next();
return false; return false;
break; break;
case 4: case 4:
@@ -428,7 +529,7 @@ export class CoreGameDarknessPhaseRule implements IDarknessPhaseRule {
break; break;
case 5: case 5:
case 7: case 7:
this.spawnRoamingMonster.next(); this.spawnMob.next();
return false; return false;
break; break;
case 6: 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] || '';
}
}
@@ -3,7 +3,7 @@ import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors, NG_
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption'; import { DropDownOption } from '../../../entity/dropDownOption';
import { MD2Service } from '../../../services/md2.service'; import { MD2Service } from '../../../services/MD2/md2.service';
import { ArrayUtils } from '../../../utilities/array-utils'; import { ArrayUtils } from '../../../utilities/array-utils';
import { HeroClass, MD2HeroInfo } from '../massive-darkness2.model'; import { HeroClass, MD2HeroInfo } from '../massive-darkness2.model';
@@ -27,7 +27,7 @@ export class MD2HeroSelectComponent implements ControlValueAccessor, Validator {
readonly: boolean = false; readonly: boolean = false;
isRequired: boolean = false; isRequired: boolean = false;
heroOptions: DropDownOption[]; heroOptions: DropDownOption[];
@Input() id?= ''; @Input() id? = '';
@Input() name = ''; @Input() name = '';
@Input() data: MD2HeroInfo; @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 {{icon}} {{iconClass}}"></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 { 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({ @Component({
selector: 'md2-icon', selector: 'md2-icon',
@@ -8,14 +9,73 @@ import { MD2Icon } from '../massive-darkness2.model';
}) })
export class MD2IconComponent implements OnInit { export class MD2IconComponent implements OnInit {
@Input() iconClass: string = ''; @Input() iconClass: string = 'mr-1';
isImageIcon: boolean = false;
imgUrl: string;
iconHtml: string;
private _icon: string | MD2Icon;
@Input("icon") icon: MD2Icon; @Input() public set icon(v: string | MD2Icon) {
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;
}
}
}
constructor() { } isMD2Icon(icon: MD2Icon | string): icon is MD2Icon {
return Number.isInteger(icon);
}
@Input() size: string = 'sm';
iconName: string;
constructor(private md2Service: MD2Service) { }
ngOnInit(): void { 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 this.isImageIcon ? 'g-width-25 img-fluid' : 'g-font-size-18'
break;
case 'med':
return this.isImageIcon ? 'g-width-35 img-fluid' : 'g-font-size-30'
break;
case 'lg':
return this.isImageIcon ? 'g-width-50 img-fluid' : 'g-font-size-50'
break;
default:
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 !== '';
}
}
@@ -1,23 +1,30 @@
<label class='label'>Weapon Info</label> <!-- <label class='label'>Weapon Info</label> -->
<div class="g-brd-3 g-brd-bottom--dashed g-brd-gray-light-v2 mb-3 mt-2 row" *ngFor="let info of mob.attackInfos"> <ng-container *ngIf="display">
<div class="col-md-4"> <div class="g-brd-3 g-brd-bottom--dashed g-brd-gray-light-v2 mb-3 mt-4 row" *ngFor="let info of mob.attackInfos">
<span class=" g-font-size-50" [innerHtml]="iconHtml(info.type)"></span> <div class="col-md-4">
</div> <md2-icon [icon]="info.type" size="lg"></md2-icon>
<div class="col-md-8">
<div *ngIf="info.yellow" class="g-height-45">
<span class="MD2Icon Yellow dice g-font-size-50">
<span class="MD2text diceAmount">x{{info.yellow}}</span>
</span>
</div> </div>
<div *ngIf="info.orange" class="g-height-45 mt-1"> <div class="col-md-8">
<span class="MD2Icon Orange dice g-font-size-50"> <div *ngIf="info.yellow" class="g-height-45">
<span class="MD2text diceAmount">x{{info.orange}}</span> <span class="MD2Icon Yellow dice g-font-size-50">
</span> <span class="MD2text diceAmount">x{{info.yellow}}</span>
</div> </span>
<div *ngIf="info.red" class="g-height-45 mt-1"> </div>
<span class="MD2Icon Red dice g-font-size-50"> <div *ngIf="info.orange" class="g-height-45 mt-1">
<span class="MD2text diceAmount">x{{info.red}}</span> <span class="MD2Icon Orange dice g-font-size-50">
</span> <span class="MD2text diceAmount">x{{info.orange}}</span>
</span>
</div>
<div *ngIf="info.red" class="g-height-45 mt-1">
<span class="MD2Icon Red dice g-font-size-50">
<span class="MD2text diceAmount">x{{info.red}}</span>
</span>
</div>
<div *ngIf="info.black" class="g-height-45 mt-1">
<span class="MD2Icon Black dice g-font-size-50">
<span class="MD2text diceAmount">x{{info.black}}</span>
</span>
</div>
</div> </div>
</div> </div>
</div> </ng-container>
@@ -0,0 +1,19 @@
.diceAmount {
color: white;
z-index: 2;
position: absolute;
left: 25px;
font-size: 30px;
}
.dice {
&::before {
position: absolute;
}
}
.blackDiceAmount {
color: white;
z-index: 2;
position: absolute;
left: 26px;
font-size: 30px;
}
@@ -1,8 +1,8 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { MD2Icon, MobInfo } from '../../../massive-darkness2.model'; import { MD2Icon, MobDlgType, MobInfo, MobType } from '../../../massive-darkness2.model';
@Component({ @Component({
selector: 'ngx-mob-attack-info', selector: 'md2-mob-attack-info',
templateUrl: './mob-attack-info.component.html', templateUrl: './mob-attack-info.component.html',
styleUrls: ['./mob-attack-info.component.scss'] styleUrls: ['./mob-attack-info.component.scss']
}) })
@@ -20,10 +20,25 @@ export class MobAttackInfoComponent implements OnInit {
} }
} }
@Input() mode: MobDlgType = MobDlgType.PreView;
display: boolean = false;
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
switch (this.mode) {
case MobDlgType.Spawn:
this.display = this.mob.type != MobType.Mob;
break;
case MobDlgType.Activating:
case MobDlgType.PreView:
case MobDlgType.Dashboard:
this.display = true;
break;
default:
this.display = false;
break;
}
} }
} }
@@ -1 +1,31 @@
<p>mob-combat-info works!</p> <!-- <div class='form-group row' *ngIf="showSkill">
<label class='label col-sm-3 form-control-label MD2text g-font-size-30'>
{{mob.combatSkill.skillRoll}} <md2-icon icon="enemySkill" size="md"></md2-icon>
</label>
<div class='col-sm' [innerHtml]="mob.combatSkill.description"></div>
</div> -->
<div *ngIf="showBlackDice" class="row">
<!-- <md2-icon></md2-icon> -->
<div class="col-md-4">
<md2-icon icon="enemySkill" size="lg"></md2-icon>
</div>
<div class="col-md-8">
<span class="MD2Icon Black dice g-font-size-50">
<span class="MD2text blackDiceAmount">x{{mob.minionAmount}}</span>
</span>
</div>
</div>
<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> -->
<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>
@@ -0,0 +1,31 @@
.diceAmount {
color: white;
z-index: 2;
position: absolute;
left: 25px;
font-size: 30px;
}
.dice {
&::before {
position: absolute;
}
}
.blackDiceAmount {
color: white;
z-index: 2;
position: absolute;
left: 26px;
font-size: 30px;
}
.skillDesc {
padding-left: 8px;
.MD2Icon {
font-size: 45px;
}
}
//override the style of the skillDesc class and sub elements
:host ::ng-deep .skillDesc .MD2Icon {
font-size: 30px;
}
@@ -1,15 +1,55 @@
import { Component, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { MD2Icon, MobDlgType, MobInfo, MobType } from '../../../massive-darkness2.model';
import { MobSkillType } from '../../../massive-darkness2.model.boss';
@Component({ @Component({
selector: 'ngx-mob-combat-info', selector: 'md2-mob-combat-info',
templateUrl: './mob-combat-info.component.html', templateUrl: './mob-combat-info.component.html',
styleUrls: ['./mob-combat-info.component.scss'] styleUrls: ['./mob-combat-info.component.scss']
}) })
export class MobCombatInfoComponent implements OnInit { export class MobCombatInfoComponent implements OnInit {
MobSkillType = MobSkillType;
MD2Icon = MD2Icon;
private _mob: MobInfo;
public get mob(): MobInfo {
return this._mob;
}
@Input() public set mob(v: MobInfo) {
if (this._mob != v) {
this._mob = v;
}
}
@Input() mode: MobDlgType = MobDlgType.PreView;
showAllSkill: boolean = false;
showBlackDice: boolean
skillTriggerHtml: string = '';
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
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;;
} }
} }
@@ -1 +1,20 @@
<p>mob-def-info works!</p> <!-- <label class='label'>Defense Info</label> -->
<ng-container *ngIf="display">
<div class="g-brd-3 g-brd-bottom--dashed g-brd-gray-light-v2 mb-3 mt-2 row">
<div class="col-md-4">
<md2-icon icon="defense" size="lg"></md2-icon>
</div>
<div class="col-md-8">
<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">
<span class="MD2Icon Black dice g-font-size-50">
<span class="MD2text diceAmount">x{{mob.defenseInfo.black}}</span>
</span>
</div>
</div>
</div>
</ng-container>
@@ -0,0 +1,19 @@
.diceAmount {
color: white;
z-index: 2;
position: absolute;
left: 25px;
font-size: 30px;
}
.dice {
&::before {
position: absolute;
}
}
.blackDiceAmount {
color: white;
z-index: 2;
position: absolute;
left: 26px;
font-size: 30px;
}
@@ -1,15 +1,46 @@
import { Component, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { MD2Icon, MobInfo, MobDlgType } from '../../../massive-darkness2.model';
import { MobSkillType } from '../../../massive-darkness2.model.boss';
@Component({ @Component({
selector: 'ngx-mob-def-info', selector: 'md2-mob-def-info',
templateUrl: './mob-def-info.component.html', templateUrl: './mob-def-info.component.html',
styleUrls: ['./mob-def-info.component.scss'] styleUrls: ['./mob-def-info.component.scss']
}) })
export class MobDefInfoComponent implements OnInit { export class MobDefInfoComponent implements OnInit {
constructor() { } MD2Icon = MD2Icon;
private _mob: MobInfo;
public get mob(): MobInfo {
return this._mob;
}
@Input() public set mob(v: MobInfo) {
if (this._mob != v) {
this._mob = v;
}
}
@Input() mode: MobDlgType = MobDlgType.PreView;
display: boolean = false;
constructor() { }
ngOnInit(): void { ngOnInit(): void {
switch (this.mode) {
case MobDlgType.BeenAttacked:
case MobDlgType.PreView:
case MobDlgType.Spawn:
case MobDlgType.Dashboard:
this.display = true;
break;
default:
this.display = false;
break;
}
if (!this.mob.defenseInfo || this.mob.defenseInfo.blue == 0 && this.mob.defenseInfo.black == 0) {
this.display = false;
}
} }
} }
@@ -1,48 +1,67 @@
<label class='label'>Level <b class="MD2text g-font-size-18">{{mob.level}}</b></label><br> <div class="row no-gutters">
<div class="col-md-6">
<ng-container *ngIf="mob.mobAmount"> <label class='label g-text-nowrap'>Level <b class="MD2text g-font-size-18">{{mob.level}}</b></label><br>
<label class='label'>Alive Units <b class="MD2text g-font-size-18">{{mob.mobAmount}}</b></label><br>
<label class='label g-text-nowrap'>Total HP <b class="MD2text g-font-size-18">{{mob.totalHp}}</b></label>
</div>
<div class="pl-2 col-md-6" *ngIf="mob.mobAmount">
<ng-container>
<label class='label g-text-nowrap'>Minions <b
class="MD2text g-font-size-18">{{mob.mobAmount-1}}</b></label><br>
</ng-container>
</div>
</div>
<div class="row">
<div class="col-md-6" *ngIf="showTokenAdjustment||mob.fireToken>0">
<adj-number-input name="mobFire" [(ngModel)]="mob.fireToken" minimum="0"
title="{{iconHtml(MD2Icon.Fire,'g-color-google-plus mr-1 g-font-size-18')}}Fire Token">
</adj-number-input>
</div>
<div class="col-md-6" *ngIf="showTokenAdjustment||mob.frozenToken>0">
<adj-number-input name="mobFrost" [(ngModel)]="mob.frozenToken" minimum="0"
title="{{iconHtml(MD2Icon.Frost,'g-color-aqua mr-1 g-font-size-18')}}Frozen Token">
</adj-number-input>
</div>
</div>
<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>
</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> </ng-container>
<label class='label'>Total HP <b class="MD2text g-font-size-18">{{mob.totalHp}}</b></label><br>
<ng-container *ngIf="mob.carriedTreasureHtml"> <ng-container *ngIf="mob.carriedTreasureHtml">
<label class='label'>Carried Treasure</label><br> <label class='label'>Carried Treasure</label><br>
<div [innerHtml]="mob.carriedTreasureHtml"></div> <div [innerHtml]="mob.carriedTreasureHtml"></div>
</ng-container> </ng-container>
<div class="g-height-20 mb-1"></div>
<ng-container *ngIf="mob.attackInfos&&mob.attackInfos.length>0&& !hideWeaponInfo"> <md2-mob-attack-info [mob]="mob" [mode]="mode">
</md2-mob-attack-info>
</ng-container> <md2-mob-def-info [mob]="mob" [mode]="mode"></md2-mob-def-info>
<md2-mob-combat-info *ngIf="!hideCombatInfo" [mob]="mob" [mode]="mode"></md2-mob-combat-info>
<ng-container *ngIf="mob.defenseInfos&& !hideWeaponInfo"> <div *ngIf="!hideCombatInfo && mob.extraRule">
<label class='label'>Defense Info</label> <div class="alert alert-warning" role="alert" [innerHtml]="mob.extraRule">
<div class="g-brd-3 g-brd-bottom--dashed g-brd-gray-light-v2 mb-3 mt-2 row">
<div class="col-md-4">
<span class="g-font-size-50" [innerHtml]="iconHtml(MD2Icon.Defense)"></span>
</div>
<div class="col-md-8">
<div *ngIf="mob.defenseInfos.blue" class="g-height-45">
<span class="MD2Icon Blue dice g-font-size-50">
<span class="MD2text diceAmount">x{{mob.defenseInfos.blue}}</span>
</span>
</div>
<div *ngIf="mob.defenseInfos.black" class="g-height-45 mt-1">
<span class="MD2Icon Black dice g-font-size-50">
<span class="MD2text diceAmount">x{{mob.defenseInfos.black}}</span>
</span>
</div>
</div>
</div>
</ng-container>
<div *ngIf="showBlackDice" class="row">
<!-- <md2-icon></md2-icon> -->
<div class="col-md-4">
<span class=" g-font-size-50" [innerHtml]="iconHtml(MD2Icon.EnemySkill)"></span>
</div>
<div class="col-md-8">
<span class="MD2Icon Black dice g-font-size-50">
<span class="MD2text blackDiceAmount">x{{mob.minionAmount}}</span>
</span>
</div> </div>
</div> </div>
@@ -1,6 +1,7 @@
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { MD2Service } from '../../../../services/md2.service'; import { MD2MobService } from '../../../../services/MD2/md2-mob.service';
import { MD2Service } from '../../../../services/MD2/md2.service';
import { StateService } from '../../../../services/state.service'; import { StateService } from '../../../../services/state.service';
import { MD2Icon, MobDlgType, MobInfo } from '../../massive-darkness2.model'; import { MD2Icon, MobDlgType, MobInfo } from '../../massive-darkness2.model';
import { MD2ComponentBase } from '../../MD2Base'; import { MD2ComponentBase } from '../../MD2Base';
@@ -11,7 +12,7 @@ import { MD2ComponentBase } from '../../MD2Base';
styleUrls: ['./mob-detail-info.component.scss'] styleUrls: ['./mob-detail-info.component.scss']
}) })
export class MobDetailInfoComponent extends MD2ComponentBase implements OnInit { export class MobDetailInfoComponent extends MD2ComponentBase implements OnInit {
MobDlgType = MobDlgType;
MD2Icon = MD2Icon; MD2Icon = MD2Icon;
private _mob: MobInfo; private _mob: MobInfo;
public get mob(): MobInfo { public get mob(): MobInfo {
@@ -32,6 +33,10 @@ export class MobDetailInfoComponent extends MD2ComponentBase implements OnInit {
return this.mode == MobDlgType.Spawn; return this.mode == MobDlgType.Spawn;
} }
public get hideCombatInfo(): boolean {
return this.mode == MobDlgType.Dashboard;
}
showAttackingInfo: boolean = false; showAttackingInfo: boolean = false;
@Input("showAttackingInfo") @Input("showAttackingInfo")
@@ -39,11 +44,9 @@ export class MobDetailInfoComponent extends MD2ComponentBase implements OnInit {
this.showAttackingInfo = typeof value !== "undefined" && value !== false; this.showAttackingInfo = typeof value !== "undefined" && value !== false;
} }
public get showBlackDice(): boolean {
return (this.mode == MobDlgType.Activating || this.mode == MobDlgType.BeenAttacked) && this.mob.minionAmount > 0;
}
constructor( constructor(
private mobService: MD2MobService,
public md2Service: MD2Service, public md2Service: MD2Service,
protected stateService: StateService, protected stateService: StateService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
@@ -64,4 +67,21 @@ export class MobDetailInfoComponent extends MD2ComponentBase implements OnInit {
} }
} }
public get showAdjustment(): boolean {
return [MobDlgType.Activating, MobDlgType.BeenAttacked, MobDlgType.Dashboard].includes(this.mode);
}
public get showTokenAdjustment(): boolean {
return [MobDlgType.Activating, MobDlgType.BeenAttacked].includes(this.mode);
}
adjustMobHp(adjustAmount: number) {
if (adjustAmount < 0) {
this.mobService.attackMob(this.mob, 1);
} else {
this.mobService.healMob(this.mob, 1);
}
this.md2Service.refreshUI$.next();
}
} }
@@ -0,0 +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)"
[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>
@@ -0,0 +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: 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%);
}
}
@@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupManagementComponent } from './group-management.component'; import { MobStandInfoComponent } from './mob-stand-info.component';
describe('GroupManagementComponent', () => { describe('MobStandInfoComponent', () => {
let component: GroupManagementComponent; let component: MobStandInfoComponent;
let fixture: ComponentFixture<GroupManagementComponent>; let fixture: ComponentFixture<MobStandInfoComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ GroupManagementComponent ] declarations: [ MobStandInfoComponent ]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(GroupManagementComponent); fixture = TestBed.createComponent(MobStandInfoComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

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