XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFESpaceNinjaServer

feat: initial stats save (#884)

Closes #203 Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/884 Reviewed-by: Sainan <sainan@calamity.inc> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>

13c68a75
AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
提交于

代码差异

7 个文件 +501 -18
Modified src/controllers/custom/deleteAccountController.ts +3 -1
@@ -6,6 +6,7 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
6 6 import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
7 7 import { PersonalRooms } from "@/src/models/personalRoomsModel";
8 8 import { Ship } from "@/src/models/shipModel";
9 import { Stats } from "@/src/models/statsModel";
9 10
10 11 export const deleteAccountController: RequestHandler = async (req, res) => {
11 12 const accountId = await getAccountIdForRequest(req);
@@ -15,7 +16,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
15 16 Inventory.deleteOne({ accountOwnerId: accountId }),
16 17 Loadout.deleteOne({ loadoutOwnerId: accountId }),
17 18 PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }),
18 Ship.deleteOne({ ShipOwnerId: accountId })
19 Ship.deleteOne({ ShipOwnerId: accountId }),
20 Stats.deleteOne({ accountOwnerId: accountId })
19 21 ]);
20 22 res.end();
21 23 };
Modified src/controllers/stats/uploadController.ts +9 -1
@@ -1,6 +1,14 @@
1 import { getJSONfromString } from "@/src/helpers/stringHelpers";
2 import { getAccountIdForRequest } from "@/src/services/loginService";
3 import { getStats, uploadStats } from "@/src/services/statsService";
4 import { IStatsUpload } from "@/src/types/statTypes";
1 5 import { RequestHandler } from "express";
2 6
3 const uploadController: RequestHandler = (_req, res) => {
7 const uploadController: RequestHandler = async (req, res) => {
8 const payload = getJSONfromString<IStatsUpload>(String(req.body));
9 const accountId = await getAccountIdForRequest(req);
10 const playerStats = await getStats(accountId);
11 await uploadStats(playerStats, payload);
4 12 res.status(200).end();
5 13 };
6 14
Modified src/controllers/stats/viewController.ts +13 -11
@@ -1,31 +1,33 @@
1 1 import { RequestHandler } from "express";
2 2 import { getAccountIdForRequest } from "@/src/services/loginService";
3 import { IStatsView } from "@/src/types/statTypes";
4 3 import { config } from "@/src/services/configService";
5 4 import allScans from "@/static/fixed_responses/allScans.json";
6 5 import { ExportEnemies } from "warframe-public-export-plus";
7 6 import { getInventory } from "@/src/services/inventoryService";
7 import { getStats } from "@/src/services/statsService";
8 import { IStatsView } from "@/src/types/statTypes";
8 9
9 10 const viewController: RequestHandler = async (req, res) => {
10 11 const accountId = await getAccountIdForRequest(req);
11 12 const inventory = await getInventory(accountId, "XPInfo");
13 const playerStats = await getStats(accountId);
12 14
13 const responseJson: IStatsView = {};
14 responseJson.Weapons = [];
15 const responseJson: IStatsView = playerStats.toJSON();
16 responseJson.Weapons ??= [];
15 17 for (const item of inventory.XPInfo) {
16 responseJson.Weapons.push({
17 type: item.ItemType,
18 xp: item.XP
19 });
18 const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType);
19 if (weaponIndex !== -1) {
20 responseJson.Weapons[weaponIndex].xp == item.XP;
21 } else {
22 responseJson.Weapons.push({ type: item.ItemType, xp: item.XP });
23 }
20 24 }
21 25 if (config.unlockAllScans) {
22 26 const scans = new Set(allScans);
23 27 for (const type of Object.keys(ExportEnemies.avatars)) {
24 if (!scans.has(type)) {
25 scans.add(type);
26 }
28 if (!scans.has(type)) scans.add(type);
27 29 }
28 responseJson.Scans = [];
30 responseJson.Scans ??= [];
29 31 for (const type of scans) {
30 32 responseJson.Scans.push({ type: type, scans: 9999 });
31 33 }
Added src/models/statsModel.ts +101 -0
@@ -0,0 +1,101 @@
1 import { Document, Schema, Types, model } from "mongoose";
2 import { IEnemy, IMission, IScan, ITutorial, IAbility, IWeapon, IStatsDatabase } from "@/src/types/statTypes";
3
4 const abilitySchema = new Schema<IAbility>(
5 {
6 type: { type: String, required: true },
7 used: Number
8 },
9 { _id: false }
10 );
11
12 const enemySchema = new Schema<IEnemy>(
13 {
14 type: { type: String, required: true },
15 executions: Number,
16 headshots: Number,
17 kills: Number,
18 assists: Number,
19 deaths: Number
20 },
21 { _id: false }
22 );
23
24 const missionSchema = new Schema<IMission>(
25 {
26 type: { type: String, required: true },
27 highScore: Number
28 },
29 { _id: false }
30 );
31
32 const scanSchema = new Schema<IScan>(
33 {
34 type: { type: String, required: true },
35 scans: Number
36 },
37 { _id: false }
38 );
39
40 const tutorialSchema = new Schema<ITutorial>(
41 {
42 stage: Number
43 },
44 { _id: false }
45 );
46
47 const weaponSchema = new Schema<IWeapon>(
48 {
49 type: { type: String, required: true },
50 equipTime: Number,
51 hits: Number,
52 kills: Number,
53 xp: Number,
54 assists: Number,
55 headshots: Number,
56 fired: Number
57 },
58 { _id: false }
59 );
60
61 const statsSchema = new Schema<IStatsDatabase>({
62 accountOwnerId: { type: Schema.Types.ObjectId, required: true },
63 CiphersSolved: Number,
64 CiphersFailed: Number,
65 CipherTime: Number,
66 Weapons: { type: [weaponSchema], default: [] },
67 Enemies: { type: [enemySchema], default: [] },
68 MeleeKills: Number,
69 MissionsCompleted: Number,
70 MissionsQuit: Number,
71 MissionsFailed: Number,
72 TimePlayedSec: Number,
73 PickupCount: Number,
74 Tutorial: { type: Map, of: tutorialSchema, default: {} },
75 Abilities: { type: [abilitySchema], default: [] },
76 Rating: Number,
77 Income: Number,
78 Rank: Number,
79 PlayerLevel: Number,
80 Scans: { type: [scanSchema], default: [] },
81 Missions: { type: [missionSchema], default: [] },
82 Deaths: Number,
83 HealCount: Number,
84 ReviveCount: Number
85 });
86
87 statsSchema.set("toJSON", {
88 transform(_document, returnedObject) {
89 delete returnedObject._id;
90 delete returnedObject.__v;
91 delete returnedObject.accountOwnerId;
92 }
93 });
94
95 export const Stats = model<IStatsDatabase>("Stats", statsSchema);
96
97 // eslint-disable-next-line @typescript-eslint/ban-types
98 export type TStatsDatabaseDocument = Document<unknown, {}, IStatsDatabase> & {
99 _id: Types.ObjectId;
100 __v: number;
101 } & IStatsDatabase;
Modified src/services/loginService.ts +2 -0
@@ -7,6 +7,7 @@ import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
7 7 import { PersonalRooms } from "@/src/models/personalRoomsModel";
8 8 import { Request } from "express";
9 9 import { config } from "@/src/services/configService";
10 import { createStats } from "@/src/services/statsService";
10 11
11 12 export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => {
12 13 return requestPassword === databasePassword;
@@ -24,6 +25,7 @@ export const createAccount = async (accountData: IDatabaseAccount): Promise<IDat
24 25 const shipId = await createShip(account._id);
25 26 await createInventory(account._id, { loadOutPresetId: loadoutId, ship: shipId });
26 27 await createPersonalRooms(account._id, shipId);
28 await createStats(account._id.toString());
27 29 return account.toJSON();
28 30 } catch (error) {
29 31 if (error instanceof Error) {
Added src/services/statsService.ts +283 -0
@@ -0,0 +1,283 @@
1 import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel";
2 import { IStatsUpload } from "@/src/types/statTypes";
3
4 export const createStats = async (accountId: string): Promise<TStatsDatabaseDocument> => {
5 const stats = new Stats({ accountOwnerId: accountId });
6 await stats.save();
7 return stats;
8 };
9
10 export const getStats = async (accountOwnerId: string): Promise<TStatsDatabaseDocument> => {
11 let stats = await Stats.findOne({ accountOwnerId: accountOwnerId });
12
13 if (!stats) stats = await createStats(accountOwnerId);
14
15 return stats;
16 };
17
18 export const uploadStats = async (playerStats: TStatsDatabaseDocument, payload: IStatsUpload): Promise<void> => {
19 if (payload.add) {
20 const {
21 MISSION_COMPLETE,
22 PICKUP_ITEM,
23 SCAN,
24 USE_ABILITY,
25 FIRE_WEAPON,
26 HIT_ENTITY_ITEM,
27 HEADSHOT_ITEM,
28 KILL_ENEMY_ITEM,
29 KILL_ENEMY,
30 EXECUTE_ENEMY,
31 HEADSHOT,
32 DIE,
33 MELEE_KILL,
34 INCOME,
35 CIPHER
36 } = payload.add;
37
38 if (MISSION_COMPLETE) {
39 for (const [key, value] of Object.entries(MISSION_COMPLETE)) {
40 switch (key) {
41 case "GS_SUCCESS":
42 playerStats.MissionsCompleted ??= 0;
43 playerStats.MissionsCompleted += value;
44 break;
45 case "GS_QUIT":
46 playerStats.MissionsQuit ??= 0;
47 playerStats.MissionsQuit += value;
48 break;
49 case "GS_FAILURE":
50 playerStats.MissionsFailed ??= 0;
51 playerStats.MissionsFailed += value;
52 break;
53 }
54 }
55 }
56
57 if (PICKUP_ITEM) {
58 for (const value of Object.values(PICKUP_ITEM)) {
59 playerStats.PickupCount ??= 0;
60 playerStats.PickupCount += value;
61 }
62 }
63
64 if (SCAN) {
65 playerStats.Scans ??= [];
66 for (const [key, scans] of Object.entries(SCAN)) {
67 const scan = playerStats.Scans.find(element => element.type === key);
68 if (scan) {
69 scan.scans ??= 0;
70 scan.scans += scans;
71 } else {
72 playerStats.Scans.push({ type: key, scans });
73 }
74 }
75 }
76
77 if (USE_ABILITY) {
78 playerStats.Abilities ??= [];
79 for (const [key, used] of Object.entries(USE_ABILITY)) {
80 const ability = playerStats.Abilities.find(element => element.type === key);
81 if (ability) {
82 ability.used ??= 0;
83 ability.used += used;
84 } else {
85 playerStats.Abilities.push({ type: key, used });
86 }
87 }
88 }
89
90 if (FIRE_WEAPON) {
91 playerStats.Weapons ??= [];
92 for (const [key, fired] of Object.entries(FIRE_WEAPON)) {
93 const weapon = playerStats.Weapons.find(element => element.type === key);
94 if (weapon) {
95 weapon.fired ??= 0;
96 weapon.fired += fired;
97 } else {
98 playerStats.Weapons.push({ type: key, fired });
99 }
100 }
101 }
102
103 if (HIT_ENTITY_ITEM) {
104 playerStats.Weapons ??= [];
105 for (const [key, hits] of Object.entries(HIT_ENTITY_ITEM)) {
106 const weapon = playerStats.Weapons.find(element => element.type === key);
107 if (weapon) {
108 weapon.hits ??= 0;
109 weapon.hits += hits;
110 } else {
111 playerStats.Weapons.push({ type: key, hits });
112 }
113 }
114 }
115
116 if (HEADSHOT_ITEM) {
117 playerStats.Weapons ??= [];
118 for (const [key, headshots] of Object.entries(HEADSHOT_ITEM)) {
119 const weapon = playerStats.Weapons.find(element => element.type === key);
120 if (weapon) {
121 weapon.headshots ??= 0;
122 weapon.headshots += headshots;
123 } else {
124 playerStats.Weapons.push({ type: key, headshots });
125 }
126 }
127 }
128
129 if (KILL_ENEMY_ITEM) {
130 playerStats.Weapons ??= [];
131 for (const [key, kills] of Object.entries(KILL_ENEMY_ITEM)) {
132 const weapon = playerStats.Weapons.find(element => element.type === key);
133 if (weapon) {
134 weapon.kills ??= 0;
135 weapon.kills += kills;
136 } else {
137 playerStats.Weapons.push({ type: key, kills });
138 }
139 }
140 }
141
142 if (KILL_ENEMY) {
143 playerStats.Enemies ??= [];
144 for (const [key, kills] of Object.entries(KILL_ENEMY)) {
145 const enemy = playerStats.Enemies.find(element => element.type === key);
146 if (enemy) {
147 enemy.kills ??= 0;
148 enemy.kills += kills;
149 } else {
150 playerStats.Enemies.push({ type: key, kills });
151 }
152 }
153 }
154
155 if (EXECUTE_ENEMY) {
156 playerStats.Enemies ??= [];
157 for (const [key, executions] of Object.entries(EXECUTE_ENEMY)) {
158 const enemy = playerStats.Enemies.find(element => element.type === key);
159 if (enemy) {
160 enemy.executions ??= 0;
161 enemy.executions += executions;
162 } else {
163 playerStats.Enemies.push({ type: key, executions });
164 }
165 }
166 }
167
168 if (HEADSHOT) {
169 playerStats.Enemies ??= [];
170 for (const [key, headshots] of Object.entries(HEADSHOT)) {
171 const enemy = playerStats.Enemies.find(element => element.type === key);
172 if (enemy) {
173 enemy.headshots ??= 0;
174 enemy.headshots += headshots;
175 } else {
176 playerStats.Enemies.push({ type: key, headshots });
177 }
178 }
179 }
180
181 if (DIE) {
182 playerStats.Enemies ??= [];
183 for (const [key, deaths] of Object.entries(DIE)) {
184 playerStats.Deaths ??= 0;
185 playerStats.Deaths += deaths;
186 const enemy = playerStats.Enemies.find(element => element.type === key);
187 if (enemy) {
188 enemy.deaths ??= 0;
189 enemy.deaths += deaths;
190 } else {
191 playerStats.Enemies.push({ type: key, deaths });
192 }
193 }
194 }
195
196 if (MELEE_KILL) {
197 playerStats.MeleeKills ??= 0;
198 for (const kills of Object.values(MELEE_KILL)) {
199 playerStats.MeleeKills += kills;
200 }
201 }
202
203 if (INCOME) {
204 playerStats.Income ??= 0;
205 playerStats.Income += INCOME;
206 }
207
208 if (CIPHER) {
209 if (CIPHER["0"] > 0) {
210 playerStats.CiphersFailed ??= 0;
211 playerStats.CiphersFailed += CIPHER["0"];
212 }
213 if (CIPHER["1"] > 0) {
214 playerStats.CiphersSolved ??= 0;
215 playerStats.CiphersSolved += CIPHER["1"];
216 }
217 }
218 }
219
220 if (payload.timers) {
221 const { EQUIP_WEAPON, CURRENT_MISSION_TIME, CIPHER_TIME } = payload.timers;
222
223 if (EQUIP_WEAPON) {
224 playerStats.Weapons ??= [];
225 for (const [key, equipTime] of Object.entries(EQUIP_WEAPON)) {
226 const weapon = playerStats.Weapons.find(element => element.type === key);
227 if (weapon) {
228 weapon.equipTime ??= 0;
229 weapon.equipTime += equipTime;
230 } else {
231 playerStats.Weapons.push({ type: key, equipTime });
232 }
233 }
234 }
235
236 if (CURRENT_MISSION_TIME) {
237 playerStats.TimePlayedSec ??= 0;
238 playerStats.TimePlayedSec += CURRENT_MISSION_TIME;
239 }
240
241 if (CIPHER_TIME) {
242 playerStats.CipherTime ??= 0;
243 playerStats.CipherTime += CIPHER_TIME;
244 }
245 }
246
247 if (payload.max) {
248 const { WEAPON_XP, MISSION_SCORE } = payload.max;
249
250 if (WEAPON_XP) {
251 playerStats.Weapons ??= [];
252 for (const [key, xp] of Object.entries(WEAPON_XP)) {
253 const weapon = playerStats.Weapons.find(element => element.type === key);
254 if (weapon) {
255 weapon.xp = xp;
256 } else {
257 playerStats.Weapons.push({ type: key, xp });
258 }
259 }
260 }
261
262 if (MISSION_SCORE) {
263 playerStats.Missions ??= [];
264 for (const [key, highScore] of Object.entries(MISSION_SCORE)) {
265 const mission = playerStats.Missions.find(element => element.type === key);
266 if (mission) {
267 mission.highScore = highScore;
268 } else {
269 playerStats.Missions.push({ type: key, highScore });
270 }
271 }
272 }
273 }
274
275 if (payload.set) {
276 const { ELO_RATING, RANK, PLAYER_LEVEL } = payload.set;
277 if (ELO_RATING) playerStats.Rating = ELO_RATING;
278 if (RANK) playerStats.Rank = RANK;
279 if (PLAYER_LEVEL) playerStats.PlayerLevel = PLAYER_LEVEL;
280 }
281
282 await playerStats.save();
283 };
Modified src/types/statTypes.ts +90 -5
@@ -1,3 +1,5 @@
1 import { Types } from "mongoose";
2
1 3 export interface IStatsView {
2 4 CiphersSolved?: number;
3 5 CiphersFailed?: number;
@@ -23,28 +25,32 @@ export interface IStatsView {
23 25 ReviveCount?: number;
24 26 }
25 27
28 export interface IStatsDatabase extends IStatsView {
29 accountOwnerId: Types.ObjectId;
30 }
31
26 32 export interface IAbility {
27 used: number;
28 33 type: string;
34 used: number;
29 35 }
30 36
31 37 export interface IEnemy {
38 type: string;
32 39 executions?: number;
33 40 headshots?: number;
34 41 kills?: number;
35 type: string;
36 42 assists?: number;
37 43 deaths?: number;
38 44 }
39 45
40 46 export interface IMission {
41 highScore: number;
42 47 type: string;
48 highScore: number;
43 49 }
44 50
45 51 export interface IScan {
46 scans: number;
47 52 type: string;
53 scans: number;
48 54 }
49 55
50 56 export interface ITutorial {
@@ -52,12 +58,91 @@ export interface ITutorial {
52 58 }
53 59
54 60 export interface IWeapon {
61 type: string;
55 62 equipTime?: number;
56 63 hits?: number;
57 64 kills?: number;
58 65 xp?: number;
59 66 assists?: number;
60 type: string;
61 67 headshots?: number;
62 68 fired?: number;
63 69 }
70
71 export interface IStatsUpload {
72 displayName: string;
73 guildId?: string;
74 PS?: string;
75 add?: IStatsAdd;
76 set?: IStatsSet;
77 max?: IStatsMax;
78 timers?: IStatsTimers;
79 }
80
81 export interface IStatsAdd {
82 GEAR_USED?: IUploadEntry;
83 SCAN?: IUploadEntry;
84 MISSION_COMPLETE?: IUploadEntry;
85 HEADSHOT_ITEM?: IUploadEntry;
86 HEADSHOT?: IUploadEntry;
87 PLAYER_COUNT?: IUploadEntry;
88 HOST_MIGRATION?: IUploadEntry;
89 PICKUP_ITEM?: IUploadEntry;
90 FIRE_WEAPON?: IUploadEntry;
91 HIT_ENTITY_ITEM?: IUploadEntry;
92 DESTROY_DECORATION?: IUploadEntry;
93 KILL_ENEMY?: IUploadEntry;
94 TAKE_DAMAGE?: IUploadEntry;
95 SQUAD_KILL_ENEMY?: IUploadEntry;
96 RECEIVE_UPGRADE?: IUploadEntry;
97 USE_ABILITY?: IUploadEntry;
98 SQUAD_VIP_KILL?: IUploadEntry;
99 HEAL_BUDDY?: IUploadEntry;
100 INCOME?: number;
101 CIPHER?: IUploadEntry;
102 EQUIP_COSMETIC?: IUploadEntry;
103 EQUIP_UPGRADE?: IUploadEntry;
104 KILL_BOSS?: IUploadEntry;
105 MISSION_TYPE?: IUploadEntry;
106 MISSION_FACTION?: IUploadEntry;
107 MISSION_PLAYED?: IUploadEntry;
108 MISSION_PLAYED_TIME?: IUploadEntry;
109 MEDALS_TOP?: IUploadEntry;
110 INPUT_ACTIVITY_TIME?: IUploadEntry;
111 KILL_ENEMY_ITEM?: IUploadEntry;
112 TAKE_DAMAGE_ITEM?: IUploadEntry;
113 SQUAD_KILL_ENEMY_ITEM?: IUploadEntry;
114 MELEE_KILL?: IUploadEntry;
115 SQUAD_MELEE_KILL?: IUploadEntry;
116 MELEE_KILL_ITEM?: IUploadEntry;
117 SQUAD_MELEE_KILL_ITEM?: IUploadEntry;
118 DIE?: IUploadEntry;
119 DIE_ITEM?: IUploadEntry;
120 EXECUTE_ENEMY?: IUploadEntry;
121 EXECUTE_ENEMY_ITEM?: IUploadEntry;
122 }
123
124 export interface IUploadEntry {
125 [key: string]: number;
126 }
127
128 export interface IStatsMax {
129 WEAPON_XP?: IUploadEntry;
130 MISSION_SCORE?: IUploadEntry;
131 }
132
133 export interface IStatsSet {
134 ELO_RATING?: number;
135 RANK?: number;
136 PLAYER_LEVEL?: number;
137 }
138
139 export interface IStatsTimers {
140 IN_SHIP_TIME?: number;
141 IN_SHIP_VIEW_TIME?: IUploadEntry;
142 EQUIP_WEAPON?: IUploadEntry;
143 MISSION_TIME?: IUploadEntry;
144 REGION_TIME?: IUploadEntry;
145 PLATFORM_TIME?: IUploadEntry;
146 CURRENT_MISSION_TIME?: number;
147 CIPHER_TIME?: number;
148 }