Skip to content

Commit 79d5fa2

Browse files
ClFeSchpistudent72
andcommitted
Introduce state-based patient model
* Continuation of #490 Co-authored-by: Florian <[email protected]>
1 parent c7ba8c1 commit 79d5fa2

File tree

32 files changed

+1298
-2033
lines changed

32 files changed

+1298
-2033
lines changed
Lines changed: 76 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
11
import type {
2+
Catering,
23
ExerciseState,
3-
HealthPoints,
44
PatientUpdate,
5-
PersonnelType,
6-
} from 'digital-fuesim-manv-shared';
7-
import {
8-
getElement,
9-
healthPointsDefaults,
10-
isAlive,
11-
Patient,
125
} from 'digital-fuesim-manv-shared';
13-
14-
/**
15-
* The count of assigned personnel and material that cater for a {@link Patient}.
16-
*/
17-
type Catering = { [key in PersonnelType | 'material']: number };
6+
import { getElement, Patient } from 'digital-fuesim-manv-shared';
7+
import { cloneDeep } from 'lodash-es';
188

199
/**
2010
* Apply the patient tick to the {@link state}
@@ -29,29 +19,37 @@ export function patientTick(
2919
return (
3020
Object.values(state.patients)
3121
// Only look at patients that are alive and have a position, i.e. are not in a vehicle
32-
.filter((patient) => isAlive(patient.health) && patient.position)
22+
.filter(
23+
(patient) =>
24+
Patient.getVisibleStatus(
25+
patient,
26+
state.configuration.pretriageEnabled,
27+
state.configuration.bluePatientsEnabled
28+
) !== 'black' && !Patient.isInVehicle(patient)
29+
)
3330
.map((patient) => {
3431
// update the time a patient is being treated, to check for pretriage later
3532
const treatmentTime = Patient.isTreatedByPersonnel(patient)
3633
? patient.treatmentTime + patientTickInterval
3734
: patient.treatmentTime;
38-
const nextHealthPoints = getNextPatientHealthPoints(
35+
const newTreatment = getDedicatedResources(state, patient);
36+
const nextStateName = getNextStateName(
3937
patient,
40-
getDedicatedResources(state, patient),
41-
patientTickInterval
38+
getAverageTreatment(patient.treatmentHistory, newTreatment)
4239
);
43-
const nextStateId = getNextStateId(patient);
4440
const nextStateTime =
45-
nextStateId === patient.currentHealthStateId
41+
nextStateName === patient.currentHealthStateName
4642
? patient.stateTime +
47-
patientTickInterval * patient.timeSpeed
43+
patientTickInterval *
44+
patient.changeSpeed *
45+
state.configuration.globalPatientChangeSpeed
4846
: 0;
4947
return {
5048
id: patient.id,
51-
nextHealthPoints,
52-
nextStateId,
49+
nextStateName,
5350
nextStateTime,
5451
treatmentTime,
52+
newTreatment,
5553
};
5654
})
5755
);
@@ -87,84 +85,76 @@ function getDedicatedResources(
8785
return cateringTypes;
8886
}
8987

90-
/**
91-
* Calculate the next {@link HealthPoints} for the {@link patient}.
92-
* @param patient The {@link Patient} to calculate the {@link HealthPoints} for.
93-
* @param treatedBy The count of personnel/material catering for the {@link patient}.
94-
* @param patientTickInterval The time in ms between calls to this function.
95-
* @returns The next {@link HealthPoints} for the {@link patient}
96-
*/
97-
// This is a heuristic and doesn't have to be 100% correct - the players don't see the healthPoints but only the color
98-
// This function could be as complex as we want it to be (Math.sin to get something periodic, higher polynoms...)
99-
function getNextPatientHealthPoints(
100-
patient: Patient,
101-
treatedBy: Catering,
102-
patientTickInterval: number
103-
): HealthPoints {
104-
let material = treatedBy.material;
105-
const notarzt = treatedBy.notarzt;
106-
const notSan = treatedBy.notSan;
107-
const rettSan = treatedBy.rettSan;
108-
// TODO: Sans should be able to treat patients too.
109-
const functionParameters =
110-
patient.healthStates[patient.currentHealthStateId]!.functionParameters;
111-
// To do anything the personnel needs material
112-
// 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.
113-
// -> find a better heuristic
114-
let equippedNotarzt = Math.min(notarzt, material);
115-
material = Math.max(material - equippedNotarzt, 0);
116-
let equippedNotSan = Math.min(notSan, material);
117-
material = Math.max(material - equippedNotSan, 0);
118-
let equippedRettSan = Math.min(rettSan, material);
119-
// much more notarzt != much better patient
120-
equippedNotarzt = Math.log2(equippedNotarzt + 1);
121-
equippedNotSan = Math.log2(equippedNotSan + 1);
122-
equippedRettSan = Math.log2(equippedRettSan + 1);
123-
// TODO: some more heuristic precalculations ...
124-
// e.g. each second we lose 100 health points
125-
const changedHealthPerSecond =
126-
functionParameters.constantChange +
127-
// e.g. if we have a notarzt we gain 500 additional health points per second
128-
functionParameters.notarztModifier * equippedNotarzt +
129-
functionParameters.notSanModifier * equippedNotSan +
130-
functionParameters.rettSanModifier * equippedRettSan;
131-
132-
return Math.max(
133-
healthPointsDefaults.min,
134-
Math.min(
135-
healthPointsDefaults.max,
136-
// our current health points
137-
patient.health +
138-
(changedHealthPerSecond / 1000) *
139-
patientTickInterval *
140-
patient.timeSpeed
141-
)
142-
);
143-
}
144-
14588
/**
14689
* Find the next {@link PatientHealthState} id for the {@link patient} by using the {@link ConditionParameters}.
14790
* @param patient The {@link Patient} to get the next {@link PatientHealthState} id for.
14891
* @returns The next {@link PatientHealthState} id.
14992
*/
150-
function getNextStateId(patient: Patient) {
151-
const currentState = patient.healthStates[patient.currentHealthStateId]!;
93+
function getNextStateName(patient: Patient, dedicatedResources: Catering) {
94+
const currentState = patient.healthStates[patient.currentHealthStateName]!;
15295
for (const nextConditions of currentState.nextStateConditions) {
15396
if (
15497
(nextConditions.earliestTime === undefined ||
15598
patient.stateTime > nextConditions.earliestTime) &&
15699
(nextConditions.latestTime === undefined ||
157100
patient.stateTime < nextConditions.latestTime) &&
158-
(nextConditions.minimumHealth === undefined ||
159-
patient.health > nextConditions.minimumHealth) &&
160-
(nextConditions.maximumHealth === undefined ||
161-
patient.health < nextConditions.maximumHealth) &&
162101
(nextConditions.isBeingTreated === undefined ||
163102
Patient.isTreatedByPersonnel(patient) ===
164-
nextConditions.isBeingTreated)
103+
nextConditions.isBeingTreated) &&
104+
(nextConditions.requiredMaterialAmount === undefined ||
105+
dedicatedResources.material >=
106+
nextConditions.requiredMaterialAmount) &&
107+
(nextConditions.requiredNotArztAmount === undefined ||
108+
dedicatedResources.notarzt >=
109+
nextConditions.requiredNotArztAmount) &&
110+
(nextConditions.requiredNotSanAmount === undefined ||
111+
dedicatedResources.notSan + dedicatedResources.notarzt >=
112+
nextConditions.requiredNotSanAmount) &&
113+
(nextConditions.requiredRettSanAmount === undefined ||
114+
dedicatedResources.rettSan +
115+
dedicatedResources.notSan +
116+
dedicatedResources.notarzt >=
117+
nextConditions.requiredRettSanAmount) &&
118+
(nextConditions.requiredSanAmount === undefined ||
119+
dedicatedResources.san +
120+
dedicatedResources.rettSan +
121+
dedicatedResources.notSan +
122+
dedicatedResources.notarzt >=
123+
nextConditions.requiredSanAmount)
165124
) {
166-
return nextConditions.matchingHealthStateId;
125+
return nextConditions.matchingHealthStateName;
167126
}
168127
}
169-
return patient.currentHealthStateId;
128+
return patient.currentHealthStateName;
129+
}
130+
131+
/**
132+
* Get the average treatment for roughly the last minute, scaled to 100% from {@link requiredPercentage}
133+
*/
134+
function getAverageTreatment(
135+
treatmentHistory: readonly Catering[],
136+
newTreatment: Catering,
137+
requiredPercentage: number = 0.8
138+
) {
139+
const averageCatering: Catering = cloneDeep(newTreatment);
140+
treatmentHistory.forEach((catering, index) => {
141+
if (index === 0) {
142+
return;
143+
}
144+
averageCatering.gf += catering.gf;
145+
averageCatering.material += catering.material;
146+
averageCatering.notarzt += catering.notarzt;
147+
averageCatering.notSan += catering.notSan;
148+
averageCatering.rettSan += catering.rettSan;
149+
averageCatering.san += catering.san;
150+
});
151+
const modifier = requiredPercentage * treatmentHistory.length;
152+
averageCatering.gf = averageCatering.gf / modifier;
153+
averageCatering.material = averageCatering.material / modifier;
154+
averageCatering.notarzt = averageCatering.notarzt / modifier;
155+
averageCatering.notSan = averageCatering.notSan / modifier;
156+
averageCatering.rettSan = averageCatering.rettSan / modifier;
157+
averageCatering.san = averageCatering.san / modifier;
158+
159+
return averageCatering;
170160
}

