Compare commits

..

2 Commits

Author SHA1 Message Date
Chris Chen
b46392bc41 WIP 2024-03-21 17:47:13 -07:00
Chris Chen
6301d6008b WIP 2024-02-28 15:17:41 -08:00
211 changed files with 7123 additions and 757 deletions

View File

@ -5,6 +5,7 @@
"git.decorations.enabled": false, "git.decorations.enabled": false,
"workbench.editor.decorations.badges": false, "workbench.editor.decorations.badges": false,
"workbench.editor.decorations.colors": false, "workbench.editor.decorations.colors": false,
"typescript.preferences.includePackageJsonAutoImports": "on",
"vscode_custom_css.imports": [ "vscode_custom_css.imports": [
"file:///C:/VScode/custom.css" "file:///C:/VScode/custom.css"
], ],

View File

@ -13,7 +13,7 @@
"ng": "ng", "ng": "ng",
"conventional-changelog": "conventional-changelog", "conventional-changelog": "conventional-changelog",
"start": "ng serve", "start": "ng serve",
"build": "ng build --output-path \\\\ArkNAS\\docker\\nginx-proxy\\data\\ChurchAngular --prod", "build": "ng build --output-path \\\\ArkNAS\\docker\\nginx-proxy\\data\\ChurchAngular --configuration production",
"build:prod": "npm run build -- --configuration production --aot", "build:prod": "npm run build -- --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",

View File

