diff --git a/backend/src/exercise/patient-ticking.ts b/backend/src/exercise/patient-ticking.ts index dab9a61e0..e535fcca7 100644 --- a/backend/src/exercise/patient-ticking.ts +++ b/backend/src/exercise/patient-ticking.ts @@ -1,30 +1,16 @@ -import type { - ExerciseState, - HealthPoints, - Patient, - PersonnelType, - UUID, -} from 'digital-fuesim-manv-shared'; -import { healthPointsDefaults, isAlive } from 'digital-fuesim-manv-shared'; - -/** - * The count of assigned personnel and material that cater for a {@link Patient}. - */ -type Catering = { [key in PersonnelType | 'material']: number }; +import type { ExerciseState, Catering, UUID } from 'digital-fuesim-manv-shared'; +import { Patient } from 'digital-fuesim-manv-shared'; +import { cloneDeep } from 'lodash-es'; interface PatientTickResult { /** * The id of the patient */ id: UUID; - /** - * The new {@link HealthPoints} the patient should have - */ - nextHealthPoints: HealthPoints; /** * The next {@link PatientHealthState} the patient should be in */ - nextStateId: UUID; + nextStateName: string; /** * The new state time of the patient */ @@ -33,6 +19,10 @@ interface PatientTickResult { * The time a patient was treated overall */ treatmentTime: number; + /** + * The Resources of the Patient in this tick + */ + newTreatment: Catering; } /** @@ -48,29 +38,37 @@ export function patientTick( return ( Object.values(state.patients) // Only look at patients that are alive and have a position, i.e. are not in a vehicle - .filter((patient) => isAlive(patient.health) && patient.position) + .filter( + (patient) => + Patient.getVisibleStatus( + patient, + state.configuration.pretriageEnabled, + state.configuration.bluePatientsEnabled + ) !== 'black' && !Patient.isInVehicle(patient) + ) .map((patient) => { // update the time a patient is being treated, to check for pretriage later const treatmentTime = patient.isBeingTreated ? patient.treatmentTime + patientTickInterval : patient.treatmentTime; - const nextHealthPoints = getNextPatientHealthPoints( + const newTreatment = getDedicatedResources(state, patient); + const nextStateName = getNextStateName( patient, - getDedicatedResources(state, patient), - patientTickInterval + getAverageTreatment(patient.treatmentHistory, newTreatment) ); - const nextStateId = getNextStateId(patient); const nextStateTime = - nextStateId === patient.currentHealthStateId + nextStateName === patient.currentHealthStateName ? patient.stateTime + - patientTickInterval * patient.timeSpeed + patientTickInterval * + patient.changeSpeed * + state.configuration.globalPatientChangeSpeed : 0; return { id: patient.id, - nextHealthPoints, - nextStateId, + nextStateName, nextStateTime, treatmentTime, + newTreatment, }; }) ); @@ -125,83 +123,75 @@ function getDedicatedResources( }; } -/** - * Calculate the next {@link HealthPoints} for the {@link patient}. - * @param patient The {@link Patient} to calculate the {@link HealthPoints} for. - * @param treatedBy The count of personnel/material catering for the {@link patient}. - * @param patientTickInterval The time in ms between calls to this function. - * @returns The next {@link HealthPoints} for the {@link patient} - */ -// This is a heuristic and doesn't have to be 100% correct - the players don't see the healthPoints but only the color -// This function could be as complex as we want it to be (Math.sin to get something periodic, higher polynoms...) -function getNextPatientHealthPoints( - patient: Patient, - treatedBy: Catering, - patientTickInterval: number -): HealthPoints { - let material = treatedBy.material; - const notarzt = treatedBy.notarzt; - const notSan = treatedBy.notSan; - const rettSan = treatedBy.rettSan; - // TODO: Sans should be able to treat patients too - const functionParameters = - patient.healthStates[patient.currentHealthStateId]!.functionParameters; - // To do anything the personnel needs material - // TODO: But a personnel should probably be able to treat a patient a bit without material - e.g. free airways, just press something on a strongly bleeding wound, etc. - // -> find a better heuristic - let equippedNotarzt = Math.min(notarzt, material); - material = Math.max(material - equippedNotarzt, 0); - let equippedNotSan = Math.min(notSan, material); - material = Math.max(material - equippedNotSan, 0); - let equippedRettSan = Math.min(rettSan, material); - // much more notarzt != much better patient - equippedNotarzt = Math.log2(equippedNotarzt + 1); - equippedNotSan = Math.log2(equippedNotSan + 1); - equippedRettSan = Math.log2(equippedRettSan + 1); - // TODO: some more heuristic precalculations ... - // e.g. each second we lose 100 health points - const changedHealthPerSecond = - functionParameters.constantChange + - // e.g. if we have a notarzt we gain 500 additional health points per second - functionParameters.notarztModifier * equippedNotarzt + - functionParameters.notSanModifier * equippedNotSan + - functionParameters.rettSanModifier * equippedRettSan; - - return Math.max( - healthPointsDefaults.min, - Math.min( - healthPointsDefaults.max, - // our current health points - patient.health + - (changedHealthPerSecond / 1000) * - patientTickInterval * - patient.timeSpeed - ) - ); -} - /** * Find the next {@link PatientHealthState} id for the {@link patient} by using the {@link ConditionParameters}. * @param patient The {@link Patient} to get the next {@link PatientHealthState} id for. * @returns The next {@link PatientHealthState} id. */ -function getNextStateId(patient: Patient) { - const currentState = patient.healthStates[patient.currentHealthStateId]!; +function getNextStateName(patient: Patient, dedicatedResources: Catering) { + const currentState = patient.healthStates[patient.currentHealthStateName]!; for (const nextConditions of currentState.nextStateConditions) { if ( (nextConditions.earliestTime === undefined || patient.stateTime > nextConditions.earliestTime) && (nextConditions.latestTime === undefined || patient.stateTime < nextConditions.latestTime) && - (nextConditions.minimumHealth === undefined || - patient.health > nextConditions.minimumHealth) && - (nextConditions.maximumHealth === undefined || - patient.health < nextConditions.maximumHealth) && (nextConditions.isBeingTreated === undefined || - patient.isBeingTreated === nextConditions.isBeingTreated) + patient.isBeingTreated === nextConditions.isBeingTreated) && + (nextConditions.requiredMaterialAmount === undefined || + dedicatedResources.material >= + nextConditions.requiredMaterialAmount) && + (nextConditions.requiredNotArztAmount === undefined || + dedicatedResources.notarzt >= + nextConditions.requiredNotArztAmount) && + (nextConditions.requiredNotSanAmount === undefined || + dedicatedResources.notSan + dedicatedResources.notarzt >= + nextConditions.requiredNotSanAmount) && + (nextConditions.requiredRettSanAmount === undefined || + dedicatedResources.rettSan + + dedicatedResources.notSan + + dedicatedResources.notarzt >= + nextConditions.requiredRettSanAmount) && + (nextConditions.requiredSanAmount === undefined || + dedicatedResources.san + + dedicatedResources.rettSan + + dedicatedResources.notSan + + dedicatedResources.notarzt >= + nextConditions.requiredSanAmount) ) { - return nextConditions.matchingHealthStateId; + return nextConditions.matchingHealthStateName; } } - return patient.currentHealthStateId; + return patient.currentHealthStateName; +} + +/** + * Get the average treatment for roughly the last minute, scaled to 100% from {@link requiredPercentage} + */ +function getAverageTreatment( + treatmentHistory: readonly Catering[], + newTreatment: Catering, + requiredPercentage: number = 0.8 +) { + const averageCatering: Catering = cloneDeep(newTreatment); + treatmentHistory.forEach((catering, index) => { + if (index === 0) { + return; + } + averageCatering.gf += catering.gf; + averageCatering.material += catering.material; + averageCatering.notarzt += catering.notarzt; + averageCatering.notSan += catering.notSan; + averageCatering.rettSan += catering.rettSan; + averageCatering.san += catering.san; + }); + const modifier = requiredPercentage * treatmentHistory.length; + averageCatering.gf = averageCatering.gf / modifier; + averageCatering.material = averageCatering.material / modifier; + averageCatering.notarzt = averageCatering.notarzt / modifier; + averageCatering.notSan = averageCatering.notSan / modifier; + averageCatering.rettSan = averageCatering.rettSan / modifier; + averageCatering.san = averageCatering.san / modifier; + + return averageCatering; } diff --git a/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts b/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts index 14ce6c0d5..c674b4755 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts @@ -3,10 +3,10 @@ import { Store } from '@ngrx/store'; import type { Client, ExerciseState, - Patient, Vehicle, } from 'digital-fuesim-manv-shared'; import { + Patient, loopTroughTime, Personnel, uuid, @@ -92,6 +92,7 @@ export class StatisticsService { draftState: ExerciseState ): StatisticsEntry { const exerciseStatistics = this.generateAreaStatistics( + draftState, Object.values(draftState.clients), Object.values(draftState.patients), Object.values(draftState.vehicles), @@ -102,6 +103,7 @@ export class StatisticsService { Object.entries(draftState.viewports).map(([id, viewport]) => [ id, this.generateAreaStatistics( + draftState, Object.values(draftState.clients).filter( (client) => client.viewRestrictedToViewportId === id ), @@ -132,6 +134,7 @@ export class StatisticsService { } private generateAreaStatistics( + state: ExerciseState, clients: Client[], patients: Patient[], vehicles: Vehicle[], @@ -142,7 +145,13 @@ export class StatisticsService { (client) => !client.isInWaitingRoom && client.role === 'participant' ).length, - patients: countBy(patients, (patient) => patient.realStatus), + patients: countBy(patients, (patient) => + Patient.getVisibleStatus( + patient, + state.configuration.pretriageEnabled, + state.configuration.bluePatientsEnabled + ) + ), vehicles: countBy(vehicles, (vehicle) => vehicle.vehicleType), personnel: countBy( personnel.filter( diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts index 6beeb4787..7bc5a2f79 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts @@ -25,7 +25,7 @@ export class PatientFeatureManager extends ElementFeatureManager< const patient = this.getElementFromFeature(feature)!.value; return { ...patient.image, - rotation: patient.pretriageInformation.isWalkable + rotation: Patient.getPretriageInformation(patient).isWalkable ? undefined : (3 * Math.PI) / 2, }; @@ -49,7 +49,7 @@ export class PatientFeatureManager extends ElementFeatureManager< color: 'white', width: 1, }), - displacement: patient.pretriageInformation.isWalkable + displacement: Patient.getPretriageInformation(patient).isWalkable ? [0, 20] : [-20, 0], }; diff --git a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/patient-popup/patient-popup.component.html b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/patient-popup/patient-popup.component.html index 4b742b5d4..b8250ca2f 100644 --- a/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/patient-popup/patient-popup.component.html +++ b/frontend/src/app/pages/exercises/exercise/shared/exercise-map/shared/patient-popup/patient-popup.component.html @@ -5,7 +5,8 @@