Basic Interaction Editor
All checks were successful
E2E Testing / test (push) Successful in 2m8s

This commit is contained in:
sebastian 2024-06-01 19:07:43 +02:00
parent bcd06d894f
commit e45a3a77ea
12 changed files with 459 additions and 75 deletions

View File

@ -3,7 +3,8 @@
"cli": { "cli": {
"schematicCollections": [ "schematicCollections": [
"@angular-eslint/schematics" "@angular-eslint/schematics"
] ],
"analytics": false
}, },
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",

View File

@ -107,6 +107,9 @@ import {
import { import {
RequieredInheritancesCreatorComponent RequieredInheritancesCreatorComponent
} from "./editor/character-editor/inventory-slot-editor/requiered-inheritances-editor/requiered-inheritances-creator/requiered-inheritances-creator.component"; } from "./editor/character-editor/inventory-slot-editor/requiered-inheritances-editor/requiered-inheritances-creator/requiered-inheritances-creator.component";
import {
CharacterInteractionEditorComponent
} from "./editor/character-editor/character-interaction-editor/character-interaction-editor.component";
// AoT requires an exported function for factories // AoT requires an exported function for factories
const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new TranslateHttpLoader(http, './assets/i18n/', '.json'); const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new TranslateHttpLoader(http, './assets/i18n/', '.json');
@ -141,75 +144,76 @@ const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new Transl
InventorySlotCharacteristicEditorComponent, InventorySlotCharacteristicEditorComponent,
CharacteristicSelectorComponent, CharacteristicSelectorComponent,
RequieredInheritancesEditorComponent, RequieredInheritancesEditorComponent,
RequieredInheritancesCreatorComponent RequieredInheritancesCreatorComponent,
], CharacterInteractionEditorComponent
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
CoreModule,
SharedModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient]
}
}),
BrowserAnimationsModule,
MatIcon,
MatToolbar,
MatButton,
MatFormField,
MatInput,
MatDrawerContainer,
MatDrawer,
MatIconButton,
MatMenuTrigger,
MatMenu,
MatMenuItem,
MatListItem,
MatActionList,
MatTabGroup,
MatTab,
MatTabLabel,
MatLabel,
MatFormField,
ReactiveFormsModule,
MatError,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatMiniFabButton,
MatTreeModule,
MatTable,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatCellDef,
MatCell,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatCheckbox,
MatSelect,
MatOption,
MatHint,
MatTooltip,
MatCard,
MatCardContent,
MatCardHeader,
MatAccordion,
MatExpansionPanel,
MatExpansionPanelTitle,
MatCardTitle,
MatExpansionPanelHeader,
MatExpansionPanelDescription,
MatAutocomplete,
MatAutocompleteTrigger,
MatNoDataRow
], ],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
CoreModule,
SharedModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: httpLoaderFactory,
deps: [HttpClient]
}
}),
BrowserAnimationsModule,
MatIcon,
MatToolbar,
MatButton,
MatFormField,
MatInput,
MatDrawerContainer,
MatDrawer,
MatIconButton,
MatMenuTrigger,
MatMenu,
MatMenuItem,
MatListItem,
MatActionList,
MatTabGroup,
MatTab,
MatTabLabel,
MatLabel,
MatFormField,
ReactiveFormsModule,
MatError,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatMiniFabButton,
MatTreeModule,
MatTable,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatCellDef,
MatCell,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatCheckbox,
MatSelect,
MatOption,
MatHint,
MatTooltip,
MatCard,
MatCardContent,
MatCardHeader,
MatAccordion,
MatExpansionPanel,
MatExpansionPanelTitle,
MatCardTitle,
MatExpansionPanelHeader,
MatExpansionPanelDescription,
MatAutocomplete,
MatAutocompleteTrigger,
MatNoDataRow
],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -66,3 +66,12 @@
<app-inventory-slot-editor [character]="character" [itemgroups]="gameModel!.itemgroupsAsList"></app-inventory-slot-editor> <app-inventory-slot-editor [character]="character" [itemgroups]="gameModel!.itemgroupsAsList"></app-inventory-slot-editor>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card>
<mat-card-header>
<mat-card-title>Character Interactions</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-character-interaction-editor [character]="character" [gameModel]="gameModel"></app-character-interaction-editor>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,109 @@
<table mat-table [dataSource]="interactionDatasource" multiTemplateDataRows class="mat-elevation-z8">
<ng-container matColumnDef="sequence">
<th mat-header-cell *matHeaderCellDef>Sequence</th>
<td mat-cell *matCellDef="let interaction">
@if(isInteractionSequence(interaction)) {
<mat-icon>done</mat-icon>
} @else {
<mat-icon>close</mat-icon>
}
</td>
</ng-container>
<ng-container matColumnDef="source">
<th mat-header-cell *matHeaderCellDef>Source</th>
<td mat-cell *matCellDef="let interaction">
@if(interaction != editedElement) {
{{interaction.sourceCharacter.componentName}}
} @else {
<mat-form-field appearance="fill" class="long-form">
<mat-label>Source Character</mat-label>
<mat-select [(ngModel)]="editedElement!.sourceCharacter">
<mat-option *ngFor="let character of gameModel!.characters" [value]="character">{{character.componentName}}</mat-option>
</mat-select>
</mat-form-field>
}
</td>
</ng-container>
<ng-container matColumnDef="target">
<th mat-header-cell *matHeaderCellDef>Target</th>
<td mat-cell *matCellDef="let interaction">
@if(interaction != editedElement) {
@if(interaction.targetCharacter != undefined) {
{{interaction!.targetCharacter!.componentName}}
} @else {
<p class="warning">UNKNOWN CHARACTER</p>
}
} @else {
<mat-form-field appearance="fill" class="long-form">
<mat-label>Target Character</mat-label>
<mat-select [(ngModel)]="editedElement!.targetCharacter">
<mat-option *ngFor="let character of gameModel!.characters" [value]="character">{{character.componentName}}</mat-option>
</mat-select>
</mat-form-field>
}
</td>
</ng-container>
<ng-container matColumnDef="label">
<th mat-header-cell *matHeaderCellDef>Label</th>
<td mat-cell *matCellDef="let interaction">
@if(interaction != editedElement) {
{{interaction.interactionLabel}}
} @else {
<mat-form-field appearance="fill" class="long-form">
<mat-label>Label</mat-label>
<input matInput [(ngModel)]="editedElement!.interactionLabel">
</mat-form-field>
}
</td>
</ng-container>
<ng-container matColumnDef="edit">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let interaction">
<button mat-icon-button *ngIf="editedElement !== interaction" [disabled]="editedElement != null" (click)="editInteraction(interaction)"><mat-icon>edit</mat-icon></button>
<button mat-icon-button *ngIf="editedElement === interaction" (click)="submitInteraction()"><mat-icon>done</mat-icon></button>
</td>
</ng-container>
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let interaction">
<button mat-icon-button color="warn" (click)="deleteInteraction(interaction)"><mat-icon>delete</mat-icon></button>
</td>
</ng-container>
<ng-container matColumnDef="expand">
<th mat-header-cell *matHeaderCellDef aria-label="row actions">
<button mat-icon-button (click)="addInteraction()"><mat-icon>add</mat-icon></button>
</th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button aria-label="expand row" (click)="(expandedElement = expandedElement === element ? null : element); $event.stopPropagation()">
@if (expandedElement === element) {
<mat-icon>keyboard_arrow_up</mat-icon>
} @else {
<mat-icon>keyboard_arrow_down</mat-icon>
}
</button>
</td>
</ng-container>
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let element" [attr.colspan]="columnsToDisplayWithExpand.length">
<div class="example-element-detail"
[@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'">
<p>Expanded Detail</p>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToDisplayWithExpand"></tr>
<tr mat-row *matRowDef="let element; columns: columnsToDisplayWithExpand;"
class="example-element-row"
[class.example-expanded-row]="expandedElement === element"
(click)="expandedElement = expandedElement === element ? null : element">
</tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="example-detail-row"></tr>
</table>

View File

@ -0,0 +1,55 @@
table {
width: 100%;
}
tr.example-detail-row {
height: 0;
}
tr.example-element-row:not(.example-expanded-row):hover {
background: #4e5157;
}
tr.example-element-row:not(.example-expanded-row):active {
background: #545456;
}
.example-element-row td {
border-bottom-width: 0;
}
.example-element-detail {
overflow: hidden;
display: flex;
}
.example-element-diagram {
min-width: 80px;
border: 2px solid black;
padding: 8px;
font-weight: lighter;
margin: 8px 0;
height: 104px;
}
.example-element-symbol {
font-weight: bold;
font-size: 40px;
line-height: normal;
}
.example-element-description {
padding: 16px;
}
.example-element-description-attribution {
opacity: 0.5;
}
.mat-column-delete, .mat-column-edit, .mat-column-expand, .mat-column-sequence {
width: 32px;
}
.warning {
color: red;
}

View File

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

View File

@ -0,0 +1,78 @@
import {Component, Input, OnInit} from '@angular/core';
import {Character} from "../../../project/game-model/characters/Character";
import {GameModel} from "../../../project/game-model/GameModel";
import {AbstractInteraction} from "../../../project/game-model/interactions/AbstractInteraction";
import {MatColumnDef, MatTable, MatTableDataSource} from "@angular/material/table";
import {InteractionSequences} from "../../../project/game-model/interactions/InteractionSequences";
import {animate, state, style, transition, trigger} from "@angular/animations";
import {Interaction} from "../../../project/game-model/interactions/Interaction";
import {Condition} from "../../../project/game-model/interactions/condition/Condition";
import {MatSnackBar} from "@angular/material/snack-bar";
@Component({
selector: 'app-character-interaction-editor',
templateUrl: './character-interaction-editor.component.html',
styleUrl: './character-interaction-editor.component.scss',
animations: [
trigger('detailExpand', [
state('collapsed,void', style({height: '0px', minHeight: '0'})),
state('expanded', style({height: '*'})),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
],
})
export class CharacterInteractionEditorComponent implements OnInit{
@Input() character: Character | undefined
@Input() gameModel: GameModel | undefined
displayedColumns: string[] = ['sequence', 'source', 'target', 'label', 'edit', 'delete']
columnsToDisplayWithExpand = [... this.displayedColumns, 'expand'];
expandedElement: AbstractInteraction | null = null;
editedElement: AbstractInteraction | null = null;
interactionDatasource: MatTableDataSource<AbstractInteraction> = new MatTableDataSource();
constructor(private snackbar: MatSnackBar) {
}
ngOnInit() {
this.interactionDatasource.data = this.gameModel!.getCharacterInteractionsByCharacter(this.character!);
}
isInteractionSequence(interaction: AbstractInteraction) {
return interaction instanceof InteractionSequences
}
addInteraction() {
const interaction = new Interaction(this.character!, undefined, "")
this.editedElement = interaction;
const interactions = this.interactionDatasource.data;
interactions.push(interaction);
this.interactionDatasource.data = interactions;
}
submitInteraction() {
if(this.editedElement == undefined) {
return;
}
if(this.editedElement!.validate(this.character!)) {
this.gameModel!.addCharacterInteraction(this.editedElement);
this.editedElement = null;
} else {
this.snackbar.open("Invalid Interaction", "", {duration: 2000});
}
}
deleteInteraction(interaction: AbstractInteraction) {
this.gameModel!.removeCharacterInteraction(interaction);
this.interactionDatasource.data = this.gameModel!.characterInteractions;
}
editInteraction(interaction: AbstractInteraction) {
this.editedElement = interaction;
}
}

View File

@ -17,6 +17,7 @@ import {ConcreteItemGroup} from "./inventory/ConcreteItemGroup";
import {Item} from "./inventory/Item"; import {Item} from "./inventory/Item";
import {ItemgroupUtilities} from "./utils/ItemgroupUtilities"; import {ItemgroupUtilities} from "./utils/ItemgroupUtilities";
import {ItemGroupCharacteristic} from "./inventory/ItemgroupCharacteristic"; import {ItemGroupCharacteristic} from "./inventory/ItemgroupCharacteristic";
import {AbstractInteraction} from "./interactions/AbstractInteraction";
export class GameModel { export class GameModel {
gameModelName: string gameModelName: string
@ -25,6 +26,7 @@ export class GameModel {
scriptAccounts: ScriptAccount[] = []; scriptAccounts: ScriptAccount[] = [];
characters: Character[] = [] characters: Character[] = []
itemgroups: ItemGroup[] = [] itemgroups: ItemGroup[] = []
characterInteractions: AbstractInteraction[] = []
constructor(gameModelName: string) { constructor(gameModelName: string) {
this.gameModelName = gameModelName; this.gameModelName = gameModelName;
@ -240,4 +242,18 @@ export class GameModel {
} }
return false; return false;
} }
getCharacterInteractionsByCharacter(character: Character) {
return this.characterInteractions.filter(interaction => interaction.targetCharacter === character || interaction.sourceCharacter === character);
}
addCharacterInteraction(characterInteraction: AbstractInteraction) {
if(this.characterInteractions.find(interaction => interaction.equals(characterInteraction)) == undefined) {
this.characterInteractions.push(characterInteraction);
}
}
removeCharacterInteraction(characterInteraction: AbstractInteraction) {
this.characterInteractions = this.characterInteractions.filter(interaction => interaction != characterInteraction);
}
} }

View File

@ -3,16 +3,19 @@ import {Condition} from "./condition/Condition";
export abstract class AbstractInteraction { export abstract class AbstractInteraction {
sourceCharacter: Character sourceCharacter: Character
targetCharacter: Character targetCharacter: Character | undefined
conditions: Condition[] = [] conditions: Condition[] = []
interactionLabel: string interactionLabel: string
constructor(sourceCharacter: Character, targetCharacter: Character, interactionLabel: string) { constructor(sourceCharacter: Character, targetCharacter: Character | undefined, interactionLabel: string) {
this.sourceCharacter = sourceCharacter; this.sourceCharacter = sourceCharacter;
this.targetCharacter = targetCharacter; this.targetCharacter = targetCharacter;
this.interactionLabel = interactionLabel; this.interactionLabel = interactionLabel;
} }
abstract equals(other: AbstractInteraction): boolean
abstract validate(requieredCharacter: Character): boolean
} }

View File

@ -8,7 +8,31 @@ export class Interaction extends AbstractInteraction{
actions: Action[] = [] actions: Action[] = []
constructor(sourceCharacter: Character, targetCharacter: Character, interactionLabel: string) { constructor(sourceCharacter: Character, targetCharacter: Character | undefined, interactionLabel: string) {
super(sourceCharacter, targetCharacter, interactionLabel); super(sourceCharacter, targetCharacter, interactionLabel);
} }
equals(other: AbstractInteraction): boolean {
if(!(other instanceof Interaction)) {
return false;
}
const equealCharacters = this.sourceCharacter === other.sourceCharacter && this.targetCharacter === other.targetCharacter;
const equalLabels = this.interactionLabel === other.interactionLabel;
const equalConditions = this.conditions.every(condition => other.conditions.includes(condition)) && other.conditions.every(condition => this.conditions.includes(condition));
const equalActions = this.actions.every(action => other.actions.includes(action) && other.actions.every(action => this.actions.includes(action)));
return equealCharacters && equalLabels && equalConditions && equalActions;
}
validate(requieredCharacter: Character): boolean {
const validCharacters = this.sourceCharacter == requieredCharacter || this.targetCharacter == requieredCharacter;
const validLabel = this.interactionLabel.length > 0;
//Todo: Check for contradicting conditions as well as double conditions and actions
return validCharacters && validLabel;
}
} }

View File

@ -4,10 +4,68 @@ import {Character} from "../characters/Character";
export class InteractionSequences extends AbstractInteraction { export class InteractionSequences extends AbstractInteraction {
interactions: Interaction[] = [] rootInteraction: InteractionSequenceNode
constructor(sourceCharacter: Character, targetCharacter: Character, interactionLabel: string) { constructor(interaction: Interaction, interactionLabel: string) {
super(sourceCharacter, targetCharacter, interactionLabel); super(interaction.sourceCharacter, interaction.targetCharacter, interactionLabel);
this.rootInteraction = new InteractionSequenceNode(interaction, []);
}
equals(other: AbstractInteraction): boolean {
if(!(other instanceof InteractionSequences)) {
return false;
}
const equealCharacters = this.sourceCharacter === other.sourceCharacter && this.targetCharacter === other.targetCharacter;
const equalLabels = this.interactionLabel === other.interactionLabel;
const equalConditions = this.conditions.every(condition => other.conditions.includes(condition)) && other.conditions.every(condition => this.conditions.includes(condition));
const equalSequenceTree = this.rootInteraction.equals(other.rootInteraction);
return equealCharacters && equalLabels && equalConditions && equalSequenceTree;
}
validate(requieredCharacter: Character): boolean {
const validCharacters = this.sourceCharacter == requieredCharacter || this.targetCharacter == requieredCharacter;
const validLabel = this.interactionLabel.length > 0;
const validSequenceTree = this.rootInteraction.validate(requieredCharacter)
return validCharacters && validLabel && validSequenceTree;
}
}
class InteractionSequenceNode {
root: Interaction
children: InteractionSequenceNode[] = [];
equals(other: InteractionSequenceNode) {
const equalsRoot = this.root.equals(other.root);
const equalsChildren = this.children.every(child => other.children.includes(child) && other.children.every(child => this.children.includes(child)));
return equalsRoot && equalsChildren;
}
validate(requiredCharacter: Character): boolean {
const validateRoot = this.root.validate(requiredCharacter);
let validateChildren = true;
for(let i=0; i<this.children.length; i++) {
if(!this.children[i].validate(requiredCharacter)) {
validateChildren = false;
break;
}
}
return validateRoot && validateChildren;
}
constructor(root: Interaction, children: InteractionSequenceNode[]) {
this.root = root;
this.children = children;
} }
} }

View File

@ -21,3 +21,7 @@ body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
.icon-btn-primary { .icon-btn-primary {
color: #3c95f8; color: #3c95f8;
} }
.long-form {
width: 100%;
}