@ -10,7 +10,7 @@ import {
LayoutService, LayoutService,
PlayerService, PlayerService,
SeoService, SeoService,
StateService, StateServiceForNB,
} from './utils'; } from './utils';
import { UserData } from './data/users'; import { UserData } from './data/users';
import { ElectricityData } from './data/electricity'; import { ElectricityData } from './data/electricity';
@ -142,7 +142,7 @@ export const NB_CORE_PROVIDERS = [
LayoutService, LayoutService,
PlayerService, PlayerService,
SeoService, SeoService,
StateService, StateServiceForNB,
]; ];
@NgModule({ @NgModule({

View File

@ -1,7 +1,7 @@
import { LayoutService } from './layout.service'; import { LayoutService } from './layout.service';
import { AnalyticsService } from './analytics.service'; import { AnalyticsService } from './analytics.service';
import { PlayerService } from './player.service'; import { PlayerService } from './player.service';
import { StateService } from './state.service'; import { StateServiceForNB } from './state.service';
import { SeoService } from './seo.service'; import { SeoService } from './seo.service';
export { export {
@ -9,5 +9,5 @@ export {
AnalyticsService, AnalyticsService,
PlayerService, PlayerService,
SeoService, SeoService,
StateService, StateServiceForNB,
}; };

View File

@ -5,7 +5,7 @@ import { takeWhile } from 'rxjs/operators';
import { NbLayoutDirectionService, NbLayoutDirection } from '@nebular/theme'; import { NbLayoutDirectionService, NbLayoutDirection } from '@nebular/theme';
@Injectable() @Injectable()
export class StateService implements OnDestroy { export class StateServiceForNB implements OnDestroy {
protected layouts: any = [ protected layouts: any = [
{ {

View File

@ -0,0 +1,34 @@
export const DEFAULT_MEDIA_BREAKPOINTS = [
{
name: 'xs',
width: 0,
},
{
name: 'is',
width: 400,
},
{
name: 'sm',
width: 576,
},
{
name: 'md',
width: 768,
},
{
name: 'lg',
width: 992,
},
{
name: 'xl',
width: 1200,
},
{
name: 'xxl',
width: 1400,
},
{
name: 'xxxl',
width: 1600,
},
];

View File

@ -6,9 +6,10 @@ import { LayoutService } from '../../../@core/utils';
import { map, takeUntil, first } from 'rxjs/operators'; import { map, takeUntil, first } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { HeaderService } from '../../../services/header.service'; import { HeaderService } from '../../../services/header.service';
import { NbAuthService } from '@nebular/auth';
import { AuthService } from '../../../services/auth.service';
import { UserProfileAction } from '../../../entity/Auth'; import { UserProfileAction } from '../../../entity/Auth';
import { Router } from '@angular/router';
import { LoginUserService } from '../../../services/login-user.service';
import { AuthService } from '../../../services/auth.service';
@Component({ @Component({
selector: 'ngx-header', selector: 'ngx-header',
@ -19,6 +20,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
header: string = ''; header: string = '';
private destroy$: Subject<void> = new Subject<void>(); private destroy$: Subject<void> = new Subject<void>();
userPictureOnly: boolean = false; userPictureOnly: boolean = false;
isLessThanMd: boolean = false;
themes = [ themes = [
{ {
@ -52,7 +54,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
}]; }];
public get user() { public get user() {
return this.authService.userAccess; return this.loginUserService.userAccess;
} }
public get userFullName() { public get userFullName() {
if (this.user) { if (this.user) {
@ -68,24 +70,22 @@ export class HeaderComponent implements OnInit, OnDestroy {
return null; return null;
} }
} }
constructor(private sidebarService: NbSidebarService, constructor(
private sidebarService: NbSidebarService,
private menuService: NbMenuService, private menuService: NbMenuService,
private themeService: NbThemeService, private themeService: NbThemeService,
private userService: UserData, private userService: UserData,
private layoutService: LayoutService, private layoutService: LayoutService,
private breakpointService: NbMediaBreakpointsService, private breakpointService: NbMediaBreakpointsService,
private headerService: HeaderService, private headerService: HeaderService,
private oAuthService: NbAuthService, private loginUserService: LoginUserService,
private authService: AuthService, private authService: AuthService,
protected router: Router,
) { ) {
this.headerService.headerChange$.pipe(takeUntil(this.destroy$)).subscribe(result => { this.headerService.headerChange$.pipe(takeUntil(this.destroy$)).subscribe(result => {
this.header = result; this.header = result;
}); });
this.menuService.onItemClick().pipe(takeUntil(this.destroy$))
.subscribe(result => {
if (result.item.title == 'Log out') this.logout();
});
} }
@ -93,13 +93,18 @@ export class HeaderComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.currentTheme = this.themeService.currentTheme; this.currentTheme = this.themeService.currentTheme;
const { xl } = this.breakpointService.getBreakpointsMap(); const { md, xl } = this.breakpointService.getBreakpointsMap();
this.themeService.onMediaQueryChange() this.themeService.onMediaQueryChange()
.pipe( .pipe(
map(([, currentBreakpoint]) => currentBreakpoint.width < xl), map(([, currentBreakpoint]) => currentBreakpoint.width),
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe((isLessThanXl: boolean) => this.userPictureOnly = isLessThanXl); .subscribe((screenWidth: number) => {
let isLessThanXl = screenWidth < xl;
this.isLessThanMd = screenWidth < md;
this.userPictureOnly = isLessThanXl
});
this.themeService.onThemeChange() this.themeService.onThemeChange()
.pipe( .pipe(
@ -108,6 +113,22 @@ export class HeaderComponent implements OnInit, OnDestroy {
) )
.subscribe(themeName => this.currentTheme = themeName); .subscribe(themeName => this.currentTheme = themeName);
this.menuService.onItemClick().subscribe(result => {
if (this.isLessThanMd && result.tag == 'NavMenu' && result.item.link) {
this.toggleSidebar();
} else if (result.tag == 'UserProfileMenu') {
switch (result.item.data as UserProfileAction) {
case UserProfileAction.None: break;
case UserProfileAction.GoToProfile:
this.router.navigate(["/myapp/profile"]);
break;
case UserProfileAction.LogOut:
this.logout();
break;
default: break;
}
}
});
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -1,4 +1,4 @@
<ngx-one-column-layout> <ngx-one-column-layout>
<nb-menu [items]="MENU_ITEMS"></nb-menu> <nb-menu [items]="MENU_ITEMS" tag="NavMenu"></nb-menu>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</ngx-one-column-layout> </ngx-one-column-layout>

View File

@ -6,10 +6,7 @@ import { AdminComponent } from './admin.component';
import { NbMenuModule, NbInputModule, NbCardModule, NbButtonModule, NbActionsModule, NbCheckboxModule, NbRadioModule, NbDatepickerModule, NbSelectModule, NbIconModule, NbTagModule, NbStepperModule, NbListModule, NbSpinnerModule, NbDialogModule, NbUserModule } from '@nebular/theme'; import { NbMenuModule, NbInputModule, NbCardModule, NbButtonModule, NbActionsModule, NbCheckboxModule, NbRadioModule, NbDatepickerModule, NbSelectModule, NbIconModule, NbTagModule, NbStepperModule, NbListModule, NbSpinnerModule, NbDialogModule, NbUserModule } from '@nebular/theme';
import { ThemeModule } from '../@theme/theme.module'; import { ThemeModule } from '../@theme/theme.module';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { BestListDlgComponent } from './happiness-groups/best-list-dlg/best-list-dlg.component';
import { AlertDlgModule } from '../ui/alert-dlg/alert-dlg.module'; import { AlertDlgModule } from '../ui/alert-dlg/alert-dlg.module';
import { HappinessWeekEditorComponent } from './happiness-groups/happiness-week-editor/happiness-week-editor.component';
import { HappinessWeekListDlgComponent } from './happiness-groups/happiness-week-list-dlg/happiness-week-list-dlg.component';
import { CellGroupRoutineEventsComponent } from './cell-group-routine-events/cell-group-routine-events.component'; import { CellGroupRoutineEventsComponent } from './cell-group-routine-events/cell-group-routine-events.component';
import { FancyTableModule } from '../ui/fancy-table/fancy-table.module'; 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';
@ -26,14 +23,9 @@ import { MaskDirectiveModule } from '../directives/mask-directive/mask-directive
import { DateInputModule } from '../ui/date-input/date-input.module'; import { DateInputModule } from '../ui/date-input/date-input.module';
import { LineMessagingAccountComponent } from './lines/line-messaging-account/line-messaging-account.component'; import { LineMessagingAccountComponent } from './lines/line-messaging-account/line-messaging-account.component';
import { LineMessagingAccountEditorComponent } from './lines/line-messaging-account-editor/line-messaging-account-editor.component'; import { LineMessagingAccountEditorComponent } from './lines/line-messaging-account-editor/line-messaging-account-editor.component';
import { WeekTaskEditorComponent } from './happiness-groups/week-task-editor/week-task-editor.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AdminComponent, AdminComponent,
BestListDlgComponent,
HappinessWeekEditorComponent,
HappinessWeekListDlgComponent,
CellGroupRoutineEventsComponent, CellGroupRoutineEventsComponent,
FamilyMembersComponent, FamilyMembersComponent,
FamilyMemberEditorComponent, FamilyMemberEditorComponent,
@ -44,7 +36,7 @@ import { WeekTaskEditorComponent } from './happiness-groups/week-task-editor/wee
LogDetailComponent, LogDetailComponent,
LineMessagingAccountComponent, LineMessagingAccountComponent,
LineMessagingAccountEditorComponent, LineMessagingAccountEditorComponent,
WeekTaskEditorComponent], ],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,

View File

@ -4,10 +4,10 @@ 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 { inherits } from 'util';
import { StateService } from '../../@core/utils';
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 { 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'; import { ObjectUtils } from '../../utilities/object-utils';

View File

@ -3,10 +3,10 @@ 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 { DialogComponent } from '../../../pages/modal-overlays/dialog/dialog.component';
import { DomainMemberShipService } from '../../../services/crudServices/pastoral-domain.service';
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';
import { DomainMemberShipService } from '../../../services/crudServices/domain-member-ship.service';
@Component({ @Component({
selector: 'ngx-assign-member-cell-group', selector: 'ngx-assign-member-cell-group',

View File

@ -3,12 +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 { StateService } from '../../@core/utils';
import { FamilyMember, Gender } from '../../entity/Member'; import { FamilyMember, Gender } from '../../entity/Member';
import { PastoralDomain } from '../../entity/PastoralDomain'; import { PastoralDomain } from '../../entity/PastoralDomain';
import { FamilyMemberService } from '../../services/crudServices/family-member.service'; import { FamilyMemberService } from '../../services/crudServices/family-member.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service'; import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { MsgBoxService } from '../../services/msg-box.service'; import { MsgBoxService } from '../../services/msg-box.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'; import { ObjectUtils } from '../../utilities/object-utils';

View File

@ -1,4 +0,0 @@
nb-card {
max-height: 90vh;
width: 700px;
}

View File

@ -1,84 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef, NbToastrService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
import { HappinessBEST } from '../../../entity/HappinessGroup';
import { PastoralDomain } from '../../../entity/PastoralDomain';
import { BestService } from '../../../services/crudServices/best.service';
import { HappinessGroupService } from '../../../services/crudServices/happiness-group.service';
import { LineGroup, LineService } from '../../../services/line.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { ADButtons, AlertDlgComponent } from '../../../ui/alert-dlg/alert-dlg.component';
@Component({
selector: 'ngx-best-list-dlg',
templateUrl: './best-list-dlg.component.html',
styleUrls: ['./best-list-dlg.component.scss']
})
export class BestListDlgComponent implements OnInit {
processing: boolean = false;
isAdding: boolean = false;
group: PastoralDomain;
datum: HappinessBEST;
constructor(
private dlgRef: NbDialogRef<BestListDlgComponent>,
private msgBpxService: MsgBoxService,
private happinessGroupService: HappinessGroupService,
private toastrService: NbToastrService,
private bestService: BestService,
private lineService: LineService
) { }
ngOnInit(): void {
this.group.bests = this.group.bests.sort((a, b) => 0 - (a.name < b.name ? 1 : -1));
}
close() {
this.dlgRef.close();
}
update() {
this.dlgRef.close(this.datum);
}
addBest() {
this.msgBpxService.showInputbox('Add Best', 'Please input Best\'s Name').pipe(first()).subscribe(result => {
if (result) {
let obj = { groupId: this.group.id, id: '', name: result } as HappinessBEST;
this.bestService.createOrUpdate(obj).pipe(first()).subscribe(result => {
this.dlgRef.close();
});
}
});
}
copyBestUrl(best: HappinessBEST) {
var dummy = document.createElement("textarea");
document.body.appendChild(dummy);
dummy.value = `${environment.invitationUrl}${best.id}`;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
this.toastrService.success(dummy.value, "複製地址至剪貼簿!");
}
copyBestList() {
let list = "".concat(...this.group.bests.map(b => `${b.name}\t\t: ${environment.invitationUrl}${b.id}\r\n`));
var dummy = document.createElement("textarea");
document.body.appendChild(dummy);
dummy.value = list;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
this.toastrService.success(dummy.value, "複製地址至剪貼簿!");
this.lineService.pushLineMessage(LineGroup.Chris, list);
}
downloadBestQRCode(best: HappinessBEST) {
this.happinessGroupService.getBestQrCode(best.name + ".png", best.id);
}
}

View File

@ -1,4 +0,0 @@
nb-card {
max-height: 90vh;
width: 700px;
}

View File

@ -1,52 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef, NbDialogService } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { HappinessWeek } from '../../../entity/HappinessGroup';
import { PastoralDomain } from '../../../entity/PastoralDomain';
import { NumberUtils } from '../../../utilities/number-utils';
import { HappinessWeekEditorComponent } from '../happiness-week-editor/happiness-week-editor.component';
import { WeekTaskEditorComponent } from '../week-task-editor/week-task-editor.component';
@Component({
selector: 'ngx-happiness-week-list-dlg',
templateUrl: './happiness-week-list-dlg.component.html',
styleUrls: ['./happiness-week-list-dlg.component.scss']
})
export class HappinessWeekListDlgComponent implements OnInit {
group: PastoralDomain;
constructor(
private dlgRef: NbDialogRef<any>,
private dlgService: NbDialogService
) { }
ngOnInit(): void {
}
close() {
this.dlgRef.close();
}
openEditDlg(week: HappinessWeek) {
this.dlgService.open(HappinessWeekEditorComponent, {
context: {
data: week
}
}).onClose.pipe(first()).subscribe(result => {
if (result) this.close();
});
}
openTaskDlg(week: HappinessWeek) {
this.dlgService.open(WeekTaskEditorComponent, {
context: {
data: week,
allData: this.group.happinessWeeks
}
}).onClose.pipe(first()).subscribe(result => {
});
}
weekDisplay(seq: number) {
return `W${seq} ${this.group.happinessWeeks[seq - 1].topic}`;
}
}

View File

@ -1,38 +0,0 @@
<form #form="ngForm">
<nb-card>
<nb-card-header>
第 {{data.seq}} 周 {{data.topic}} 分工
</nb-card-header>
<nb-card-body *ngIf="data">
<div class="row" *ngFor="let task of data.tasks;let i = index">
<div class="col-md-4">
<div class='form-group'>
<label for='tasker{{i}}' class='label'>{{getTaskTitle(task.type)}}</label>
<op-drop-down name="tasker{{i}}" [editable]="true" [(ngModel)]="task.tasker"
[source]="taskrOptions">
</op-drop-down>
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<label for="content{{i}}" class="label">內容</label>
<input type="text" name="content{{i}}" nbInput fullWidth id="content{{i}}"
[(ngModel)]="task.content">
</div>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
[nbSpinner]="processing">
Close
</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small"
(click)="!form.invalid&&!processing&&update()" [disabled]="form.invalid" [nbSpinner]="processing">
Save
</button>
</nb-card-footer>
</nb-card>
</form>

View File

@ -1,79 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { HappinessTask, HappinessTaskType, HappinessWeek } from '../../../entity/HappinessGroup';
import { HappinessTaskService } from '../../../services/crudServices/happiness-group.service';
import { first } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption';
import { StringUtils } from '../../../utilities/string-utils';
@Component({
selector: 'ngx-week-task-editor',
templateUrl: './week-task-editor.component.html',
styleUrls: ['./week-task-editor.component.scss']
})
export class WeekTaskEditorComponent implements OnInit {
processing: boolean = false;
allData: HappinessWeek[];
data: HappinessWeek;
taskrOptions: DropDownOption[] = [];
constructor(
private happinessTaskService: HappinessTaskService,
private dlgRef: NbDialogRef<WeekTaskEditorComponent>
) {
}
ngOnInit(): void {
if (this.data.tasks.length == 0) {
this.data.tasks = [];
for (let i = HappinessTaskType.IceBreak; i <= HappinessTaskType.Dessert; i++) {
this.data.tasks.push(
{
weekId: this.data.id,
id: 'new',
type: i as HappinessTaskType,
tasker: '',
content: ''
} as HappinessTask
)
}
}
let names = [] as string[];
this.allData.forEach(week => {
if (week.tasks) {
names = names.concat(week.tasks.map(t => t.tasker.trim()));
}
});
this.taskrOptions = names.filter(function (item, pos) {
return !StringUtils.isNullOrWhitespace(item) && names.indexOf(item) == pos;
}).map(name => new DropDownOption(name, name));
}
close() {
this.dlgRef.close();
}
update() {
if (this.processing == false) {
this.processing = true;
this.happinessTaskService.createOrUpdateAll(this.data.tasks).pipe(first()).subscribe(result => {
this.processing = false;
this.dlgRef.close(true);
});
}
}
getTaskTitle(t: HappinessTaskType) {
switch (t) {
case HappinessTaskType.IceBreak: return '帶遊戲';
case HappinessTaskType.Worship: return '唱歌';
case HappinessTaskType.Testimony: return '見證';
case HappinessTaskType.Message: return '信息';
case HappinessTaskType.Gift: return '準備禮物';
case HappinessTaskType.Dessert: return '準備點心';
default: break;
}
}
}

View File

@ -33,7 +33,7 @@ export class LineMessagingAccountEditorComponent implements OnInit {
this.processing = true; this.processing = true;
let func = this.isAdding ? this.lineMessagingAccountService.createOrUpdate(this.data) : this.lineMessagingAccountService.update(this.data); let func = this.lineMessagingAccountService.createOrUpdate(this.data);//this.isAdding ? this.lineMessagingAccountService.createOrUpdate(this.data) : this.lineMessagingAccountService.update(this.data);
func.pipe(first()).subscribe(result => { func.pipe(first()).subscribe(result => {
this.processing = false; this.processing = false;
if (result) { if (result) {

View File

@ -3,11 +3,11 @@ 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 { StateService } from '../../@core/utils';
import { Log, LogLevel } from '../../entity/Log'; import { Log, LogLevel } from '../../entity/Log';
import { ScreenBase } from '../../ScreenBase'; import { ScreenBase } from '../../ScreenBase';
import { LogService } from '../../services/crudServices/log.service'; import { LogService } from '../../services/crudServices/log.service';
import { MsgBoxService } from '../../services/msg-box.service'; import { MsgBoxService } from '../../services/msg-box.service';
import { StateService } from '../../services/state.service';
import { FancyTag } from '../../ui/fancy-table/fancy-row-column.model'; import { FancyTag } from '../../ui/fancy-table/fancy-row-column.model';
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';

View File

@ -3,17 +3,18 @@ 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 { StateService } from '../../@core/utils';
import { FamilyMember } from '../../entity/Member'; import { FamilyMember } from '../../entity/Member';
import { DomainType, PastoralDomain } from '../../entity/PastoralDomain'; import { DomainType, PastoralDomain } from '../../entity/PastoralDomain';
import { BestListDlgComponent } from '../../happiness/best-list-dlg/best-list-dlg.component';
import { HappinessWeekListDlgComponent } from '../../happiness/happiness-week-list-dlg/happiness-week-list-dlg.component';
import { DomainMemberShipService } from '../../services/crudServices/domain-member-ship.service';
import { FamilyMemberService } from '../../services/crudServices/family-member.service'; import { FamilyMemberService } from '../../services/crudServices/family-member.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service'; import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { MsgBoxService } from '../../services/msg-box.service'; import { MsgBoxService } from '../../services/msg-box.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'; import { ObjectUtils } from '../../utilities/object-utils';
import { BestListDlgComponent } from '../happiness-groups/best-list-dlg/best-list-dlg.component';
import { HappinessWeekListDlgComponent } from '../happiness-groups/happiness-week-list-dlg/happiness-week-list-dlg.component';
import { PastoralDomainEditorComponent } from './pastoral-domain-editor/pastoral-domain-editor.component'; import { PastoralDomainEditorComponent } from './pastoral-domain-editor/pastoral-domain-editor.component';
@Component({ @Component({
@ -35,7 +36,8 @@ export class PastoralDomainsComponent implements OnInit {
private dlgService: NbDialogService, private dlgService: NbDialogService,
protected stateService: StateService, protected stateService: StateService,
protected route: ActivatedRoute, protected route: ActivatedRoute,
private memberService: FamilyMemberService private memberService: FamilyMemberService,
private memberShipService: DomainMemberShipService
) { ) {
} }
@ -87,6 +89,15 @@ export class PastoralDomainsComponent implements OnInit {
callback: (datum, element) => { callback: (datum, element) => {
this.openHappinessWeekListDlg(datum); this.openHappinessWeekListDlg(datum);
}, },
},
{
enabled: true,
id: 'members',
title: 'Members',
icon: 'people-outline',
callback: (datum, element) => {
this.openMemberAssignDlg(datum);
},
} }
], ],
onContextMenuOpening: (datum, menuItems) => { onContextMenuOpening: (datum, menuItems) => {
@ -94,9 +105,18 @@ export class PastoralDomainsComponent implements OnInit {
menuItems.find(i => i.id == 'edit').visible = !!datum; menuItems.find(i => i.id == 'edit').visible = !!datum;
menuItems.find(i => i.id == 'happinessBest').visible = datum.type == DomainType.HappinessGroup; menuItems.find(i => i.id == 'happinessBest').visible = datum.type == DomainType.HappinessGroup;
menuItems.find(i => i.id == 'happinessWeek').visible = datum.type == DomainType.HappinessGroup; menuItems.find(i => i.id == 'happinessWeek').visible = datum.type == DomainType.HappinessGroup;
//menuItems.find(i => i.id == 'members').visible = datum.type == DomainType.HappinessGroup;
return datum; return datum;
}, },
columns: [ columns: [
{
name: 'type',
title: 'Type',
type: 'number',
group: true,
visible: false,
groupValuePrepareFunction: (v) => this.getDomainTypeName(v.type)
},
{ {
name: 'name', name: 'name',
title: 'Name', title: 'Name',
@ -108,7 +128,7 @@ export class PastoralDomainsComponent implements OnInit {
name: 'description', name: 'description',
title: 'Description', title: 'Description',
type: 'text', type: 'text',
widthPx: 200, widthPct: 70,
//valuePrepareFunction: (item) => , //valuePrepareFunction: (item) => ,
}, },
{ {
@ -136,7 +156,6 @@ export class PastoralDomainsComponent implements OnInit {
// }, // },
], ],
}; };
//#endregion //#endregion
@ -222,5 +241,33 @@ export class PastoralDomainsComponent implements OnInit {
}); });
} }
private getDomainTypeName(type: DomainType): string {
switch (type) {
case DomainType.CellGroup: return "細胞小組";
case DomainType.HappinessGroup: return "幸福小組";
case DomainType.CellGroupCoworker: return "細胞同工";
case DomainType.ChurchCoworker: return "教會事工";
case DomainType.Administrator: return "管理員";
default: break;
}
}
private openMemberAssignDlg(datum: PastoralDomain) {
// this.msgBoxService.showListBox<FamilyMember>(
// `Members in ${datum.name}`,
// this.members,
// 'id',
// datum.familyMembers.map(m => m.id),
// (e) => `${e.firstName} ${e.lastName} (${e.email})`
// ).pipe(first()).subscribe(result => {
// if (result) {
// datum.familyMembers = result;
// this.memberShipService.updateMembersInGroup(datum).pipe(first()).subscribe(result => {
// });
// }
// });
}
} }

View File

@ -73,11 +73,11 @@ export const routes: Routes = [
path: 'games', loadChildren: () => import('./games/games.module').then(m => m.GamesModule), path: 'games', loadChildren: () => import('./games/games.module').then(m => m.GamesModule),
canActivateChild: [AuthGuardService], canActivateChild: [AuthGuardService],
data: { data: {
roles: Role.Admin | Role.Pastor roles: Role.All
} }
}, },
{ {
path: 'CellGroup', loadChildren: () => import('./cell-group/cell-group.module').then(m => m.CellGroupModule), path: 'myapp', loadChildren: () => import('./cell-group/cell-group.module').then(m => m.CellGroupModule),
canActivateChild: [AuthGuardService], canActivateChild: [AuthGuardService],
data: { data: {
roles: Role.All roles: Role.All

View File

@ -0,0 +1,93 @@
import { ActivatedRoute } from "@angular/router";
import { basename } from "path";
import { Observable, Subject } from "rxjs";
import { ScreenBase } from "../ScreenBase";
import { ICrudService } from "../services/crudServices/crud.service";
import { PastoralDomainService } from "../services/crudServices/pastoral-domain.service";
import { StateService } from "../services/state.service";
import { first } from 'rxjs/operators';
import { PastoralDomain } from "../entity/PastoralDomain";
import { Injectable } from "@angular/core";
@Injectable()
export abstract class MyAppBase {
/**
*
*/
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
// super(pastoralDomainService, stateService, route);
// this.isSnapshotView = true;
}
isLoading: boolean = true;
redirectIfNoDomains: boolean = true;
pageGroupId: string;
private domainInitialized = new Subject();;
ngOnInit(): void {
this.isLoading = true;
// this.route.paramMap.subscribe((params: ParamMap) => {
// this.id = +params.get('id')
// });
this.route.paramMap.pipe(first()).subscribe(params => {
if (params.get('groupId')) {
this.pageGroupId = params.get('groupId');
}
this.pastoralDomainService.initialized.takeUntil(this.domainInitialized).subscribe(result => {
if (result) {
this.domainInitialized.next();
if (this.redirectIfNoDomains) {
if (this.pageGroupId && !this.domains.find(d => d.id == this.pageGroupId)) {
this.pastoralDomainService.joinDomain(this.pageGroupId).pipe(first()).subscribe(result => {
this.pageOnInit();
});
return;
} else if (this.domains.length == 0) {
//TODO:SetUser
return;
}
}
this.pageOnInit();
}
});
});
}
public get domains(): PastoralDomain[] {
return this.pastoralDomainService.domains;
}
abstract pageOnInit(): void;
get saveMethod(): Observable<any> {
throw new Error("Method not implemented.");
}
public get happinessGroup(): PastoralDomain {
return this.pastoralDomainService.myHappinessGroup;
}
public get cellGroup(): PastoralDomain {
return this.pastoralDomainService.myCellGroup;
}
public get currentDomain(): PastoralDomain {
return this.domains.find(d => d.id == this.pageGroupId);
}
public get currentDomainName(): string {
if (this.domains && this.pageGroupId) {
//let group = this.domains.find(d => d.id == this.pageGroupId);
return this.domains.find(d => d.id == this.pageGroupId)?.name;
}
return "";
}
}

View File

@ -2,25 +2,58 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { NbMenuItem } from '@nebular/theme'; import { NbMenuItem } from '@nebular/theme';
import { Role } from '../entity/Auth'; import { Role } from '../entity/Auth';
import { DomainType } from '../entity/PastoralDomain';
import { CellGroupComponent } from './cell-group.component'; import { CellGroupComponent } from './cell-group.component';
import { DinnerComponent } from './dinner/dinner.component'; import { DinnerComponent } from './dinner/dinner.component';
import { FinanceComponent } from './finance/finance.component';
import { GoogleLoginComponent } from './google-login/google-login.component'; import { GoogleLoginComponent } from './google-login/google-login.component';
import { BestListComponent, HappinessWeekListComponent } from './happiness-group/happiness-group.component';
import { PrayerComponent } from './prayer/prayer.component'; import { PrayerComponent } from './prayer/prayer.component';
import { UserProfileComponent } from './user-profile/user-profile.component';
export class CellGroupRoutingConfig { export class CellGroupRoutingConfig {
public static MENU_ITEMS: NbMenuItem[] = [ public static MENU_ITEMS: NbMenuItem[] = [
{ {
title: '小組禱告', title: '細胞小組',
icon: 'people-outline', icon: 'people-outline',
link: '/CellGroup/prayer', children: [
home: true,
{
title: '小組禱告',
//icon: 'people-outline',
link: '/myapp/prayer'
}, },
{ {
title: '小組晚餐', title: '小組晚餐',
icon: 'people-outline', //icon: 'people-outline',
link: '/CellGroup/dinner', link: '/myapp/dinner',
}, },
],
data: DomainType.CellGroup
},
{
title: '幸福小組',
icon: 'smiling-face-outline',
children: [
{
title: 'Bests',
//icon: 'people-outline',
link: '/myapp/bests'
},
{
title: '幸福周',
//icon: 'people-outline',
link: '/myapp/happinessWeeks',
},
{
title: '財務表',
//icon: 'people-outline',
link: '/myapp/finance',
},
],
data: DomainType.HappinessGroup
},
] ]
} }
const routes: Routes = [ const routes: Routes = [
@ -29,7 +62,16 @@ const routes: Routes = [
children: children:
[ [
{ path: 'dinner', component: DinnerComponent, }, { path: 'dinner', component: DinnerComponent, },
{ path: 'dinner/:groupId', component: DinnerComponent, },
{ path: 'prayer', component: PrayerComponent, }, { path: 'prayer', component: PrayerComponent, },
{ path: 'prayer/:groupId', component: PrayerComponent, },
{ path: 'profile', component: UserProfileComponent, },
{ path: 'bests', component: BestListComponent, },
{ path: 'bests/:groupId', component: BestListComponent, },
{ path: 'happinessWeeks', component: HappinessWeekListComponent, },
{ path: 'happinessWeeks/:groupId', component: HappinessWeekListComponent, },
{ path: 'finance/:groupId', component: FinanceComponent, },
{ {
path: 'management', component: GoogleLoginComponent, path: 'management', component: GoogleLoginComponent,
data: { data: {

View File

@ -1,4 +1,4 @@
<ngx-one-column-layout> <ngx-one-column-layout>
<nb-menu [items]="MENU_ITEMS"></nb-menu> <nb-menu [items]="MENU_ITEMS" tag="NavMenu"></nb-menu>
<router-outlet></router-outlet> <router-outlet></router-outlet>
</ngx-one-column-layout> </ngx-one-column-layout>

View File

@ -1,5 +1,9 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { NbMenuItem } from '@nebular/theme';
import { PastoralDomainService } from '../services/crudServices/pastoral-domain.service';
import { ObjectUtils } from '../utilities/object-utils';
import { CellGroupRoutingConfig } from './cell-group-routing.module'; import { CellGroupRoutingConfig } from './cell-group-routing.module';
import { first } from 'rxjs/operators';
@Component({ @Component({
selector: 'ngx-cell-group', selector: 'ngx-cell-group',
@ -8,10 +12,45 @@ import { CellGroupRoutingConfig } from './cell-group-routing.module';
}) })
export class CellGroupComponent implements OnInit { export class CellGroupComponent implements OnInit {
MENU_ITEMS = CellGroupRoutingConfig.MENU_ITEMS; MENU_ITEMS = ObjectUtils.CloneValue(CellGroupRoutingConfig.MENU_ITEMS);
constructor() { } constructor(
private pastoralDomainService: PastoralDomainService
) {
let subscription = pastoralDomainService.initialized.subscribe(result => {
if (result) {
this.initializeSideMenu();
subscription.unsubscribe();
}
});
}
ngOnInit(): void { ngOnInit(): void {
}
} }
private initializeSideMenu() {
for (let i = 0; i < this.MENU_ITEMS.length; i++) {
const item = this.MENU_ITEMS[i];
this.checkDisplayItem(item);
}
}
private checkDisplayItem(item: NbMenuItem) {
if (item.data != undefined) {
var group = this.pastoralDomainService.domains.find(d => d.type == item.data);
if (!group) {
item.hidden = true;
return;
}
item.title = `${group.name}`;
item.children.forEach(element => {
element.link += `/${group.id}`;
});
} else if (item.children) {
item.children.forEach(element => {
this.checkDisplayItem(element);
});
}
}
}

View File

@ -5,11 +5,19 @@ import { CellGroupRoutingModule } from './cell-group-routing.module';
import { CellGroupComponent } from './cell-group.component'; import { CellGroupComponent } from './cell-group.component';
import { DinnerComponent } from './dinner/dinner.component'; import { DinnerComponent } from './dinner/dinner.component';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NbButtonModule, NbCardModule, NbIconModule, NbInputModule, NbMenuModule, NbSpinnerModule, NbToastrModule, NbToggleModule } from '@nebular/theme'; import { NbActionsModule, NbButtonModule, NbCardModule, NbCheckboxModule, NbIconModule, NbInputModule, NbMenuModule, NbSpinnerModule, NbToastrModule, NbToggleModule } from '@nebular/theme';
import { ThemeModule } from '../@theme/theme.module'; import { ThemeModule } from '../@theme/theme.module';
import { GoogleLoginComponent } from './google-login/google-login.component'; import { GoogleLoginComponent } from './google-login/google-login.component';
import { PrayerComponent } from './prayer/prayer.component'; import { PrayerComponent } from './prayer/prayer.component';
import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserProfileComponent } from './user-profile/user-profile.component';
import { DateInputModule } from '../ui/date-input/date-input.module';
import { DropDownListModule } from '../ui/drop-down-list/drop-down-list.module';
import { HappinessModule } from '../happiness/happiness.module';
import { BestListComponent, HappinessWeekListComponent } from './happiness-group/happiness-group.component';
import { JoinDomainComponent } from './join-domain/join-domain.component';
import { FinanceComponent } from './finance/finance.component';
import { AddContributionDlgComponent } from './finance/add-contribution-dlg/add-contribution-dlg.component';
import { FancyTableModule } from '../ui/fancy-table/fancy-table.module';
@NgModule({ @NgModule({
@ -18,7 +26,12 @@ import { UserProfileComponent } from './user-profile/user-profile.component';
DinnerComponent, DinnerComponent,
GoogleLoginComponent, GoogleLoginComponent,
PrayerComponent, PrayerComponent,
UserProfileComponent UserProfileComponent,
BestListComponent,
HappinessWeekListComponent,
JoinDomainComponent,
FinanceComponent,
AddContributionDlgComponent
], ],
imports: [ imports: [
ThemeModule, ThemeModule,
@ -32,8 +45,13 @@ import { UserProfileComponent } from './user-profile/user-profile.component';
NbToastrModule, NbToastrModule,
NbToggleModule, NbToggleModule,
NbIconModule, NbIconModule,
NbMenuModule NbMenuModule,
NbActionsModule,
DateInputModule,
NbCheckboxModule,
DropDownListModule,
HappinessModule,
FancyTableModule
] ]
}) })
export class CellGroupModule { } export class CellGroupModule { }

View File

@ -7,18 +7,21 @@ import { SessionService } from '../../services/session.service';
import { LineService } from '../../services/line.service'; import { LineService } from '../../services/line.service';
import { HeaderService } from '../../services/header.service'; import { HeaderService } from '../../services/header.service';
import { StringUtils } from '../../utilities/string-utils'; import { StringUtils } from '../../utilities/string-utils';
import { AuthService } from '../../services/auth.service';
import { DateUtils } from '../../utilities/date-utils'; import { DateUtils } from '../../utilities/date-utils';
import { NumberUtils } from '../../utilities/number-utils'; import { NumberUtils } from '../../utilities/number-utils';
import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service'; import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service';
import { ActivatedRoute } from '@angular/router';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { MyAppBase } from '../MyAppBase';
import { StateService } from '../../services/state.service';
import { LoginUserService } from '../../services/login-user.service';
@Component({ @Component({
selector: 'ngx-dinner', selector: 'ngx-dinner',
templateUrl: './dinner.component.html', templateUrl: './dinner.component.html',
styleUrls: ['./dinner.component.scss'] styleUrls: ['./dinner.component.scss']
}) })
export class DinnerComponent implements OnInit { export class DinnerComponent extends MyAppBase {
constructor( constructor(
private cellGroupRoutineEventsService: CellGroupRoutineEventsService, private cellGroupRoutineEventsService: CellGroupRoutineEventsService,
private messageService: MsgBoxService, private messageService: MsgBoxService,
@ -26,10 +29,12 @@ export class DinnerComponent implements OnInit {
private sessionService: SessionService, private sessionService: SessionService,
private lineService: LineService, private lineService: LineService,
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private headerService: HeaderService, private loginUserService: LoginUserService,
private authService: AuthService protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) { ) {
super(stateService, route, pastoralDomainService);
} }
cellGroupEvent: CellGroupRoutineEvents; cellGroupEvent: CellGroupRoutineEvents;
@ -40,20 +45,22 @@ export class DinnerComponent implements OnInit {
isLoading: boolean = true; isLoading: boolean = true;
processing: boolean = false; processing: boolean = false;
ngOnInit(): void { pageOnInit(): void {
this.headerService.setHeader("晚宴系統") this.stateService.SetPageTitle("晚宴系統");
this.getAllData(); this.getAllData();
} }
getAllData() { getAllData() {
this.isLoading = true; this.isLoading = true;
this.cellGroupRoutineEventsService.getComingEvent().pipe(first()).subscribe(result => { this.cellGroupRoutineEventsService.getComingEvent().pipe(first()).subscribe(result => {
this.cellGroupEvent = result; this.cellGroupEvent = result;
this.data = this.cellGroupEvent.attendees.find(a => a.id == this.authService.userAccess.memberId); this.data = this.cellGroupEvent.attendees.find(a => a.id == this.loginUserService.userAccess.memberId);
if (!this.data) { if (!this.data) {
this.data = new CellGroupRoutineEventAttendee(this.cellGroupEvent.id, this.authService.userAccess.memberId); this.data = new CellGroupRoutineEventAttendee(this.cellGroupEvent.id, this.loginUserService.userAccess.memberId);
this.data.name = this.authService.userAccess.firstName; this.data.name = this.loginUserService.userAccess.firstName;
this.cellGroupEvent.attendees.push(this.data); this.cellGroupEvent.attendees.push(this.data);
} else { } else {
this.potluckItems = this.data.potluckItem.split("|").map(s => new DinnerInfo(s)); this.potluckItems = this.data.potluckItem.split("|").map(s => new DinnerInfo(s));
@ -70,7 +77,7 @@ export class DinnerComponent implements OnInit {
this.processing = false; this.processing = false;
this.toastrService.success('菜單更新完成!'); this.toastrService.success('菜單更新完成!');
this.lineService.pushCommandMessage('Cac4ac5a8d7fc52daa444d71dc7c360a9', 'dinner'); this.lineService.pushCommandMessage(this.cellGroupEvent.pastoralDomainId, 'dinner');
}); });
} }
@ -88,9 +95,6 @@ export class DinnerComponent implements OnInit {
this.potluckItems.push(new DinnerInfo('')); this.potluckItems.push(new DinnerInfo(''));
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
logout() {
this.authService.logout();
}
} }
export class DinnerInfo { export class DinnerInfo {

View File

@ -0,0 +1,36 @@
<nb-card status="success">
<nb-card-header>
新增奉獻
</nb-card-header>
<nb-card-body>
<div class="row form-group">
<div class="col-6 col-md-4">
<div class='form-group'>
<label for='contributor' class='label'>奉獻人</label>
<input type="text" name="contributor" nbInput fullWidth id="contributor"
[(ngModel)]="contribution.contributor">
</div>
</div>
<div class="col-6 col-md-3">
<div class="form-group">
<label for="costAmount" class="label">金額</label>
<input type="number" name="costAmount" nbInput fullWidth id="costAmount"
[(ngModel)]="contribution.amount">
</div>
</div>
<div class="col-12 col-md-5">
<div class="form-group">
<label for="costContent" class="label">備註</label>
<input type="text" name="costContent" nbInput fullWidth id="costContent"
[(ngModel)]="contribution.comment">
</div>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()">Close</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small" (click)="submit()">Submit</button>
</nb-card-footer>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddContributionDlgComponent } from './add-contribution-dlg.component';
describe('AddContributionDlgComponent', () => {
let component: AddContributionDlgComponent;
let fixture: ComponentFixture<AddContributionDlgComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AddContributionDlgComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddContributionDlgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { Contribution } from '../../../entity/contribution.model';
import { DropDownOption } from '../../../entity/dropDownOption';
import { AuthService } from '../../../services/auth.service';
@Component({
selector: 'ngx-add-contribution-dlg',
templateUrl: './add-contribution-dlg.component.html',
styleUrls: ['./add-contribution-dlg.component.scss']
})
export class AddContributionDlgComponent implements OnInit {
taskrOptions: DropDownOption[] = [];
contribution: Contribution;
constructor(
private authService: AuthService,
private dlgRef: NbDialogRef<AddContributionDlgComponent>
) { }
ngOnInit(): void {
this.contribution = { time: new Date() } as Contribution;
}
close() {
this.dlgRef.close();
}
submit() {
if (this.contribution.contributor && this.contribution.amount > 0) {
this.dlgRef.close(this.contribution);
}
}
}

View File

@ -0,0 +1,13 @@
<nb-card [nbSpinner]="isLoading" nbSpinnerStatus="info" nbSpinnerSize="giant">
<nb-card-header>
{{currentDomainName}} 財務表
<nb-actions size="small" fullWidth class="float-right">
<nb-action icon="plus-outline" title="Add New" (click)="addContribution()"></nb-action>
</nb-actions>
</nb-card-header>
<nb-card-body>
<ng-container *ngIf="!isLoading">
<fancy-table [data]="contributions" [settings]="contributionSettings" [footerData]="summary"></fancy-table>
</ng-container>
</nb-card-body>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FinanceComponent } from './finance.component';
describe('FinanceComponent', () => {
let component: FinanceComponent;
let fixture: ComponentFixture<FinanceComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FinanceComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FinanceComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,208 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NbDialogService, NbToastrService } from '@nebular/theme';
import { Contribution } from '../../entity/contribution.model';
import { AuthService } from '../../services/auth.service';
import { ContributionService } from '../../services/crudServices/contribution.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { LineService } from '../../services/line.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { SessionService } from '../../services/session.service';
import { StateService } from '../../services/state.service';
import { MyAppBase } from '../MyAppBase';
import { AddContributionDlgComponent } from './add-contribution-dlg/add-contribution-dlg.component';
import { first } from 'rxjs/operators';
import { FancySettings } from '../../ui/fancy-table/fancy-settings.model';
import { ObjectUtils } from '../../utilities/object-utils';
import { NumberUtils } from '../../utilities/number-utils';
import { StringUtils } from '../../utilities/string-utils';
@Component({
selector: 'ngx-finance',
templateUrl: './finance.component.html',
styleUrls: ['./finance.component.scss']
})
export class FinanceComponent extends MyAppBase {
contributions: Contribution[];
summary: Contribution[];
//#region Table Setting
contributionSettings = <FancySettings<Contribution>>{
contextMenuItems: [
// {
// id: 'add',
// enabled: true,
// title: 'Add New',
// icon: 'plus-circle-outline',
// callback: (datum, element) => {
// this.openEditingDialog(true);
// },
// },
// {
// id: 'edit',
// enabled: true,
// title: 'Edit',
// icon: 'edit',
// callback: (datum, element) => {
// this.openEditingDialog(false);
// },
// },
{
enabled: true,
id: 'delete',
title: 'Delete',
icon: 'trash-2-outline',
callback: (datum, element) => {
this.contributionService.delete(datum.id).pipe(first()).subscribe(result => {
});
},
}],
onContextMenuOpening: (datum, menuItems) => {
menuItems.find(i => i.id == 'delete').visible = !!datum;
//menuItems.find(i => i.id == 'edit').visible = !!datum;
return datum;
},
columns: [
{
name: 'time',
title: 'Time',
type: 'date',
widthPx: 120,
//valuePrepareFunction: (item) => ,
},
{
name: 'contributor',
title: 'Contributor',
type: 'text',
widthPx: 150,
//valuePrepareFunction: (item) => ,
},
{
name: 'amount',
title: 'Amount',
type: 'text',
widthPx: 100,
class: 'text-right',
footerClass: 'text-right',
valuePrepareFunction: getAmountDisplay,
},
{
name: 'comment',
title: 'Comment',
type: 'text',
widthPx: 200,
//valuePrepareFunction: (item) => ,
},
],
showFooter: true
};
//#endregion
constructor(
private contributionService: ContributionService,
private messageService: MsgBoxService,
private toastrService: NbToastrService,
private sessionService: SessionService,
private lineService: LineService,
private cdRef: ChangeDetectorRef,
private authService: AuthService,
private dlgService: NbDialogService,
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
pageOnInit(): void {
this.initializeData();
}
addContribution() {
this.dlgService.open(AddContributionDlgComponent, {
context: {
}
}).onClose.pipe(first()).subscribe(result => {
if (result) {
result.groupId = this.currentDomain.id;
this.isLoading = true;
this.contributionService.createOrUpdate(result).pipe(first()).subscribe(result => {
this.refreshData();
});
}
});
}
private refreshData() {
this.isLoading = true;
this.pastoralDomainService.initialize().pipe(first()).subscribe(result => {
this.initializeData();
});
}
private initializeData() {
this.contributions = ObjectUtils.CloneValue(this.currentDomain.contributions);
this.summary = [];
let totalComment = '';
let totalAmount = this.contributions.map(d => d.amount).reduce((a, b) => a + b, 0);
//Append Cost
if (this.currentDomain.happinessWeeks) {
for (let i = 0; i < this.currentDomain.happinessWeeks.length; i++) {
const week = this.currentDomain.happinessWeeks[i];
let weekCost = 0;
for (let j = 0; j < week.costs.length; j++) {
const weekCostItem = week.costs[j];
this.contributions.push(
{
time: week.date,
contributor: `W${i + 1} - ${weekCostItem.tasker}`,
amount: -1 * weekCostItem.amount,
comment: weekCostItem.content
} as Contribution);
weekCost += weekCostItem.amount;
}
if (weekCost > 0) {
this.contributions.push(
{
time: week.date,
contributor: `${week.topic} - Sub Total`,
amount: 0,
comment: '$ ' + NumberUtils.FormatCurrency(weekCost)
} as Contribution);
}
}
let remainWeeks = this.currentDomain.happinessWeeks.filter(w => w.date >= new Date()).length;
totalAmount = this.contributions.map(d => d.amount).reduce((a, b) => a + b, 0);
totalComment = StringUtils.getHtmlBadge(`剩餘每周(${remainWeeks}) 預算: $${NumberUtils.FormatCurrency(totalAmount / remainWeeks)}`, 'info');
}
this.summary.push(
{
time: null,
contributor: `Summary`,
amount: totalAmount,
comment: totalComment
} as Contribution);
this.isLoading = false;
}
}
function getAmountDisplay(item: Contribution): string {
if (item.amount != 0) {
return StringUtils.getHtmlBadge(NumberUtils.FormatCurrency(item.amount), item.amount > 0 ? 'success' : 'warning');
}
return "---------------";
}

View File

@ -0,0 +1,48 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { StateService } from '../../services/state.service';
import { MyAppBase } from '../MyAppBase';
@Component({
selector: 'ngx-happiness-group',
template: `
<best-list [group]="happinessGroup"></best-list>
`,
styleUrls: ['./happiness-group.component.scss']
})
export class BestListComponent extends MyAppBase {
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
pageOnInit(): void {
this.stateService.SetPageTitle("Best");
}
}
@Component({
selector: 'ngx-happiness-week-group',
template: `<happiness-week-list [group]="happinessGroup"></happiness-week-list>
`,
styleUrls: ['./happiness-group.component.scss']
})
export class HappinessWeekListComponent extends MyAppBase {
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
}
pageOnInit(): void {
this.stateService.SetPageTitle("Happiness Weeks");
}
}

View File

@ -0,0 +1,17 @@
<nb-card status="success">
<nb-card-header>
加入小組
</nb-card-header>
<nb-card-body>
<div class='col-12 col-md-6'>
<div class='form-group'>
<label for='domain' class='label'>小組</label>
<op-drop-down name='domain' id='domain' [(ngModel)]='domainId' [source]='domainOptions'></op-drop-down>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right mr-2" fullWidth nbButton hero status="primary" size="large"
(click)="joinDomain()">加入小組</button>
</nb-card-footer>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JoinDomainComponent } from './join-domain.component';
describe('JoinDomainComponent', () => {
let component: JoinDomainComponent;
let fixture: ComponentFixture<JoinDomainComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ JoinDomainComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(JoinDomainComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DomainType, PastoralDomain } from '../../entity/PastoralDomain';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { StateService } from '../../services/state.service';
import { MyAppBase } from '../MyAppBase';
import { first } from 'rxjs/operators';
import { DropDownOption } from '../../entity/dropDownOption';
@Component({
selector: 'ngx-join-domain',
templateUrl: './join-domain.component.html',
styleUrls: ['./join-domain.component.scss']
})
export class JoinDomainComponent extends MyAppBase {
constructor(
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) {
super(stateService, route, pastoralDomainService);
this.redirectIfNoDomains = false;
}
domainId: string;
allDomains: PastoralDomain[];
domainOptions: DropDownOption[] = [];
pageOnInit(): void {
this.isLoading = true;
this.pastoralDomainService.getAll().pipe(first()).subscribe(result => {
this.allDomains = result.filter(d => d.type == DomainType.CellGroup || d.type == DomainType.HappinessGroup);
this.domainOptions = this.allDomains.map(d => new DropDownOption(d.id, d.name));
this.isLoading = false;
});
}
joinDomain() {
}
}

View File

@ -1,37 +1,40 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NbToastrService } from '@nebular/theme'; import { NbToastrService } from '@nebular/theme';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { BindingHelper } from '../../entity/BindingHelper'; import { BindingHelper } from '../../entity/BindingHelper';
import { CellGroupRoutineEvents, CellGroupRoutineEventPrayer, CellGroupRoutineEventAttendee } from '../../entity/CellGroupRoutineEvents'; import { CellGroupRoutineEvents, CellGroupRoutineEventPrayer, CellGroupRoutineEventAttendee } from '../../entity/CellGroupRoutineEvents';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service'; import { CellGroupRoutineEventsService } from '../../services/crudServices/cell-group-routine-events.service';
import { PastoralDomainService } from '../../services/crudServices/pastoral-domain.service';
import { HeaderService } from '../../services/header.service'; import { HeaderService } from '../../services/header.service';
import { LineService } from '../../services/line.service'; import { LineService } from '../../services/line.service';
import { LoginUserService } from '../../services/login-user.service';
import { MsgBoxService } from '../../services/msg-box.service'; import { MsgBoxService } from '../../services/msg-box.service';
import { SessionService } from '../../services/session.service'; import { SessionService } from '../../services/session.service';
import { StateService } from '../../services/state.service';
import { DateUtils } from '../../utilities/date-utils'; import { DateUtils } from '../../utilities/date-utils';
import { NumberUtils } from '../../utilities/number-utils'; import { NumberUtils } from '../../utilities/number-utils';
import { DinnerInfo } from '../dinner/dinner.component'; import { DinnerInfo } from '../dinner/dinner.component';
import { MyAppBase } from '../MyAppBase';
@Component({ @Component({
selector: 'ngx-prayer', selector: 'ngx-prayer',
templateUrl: './prayer.component.html', templateUrl: './prayer.component.html',
styleUrls: ['./prayer.component.scss'] styleUrls: ['./prayer.component.scss']
}) })
export class PrayerComponent implements OnInit { export class PrayerComponent extends MyAppBase {
constructor( constructor(
private cellGroupRoutineEventsService: CellGroupRoutineEventsService, private cellGroupRoutineEventsService: CellGroupRoutineEventsService,
private messageService: MsgBoxService,
private toastrService: NbToastrService, private toastrService: NbToastrService,
private sessionService: SessionService,
private lineService: LineService,
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private headerService: HeaderService, private loginUserService: LoginUserService,
private authService: AuthService private authService: AuthService,
protected stateService: StateService,
protected route: ActivatedRoute,
protected pastoralDomainService: PastoralDomainService
) { ) {
super(stateService, route, pastoralDomainService);
} }
cellGroupEvent: CellGroupRoutineEvents; cellGroupEvent: CellGroupRoutineEvents;
@ -42,8 +45,8 @@ export class PrayerComponent implements OnInit {
isLoading: boolean = true; isLoading: boolean = true;
processing: boolean = false; processing: boolean = false;
ngOnInit(): void { pageOnInit(): void {
this.headerService.setHeader("禱告中心") this.stateService.SetPageTitle("禱告中心");
this.getAllData(); this.getAllData();
} }
getAllData() { getAllData() {
@ -52,9 +55,9 @@ export class PrayerComponent implements OnInit {
this.cellGroupRoutineEventsService.getLastEvent().pipe(first()).subscribe(result => { this.cellGroupRoutineEventsService.getLastEvent().pipe(first()).subscribe(result => {
this.cellGroupEvent = result; this.cellGroupEvent = result;
this.data = this.cellGroupEvent.prayers.find(a => a.memberId == this.authService.userAccess.memberId); this.data = this.cellGroupEvent.prayers.find(a => a.memberId == this.loginUserService.userAccess.memberId);
if (!this.data) { if (!this.data) {
this.data = new CellGroupRoutineEventPrayer(this.cellGroupEvent.id, this.authService.userAccess.memberId); this.data = new CellGroupRoutineEventPrayer(this.cellGroupEvent.id, this.loginUserService.userAccess.memberId);
this.cellGroupEvent.prayers.push(this.data); this.cellGroupEvent.prayers.push(this.data);
this.prayer = new BindingHelper(['']); this.prayer = new BindingHelper(['']);
} else { } else {

View File

@ -5,20 +5,21 @@
</nb-card-header> </nb-card-header>
<nb-card-body> <nb-card-body>
<div class="row" *ngIf="data"> <div class="row" *ngIf="data">
<div class="col-md-3"> <div class="col-6 col-md-3">
<div class="form-group"> <div class="form-group">
<label for="firstName" class="label">First Name</label> <label for="firstName" class="label">First Name</label>
<input type="text" name="firstName" nbInput fullWidth id="firstName" <input type="text" name="firstName" nbInput fullWidth id="firstName"
[(ngModel)]="data.firstName"> [(ngModel)]="data.firstName">
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-6 col-md-3">
<div class="form-group"> <div class="form-group">
<label for="lastName" class="label">Last Name</label> <label for="lastName" class="label">Last Name</label>
<input type="text" name="lastName" nbInput fullWidth id="lastName" [(ngModel)]="data.lastName"> <input type="text" name="lastName" nbInput fullWidth id="lastName" [(ngModel)]="data.lastName">
</div> </div>
</div> </div>
<div class="col-md-2">
<div class="col-4 col-md-2">
<div class="form-group"> <div class="form-group">
<label for="gender" class="label">Gender</label> <label for="gender" class="label">Gender</label>
<op-drop-down id="gender" name="gender" [(ngModel)]='data.gender' [source]="GenderOptions"> <op-drop-down id="gender" name="gender" [(ngModel)]='data.gender' [source]="GenderOptions">
@ -27,25 +28,21 @@
</div> </div>
</div> </div>
<div class="col-md-2">
<div class="form-group">
<nb-checkbox id="married" name="married" [(ngModel)]="data.married">Married</nb-checkbox>
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<nb-checkbox id="baptized" name="baptized" [(ngModel)]="data.baptized">Baptized</nb-checkbox>
</div>
</div>
<div class="col-md-6"> <div class="col-8 col-md-4">
<div class='form-group'>
<label for='birthday' class='label'>Birthday</label>
<op-date-input id='birthday' name='birthday' [(ngModel)]='data.birthday'></op-date-input>
</div>
</div>
<div class="col-12 col-md-6">
<div class="form-group"> <div class="form-group">
<label for="email" class="label">Email</label> <label for="email" class="label">Email</label>
<input type="text" name="email" nbInput fullWidth id="email" [(ngModel)]="data.email" readonly> <input type="text" name="email" nbInput fullWidth id="email" [(ngModel)]="data.email" readonly>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-12 col-md-6">
<div class="form-group"> <div class="form-group">
<label for="address" class="label">Address</label> <label for="address" class="label">Address</label>
<input type="text" name="address" nbInput fullWidth id="address" [(ngModel)]="data.address"> <input type="text" name="address" nbInput fullWidth id="address" [(ngModel)]="data.address">
@ -58,30 +55,30 @@
[(ngModel)]="data.avatarImage"> [(ngModel)]="data.avatarImage">
</div> </div>
</div> --> </div> -->
<div class="col-md-4">
<div class='form-group'>
<label for='birthday' class='label'>Birthday</label>
<input type='text' nbInput fullWidth id='birthday' name='birthday' [(ngModel)]='data.birthday'
[nbDatepicker]="dateTimePickerbirthday">
<nb-date-timepicker format="yyyy/MM/dd" #dateTimePickerbirthday></nb-date-timepicker>
</div>
</div>
<div class="col-md-4">
<div class='form-group'>
<label for='dateOfBaptized' class='label'>Date Of Baptized</label>
<input type='text' nbInput fullWidth id='dateOfBaptized' name='dateOfBaptized'
[(ngModel)]='data.dateOfBaptized' [nbDatepicker]="dateTimePickerdateOfBaptized">
<nb-datepicker format="yyyy/MM/dd" #dateTimePickerdateOfBaptized></nb-datepicker>
</div>
</div>
<div class="col-md-4"> <div class="col-6 col-md-2 align-self-md-end">
<div class="form-group">
<nb-checkbox id="married" name="married" [(ngModel)]="data.married">Married</nb-checkbox>
</div>
</div>
<div class="col-6 col-md-2 align-self-md-end">
<div class="form-group">
<nb-checkbox id="baptized" name="baptized" [(ngModel)]="data.baptized">Baptized</nb-checkbox>
</div>
</div>
<div class="col-12 col-md-4">
<div class='form-group'> <div class='form-group'>
<label for='dateOfWalkIn' class='label'>Date Of Walk In</label> <label for='dateOfWalkIn' class='label'>Date Of Walk In</label>
<input type='text' nbInput fullWidth id='dateOfWalkIn' name='dateOfWalkIn' <op-date-input id='dateOfWalkIn' name='dateOfWalkIn' [(ngModel)]='data.dateOfWalkIn'>
[(ngModel)]='data.dateOfWalkIn' [nbDatepicker]="dateTimePickerdateOfWalkIn"> </op-date-input>
<nb-date-timepicker format="yyyy/MM/dd" #dateTimePickerdateOfWalkIn></nb-date-timepicker> </div>
</div>
<div class="col-12 col-md-4">
<div class='form-group' *ngIf="data.baptized">
<label for='dateOfBaptized' class='label'>Date Of Baptized</label>
<op-date-input id='dateOfBaptized' name='dateOfBaptized' [(ngModel)]='data.dateOfBaptized'>
</op-date-input>
</div> </div>
</div> </div>
</div> </div>
@ -89,10 +86,10 @@
</nb-card-body> </nb-card-body>
<nb-card-footer> <nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()" <!-- <button class="float-right" nbButton hero status="danger" size="small" (click)="close()"
[nbSpinner]="processing"> [nbSpinner]="processing">
Cancel Cancel
</button> </button> -->
<button class="float-right mr-2" nbButton hero status="primary" size="small" <button class="float-right mr-2" nbButton hero status="primary" size="small"
(click)="!form.invalid&&!processing&&update()" [disabled]="form.invalid" [nbSpinner]="processing"> (click)="!form.invalid&&!processing&&update()" [disabled]="form.invalid" [nbSpinner]="processing">
Save Save

View File

@ -1,25 +1,31 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FamilyMember } from '../../entity/Member'; import { FamilyMember } from '../../entity/Member';
import { AuthService } from '../../services/auth.service';
import { FamilyMemberService } from '../../services/crudServices/family-member.service'; import { FamilyMemberService } from '../../services/crudServices/family-member.service';
import { SessionService } from '../../services/session.service';
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
import { DropDownOptions } from '../../entity/dropDownOption';
import { NbToastrService } from '@nebular/theme';
import { LoginUserService } from '../../services/login-user.service';
@Component({ @Component({
selector: 'ngx-user-profile', selector: 'ngx-user-profile',
templateUrl: './user-profile.component.html', templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss'] styleUrls: ['./user-profile.component.scss']
}) })
export class UserProfileComponent implements OnInit { export class UserProfileComponent implements OnInit {
GenderOptions = DropDownOptions.GenderOptions;
constructor( constructor(
private memberService: FamilyMemberService, private memberService: FamilyMemberService,
private authService: AuthService private loginUserService: LoginUserService,
private toastrService: NbToastrService,
) { } ) { }
data: FamilyMember; data: FamilyMember;
processing: boolean; processing: boolean;
ngOnInit(): void { ngOnInit(): void {
this.memberService.getById(this.authService.userAccess.memberId).pipe(first()).subscribe(result => { this.memberService.getById(this.loginUserService.userAccess.memberId).pipe(first()).subscribe(result => {
this.data = result; this.data = result;
if (this.data.dateOfBaptized == undefined) this.data.dateOfBaptized = null;
if (this.data.dateOfWalkIn == undefined) this.data.dateOfWalkIn = null;
if (this.data.birthday == undefined) this.data.birthday = null;
}); });
} }
@ -29,6 +35,7 @@ export class UserProfileComponent implements OnInit {
update() { update() {
this.memberService.createOrUpdate(this.data).pipe(first()).subscribe(result => { this.memberService.createOrUpdate(this.data).pipe(first()).subscribe(result => {
this.toastrService.success('更新完成!', '個人資料');
}); });
} }
} }

View File

@ -20,6 +20,8 @@ export interface LoginTokenViewModel {
avatarImage: string; avatarImage: string;
role: Role; role: Role;
cellGroup: PastoralDomain; cellGroup: PastoralDomain;
signalRSessionId;
sessionTabId: string;
} }
export interface GoogleUserInfo { export interface GoogleUserInfo {

View File

@ -4,6 +4,7 @@ export interface CellGroupRoutineEvents {
address: string; address: string;
attendees: CellGroupRoutineEventAttendee[]; attendees: CellGroupRoutineEventAttendee[];
prayers: CellGroupRoutineEventPrayer[]; prayers: CellGroupRoutineEventPrayer[];
pastoralDomainId: string;
} }

View File

@ -1,3 +1,4 @@
import { HappinessCost } from "./happiness-cost.model";
import { PastoralDomain } from "./PastoralDomain"; import { PastoralDomain } from "./PastoralDomain";
export interface HappinessWeek { export interface HappinessWeek {
@ -11,6 +12,7 @@ export interface HappinessWeek {
seq: number; seq: number;
updateRestWeekDate: boolean; updateRestWeekDate: boolean;
tasks: HappinessTask[]; tasks: HappinessTask[];
costs: HappinessCost[];
topic: string topic: string
} }

View File

@ -1,3 +1,4 @@
import { Contribution } from "./contribution.model";
import { HappinessBEST, HappinessWeek } from "./HappinessGroup"; import { HappinessBEST, HappinessWeek } from "./HappinessGroup";
import { FamilyMember } from "./Member"; import { FamilyMember } from "./Member";
@ -6,7 +7,7 @@ export enum DomainType {
HappinessGroup, HappinessGroup,
CellGroupCoworker, CellGroupCoworker,
ChurchCoworker, ChurchCoworker,
Person = 99 Administrator = 99
} }
export interface PastoralDomain { export interface PastoralDomain {
@ -27,6 +28,7 @@ export interface PastoralDomain {
bests: HappinessBEST[]; bests: HappinessBEST[];
happinessWeeks: HappinessWeek[]; happinessWeeks: HappinessWeek[];
contributions: Contribution[];
serviceAddress: AddressInfo; serviceAddress: AddressInfo;
type: DomainType; type: DomainType;

View File

@ -0,0 +1,7 @@
import { Contribution } from './contribution.model';
describe('Contribution', () => {
it('should create an instance', () => {
expect(new Contribution()).toBeTruthy();
});
});

View File

@ -0,0 +1,8 @@
export interface Contribution {
groupId: string;
id: string;
contributor: string;
amount: number;
comment: string;
time: Date;
}

View File

@ -0,0 +1,7 @@
import { HappinessCost } from './happiness-cost.model';
describe('HappinessCost', () => {
it('should create an instance', () => {
expect(new HappinessCost()).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
export class HappinessCost {
weekId: string;
id: string;
tasker: string;
content: string;
amount: number;
}

View File

@ -8,12 +8,12 @@ import * as signalR from "@microsoft/signalr"
import { StringUtils } from '../../utilities/string-utils'; import { StringUtils } from '../../utilities/string-utils';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { MsgBoxService } from '../../services/msg-box.service'; import { MsgBoxService } from '../../services/msg-box.service';
import { ADButtons, ADIcon } from '../../ui/alert-dlg/alert-dlg.component';
import { UuidUtils } from '../../utilities/uuid-utils'; import { UuidUtils } from '../../utilities/uuid-utils';
import { ObjectUtils } from '../../utilities/object-utils'; import { ObjectUtils } from '../../utilities/object-utils';
import { NbToastrService } from '@nebular/theme'; import { NbToastrService } from '@nebular/theme';
import { AvalonBase } from './avalonBase'; import { AvalonBase } from './avalonBase';
import { HeaderService } from '../../services/header.service'; import { HeaderService } from '../../services/header.service';
import { ADIcon, ADButtons } from '../../ui/alert-dlg/alert-dlg.model';
const minimumPlayers = 5; const minimumPlayers = 5;
const maximumPlayers = 10; const maximumPlayers = 10;
@ -181,7 +181,7 @@ export class AvalonComponent extends AvalonBase implements OnInit {
public showMyRole() { public showMyRole() {
let roleInfo = this.getRoleInfoByRole(this.me.role); let roleInfo = this.getRoleInfoByRole(this.me.role);
this.msgBoxService.show(roleInfo.name, roleInfo.description, ADIcon.INFO) this.msgBoxService.show(roleInfo.name, { text: roleInfo.description, icon: ADIcon.INFO });
} }
public showMySecret() { public showMySecret() {
//let roleInfo = this.getRoleInfoByRole(this.me.role); //let roleInfo = this.getRoleInfoByRole(this.me.role);
@ -208,7 +208,7 @@ export class AvalonComponent extends AvalonBase implements OnInit {
break; break;
} }
this.msgBoxService.show('噓...', description, ADIcon.INFO); this.msgBoxService.show('噓...', { text: description, icon: ADIcon.INFO });
} }
private initializeQuestInfo() { private initializeQuestInfo() {
@ -251,7 +251,7 @@ export class AvalonComponent extends AvalonBase implements OnInit {
previousStage() { previousStage() {
this.msgBoxService.show('上一棟', '你確定?', ADIcon.QUESTION, ADButtons.YesNo).pipe(first()).subscribe(result => { this.msgBoxService.show('上一棟', { text: '你確定?', icon: ADIcon.QUESTION, buttons: ADButtons.YesNo }).pipe(first()).subscribe(result => {
if (result) { if (result) {
this.avalonService.data.stage -= 1; this.avalonService.data.stage -= 1;
this.clearVoteStatus(); this.clearVoteStatus();
@ -264,14 +264,14 @@ export class AvalonComponent extends AvalonBase implements OnInit {
switch (this.avalonService.data.stage) { switch (this.avalonService.data.stage) {
case AvalonStage.JoinGame: case AvalonStage.JoinGame:
if (this.players.length < 5) { if (this.players.length < 5) {
this.msgBoxService.show("人數不足!", "遊戲最少需要五人"); this.msgBoxService.show("人數不足!", { text: "遊戲最少需要五人" });
return; return;
} }
this.gameSize = Math.max(0, this.players.length - 6); this.gameSize = Math.max(0, this.players.length - 6);
if (this.avalonService.data.roles.filter(r => r.enabled && false == r.isGood).length != players_evil[this.gameSize]) { if (this.avalonService.data.roles.filter(r => r.enabled && false == r.isGood).length != players_evil[this.gameSize]) {
this.msgBoxService.show("反派人數錯誤!", "反派人數需要 " + players_evil[this.gameSize].toString() + " 人"); this.msgBoxService.show("反派人數錯誤!", { text: "反派人數需要 " + players_evil[this.gameSize].toString() + " 人" });
return; return;
} }

View File

@ -4,7 +4,7 @@ import { first } from 'rxjs/operators';
import { GameInfo, Player, Role, RoleInfo } from '../../../entity/Avalon'; import { GameInfo, Player, Role, RoleInfo } from '../../../entity/Avalon';
import { AvalonService } from '../../../services/avalon.service'; import { AvalonService } from '../../../services/avalon.service';
import { MsgBoxService } from '../../../services/msg-box.service'; import { MsgBoxService } from '../../../services/msg-box.service';
import { ADButtons, ADIcon } from '../../../ui/alert-dlg/alert-dlg.component'; import { ADButtons, ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
import { AvalonBase } from '../avalonBase'; import { AvalonBase } from '../avalonBase';
@Component({ @Component({
@ -38,7 +38,7 @@ export class ChooseCharacterComponent extends AvalonBase implements OnInit {
// return; // return;
this.ngZone.run( this.ngZone.run(
_ => { _ => {
this.msgBoxService.show(roleInfo.name, roleInfo.description + '<br>你確定你是?', ADIcon.QUESTION, ADButtons.YesNo).pipe(first()).subscribe(result => { this.msgBoxService.show(roleInfo.name, { text: roleInfo.description + '<br>你確定你是?', icon: ADIcon.QUESTION, buttons: ADButtons.YesNo }).pipe(first()).subscribe(result => {
if (result) { if (result) {
if (this.players.find(p => p.role == roleInfo.role) && roleInfo.role != Role.ArthurKnight) { if (this.players.find(p => p.role == roleInfo.role) && roleInfo.role != Role.ArthurKnight) {
this.toastrService.danger('腳色重複:' + roleInfo.name, '你確定沒有選錯腳色??') this.toastrService.danger('腳色重複:' + roleInfo.name, '你確定沒有選錯腳色??')

View File

@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, NgZone, OnInit } from '@angular/core';
import { Player, AvalonStage } from '../../../entity/Avalon'; import { Player, AvalonStage } from '../../../entity/Avalon';
import { AvalonService } from '../../../services/avalon.service'; import { AvalonService } from '../../../services/avalon.service';
import { MsgBoxService } from '../../../services/msg-box.service'; import { MsgBoxService } from '../../../services/msg-box.service';
import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.component'; import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
import { AvalonBase } from '../avalonBase'; import { AvalonBase } from '../avalonBase';
@Component({ @Component({
@ -38,7 +38,7 @@ export class PickTeammateComponent extends AvalonBase implements OnInit {
this.avalonService.applyCdChange$.next(); this.avalonService.applyCdChange$.next();
} else { } else {
this.ngZone.run(_ => { this.ngZone.run(_ => {
this.msgBoxService.show('任務編組錯誤!', `本次任務出場人數為 ${this.currentQuest.teamSize} 人!`, ADIcon.WARNING); this.msgBoxService.show('任務編組錯誤!', { text: `本次任務出場人數為 ${this.currentQuest.teamSize} 人!`, icon: ADIcon.WARNING });
}); });
} }
} }

View File

@ -1,8 +1,41 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { NbMenuItem } from '@nebular/theme';
import { AvalonComponent } from './avalon/avalon.component'; 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 { MassiveDarkness2Component } from './massive-darkness2/massive-darkness2.component';
export class GameRoomMenuConfig {
public static HostMenu: NbMenuItem[] = [
{
title: 'Dashboard',
icon: 'people-outline',
children: [
{
title: '小組禱告',
//icon: 'people-outline',
link: '/games/MD2'
},
],
},
];
public static PlayerMenu: NbMenuItem[] = [
{
title: 'Hero Dashboard',
icon: 'people-outline',
children: [
{
title: '小組禱告',
//icon: 'people-outline',
link: '/myapp/prayer'
},
],
},
];
}
const routes: Routes = [ const routes: Routes = [
{ {
path: '', component: GamesComponent path: '', component: GamesComponent
@ -12,6 +45,8 @@ const routes: Routes = [
[ [
{ path: 'avalon', component: AvalonComponent }, { path: 'avalon', component: AvalonComponent },
{ path: 'avalonHost', component: AvalonComponent }, { path: 'avalonHost', component: AvalonComponent },
{ path: 'MD2', component: MassiveDarkness2Component },
{ path: 'MD2_Hero/:roomId', component: HeroDashboardComponent },
] ]
}, },

View File

@ -1,4 +1,6 @@
<ngx-one-column-layout> <ngx-plain-layout>
<!-- <nb-menu [items]="MENU_ITEMS"></nb-menu> --> <!-- <nb-menu [items]="gameRoomService.sideMenu"></nb-menu> -->
<router-outlet></router-outlet> <router-outlet></router-outlet>
</ngx-one-column-layout> </ngx-plain-layout>
<!-- ngx-plain-layout ngx-one-column-layout-->

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { GameRoomService } from '../services/game-room.service';
@Component({ @Component({
selector: 'ngx-games', selector: 'ngx-games',
@ -8,7 +9,9 @@ import { Title } from '@angular/platform-browser';
}) })
export class GamesComponent implements OnInit { export class GamesComponent implements OnInit {
constructor() { } constructor(
public gameRoomService: GameRoomService
) { }
ngOnInit(): void { ngOnInit(): void {
//this.browserTitleService.setTitle('我是遊戲王'); //this.browserTitleService.setTitle('我是遊戲王');

View File

@ -0,0 +1,17 @@
export interface IGamePlayer {
id: string;
name: string;
gameRoomId: string;
isPlayer: boolean;
signalRClientId: string;
tabId: string;
}
export class GamePlayer implements IGamePlayer {
id: string;
name: string;
gameRoomId: string;
isPlayer: boolean;
signalRClientId: string;
tabId: string;
}

View File

@ -4,7 +4,7 @@ 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';
import { NbMenuModule, NbInputModule, NbCardModule, NbButtonModule, NbActionsModule, NbCheckboxModule, NbRadioModule, NbDatepickerModule, NbSelectModule, NbIconModule, NbTagModule, NbStepperModule, NbListModule, NbUserModule, NbSpinnerModule, NbDialogModule, NbToggleModule } from '@nebular/theme'; import { NbMenuModule, NbInputModule, NbCardModule, NbButtonModule, NbActionsModule, NbCheckboxModule, NbRadioModule, NbDatepickerModule, NbSelectModule, NbIconModule, NbTagModule, NbStepperModule, NbListModule, NbUserModule, NbSpinnerModule, NbDialogModule, NbToggleModule, NbAccordionModule, NbBadgeModule, NbAlertModule } from '@nebular/theme';
import { ThemeModule } from '../@theme/theme.module'; import { ThemeModule } from '../@theme/theme.module';
import { AdminRoutingModule } from '../admin/admin-routing.module'; import { AdminRoutingModule } from '../admin/admin-routing.module';
import { AlertDlgModule } from '../ui/alert-dlg/alert-dlg.module'; import { AlertDlgModule } from '../ui/alert-dlg/alert-dlg.module';
@ -16,6 +16,22 @@ import { TeamVoteComponent } from './avalon/team-vote/team-vote.component';
import { QuestVoteComponent } from './avalon/quest-vote/quest-vote.component'; import { QuestVoteComponent } from './avalon/quest-vote/quest-vote.component';
import { VoteResultComponent } from './avalon/vote-result/vote-result.component'; import { VoteResultComponent } from './avalon/vote-result/vote-result.component';
import { QuestTableComponent } from './avalon/quest-table/quest-table.component'; import { QuestTableComponent } from './avalon/quest-table/quest-table.component';
import { MassiveDarkness2Component } from './massive-darkness2/massive-darkness2.component';
import { TreasureBagComponent } from './massive-darkness2/treasure-bag/treasure-bag.component';
import { MobsComponent } from './massive-darkness2/mobs/mobs.component';
import { CurrencyInputModule } from '../ui/currency-input/currency-input.module';
import { DoorEventsComponent } from './massive-darkness2/door-events/door-events.component';
import { HeroDashboardComponent } from './massive-darkness2/hero-dashboard/hero-dashboard.component';
import { SpawnMobDlgComponent } from './massive-darkness2/mobs/spawn-mob-dlg/spawn-mob-dlg.component';
import { MD2IconComponent } from './massive-darkness2/md2-icon/md2-icon.component';
import { MobDetailInfoComponent } from './massive-darkness2/mobs/mob-detail-info/mob-detail-info.component';
import { MD2HeroSelectComponent } from './massive-darkness2/md2-hero-select/md2-hero-select.component';
import { DropDownListModule } from '../ui/drop-down-list/drop-down-list.module';
import { BossFightComponent } from './massive-darkness2/boss-fight/boss-fight.component';
import { BossActivationComponent } from './massive-darkness2/boss-fight/boss-activation/boss-activation.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 { MobCombatInfoComponent } from './massive-darkness2/mobs/mob-detail-info/mob-combat-info/mob-combat-info.component';
@NgModule({ @NgModule({
@ -28,7 +44,21 @@ import { QuestTableComponent } from './avalon/quest-table/quest-table.component'
TeamVoteComponent, TeamVoteComponent,
QuestVoteComponent, QuestVoteComponent,
VoteResultComponent, VoteResultComponent,
QuestTableComponent QuestTableComponent,
MassiveDarkness2Component,
TreasureBagComponent,
MobsComponent,
DoorEventsComponent,
HeroDashboardComponent,
SpawnMobDlgComponent,
MD2IconComponent,
MobDetailInfoComponent,
MD2HeroSelectComponent,
BossFightComponent,
BossActivationComponent,
MobAttackInfoComponent,
MobDefInfoComponent,
MobCombatInfoComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -52,9 +82,14 @@ import { QuestTableComponent } from './avalon/quest-table/quest-table.component'
NbToggleModule, NbToggleModule,
NbUserModule, NbUserModule,
NbSpinnerModule, NbSpinnerModule,
NbAccordionModule,
NbBadgeModule,
NbAlertModule,
CurrencyInputModule,
NbDialogModule.forRoot(), NbDialogModule.forRoot(),
AlertDlgModule, AlertDlgModule,
QRCodeModule, QRCodeModule,
DropDownListModule
] ]
}) })
export class GamesModule { } export class GamesModule { }

View File

@ -0,0 +1,230 @@
import { ChangeDetectorRef, Injectable } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject } from "rxjs";
import { first, takeUntil } from "rxjs/operators";
import { MD2GameInfo, MD2Service } from "../../services/md2.service";
import { SignalRMessage } from "../../services/signal-r.service";
import { StateService } from "../../services/state.service";
import { ADIcon, MessageBoxConfig } from "../../ui/alert-dlg/alert-dlg.model";
import { MD2HeroInfo, MD2Icon, MobInfo, RoundPhase } from "./massive-darkness2.model";
@Injectable()
export abstract class MD2Base {
MD2Icon = MD2Icon;
public get gameInfo() {
return this.md2Service.info;
}
protected isHeroDashboard: boolean = false;
protected roomId: string;
protected destroy$: Subject<void> = new Subject<void>();
/**
*
*/
constructor(
protected md2Service: MD2Service,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
) {
}
ngOnInit(): void {
this.route.paramMap.pipe(first()).subscribe(params => {
if (params.get('roomId')) {
this.roomId = params.get('roomId');
}
});
this.stateService.loginUserService.signalRInitialized.pipe(first()).subscribe(result => {
console.log('signalRInitialized');
this.signalRInitialized();
});
this.md2Service.signalRService.ReceivedSignalRMessageSubject.pipe(takeUntil(this.destroy$)).subscribe(result => this.handleSignalRCallback(result));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
signalRInitialized() {
}
imgUrl(imgPath: string) {
return this.md2Service.imgUrl(imgPath);
}
fileList(folderPath: string) {
return this.md2Service.fileList(folderPath);
}
iconHtml(icon: MD2Icon, cssClass = '') {
return this.md2Service.iconHtml(icon, cssClass);
}
imgHtml(imgFile: string, cssClass = '') {
return `<img src="${this.imgUrl(imgFile)}" class='${cssClass}'>`
}
detectChanges() {
if (!this.cdRef['destroyed']) {
this.cdRef.detectChanges();
this.refreshUI();
this.md2Service.refreshUI$.next();
}
}
abstract refreshUI();
handleSignalRCallback(message: SignalRMessage): void {
switch (message.actionType) {
case 'hero':
let heroInfo = JSON.parse(message.parameters['hero']) as MD2HeroInfo;
switch (message.actionName) {
case 'join':
this.md2Service.heros.push(heroInfo);
break;
case 'update':
let exitingHero = this.md2Service.heros.find(h => h.playerInfo.signalRClientId == heroInfo.playerInfo.signalRClientId);
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);
break;
default:
break;
}
this.detectChanges();
break;
case 'GameRoom':
switch (message.actionName) {
case 'Leaving':
this.md2Service.heros.splice(this.md2Service.heros.findIndex(h => h.playerInfo.signalRClientId == message.from.sessionId));
this.detectChanges();
break;
case 'update':
if (this.isHeroDashboard) {
this.md2Service.info = new MD2GameInfo(JSON.parse(message.parameters['gameInfo']) as MD2GameInfo);
this.detectChanges();
}
break;
default:
break;
}
break;
case 'message':
switch (message.actionName) {
case 'popup':
let msg = JSON.parse(message.parameters['msg']) as MessageBoxConfig;
this.md2Service.msgBoxService.show(msg.title, msg);
break;
default:
break;
}
break;
case 'roundPhase':
switch (message.actionName) {
case 'bossFight':
this.gameInfo.isBossFight = true;
break;
default:
this.gameInfo.roundPhase = Number.parseInt(message.parameters['phase']);
break;
}
this.detectChanges();
break;
case 'mobs':
if (this.isHeroDashboard) {
this.gameInfo.roamingMonsters = JSON.parse(message.parameters['roamingMonsters']);
this.gameInfo.mobs = JSON.parse(message.parameters['mobs']);
this.detectChanges();
}
break;
case 'heroAction':
if (!this.isHeroDashboard) {
this.gameInfo.currentActivateHero = this.md2Service.heros.find(h => h.playerInfo.tabId == message.parameters['tabId']);
switch (message.actionName) {
case 'attackAction':
this.gameInfo.showAttackBtn = true;
break;
case 'openDoor':
//Door component listen for it
break;
case 'tradeAction':
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
equip items!`,
icon: ADIcon.INFO
});
break;
default:
//this.md2Service.roundPhase = Number.parseInt(message.parameters['phase']);
break;
}
this.heroAction(this.gameInfo.currentActivateHero, message.actionName);
this.detectChanges();
}
break;
default:
break;
}
}
abstract heroAction(hero: MD2HeroInfo, action: string);
}
@Injectable()
export abstract class MD2ComponentBase {
protected roomId: string;
protected destroy$: Subject<void> = new Subject<void>();
/**
*
*/
constructor(
protected md2Service: MD2Service,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
) {
}
ngOnInit(): void {
this.md2Service.refreshUI$.pipe(takeUntil(this.destroy$)).subscribe(result => {
this.cdRef.detectChanges();
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
imgUrl(imgPath: string) {
return this.md2Service.imgUrl(imgPath);
}
fileList(folderPath: string) {
return this.md2Service.fileList(folderPath);
}
iconHtml(icon: MD2Icon, cssClass = '') {
return this.md2Service.iconHtml(icon, cssClass);
}
}

View File

@ -0,0 +1,26 @@
<nb-card>
<nb-card-body>
<div class="row form-group" style="
height: 53vh;
overflow: auto;
">
<div class="col-md-5">
<img src="{{boss.standUrl}}" class="img-fluid">
</div>
<div class="col-md-7">
<label class="MD2text g-font-size-40 mt-5" [innerHtml]="bossAction.skillName">
</label>
<label class="g-font-size-20 mt-3" [innerHtml]="bossAction.skillDescription">
</label>
</div>
</div>
</nb-card-body>
<nb-card-footer>
<button class="float-right" nbButton hero status="danger" size="small" (click)="close()">Close</button>
<button class="float-right mr-2" nbButton hero status="primary" size="small" (click)="close()">Submit</button>
</nb-card-footer>
</nb-card>

View File

@ -0,0 +1,4 @@
nb-card {
height: 70vh;
width: 80vw;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BossActivationComponent } from './boss-activation.component';
describe('BossActivationComponent', () => {
let component: BossActivationComponent;
let fixture: ComponentFixture<BossActivationComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BossActivationComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BossActivationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NbDialogRef } from '@nebular/theme';
import { MD2Service } from '../../../../services/md2.service';
import { MsgBoxService } from '../../../../services/msg-box.service';
import { StateService } from '../../../../services/state.service';
import { MobDlgType, MD2Icon, MD2HeroInfo } from '../../massive-darkness2.model';
import { MobSkill, IBossFight } from '../../massive-darkness2.model.boss';
import { MD2ComponentBase } from '../../MD2Base';
import { SpawnMobDlgComponent } from '../../mobs/spawn-mob-dlg/spawn-mob-dlg.component';
@Component({
selector: 'ngx-boss-activation',
templateUrl: './boss-activation.component.html',
styleUrls: ['./boss-activation.component.scss']
})
export class BossActivationComponent extends MD2ComponentBase implements OnInit {
boss: IBossFight;
bossAction: MobSkill;
MobDlgType = MobDlgType;
mode: MobDlgType;
title: string;
titleHtml: string;
actionHtml: string;
MD2Icon = MD2Icon;
beenAttackedHero = [] as MD2HeroInfo[];
attackTarget: string;
otherAttackTarget: string;
constructor(
private dlgRef: NbDialogRef<SpawnMobDlgComponent>,
private msgBoxService: MsgBoxService,
public md2Service: MD2Service,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
) {
super(md2Service, stateService, route, cdRef);
}
close() {
this.boss.standUrl
this.dlgRef.close();
}
}

View File

@ -0,0 +1,29 @@
<nb-card>
<nb-card-header>
{{boss.name}}
<button nbButton hero status="primary" (click)="activate()">Action</button>
</nb-card-header>
<nb-card-body>
<div class="row">
<div class="col-md-5">
<img src="{{boss.standUrl}}" class="w-100 g-max-height-80vh">
</div>
<div class="col-md-7">
<md2-mob-detail-info [mob]="boss.info">
</md2-mob-detail-info>
<adj-number-input name="mob{{boss.info.name}}" [(ngModel)]="boss.info.unitRemainHp" minimum="0"
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">
</label>
<label class="MD2Text" [innerHtml]="boss.combatInfo.skillDescription">
</label>
</div>
</div>
</nb-card-body>
</nb-card>

View File

@ -0,0 +1,4 @@
nb-card {
height: 80vh;
//width: 80vw;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BossFightComponent } from './boss-fight.component';
describe('BossFightComponent', () => {
let component: BossFightComponent;
let fixture: ComponentFixture<BossFightComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ BossFightComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BossFightComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,63 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NbDialogService } from '@nebular/theme';
import { Subject, Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { MD2Service } from '../../../services/md2.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { StateService } from '../../../services/state.service';
import { ADIcon } from '../../../ui/alert-dlg/alert-dlg.model';
import { MD2Icon, MobDlgType, MobInfo } from '../massive-darkness2.model';
import { MD2ComponentBase } from '../MD2Base';
import { SpawnMobDlgComponent } from '../mobs/spawn-mob-dlg/spawn-mob-dlg.component';
@Component({
selector: 'md2-boss-fight',
templateUrl: './boss-fight.component.html',
styleUrls: ['./boss-fight.component.scss']
})
export class BossFightComponent extends MD2ComponentBase {
public get boss() {
return this.md2Service.info.boss;
}
constructor(
private msgBoxService: MsgBoxService,
private dlgService: NbDialogService,
public md2Service: MD2Service,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
) {
super(md2Service, stateService, route, cdRef);
}
ngOnInit(): void {
super.ngOnInit();
}
activate() {
this.boss.activating();
}
WIN() {
}
attack(mob: MobInfo) {
this.dlgService.open(SpawnMobDlgComponent, { context: { title: `Attack ${this.boss.name}`, mode: MobDlgType.BeenAttacked, mob: mob } })
.onClose.pipe(first()).subscribe(mobResult => {
if (mobResult) {
let attackDamage = mobResult.uiWounds;
if (attackDamage) {
this.boss.info.hp -= attackDamage;
this.cdRef.detectChanges();
}
}
});
}
}

View File

@ -0,0 +1,38 @@
<nb-card>
<nb-card-header>
<img src="{{imgUrl('DoorEvents/Cover.png')}}" width="40px"> Door Event
<!-- <button nbButton hero status="warning" size="small" (click)="initMobDecks()" class="float-right">Reset
Mobs</button> -->
<button nbButton hero status="info" size="small" (click)="drawDoorCard()" class="float-right mr-2">Draw</button>
<!-- <button nbButton hero status="warning" size="tiny" (click)="resetTreasureBag()"
class="float-right">Reset</button>
<button nbButton hero status="primary" size="tiny" (click)="addTreasure(TreasureType.Epic)"
class="float-right mr-1">Add
Epic</button>
<button nbButton hero status="info" size="tiny" (click)="addTreasure(TreasureType.Rare)"
class="float-right mr-1">Add
Rare</button> -->
</nb-card-header>
<nb-card-body>
<!-- <b>Content of the bag</b>
<hr class="my-1"> -->
<div class="row">
<!-- this.mobs <div class="col" *ngFor="let treasure of treasureBag.drawingItems">
<img src="{{treasure.imageUrl}}" width="40px"> X {{treasure.unitAmount}}
</div> -->
<div class="col col-md-3 mb-4" *ngFor="let door of this.drawDoorEvents">
<img class="g-width-95x" src="{{door.imageUrl}}" (click)="showMobImage(mob)" /><br>
<label class="badge badge-info">{{door.name}}</label>
<button nbButton hero status="primary" size="small" class="float-right"
(click)="discard(door)">Discard</button>
</div>
</div>
<!-- <div class="container">
<img id="clip" src="{{imgUrl('Mobs/CoreGame/CoreGameMob.jpg')}}" />
</div> -->
<!-- <button nbButton hero fullWidth="" status="primary" class="mt-3" (click)="drawTreasure()">I feel
LUCKY!!!!!</button> -->
</nb-card-body>
</nb-card>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DoorEventsComponent } from './door-events.component';
describe('DoorEventsComponent', () => {
let component: DoorEventsComponent;
let fixture: ComponentFixture<DoorEventsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DoorEventsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DoorEventsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,72 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { FileService } from '../../../services/file.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { ADButtons } from '../../../ui/alert-dlg/alert-dlg.model';
import { DrawingBag, DrawingItem } from '../massive-darkness2.model';
import { first } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { MD2Service } from '../../../services/md2.service';
import { StateService } from '../../../services/state.service';
import { MD2Base, MD2ComponentBase } from '../MD2Base';
import { SignalRMessage } from '../../../services/signal-r.service';
@Component({
selector: 'md2-door-events',
templateUrl: './door-events.component.html',
styleUrls: ['./door-events.component.scss']
})
export class DoorEventsComponent extends MD2ComponentBase implements OnInit {
constructor(
private msgBoxService: MsgBoxService,
public md2Service: MD2Service,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
) {
super(md2Service, stateService, route, cdRef);
}
drawDoorEvents: DrawingItem[];
treasureBag: DrawingBag<DrawingItem> = new DrawingBag<DrawingItem>();
ngOnInit(): void {
this.initDoorEvents();
this.md2Service.signalRService.ReceivedSignalRMessageSubject.subscribe(result => this.handleSignalRCallback(result));
}
handleSignalRCallback(result: SignalRMessage): void {
if (result.actionName == 'openDoor') {
this.drawDoorCard();
}
}
initDoorEvents() {
this.drawDoorEvents = [];
this.treasureBag = new DrawingBag<DrawingItem>();
for (let i = 1; i <= 30; i++) {
this.treasureBag.AddItem(new DrawingItem(i.toString(), '', this.imgUrl(`DoorEvents/DoorEvent-${i}.png`), 1));
}
this.cdRef.detectChanges();
}
drawDoorCard() {
if (this.treasureBag.bagIsEmpty()) {
this.treasureBag.RestoreRemoveItems();
}
let door = this.treasureBag.DrawAndRemove()[0];
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 => {
if (result) {
door.name = this.md2Service.heroFullName(this.md2Service.info.currentActivateHero);
this.drawDoorEvents.push(door);
}
this.cdRef.detectChanges();
});
}
discard(door: DrawingItem) {
this.drawDoorEvents.splice(this.drawDoorEvents.indexOf(door), 1);
this.cdRef.detectChanges();
}
}

View File

@ -0,0 +1,141 @@
<nb-card *ngIf="!md2Service.playerHero">
<nb-card-body>
<button nbButton hero status="primary" fullWidth (click)="initHero()">Choose Hero</button>
</nb-card-body>
</nb-card>
<div *ngIf="md2Service.playerHero">
<div class="row no-gutters">
<div class="col-12 col-sm-7">
<div class="tp-wrapper mb-2">
<div class="tp-box g-height-300 g-height-350--sm g-height-500--md" (click)="toggleFlip()"
[@flipState]="flip">
<div class="tp-box__side tp-box__front ">
<img class="MD2HeroCard " src="{{md2Service.playerHero.imgUrl}}">
</div>
<div class="tp-box__side tp-box__back">
<img class="MD2HeroCard " src="{{imgUrl('Heros/Guide/'+className+'.jpg')}}">
</div>
</div>
</div>
<!-- <div class="g-max-height-80vh mb-2">
<img class="MD2HeroCard" src="{{md2Service.playerHero.imgUrl}}">
<div class="MD2HeroCard">
<span class="MD2text MD2Name">{{md2Service.playerHero.name}}</span>
<span class="MD2text MD2Hp">{{md2Service.playerHero.hpMaximum}}</span>
<span class="MD2text MD2Mp">{{md2Service.playerHero.mpMaximum}}</span>
</div>
<img class="MD2HeroCard" src="{{md2Service.playerHero.imgUrl}}">
<img class="MD2HeroCard HpMpBar" src="{{imgUrl('/Heros/Template/Border.png')}}">
</div> -->
</div>
<div class="col-12 col-sm-5">
<nb-card>
<nb-card-body>
<div class="row no-gutters">
<div class="col-6">
<!-- <adj-number-input name="heroHP" [(ngModel)]="md2Service.playerHero.hp"
[maximum]="md2Service.playerHero.hpMaximum" minimum="0"
title="{{iconHtml(MD2Icon.HP,'g-color-google-plus mr-1 g-font-size-18')}}HP" showMaximum
(blur)="heroUpdateDebounceTimer.resetTimer()" (hitDecreasing)="increaseRage()">
</adj-number-input> -->
<adj-number-input name="heroHP" [(ngModel)]="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()">
</adj-number-input>
<adj-number-input name="heroMana" [(ngModel)]="md2Service.playerHero.mp"
[maximum]="md2Service.playerHero.mpMaximum" minimum="0"
title="{{imgHtml('HeroIcon.png','g-height-25 mr-1')}}Mana" showMaximum
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
<adj-number-input name="heroFire" [(ngModel)]="md2Service.playerHero.fireToken" minimum="0"
title="{{iconHtml(MD2Icon.Fire,'g-color-google-plus mr-1 g-font-size-18')}}Fire Token"
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
<adj-number-input name="heroFire" [(ngModel)]="md2Service.playerHero.frozenToken"
minimum="0"
title="{{iconHtml(MD2Icon.Frost,'g-color-aqua mr-1 g-font-size-18')}}Frozen Token"
(blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
</div>
<div class="col-6">
<adj-number-input name="remainActions" [(ngModel)]="md2Service.playerHero.remainActions"
minimum="0" title="Remain Actions" (blur)="heroUpdateDebounceTimer.resetTimer()"
hideIncreaseBtn>
</adj-number-input>
<adj-number-input name="heroLevel" [(ngModel)]="md2Service.playerHero.level" minimum="1"
maximum="5" title="Level" (blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
<adj-number-input name="heroExp" [(ngModel)]="md2Service.playerHero.exp" minimum="0"
title="Exp" (blur)="heroUpdateDebounceTimer.resetTimer()">
</adj-number-input>
<adj-number-input name="heroRage" [(ngModel)]="md2Service.playerHero.rage" minimum="0"
maximum="7"
title="{{iconHtml(MD2Icon.Rage,'g-color-google-plus mr-1 g-font-size-18')}}Rage"
(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>
</div>
</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>
</div>
</div>
</div>
<!-- <nb-flip-card *ngIf="md2Service.playerHero">
<nb-card-front>
<nb-card>
<nb-card-body>
</nb-card-body>
</nb-card>
</nb-card-front>
<nb-card-back>
<nb-card>
<nb-card-body>
Back card text
</nb-card-body>
</nb-card>
</nb-card-back>
</nb-flip-card> -->

View File

@ -0,0 +1,80 @@
.MD2Hp {
font-size: xx-large;
position: fixed;
z-index: 92;
color: white;
bottom: 23%;
left: 46%;
}
.MD2Mp {
font-size: xx-large;
position: fixed;
z-index: 92;
color: white;
bottom: 22%;
left: 7%;
}
.MD2Name {
font-size: xx-large;
position: fixed;
z-index: 92;
color: #2e2e30;
bottom: 20%;
left: 23%;
}
.MD2HeroCard {
//position: absolute;
max-width: 100%;
max-height: 100%;
&.HpMpBar {
z-index: 10;
}
}
.tp-wrapper {
-webkit-perspective: 800px;
perspective: 800px;
}
.tp-box {
position: relative;
//width: 200px;
//height: 100px;
//margin: 3rem auto;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
-webkit-transform: transform 1s;
-ms-transform: transform 1s;
transform: transform 1s;
}
.tp-box__side {
width: 100%;
height: 100%;
position: absolute;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
color: #fff;
text-align: center;
line-height: 100px;
font-size: 24px;
font-weight: 700;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tp-box__front {
-webkit-transform: rotateY(0deg);
-ms-transform: rotateY(0deg);
transform: rotateY(0deg);
}
.tp-box__back {
-webkit-transform: rotateY(-180deg);
-ms-transform: rotateY(-180deg);
transform: rotateY(-180deg);
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroDashboardComponent } from './hero-dashboard.component';
describe('HeroDashboardComponent', () => {
let component: HeroDashboardComponent;
let fixture: ComponentFixture<HeroDashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HeroDashboardComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HeroDashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,205 @@
import { trigger, state, style, transition, animate } from '@angular/animations';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { first } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption';
import { GameRoomService } from '../../../services/game-room.service';
import { MD2Service } from '../../../services/md2.service';
import { MsgBoxService } from '../../../services/msg-box.service';
import { StateService } from '../../../services/state.service';
import { ADButtonColor, ADButtons } from '../../../ui/alert-dlg/alert-dlg.model';
import { StringUtils } from '../../../utilities/string-utils';
import { DebounceTimer } from '../../../utilities/timer-utils';
import { HeroClass, MD2HeroInfo } from '../massive-darkness2.model';
import { MD2Base } from '../MD2Base';
@Component({
selector: 'ngx-hero-dashboard',
templateUrl: './hero-dashboard.component.html',
styleUrls: ['./hero-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('flipState', [
state('active', style({
transform: 'rotateY(179deg)'
})),
state('inactive', style({
transform: 'rotateY(0)'
})),
transition('active => inactive', animate('500ms ease-out')),
transition('inactive => active', animate('500ms ease-in'))
])
]
})
export class HeroDashboardComponent extends MD2Base implements OnInit {
heroAction(hero: MD2HeroInfo, action: string) {
throw new Error('Method not implemented.');
}
showMoveAction: boolean = false;
HeroClass = HeroClass;
refreshUI() {
console.log('HeroDashboard RefreshUI');
}
heroUpdateDebounceTimer = new DebounceTimer(1000, () => { this.broadcastHeroInfo(); })
classOptions: DropDownOption[] = [
new DropDownOption(HeroClass.Berserker, 'Berserker'),
new DropDownOption(HeroClass.Paladin, 'Paladin'),
new DropDownOption(HeroClass.Ranger, 'Ranger'),
new DropDownOption(HeroClass.Rogue, 'Rogue'),
new DropDownOption(HeroClass.Wizard, 'Wizard'),
new DropDownOption(HeroClass.Shaman, 'Shaman'),
];
heros = [] as MD2HeroInfo[];
wizards: MD2HeroInfo[] = [
new MD2HeroInfo({ name: 'Ajax', mpMaximum: 6, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Baldric', mpMaximum: 5, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Ego', mpMaximum: 5, hpMaximum: 6, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Elias', mpMaximum: 6, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Megan', mpMaximum: 5, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Moira', mpMaximum: 6, hpMaximum: 5, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Myriam', mpMaximum: 7, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' }),
new MD2HeroInfo({ name: 'Valdis', mpMaximum: 6, hpMaximum: 4, skillHtml: '', shadowSkillHtml: '' })
]
constructor(
private gameRoomService: GameRoomService,
public md2Service: MD2Service,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
private msgBoxService: MsgBoxService,
) {
super(md2Service, stateService, route, cdRef);
this.isHeroDashboard = true;
}
public get allowAttack(): boolean {
return (!!this.md2Service.mobs && this.md2Service.mobs.length > 0) || (!!this.md2Service.roamingMonsters && this.md2Service.roamingMonsters.length > 0);
}
ngOnInit(): void {
super.ngOnInit();
}
override signalRInitialized() {
// this.gameRoomService.joinGameRoom(this.roomId);
// if (this.md2Service.initialized == false) {
// this.gameRoomService.createGameRoom('MD2');
// this.md2Service.initialized = true;
// }
}
initHero() {
this.gameRoomService.gameRoomId = this.roomId;
this.gameRoomService.joinGameRoom(this.roomId);
if (!this.md2Service.heros.some(h => h.playerInfo.signalRClientId == this.stateService.loginUserService.userAccess.signalRSessionId)) {
this.msgBoxService.showInputbox('Select Hero Class', '', { dropDownOptions: this.classOptions, inputType: 'dropdown' })
.pipe(first()).subscribe(heroClass => {
if (heroClass != null) {
// switch (heroClass) {
// case HeroClass.Berserker: break;
// case HeroClass.Wizard:
// this.heros = this.wizards;
// break;
// case HeroClass.Rogue: break;
// case HeroClass.Ranger: break;
// case HeroClass.Shaman: break;
// case HeroClass.Paladin: break;
// default: break;
// }
// this.showHeroList(heroClass, 0);
this.initClassHeroList(heroClass);
}
});
}
}
className: string;
initClassHeroList(heroClass: HeroClass) {
this.heros = [];
this.className = HeroClass[heroClass];
this.fileList(`Heros/${this.className}`).pipe(first()).subscribe(fileNames => {
for (let i = 0; i < fileNames.length; i++) {
const heroNames = fileNames[i].split('.')[0].split('-');
this.heros.push(new MD2HeroInfo({
name: heroNames[0].replace('/', ''),
mpMaximum: Number.parseInt(heroNames[1]),
hpMaximum: Number.parseInt(heroNames[2]),
imgUrl: this.imgUrl(`Heros/${this.className}/${fileNames[i]}`),
class: heroClass
}))
}
this.heros = this.heros.sort((a, b) => StringUtils.compareSemVer(a.name, b.name));
this.showHeroList(heroClass, 0);
});
}
showHeroList(heroClass: HeroClass, index: number) {
let className = HeroClass[heroClass];
let heroInfo = this.heros[index];
this.msgBoxService.show(`${className}(${index + 1}/${this.heros.length})`, {
text: `<img src='${heroInfo.imgUrl}' class="g-width-50vw-md g-width-80vw">`,
buttons: ADButtons.YesNo,
cardWidthClass: '',
confirmButtonText: 'It\'s Me!',
cancelButtonText: 'Next',
cancelButtonColor: ADButtonColor.INFO
}).pipe(first()).subscribe(result => {
if (result) {
this.md2Service.playerJoin(heroInfo);
this.detectChanges();
} else {
index++;
if (index == this.heros.length) index = 0;
this.showHeroList(heroClass, index);
}
});
}
broadcastHeroInfo() {
this.md2Service.broadcastMyHeroInfo();
this.heroUpdateDebounceTimer.clearOut();
}
increaseRage() {
if (this.md2Service.playerHero.rage < 7) {
this.md2Service.playerHero.rage++;
}
}
openDoor() {
this.md2Service.broadcastHeroAction('openDoor');
this.showMoveAction = false;
this.detectChanges();
}
moveAction() {
this.showMoveAction = true;
this.detectChanges();
}
moveActionEnd() {
this.showMoveAction = false;
this.reduceAction();
this.detectChanges();
}
action(action: string) {
this.showMoveAction = false;
switch (action) {
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).' })
break;
default:
break;
}
this.md2Service.broadcastHeroAction(action);
this.reduceAction();
}
reduceAction() {
this.md2Service.playerHero.remainActions -= 1;
this.detectChanges();
this.broadcastHeroInfo();
}
flip: string = 'inactive';
toggleFlip() {
this.flip = (this.flip == 'inactive') ? 'active' : 'inactive';
}
}

View File

@ -0,0 +1,101 @@
<div class="row">
<!-- <div class="col-12 mb-1">
<nb-accordion>
<nb-accordion-item>
<nb-accordion-item-header class="my-0">
Global Config
</nb-accordion-item-header>
<nb-accordion-item-body>
</nb-accordion-item-body>
</nb-accordion-item>
</nb-accordion>
</div> -->
<div class="col-12 col-md-5">
<nb-card>
<nb-card-header>
<img src="{{imgUrl('HeroIcon.png')}}" width="40px"> Game Info
<button nbButton hero status="info" size="small" (click)="showQrCode()"
class="float-right">Invite</button>
<button nbButton hero status="info" size="small" [disabled]="anyHeroRemainAction"
(click)="md2Service.runNextPhase()" class="float-right mr-2">Next Phase</button>
</nb-card-header>
<nb-card-body>
<div class="row" *ngIf="md2Service.heros.length==0">
<div class="col-6">
<adj-number-input name="heroLevel" [(ngModel)]="md2Service.playerAmount" [maximum]="6"
minimum="1" title="Hero Amount">
</adj-number-input>
</div>
<div class="col-6">
<adj-number-input name="heroLevel" [(ngModel)]="md2Service.highestPlayerLevel" [maximum]="5"
minimum="1" title="Highest Hero Level">
</adj-number-input>
</div>
</div>
<div class="row" *ngIf="md2Service.heros.length>0">
<div class="col-12 g-font-size-17" [innerHtml]="roundPhase"></div>
<!-- <div class="col-6">
<label for='playerAmount' class='label'>Hero Amount ({{md2Service.playerAmount}})</label>
</div>
<div class="col-6">
<label for='playerAmount' class='label'>Highest Hero Level
({{md2Service.highestPlayerLevel}})</label>
</div> -->
<div class="col-12" *ngFor="let hero of md2Service.heros">
<label class='label mr-1'>{{hero.playerInfo.name}} ({{heroClassName(hero)}} -
{{hero.name}})</label>
<span class="badge badge-primary mr-1">Lv.:{{hero.level}}</span>
<span class="badge badge-primary mr-1">HP: {{hero.hp}}/{{hero.hpMaximum}}</span>
<span class="badge badge-primary mr-1">Mana: {{hero.mp}}/{{hero.mpMaximum}}</span>
<span class="badge badge-success mr-1">Exp: {{hero.exp}}</span>
<span class="badge badge-danger mr-1" *ngIf="hero.fireToken">Fire:{{hero.fireToken}}</span>
<span class="badge badge-info mr-1" *ngIf="hero.frozenToken">Frozen:{{hero.frozenToken}}</span>
<span class="badge badge-light mr-1" *ngIf="hero.remainActions==0">Inactive</span>
<span class="badge badge-success mr-1" *ngIf="hero.remainActions>0">Remain
Actions: {{hero.remainActions}}</span>
<!-- <span class="badge badge-success mr-1">{{hero.playerInfo.signalRClientId}}</span> -->
</div>
</div>
</nb-card-body>
</nb-card>
</div>
<div class="col-12 col-md-4">
<md2-treasure-bag></md2-treasure-bag>
</div>
<div class="col-12 col-md-3">
<div class="row">
<div class="form-group col-12">
<button nbButton hero fullWidth status="primary" (click)="enterBossFight()">Enter Boss Fight</button>
</div>
</div>
</div>
<div class="col-12 col-md-12" *ngIf="md2Service.info.isBossFight">
<md2-boss-fight></md2-boss-fight>
</div>
<div class="col-12 col-md-12">
<md2-mobs isRoamingMonster="true"></md2-mobs>
</div>
<div class="col-12 col-md-12">
<md2-mobs></md2-mobs>
</div>
<div class="col-12 col-md-12">
<md2-door-events></md2-door-events>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MassiveDarkness2Component } from './massive-darkness2.component';
describe('MassiveDarkness2Component', () => {
let component: MassiveDarkness2Component;
let fixture: ComponentFixture<MassiveDarkness2Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MassiveDarkness2Component ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MassiveDarkness2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,134 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { FileService } from '../../services/file.service';
import { MsgBoxService } from '../../services/msg-box.service';
import { ArrayUtils } from '../../utilities/array-utils';
import { ObjectUtils } from '../../utilities/object-utils';
import { first, map, take, takeUntil } from 'rxjs/operators';
import { TreasureType, DrawingBag, DrawingItem, HeroClass, MD2HeroInfo, RoundPhase, MobInfo, MobDlgType } from './massive-darkness2.model';
import { MD2Service } from '../../services/md2.service';
import { GameRoomService } from '../../services/game-room.service';
import { MD2Base } from './MD2Base';
import { StateService } from '../../services/state.service';
import { ActivatedRoute } from '@angular/router';
import { QRCodeService } from '../../services/qrcode.service';
import { StringUtils } from '../../utilities/string-utils';
import { SpawnMobDlgComponent } from './mobs/spawn-mob-dlg/spawn-mob-dlg.component';
import { BossMicheal } from './massive-darkness2.model.boss';
@Component({
selector: 'ngx-massive-darkness2',
templateUrl: './massive-darkness2.component.html',
styleUrls: ['./massive-darkness2.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MassiveDarkness2Component extends MD2Base implements OnInit {
HeroClass: HeroClass
constructor(
private fileService: FileService,
private msgBoxService: MsgBoxService,
private qrCodeService: QRCodeService,
public gameRoomService: GameRoomService,
public md2Service: MD2Service,
protected stateService: StateService,
protected route: ActivatedRoute,
protected cdRef: ChangeDetectorRef,
) {
super(md2Service, stateService, route, cdRef);
}
ngOnInit(): void {
super.ngOnInit();
this.md2Service.enemyPhaseSubject.pipe(takeUntil(this.destroy$)).subscribe(result => {
this.showEnemyPhaseAction(0);
});
}
override signalRInitialized() {
}
showQrCode() {
if (this.md2Service.initialized == false) {
this.gameRoomService.createGameRoom('MD2');
this.md2Service.initialized = true;
}
let initUrl = `${window.location.origin}/games/MD2_Hero/${this.gameRoomService.gameRoomId}`;
this.msgBoxService.show("Scan To Join", { text: `<img src='${this.qrCodeService.QRCodeUrl(initUrl, 5)}'><br><a href='${initUrl}' target='_blank'>Link</a>` });
}
refreshUI() {
console.log('Dashboard RefreshUI');
}
heroClassName(heroInfo: MD2HeroInfo) {
return HeroClass[heroInfo.class];
}
showEnemyPhaseAction(index: number) {
let mob = new MobInfo(this.md2Service.enemyPhaseMobs[index]);
let enemyInfo = `<img src="${mob.imageUrl}" class='g-height-70vh'><br>`;
let extraRule = '';
// switch (Math.random() * 3) {
// case 1:
// break;
// case 2:
// break;
// case 3:
// break;
// case 0:
// default:
// break;
// }
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 => {
index++;
if (index < this.md2Service.enemyPhaseMobs.length) {
this.showEnemyPhaseAction(index);
} else {
this.md2Service.runNextPhase();
}
});
// return this.msgBoxService.show(`Enemy Phase(${(this.enemyPhaseMobs.indexOf(mob) + 1)}/${this.enemyPhaseMobs.length})`, {
// text: enemyInfo,
// confirmButtonText: 'Next', buttons: ADButtons.OK
// }).pipe(first()).subscribe(result => {
// if ((this.enemyPhaseMobs.indexOf(mob) + 1) < this.enemyPhaseMobs.length) {
// this.showEnemyPhaseAction(this.enemyPhaseMobs[this.enemyPhaseMobs.indexOf(mob) + 1]);
// }
// });
}
public get roundPhase(): string {
switch (this.md2Service.info.roundPhase) {
case RoundPhase.HeroPhase:
return StringUtils.getHtmlBadge("Hero Action Phase", "primary")
case RoundPhase.EnemyPhase:
return StringUtils.getHtmlBadge("Enemy Action Phase", "danger")
case RoundPhase.LevelUpPhase:
return StringUtils.getHtmlBadge("Level Up Phase", "success")
case RoundPhase.DarknessPhase:
return StringUtils.getHtmlBadge("Darkness Phase", "dark")
default: break;
}
}
public get anyHeroRemainAction(): boolean {
return this.md2Service.heros.some(h => h.remainActions > 0);
}
heroAction(hero: MD2HeroInfo, action: string) {
}
enterBossFight() {
this.msgBoxService.showInputbox('Boss Fight', 'Choose the boss').pipe(first()).subscribe(result => {
this.md2Service.info.isBossFight = true;
this.md2Service.info.boss = new BossMicheal(this.md2Service);
this.detectChanges();
});
}
}

View File

@ -0,0 +1,135 @@
import { Subject } from "rxjs"
import { first } from "rxjs/operators"
import { MD2Service } from "../../services/md2.service"
import { StringUtils } from "../../utilities/string-utils"
import { BossActivationComponent } from "./boss-fight/boss-activation/boss-activation.component"
import { TreasureType, AttackInfo, DefenseInfo, AttackType, MD2Icon, MD2HeroInfo, AttackTarget, MobInfo } from "./massive-darkness2.model"
import { RollingBlackDice } from "./massive-darkness2.model.dice"
export interface IBossFight {
name: string
addTreasureToken: Subject<TreasureType>
spawnMob: Subject<void>
spawnRoamingMonster: Subject<void>
rounds: number
actions: number
hpPerHero: number
info: MobInfo
actionBlackDice: number
imgUrl: string
standUrl: string
combatInfo: MobSkill
activating(): boolean
prepareForBossFight(): void
nextRound(): void
}
export class BossMicheal implements IBossFight {
constructor(private md2Service: MD2Service) {
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
addTreasureToken: Subject<TreasureType>
spawnMob: Subject<void>
spawnRoamingMonster: Subject<void>
rounds: number
actions: number
hpPerHero: number
info: MobInfo
actionBlackDice: number
imgUrl: string
standUrl: string
combatInfo: MobSkill
corruptionTokenHtml: string
activating(): boolean {
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(AttackTarget.MostCorruption, true);
bossAction = new MobSkill('Justice From Above',
`Place Michael in the Zone at ${this.md2Service.getTargetHerosHtml(beenAttackedHero)} and attack Him/Her.`, beenAttackedHero);
break;
case 1:
//Lance Dash
beenAttackedHero = this.md2Service.getTargetHerosByFilter(AttackTarget.LeastCorruption, true);
bossAction = new MobSkill('Lance Dash',
`Move Michael and Place 1 ${this.corruptionTokenHtml} in the Zone at ${this.md2Service.getTargetHerosHtml(beenAttackedHero)} and attack Him/Her.`, beenAttackedHero);
break;
case 2:
//Dark Blessing
bossAction = new MobSkill('Dark Blessing',
`Place Michael ion 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>.`, beenAttackedHero);
break;
default:
break;
}
this.md2Service.dlgService.open(BossActivationComponent, { context: { boss: this, bossAction: bossAction } }).onClose
.pipe(first()).subscribe(result => {
});
return true;
}
prepareForBossFight(): void {
}
darkBlessingCorruptionAmt: number = 1;
nextRound(): void {
this.rounds++;
switch (this.rounds) {
case 3:
case 5:
this.darkBlessingCorruptionAmt++;
break;
case 2:
case 4:
this.info.defenseInfos[0].black += 1;
this.info.attackInfos[0].black += 1;
break;
// case 4:
// this.defInfo.black += 2;
// this.atkInfos[0].black += 2;
// break;
default:
break;
}
}
}
export class MobSkill {
constructor(skillName: string, skillDescription: string, targetHeros: MD2HeroInfo[] = []) {
this.skillName = skillName
this.skillDescription = skillDescription
this.targetHeros = targetHeros
}
skillName: string
skillDescription: string
targetHeros: MD2HeroInfo[]
}

View File

@ -0,0 +1,24 @@
export class RollingBlackDice {
roll(times: number) {
let wounds = 0;
let claws = 0;
//miss 33%
//1 claw 33%
//1 wound 17%
//1 claw, 1 wound 17%
for (let i = 0; i < times; i++) {
let result = Math.random() * 100;
if (result <= 33) {
} else if (result <= 67) {
claws += 1;
} else if (result <= 83) {
wounds += 1;
} else {
claws += 1;
wounds += 1;
}
}
return { claws, wounds };
}
}

View File

@ -0,0 +1,447 @@
import { Subject } from "rxjs";
import { ObjectUtils } from "../../utilities/object-utils";
import { GamePlayer } from "../games.model";
import { MobSkill } from "./massive-darkness2.model.boss";
export enum MobDlgType {
Spawn,
Activating,
BeenAttacked,
PreView
}
export enum RoundPhase {
HeroPhase,
EnemyPhase,
LevelUpPhase,
DarknessPhase,
BossActivation
}
export enum TreasureType {
Common,
Rare,
Epic,
Legendary
}
export enum HeroClass {
Berserker,
Wizard,
Rogue,
Ranger,
Shaman,
Paladin,
}
export enum MD2Icon {
Attack,
Defense,
Mana,
Shadow,
EnemySkill,
EnemyClaw,
Reroll,
Fire,
Frost,
OneHand,
TwoHand,
Helmet,
Armor,
Ring,
Foot,
Melee,
Range,
Magic,
HP,
MP,
Dice,
Arrow,
ArrowBullseye,
ArrowOverload,
SoulToken,
Rage,
RedDice,
BlueDice,
YellowDice,
OrangeDice
}
export enum AttackTarget {
Random = 40,
LowestHp = 50,
HighestHp = 60,
HighestMp = 70,
LowestLevel = 80,
MostCorruption = 200,
LeastCorruption = 201
}
export enum AttackType {
Melee = 15,
Range = 16,
Magic = 17
}
export class AttackInfo {
constructor(
type: MD2Icon,
yellow: number = 0,
orange: number = 0,
red: number = 0,
black: number = 0
) {
this.type = type
this.orange = orange
this.red = red
this.yellow = yellow
this.black = black
}
type: MD2Icon
orange: number
red: number
yellow: number
black: number
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 {
constructor(config: Partial<MD2LevelUpReward>) {
Object.assign(this, config);
}
level: number = 1;
needExp: number = 0;
currentExp = 0
extraHp = 0
extraMp = 0
extraRareToken = 0
extraEpicToken = 0
}
export class DrawingBag<T extends IDrawingItem> {
constructor(drawingItems: IDrawingItem[] = []) {
this.drawingItems = drawingItems;
this.removedItems = [];
}
drawingItems: IDrawingItem[]
removedItems: IDrawingItem[]
public bagIsEmpty() {
return this.drawingItems.reduce((sum, current) => sum + current.drawingWeight, 0) == 0;
}
public Draw(amount: number): T[] {
let drawItems: T[] = this.DrawAndRemove(amount);
this.RestoreRemoveItems();
return drawItems;
}
public DrawAndRemove(amount: number = 1, predicate: (value: T) => boolean = undefined): T[] {
let drawItems: T[] = [];
for (let i = 0; i < amount; i++) {
if (!this.bagIsEmpty()) {
let drawItem = null as T;
let drawingPool = [] as T[];
if (predicate) {
drawingPool = this.drawingItems.filter(predicate) as T[];
} else {
drawingPool = this.drawingItems as T[];
}
let drawIndex = Math.random() * drawingPool.reduce((sum, current) => sum + current.drawingWeight, 0);
let drawCalc = 0;
for (let i = 0; i < drawingPool.length; i++) {
const item = drawingPool[i];
drawCalc += item.drawingWeight;
if (drawCalc >= drawIndex) {
drawItem = ObjectUtils.CloneValue(item);
drawItem.drawingWeight = 1;
break;
}
}
//ObjectUtils.CloneValue
this.RemoveItem(drawItem);
drawItems.push(drawItem);
} else {
break;
}
}
return drawItems;
}
public RestoreRemoveItems() {
for (let i = 0; i < this.removedItems.length; i++) {
const removedItem = this.removedItems[i];
this.AddItem(removedItem);
}
this.removedItems = [];
}
public AddItem(item: IDrawingItem) {
let existingItem = this.drawingItems.find(i => i.name == item.name);
if (existingItem) {
existingItem.drawingWeight += item.drawingWeight;
} else {
this.drawingItems.push(item);
}
}
public RemoveItem(item: IDrawingItem) {
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 (removedItem) {
removedItem.drawingWeight += item.drawingWeight;
} else {
this.removedItems.push(item);
}
}
}
}
public ClearAllItems() {
this.drawingItems = [];
this.removedItems = [];
}
}
export interface IDrawingItem {
imageUrl: string
name: string
description: string
drawingWeight: number
}
export class DrawingItem implements IDrawingItem {
constructor(
name: string,
description: string,
imageUrl: string,
drawingWeight: number = 1
) {
this.imageUrl = imageUrl
this.name = name
this.description = description
this.drawingWeight = drawingWeight
}
imageUrl: string
name: string
description: string
drawingWeight: number
}
export class MobInfo implements IDrawingItem {
constructor(
config: Partial<MobInfo> = {}
) {
Object.assign(this, config);
this.description = config.name;
this.drawingWeight = 1;
this.unitRemainHp = config.hp
}
imageUrl: string
standUrl: string
name: string
description: string
drawingWeight: number
level: number;
rewardTokens: number;
hp: number;
mobAmount: number;
carriedTreasure: DrawingItem[];
fixedCarriedTreasure: DrawingItem[];
unitRemainHp: number;
isRoamingMonster: boolean = false;
attackInfos: AttackInfo[];
defenseInfos: DefenseInfo;
fireToken: number = 0;
frozenToken: number = 0;
corruptionToken: number = 0;
uiWounds: number;
uiFireTokens: number;
uiFrozenTokens: number;
uiCorruptionTokens: number;
uiAttackedBy: string;
public get carriedTreasureHtml(): string {
if (!this.carriedTreasure) return '';
return this.carriedTreasure.map(i => `<img src="${i.imageUrl}" class='mr-1' width="40px">`)
.concat(this.fixedCarriedTreasure?.map(i => `<img src="${i.imageUrl}" class='mr-1' width="40px">`)).join();
}
public get totalHp(): number {
return this.isRoamingMonster ? this.unitRemainHp : (this.mobAmount - 1) * this.hp + this.unitRemainHp;
}
public get minionAmount(): number {
return (this.mobAmount - 1);
}
public get leaderExp(): number {
return this.isRoamingMonster ? 4 : 2;
}
public get mobInfoHtml(): string {
let html = `<img src="${this.imageUrl}" class="g-height-50vh">`
+ `<br>Target Unit HP:${this.unitRemainHp}`;
if (this.isRoamingMonster) {
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}`;
}
}
export class MD2HeroInfo {
constructor(
config: Partial<MD2HeroInfo> = {}
) {
Object.assign(this, config);
}
class: HeroClass;
name: string;
hp: number;
mp: number;
ap: number;
hpMaximum: number;
mpMaximum: number;
exp: number = 0;
level: number = 1;
fireToken: number = 0;
frozenToken: number = 0;
corruptionToken: number = 0;
playerInfo: GamePlayer;
imgUrl: string;
skillHtml: string;
shadowSkillHtml: string;
remainActions: number = 3;
rage: number = 0;
public get heroFullName(): string {
return `${this.playerInfo.name} (${HeroClass[this.class]} - ${this.name}`
}
}
export class MD2Rules {
public static CoreGameLevelBoard = [
new MD2LevelUpReward({ level: 2, needExp: 5, extraHp: 1, extraMp: 0, extraRareToken: 1 }),
new MD2LevelUpReward({ level: 3, needExp: 10, extraHp: 1, extraMp: 1, extraEpicToken: 1 }),
new MD2LevelUpReward({ level: 4, needExp: 12, extraHp: 2, extraMp: 2, extraEpicToken: 1 }),
new MD2LevelUpReward({ level: 5, needExp: 18, extraHp: 2, extraMp: 2, extraEpicToken: 1 }),
];
public static checkCoreGameLevelup(currentLevel: number, currentExp: number): MD2LevelUpReward {
let result = null as MD2LevelUpReward;
let nextLevel = this.CoreGameLevelBoard.find(r => r.level > currentLevel && currentExp >= r.needExp);
if (nextLevel) {
result = ObjectUtils.CloneValue(nextLevel) as MD2LevelUpReward;
result.currentExp = currentExp - nextLevel.needExp;
}
return result;
}
}
export class MD2EnemyPhaseSpecialInfo {
specialRule: MD2EnemyPhaseSpecialRule
specialRules = [
new MD2EnemyPhaseSpecialRule(30, '', ''),
new MD2EnemyPhaseSpecialRule(44, 'Surprise Attack',
'One Mob with line of sight to a hero changes its attack profile to ranged during its <b>SECOND</b> action.<br>' +
'You have to apply this to a Mob that is not in the same zone as a hero, and that could not attack otherwise.<br>' +
'If no Mob has line of sight to a hero, one Mob moves an additional zone instead.<br>'),
new MD2EnemyPhaseSpecialRule(22, 'Fast Advance', 'Each Mob moves 1 additional zone in its first move action.<br>' +
'If a Mob doesnt move, it adds one additional black enemy die to its first attack action instead.<br>' +
'(If it already has all six black enemy dice in its attack pool, it rerolls one blank result on a black die instead.)<br>'),
new MD2EnemyPhaseSpecialRule(4, 'A Dark Portal Appears, Guiding Predators To Their Prey',
'First, move one green portal token from the board to the zone with the hero who has the least amount of health tokens. (If there is no green portal token on the board, put one onto the board.)<br>' +
'Then, move the Mob that is furthest away from any heroes into this new portal zone. In its following activation, this Mob only has one action.'),
]
}
export class MD2EnemyPhaseSpecialRule implements IDrawingItem {
constructor(drawingWeight: number, title: string, description: string) {
this.drawingWeight = drawingWeight
this.title = title
this.description = description
}
imageUrl: string;
name: string;
drawingWeight: number;
title: string
description: string
}
export interface IDarknessPhaseRule {
addTreasureToken: Subject<TreasureType>
spawnMob: Subject<void>
spawnRoamingMonster: Subject<void>
runDarknessPhase(): boolean
}
export class CoreGameDarknessPhaseRule implements IDarknessPhaseRule {
round: number = 1;
frontEndRound: number = 9;
extraRound: number = 0;
constructor() {
}
addTreasureToken = new Subject<TreasureType>();
spawnMob = new Subject<void>();
spawnRoamingMonster = new Subject<void>();
runDarknessPhase() {
if (this.round >= this.frontEndRound) {
this.extraRound++;
if (this.extraRound % 4 == 0) {
this.spawnRoamingMonster.next();
return false;
} else if (this.extraRound % 2 == 0) {
this.spawnMob.next();
return false;
}
return true;
}
this.round++;
switch (this.round) {
case 3:
case 9:
this.spawnMob.next();
return false;
break;
case 4:
this.addTreasureToken.next(TreasureType.Rare);
break;
case 5:
case 7:
this.spawnRoamingMonster.next();
return false;
break;
case 6:
case 8:
this.addTreasureToken.next(TreasureType.Epic);
break;
default:
break;
}
return true;
}
}

View File

@ -0,0 +1 @@
<op-drop-down [(ngModel)]="playerTabId" [source]="heroOptions" (blur)="onBlur()"></op-drop-down>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MD2HeroSelectComponent } from './md2-hero-select.component';
describe('MD2HeroSelectComponent', () => {
let component: MD2HeroSelectComponent;
let fixture: ComponentFixture<MD2HeroSelectComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MD2HeroSelectComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MD2HeroSelectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,105 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, Renderer2 } from '@angular/core';
import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption';
import { MD2Service } from '../../../services/md2.service';
import { ArrayUtils } from '../../../utilities/array-utils';
import { HeroClass, MD2HeroInfo } from '../massive-darkness2.model';
@Component({
selector: 'md2-hero-select',
templateUrl: './md2-hero-select.component.html',
styleUrls: ['./md2-hero-select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MD2HeroSelectComponent),
multi: true
},
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MD2HeroSelectComponent implements ControlValueAccessor, Validator {
private _lastBlurValue: string;
playerTabId: string;
readonly: boolean = false;
isRequired: boolean = false;
heroOptions: DropDownOption[];
@Input() id?= '';
@Input() name = '';
@Input() data: MD2HeroInfo;
@Output() focus = new EventEmitter();
@Output() blur = new EventEmitter<MD2HeroInfo>();
@Input('readonly')
public set input_readonly(value) {
this.readonly = typeof value !== 'undefined' && value !== false;
}
@Input('isRequired')
public set input_isRequired(value) {
this.isRequired = typeof value !== 'undefined' && value !== false;
}
constructor(
private elementRef: ElementRef,
private renderer: Renderer2,
private md2Service: MD2Service,
private cdRef: ChangeDetectorRef
) {
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();
}
validate(control: AbstractControl): ValidationErrors {
// if (this.required && (this.value == null || this.value == 0)) {
// return { 'currency': '' };
// }
return null;
}
registerOnValidatorChange?(fn: () => void): void {
}
onChange = (value: number) => { };
onTouched = () => { };
writeValue(obj: MD2HeroInfo): void {
this.data = obj;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.readonly = isDisabled;
}
ngOnInit() {
this.heroOptions = ArrayUtils.ToDropDownOptions(this.md2Service.heros, h => h.playerInfo.tabId,
hero => this.md2Service.heroFullName(hero));
}
ngAfterViewInit() {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'id')
}
onBlur() {
if (this._lastBlurValue != this.playerTabId) {
this._lastBlurValue = this.playerTabId;
this.data = this.md2Service.heros.find(h => h.playerInfo.tabId == this.playerTabId);
this.blur.emit(this.data);
}
}
}

View File

@ -0,0 +1 @@
<span class="MD2Icon {{icon}} {{iconClass}}"></span>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MD2IconComponent } from './md2-icon.component';
describe('MD2IconComponent', () => {
let component: MD2IconComponent;
let fixture: ComponentFixture<MD2IconComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MD2IconComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MD2IconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,21 @@
import { Component, Input, OnInit } from '@angular/core';
import { MD2Icon } from '../massive-darkness2.model';
@Component({
selector: 'md2-icon',
templateUrl: './md2-icon.component.html',
styleUrls: ['./md2-icon.component.scss']
})
export class MD2IconComponent implements OnInit {
@Input() iconClass: string = '';
@Input("icon") icon: MD2Icon;
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,23 @@
<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">
<div class="col-md-4">
<span class=" g-font-size-50" [innerHtml]="iconHtml(info.type)"></span>
</div>
<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 *ngIf="info.orange" class="g-height-45 mt-1">
<span class="MD2Icon Orange dice g-font-size-50">
<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>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MobAttackInfoComponent } from './mob-attack-info.component';
describe('MobAttackInfoComponent', () => {
let component: MobAttackInfoComponent;
let fixture: ComponentFixture<MobAttackInfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MobAttackInfoComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MobAttackInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,29 @@
import { Component, Input, OnInit } from '@angular/core';
import { MD2Icon, MobInfo } from '../../../massive-darkness2.model';
@Component({
selector: 'ngx-mob-attack-info',
templateUrl: './mob-attack-info.component.html',
styleUrls: ['./mob-attack-info.component.scss']
})
export class MobAttackInfoComponent implements OnInit {
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;
}
}
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1 @@
<p>mob-combat-info works!</p>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MobCombatInfoComponent } from './mob-combat-info.component';
describe('MobCombatInfoComponent', () => {
let component: MobCombatInfoComponent;
let fixture: ComponentFixture<MobCombatInfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MobCombatInfoComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MobCombatInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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