frontend/src/app/pages/exercises/exercise/shared/core/statistics/statistics.service.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { Injectable } from '@angular/core';
22
import { Store } from '@ngrx/store';
33
import type {
4-
Client,
54
ExerciseState,
6-
Patient,
5+
Client,
76
Vehicle,
87
} from 'digital-fuesim-manv-shared';
98
import {
109
loopTroughTime,
11-
Personnel,
12-
uuid,
1310
Viewport,
11+
uuid,
12+
Patient,
13+
Personnel,
1414
} from 'digital-fuesim-manv-shared';
1515
import { countBy } from 'lodash-es';
1616
import { ReplaySubject } from 'rxjs';
@@ -93,6 +93,7 @@ export class StatisticsService {
9393
draftState: ExerciseState
9494
): StatisticsEntry {
9595
const exerciseStatistics = this.generateAreaStatistics(
96+
draftState,
9697
Object.values(draftState.clients),
9798
Object.values(draftState.patients),
9899
Object.values(draftState.vehicles),
@@ -103,6 +104,7 @@ export class StatisticsService {
103104
Object.entries(draftState.viewports).map(([id, viewport]) => [
104105
id,
105106
this.generateAreaStatistics(
107+
draftState,
106108
Object.values(draftState.clients).filter(
107109
(client) => client.viewRestrictedToViewportId === id
108110
),
@@ -133,6 +135,7 @@ export class StatisticsService {
133135
}
134136

135137
private generateAreaStatistics(
138+
state: ExerciseState,
136139
clients: Client[],
137140
patients: Patient[],
138141
vehicles: Vehicle[],
@@ -143,7 +146,13 @@ export class StatisticsService {
143146
(client) =>
144147
!client.isInWaitingRoom && client.role === 'participant'
145148
).length,
146-
patients: countBy(patients, (patient) => patient.realStatus),
149+
patients: countBy(patients, (patient) =>
150+
Patient.getVisibleStatus(
151+
patient,
152+
state.configuration.pretriageEnabled,
153+
state.configuration.bluePatientsEnabled
154+
)
155+
),
147156
vehicles: countBy(vehicles, (vehicle) => vehicle.vehicleType),
148157
personnel: countBy(
149158
personnel.filter(

frontend/src/app/pages/exercises/exercise/shared/exercise-map/feature-managers/patient-feature-manager.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class PatientFeatureManager extends ElementFeatureManager<
2828
const patient = this.getElementFromFeature(feature)!.value;
2929
return {
3030
...patient.image,
31-
rotation: patient.pretriageInformation.isWalkable
31+
rotation: Patient.getPretriageInformation(patient).isWalkable
3232
? undefined
3333
: (3 * Math.PI) / 2,
3434
};
@@ -59,8 +59,9 @@ export class PatientFeatureManager extends ElementFeatureManager<
5959
},
6060
0.025,
6161
(feature) =>
62-
this.getElementFromFeature(feature)!.value.pretriageInformation
63-
.isWalkable
62+
Patient.getPretriageInformation(
63+
this.getElementFromFeature(feature)!.value
64+
).isWalkable
6465
? [0, 0.25]
6566
: [-0.25, 0]
6667
);

0 commit comments

Comments
 (0)