From e45a3a77ea23cf8f7a7e4f12313b22253ab46d17 Mon Sep 17 00:00:00 2001 From: sebastian Date: Sat, 1 Jun 2024 19:07:43 +0200 Subject: [PATCH] Basic Interaction Editor --- angular.json | 3 +- src/app/app.module.ts | 140 +++++++++--------- .../character-editor.component.html | 9 ++ ...haracter-interaction-editor.component.html | 109 ++++++++++++++ ...haracter-interaction-editor.component.scss | 55 +++++++ ...acter-interaction-editor.component.spec.ts | 23 +++ .../character-interaction-editor.component.ts | 78 ++++++++++ src/app/project/game-model/GameModel.ts | 16 ++ .../interactions/AbstractInteraction.ts | 7 +- .../game-model/interactions/Interaction.ts | 26 +++- .../interactions/InteractionSequences.ts | 64 +++++++- src/styles.css | 4 + 12 files changed, 459 insertions(+), 75 deletions(-) create mode 100644 src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.html create mode 100644 src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.scss create mode 100644 src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.spec.ts create mode 100644 src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.ts diff --git a/angular.json b/angular.json index d7425fa..9380e3b 100644 --- a/angular.json +++ b/angular.json @@ -3,7 +3,8 @@ "cli": { "schematicCollections": [ "@angular-eslint/schematics" - ] + ], + "analytics": false }, "version": 1, "newProjectRoot": "projects", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7daa0e0..8289e18 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -107,6 +107,9 @@ import { import { RequieredInheritancesCreatorComponent } 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 const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -141,75 +144,76 @@ const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new Transl InventorySlotCharacteristicEditorComponent, CharacteristicSelectorComponent, RequieredInheritancesEditorComponent, - RequieredInheritancesCreatorComponent - ], - 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 + 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 + ], providers: [], bootstrap: [AppComponent] }) diff --git a/src/app/editor/character-editor/character-editor.component.html b/src/app/editor/character-editor/character-editor.component.html index a8a7060..d05c0fe 100644 --- a/src/app/editor/character-editor/character-editor.component.html +++ b/src/app/editor/character-editor/character-editor.component.html @@ -66,3 +66,12 @@ + + + + Character Interactions + + + + + diff --git a/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.html b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.html new file mode 100644 index 0000000..f0c1d8c --- /dev/null +++ b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sequence + @if(isInteractionSequence(interaction)) { + done + } @else { + close + } + Source + @if(interaction != editedElement) { + {{interaction.sourceCharacter.componentName}} + } @else { + + Source Character + + {{character.componentName}} + + + } + Target + @if(interaction != editedElement) { + @if(interaction.targetCharacter != undefined) { + {{interaction!.targetCharacter!.componentName}} + } @else { +

UNKNOWN CHARACTER

+ } + } @else { + + Target Character + + {{character.componentName}} + + + } +
Label + @if(interaction != editedElement) { + {{interaction.interactionLabel}} + } @else { + + Label + + + } + + + + + + + + + + +
+

Expanded Detail

+
+
diff --git a/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.scss b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.scss new file mode 100644 index 0000000..fb17cfc --- /dev/null +++ b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.scss @@ -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; +} diff --git a/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.spec.ts b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.spec.ts new file mode 100644 index 0000000..954b13a --- /dev/null +++ b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CharacterInteractionEditorComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CharacterInteractionEditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.ts b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.ts new file mode 100644 index 0000000..ba5a976 --- /dev/null +++ b/src/app/editor/character-editor/character-interaction-editor/character-interaction-editor.component.ts @@ -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 = 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; + } +} diff --git a/src/app/project/game-model/GameModel.ts b/src/app/project/game-model/GameModel.ts index 2a34fc9..ea5d397 100644 --- a/src/app/project/game-model/GameModel.ts +++ b/src/app/project/game-model/GameModel.ts @@ -17,6 +17,7 @@ import {ConcreteItemGroup} from "./inventory/ConcreteItemGroup"; import {Item} from "./inventory/Item"; import {ItemgroupUtilities} from "./utils/ItemgroupUtilities"; import {ItemGroupCharacteristic} from "./inventory/ItemgroupCharacteristic"; +import {AbstractInteraction} from "./interactions/AbstractInteraction"; export class GameModel { gameModelName: string @@ -25,6 +26,7 @@ export class GameModel { scriptAccounts: ScriptAccount[] = []; characters: Character[] = [] itemgroups: ItemGroup[] = [] + characterInteractions: AbstractInteraction[] = [] constructor(gameModelName: string) { this.gameModelName = gameModelName; @@ -240,4 +242,18 @@ export class GameModel { } 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); + } } diff --git a/src/app/project/game-model/interactions/AbstractInteraction.ts b/src/app/project/game-model/interactions/AbstractInteraction.ts index 94ade42..0c7716f 100644 --- a/src/app/project/game-model/interactions/AbstractInteraction.ts +++ b/src/app/project/game-model/interactions/AbstractInteraction.ts @@ -3,16 +3,19 @@ import {Condition} from "./condition/Condition"; export abstract class AbstractInteraction { sourceCharacter: Character - targetCharacter: Character + targetCharacter: Character | undefined conditions: Condition[] = [] interactionLabel: string - constructor(sourceCharacter: Character, targetCharacter: Character, interactionLabel: string) { + constructor(sourceCharacter: Character, targetCharacter: Character | undefined, interactionLabel: string) { this.sourceCharacter = sourceCharacter; this.targetCharacter = targetCharacter; this.interactionLabel = interactionLabel; } + + abstract equals(other: AbstractInteraction): boolean + abstract validate(requieredCharacter: Character): boolean } diff --git a/src/app/project/game-model/interactions/Interaction.ts b/src/app/project/game-model/interactions/Interaction.ts index fab9192..d83cbd2 100644 --- a/src/app/project/game-model/interactions/Interaction.ts +++ b/src/app/project/game-model/interactions/Interaction.ts @@ -8,7 +8,31 @@ export class Interaction extends AbstractInteraction{ actions: Action[] = [] - constructor(sourceCharacter: Character, targetCharacter: Character, interactionLabel: string) { + constructor(sourceCharacter: Character, targetCharacter: Character | undefined, interactionLabel: string) { 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; + } + + } diff --git a/src/app/project/game-model/interactions/InteractionSequences.ts b/src/app/project/game-model/interactions/InteractionSequences.ts index 121e1b4..3670d48 100644 --- a/src/app/project/game-model/interactions/InteractionSequences.ts +++ b/src/app/project/game-model/interactions/InteractionSequences.ts @@ -4,10 +4,68 @@ import {Character} from "../characters/Character"; export class InteractionSequences extends AbstractInteraction { - interactions: Interaction[] = [] + rootInteraction: InteractionSequenceNode - constructor(sourceCharacter: Character, targetCharacter: Character, interactionLabel: string) { - super(sourceCharacter, targetCharacter, interactionLabel); + constructor(interaction: Interaction, interactionLabel: string) { + 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