| import { |
| Component, |
| ElementRef, |
| EventEmitter, |
| Input, |
| OnInit, |
| Output, |
| ViewChild, |
| } from '@angular/core'; |
| import {FormsModule} from '@angular/forms'; |
| import {Builder, ByteBuffer} from 'flatbuffers'; |
| import {ErrorResponse} from '../../webserver/requests/messages/error_response_generated'; |
| import { |
| ObjectType, |
| ScoreLevel, |
| SubmitActions, |
| StartMatchAction, |
| AutoBalanceAction, |
| PickupObjectAction, |
| PlaceObjectAction, |
| RobotDeathAction, |
| EndMatchAction, |
| ActionType, |
| Action, |
| } from '../../webserver/requests/messages/submit_actions_generated'; |
| |
| type Section = |
| | 'Team Selection' |
| | 'Init' |
| | 'Pickup' |
| | 'Place' |
| | 'Endgame' |
| | 'Dead' |
| | 'Review and Submit' |
| | 'Success'; |
| |
| // TODO(phil): Deduplicate with match_list.component.ts. |
| const COMP_LEVELS = ['qm', 'ef', 'qf', 'sf', 'f'] as const; |
| type CompLevel = typeof COMP_LEVELS[number]; |
| |
| // TODO(phil): Deduplicate with match_list.component.ts. |
| const COMP_LEVEL_LABELS: Record<CompLevel, string> = { |
| qm: 'Qualifications', |
| ef: 'Eighth Finals', |
| qf: 'Quarter Finals', |
| sf: 'Semi Finals', |
| f: 'Finals', |
| }; |
| |
| type ActionT = |
| | { |
| type: 'startMatchAction'; |
| timestamp?: number; |
| position: number; |
| } |
| | { |
| type: 'autoBalanceAction'; |
| timestamp?: number; |
| docked: boolean; |
| engaged: boolean; |
| balanceAttempt: boolean; |
| } |
| | { |
| type: 'pickupObjectAction'; |
| timestamp?: number; |
| objectType: ObjectType; |
| auto?: boolean; |
| } |
| | { |
| type: 'placeObjectAction'; |
| timestamp?: number; |
| objectType?: ObjectType; |
| scoreLevel: ScoreLevel; |
| auto?: boolean; |
| } |
| | { |
| type: 'robotDeathAction'; |
| timestamp?: number; |
| robotOn: boolean; |
| } |
| | { |
| type: 'endMatchAction'; |
| docked: boolean; |
| engaged: boolean; |
| balanceAttempt: boolean; |
| timestamp?: number; |
| } |
| | { |
| // This is not a action that is submitted, |
| // It is used for undoing purposes. |
| type: 'endAutoPhase'; |
| timestamp?: number; |
| }; |
| |
| @Component({ |
| selector: 'app-entry', |
| templateUrl: './entry.ng.html', |
| styleUrls: ['../app/common.css', './entry.component.css'], |
| }) |
| export class EntryComponent implements OnInit { |
| // Re-export the type here so that we can use it in the `[value]` attribute |
| // of radio buttons. |
| readonly COMP_LEVELS = COMP_LEVELS; |
| readonly COMP_LEVEL_LABELS = COMP_LEVEL_LABELS; |
| readonly ObjectType = ObjectType; |
| readonly ScoreLevel = ScoreLevel; |
| |
| section: Section = 'Team Selection'; |
| @Input() matchNumber: number = 1; |
| @Input() teamNumber: number = 1; |
| @Input() setNumber: number = 1; |
| @Input() compLevel: CompLevel = 'qm'; |
| @Input() skipTeamSelection = false; |
| |
| actionList: ActionT[] = []; |
| errorMessage: string = ''; |
| autoPhase: boolean = true; |
| lastObject: ObjectType = null; |
| |
| matchStartTimestamp: number = 0; |
| |
| ngOnInit() { |
| // When the user navigated from the match list, we can skip the team |
| // selection. I.e. we trust that the user clicked the correct button. |
| this.section = this.skipTeamSelection ? 'Init' : 'Team Selection'; |
| } |
| |
| addAction(action: ActionT): void { |
| if (action.type == 'startMatchAction') { |
| // Unix nanosecond timestamp. |
| this.matchStartTimestamp = Date.now() * 1e6; |
| action.timestamp = 0; |
| } else { |
| // Unix nanosecond timestamp relative to match start. |
| action.timestamp = Date.now() * 1e6 - this.matchStartTimestamp; |
| } |
| |
| if ( |
| action.type == 'pickupObjectAction' || |
| action.type == 'placeObjectAction' |
| ) { |
| action.auto = this.autoPhase; |
| if (action.type == 'pickupObjectAction') { |
| this.lastObject = action.objectType; |
| } else if (action.type == 'placeObjectAction') { |
| action.objectType = this.lastObject; |
| } |
| } |
| this.actionList.push(action); |
| } |
| |
| undoLastAction() { |
| if (this.actionList.length > 0) { |
| let lastAction = this.actionList.pop(); |
| switch (lastAction?.type) { |
| case 'endAutoPhase': |
| this.autoPhase = true; |
| case 'pickupObjectAction': |
| this.section = 'Pickup'; |
| break; |
| case 'placeObjectAction': |
| this.section = 'Place'; |
| break; |
| case 'endMatchAction': |
| this.section = 'Pickup'; |
| break; |
| case 'robotDeathAction': |
| // TODO(FILIP): Return user to the screen they |
| // clicked dead robot on. Pickup is fine for now but |
| // might cause confusion. |
| this.section = 'Pickup'; |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| changeSectionTo(target: Section) { |
| this.section = target; |
| } |
| |
| @ViewChild('header') header: ElementRef; |
| |
| private scrollToTop() { |
| this.header.nativeElement.scrollIntoView(); |
| } |
| |
| async submitActions() { |
| const builder = new Builder(); |
| const actionOffsets: number[] = []; |
| |
| for (const action of this.actionList) { |
| let actionOffset: number | undefined; |
| console.log(action.type); |
| |
| switch (action.type) { |
| case 'startMatchAction': |
| const startMatchActionOffset = |
| StartMatchAction.createStartMatchAction(builder, action.position); |
| actionOffset = Action.createAction( |
| builder, |
| BigInt(action.timestamp || 0), |
| ActionType.StartMatchAction, |
| startMatchActionOffset |
| ); |
| break; |
| |
| case 'pickupObjectAction': |
| const pickupObjectActionOffset = |
| PickupObjectAction.createPickupObjectAction( |
| builder, |
| action.objectType, |
| action.auto || false |
| ); |
| actionOffset = Action.createAction( |
| builder, |
| BigInt(action.timestamp || 0), |
| ActionType.PickupObjectAction, |
| pickupObjectActionOffset |
| ); |
| break; |
| |
| case 'autoBalanceAction': |
| const autoBalanceActionOffset = |
| AutoBalanceAction.createAutoBalanceAction( |
| builder, |
| action.docked, |
| action.engaged, |
| action.balanceAttempt |
| ); |
| actionOffset = Action.createAction( |
| builder, |
| BigInt(action.timestamp || 0), |
| ActionType.AutoBalanceAction, |
| autoBalanceActionOffset |
| ); |
| break; |
| |
| case 'placeObjectAction': |
| const placeObjectActionOffset = |
| PlaceObjectAction.createPlaceObjectAction( |
| builder, |
| action.objectType, |
| action.scoreLevel, |
| action.auto || false |
| ); |
| actionOffset = Action.createAction( |
| builder, |
| BigInt(action.timestamp || 0), |
| ActionType.PlaceObjectAction, |
| placeObjectActionOffset |
| ); |
| break; |
| |
| case 'robotDeathAction': |
| const robotDeathActionOffset = |
| RobotDeathAction.createRobotDeathAction(builder, action.robotOn); |
| actionOffset = Action.createAction( |
| builder, |
| BigInt(action.timestamp || 0), |
| ActionType.RobotDeathAction, |
| robotDeathActionOffset |
| ); |
| break; |
| |
| case 'endMatchAction': |
| const endMatchActionOffset = EndMatchAction.createEndMatchAction( |
| builder, |
| action.docked, |
| action.engaged, |
| action.balanceAttempt |
| ); |
| actionOffset = Action.createAction( |
| builder, |
| BigInt(action.timestamp || 0), |
| ActionType.EndMatchAction, |
| endMatchActionOffset |
| ); |
| break; |
| |
| case 'endAutoPhase': |
| // Not important action. |
| break; |
| |
| default: |
| throw new Error(`Unknown action type`); |
| } |
| |
| if (actionOffset !== undefined) { |
| actionOffsets.push(actionOffset); |
| } |
| } |
| const teamNumberFb = builder.createString(this.teamNumber.toString()); |
| const compLevelFb = builder.createString(this.compLevel); |
| |
| const actionsVector = SubmitActions.createActionsListVector( |
| builder, |
| actionOffsets |
| ); |
| SubmitActions.startSubmitActions(builder); |
| SubmitActions.addTeamNumber(builder, teamNumberFb); |
| SubmitActions.addMatchNumber(builder, this.matchNumber); |
| SubmitActions.addSetNumber(builder, this.setNumber); |
| SubmitActions.addCompLevel(builder, compLevelFb); |
| SubmitActions.addActionsList(builder, actionsVector); |
| builder.finish(SubmitActions.endSubmitActions(builder)); |
| |
| const buffer = builder.asUint8Array(); |
| const res = await fetch('/requests/submit/submit_actions', { |
| method: 'POST', |
| body: buffer, |
| }); |
| |
| if (res.ok) { |
| // We successfully submitted the data. Report success. |
| this.section = 'Success'; |
| this.actionList = []; |
| } else { |
| const resBuffer = await res.arrayBuffer(); |
| const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer)); |
| const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer); |
| |
| const errorMessage = parsedResponse.errorMessage(); |
| this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`; |
| } |
| } |
| } |