@playcademy/sandbox 0.1.0-beta.3 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +483 -15
- package/dist/constants.d.ts +3 -14
- package/dist/database/seed.d.ts +10 -5
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/levels.d.ts +3 -0
- package/dist/server.js +483 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13024,7 +13024,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
13024
13024
|
}
|
|
13025
13025
|
};
|
|
13026
13026
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
13027
|
-
this.debug("%s
|
|
13027
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
13028
13028
|
if (escaping) {
|
|
13029
13029
|
if (c2 === "/") {
|
|
13030
13030
|
return false;
|
|
@@ -21547,7 +21547,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
|
|
|
21547
21547
|
IS_LINE_JUNK = function(line2, pat = /^\s*#?\s*$/) {
|
|
21548
21548
|
return pat.test(line2);
|
|
21549
21549
|
};
|
|
21550
|
-
IS_CHARACTER_JUNK = function(ch, ws = "
|
|
21550
|
+
IS_CHARACTER_JUNK = function(ch, ws = " ") {
|
|
21551
21551
|
return indexOf.call(ws, ch) >= 0;
|
|
21552
21552
|
};
|
|
21553
21553
|
_formatRangeUnified = function(start2, stop2) {
|
|
@@ -31137,7 +31137,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
31137
31137
|
}
|
|
31138
31138
|
};
|
|
31139
31139
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
31140
|
-
this.debug("%s
|
|
31140
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
31141
31141
|
if (escaping) {
|
|
31142
31142
|
if (c2 === "/") {
|
|
31143
31143
|
return false;
|
|
@@ -52844,8 +52844,8 @@ var DEMO_USER = {
|
|
|
52844
52844
|
email: "demo@playcademy.com",
|
|
52845
52845
|
emailVerified: true,
|
|
52846
52846
|
image: null,
|
|
52847
|
-
role: "
|
|
52848
|
-
developerStatus: "
|
|
52847
|
+
role: "developer",
|
|
52848
|
+
developerStatus: "approved",
|
|
52849
52849
|
createdAt: now,
|
|
52850
52850
|
updatedAt: now
|
|
52851
52851
|
};
|
|
@@ -72673,6 +72673,8 @@ __export(exports_schemas, {
|
|
|
72673
72673
|
verification: () => verification,
|
|
72674
72674
|
users: () => users,
|
|
72675
72675
|
userRoleEnum: () => userRoleEnum,
|
|
72676
|
+
userLevelsRelations: () => userLevelsRelations,
|
|
72677
|
+
userLevels: () => userLevels,
|
|
72676
72678
|
shopListingsRelations: () => shopListingsRelations,
|
|
72677
72679
|
shopListings: () => shopListings,
|
|
72678
72680
|
sessions: () => sessions,
|
|
@@ -72680,6 +72682,7 @@ __export(exports_schemas, {
|
|
|
72680
72682
|
maps: () => maps,
|
|
72681
72683
|
mapElementsRelations: () => mapElementsRelations,
|
|
72682
72684
|
mapElements: () => mapElements,
|
|
72685
|
+
levelConfigs: () => levelConfigs,
|
|
72683
72686
|
itemsRelations: () => itemsRelations,
|
|
72684
72687
|
items: () => items,
|
|
72685
72688
|
itemTypeEnum: () => itemTypeEnum,
|
|
@@ -72696,11 +72699,14 @@ __export(exports_schemas, {
|
|
|
72696
72699
|
currenciesRelations: () => currenciesRelations,
|
|
72697
72700
|
currencies: () => currencies,
|
|
72698
72701
|
accounts: () => accounts,
|
|
72702
|
+
XPActionInputSchema: () => XPActionInputSchema,
|
|
72699
72703
|
VersionSchema: () => VersionSchema,
|
|
72700
72704
|
UpsertGameMetadataSchema: () => UpsertGameMetadataSchema,
|
|
72701
72705
|
UpdateUserSchema: () => UpdateUserSchema,
|
|
72706
|
+
UpdateUserLevelSchema: () => UpdateUserLevelSchema,
|
|
72702
72707
|
UpdateShopListingSchema: () => UpdateShopListingSchema,
|
|
72703
72708
|
UpdateMapElementSchema: () => UpdateMapElementSchema,
|
|
72709
|
+
UpdateLevelConfigSchema: () => UpdateLevelConfigSchema,
|
|
72704
72710
|
UpdateItemSchema: () => UpdateItemSchema,
|
|
72705
72711
|
UpdateInventoryItemSchema: () => UpdateInventoryItemSchema,
|
|
72706
72712
|
UpdateGameStateSchema: () => UpdateGameStateSchema,
|
|
@@ -72711,10 +72717,12 @@ __export(exports_schemas, {
|
|
|
72711
72717
|
StartSessionInputSchema: () => StartSessionInputSchema,
|
|
72712
72718
|
SelectVerificationSchema: () => SelectVerificationSchema,
|
|
72713
72719
|
SelectUserSchema: () => SelectUserSchema,
|
|
72720
|
+
SelectUserLevelSchema: () => SelectUserLevelSchema,
|
|
72714
72721
|
SelectShopListingSchema: () => SelectShopListingSchema,
|
|
72715
72722
|
SelectSessionSchema: () => SelectSessionSchema,
|
|
72716
72723
|
SelectMapSchema: () => SelectMapSchema,
|
|
72717
72724
|
SelectMapElementSchema: () => SelectMapElementSchema,
|
|
72725
|
+
SelectLevelConfigSchema: () => SelectLevelConfigSchema,
|
|
72718
72726
|
SelectItemSchema: () => SelectItemSchema,
|
|
72719
72727
|
SelectInventoryItemSchema: () => SelectInventoryItemSchema,
|
|
72720
72728
|
SelectGameStateSchema: () => SelectGameStateSchema,
|
|
@@ -72726,13 +72734,16 @@ __export(exports_schemas, {
|
|
|
72726
72734
|
ProcessZipSchema: () => ProcessZipSchema,
|
|
72727
72735
|
MapElementMetadataZodSchema: () => MapElementMetadataZodSchema,
|
|
72728
72736
|
ManifestV1Schema: () => ManifestV1Schema,
|
|
72737
|
+
MAX_LEVEL: () => MAX_LEVEL,
|
|
72729
72738
|
ItemMetadataSchema: () => ItemMetadataSchema,
|
|
72730
72739
|
InventoryActionInputSchema: () => InventoryActionInputSchema,
|
|
72731
72740
|
InsertVerificationSchema: () => InsertVerificationSchema,
|
|
72732
72741
|
InsertUserSchema: () => InsertUserSchema,
|
|
72742
|
+
InsertUserLevelSchema: () => InsertUserLevelSchema,
|
|
72733
72743
|
InsertShopListingSchema: () => InsertShopListingSchema,
|
|
72734
72744
|
InsertSessionSchema: () => InsertSessionSchema,
|
|
72735
72745
|
InsertMapElementSchema: () => InsertMapElementSchema,
|
|
72746
|
+
InsertLevelConfigSchema: () => InsertLevelConfigSchema,
|
|
72736
72747
|
InsertItemSchema: () => InsertItemSchema,
|
|
72737
72748
|
InsertInventoryItemSchema: () => InsertInventoryItemSchema,
|
|
72738
72749
|
InsertGameStateSchema: () => InsertGameStateSchema,
|
|
@@ -77508,6 +77519,54 @@ var InsertShopListingSchema = createInsertSchema(shopListings, {
|
|
|
77508
77519
|
}).omit({ id: true, createdAt: true, updatedAt: true });
|
|
77509
77520
|
var SelectShopListingSchema = createSelectSchema(shopListings);
|
|
77510
77521
|
var UpdateShopListingSchema = InsertShopListingSchema.partial().extend({});
|
|
77522
|
+
// ../data/src/schemas/levels.ts
|
|
77523
|
+
var MAX_LEVEL = 100;
|
|
77524
|
+
var userLevels = pgTable("user_levels", {
|
|
77525
|
+
userId: text("user_id").primaryKey().references(() => users.id, { onDelete: "cascade" }),
|
|
77526
|
+
currentLevel: integer("current_level").notNull().default(1),
|
|
77527
|
+
currentXp: integer("current_xp").notNull().default(0),
|
|
77528
|
+
totalXP: integer("total_xp").notNull().default(0),
|
|
77529
|
+
lastLevelUpAt: timestamp("last_level_up_at", { withTimezone: true }),
|
|
77530
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
77531
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().$onUpdate(() => new Date)
|
|
77532
|
+
});
|
|
77533
|
+
var levelConfigs = pgTable("level_configs", {
|
|
77534
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
77535
|
+
level: integer("level").notNull().unique(),
|
|
77536
|
+
xpRequired: integer("xp_required").notNull(),
|
|
77537
|
+
creditsReward: integer("credits_reward").notNull().default(0),
|
|
77538
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
77539
|
+
}, (table) => [uniqueIndex("unique_level_config_idx").on(table.level)]);
|
|
77540
|
+
var userLevelsRelations = relations(userLevels, ({ one }) => ({
|
|
77541
|
+
user: one(users, {
|
|
77542
|
+
fields: [userLevels.userId],
|
|
77543
|
+
references: [users.id]
|
|
77544
|
+
})
|
|
77545
|
+
}));
|
|
77546
|
+
var InsertUserLevelSchema = createInsertSchema(userLevels, {
|
|
77547
|
+
userId: exports_external.string().min(1, "User ID is required"),
|
|
77548
|
+
currentLevel: exports_external.number().int().min(1, "Level must be at least 1").default(1),
|
|
77549
|
+
currentXp: exports_external.number().int().min(0, "XP cannot be negative").default(0),
|
|
77550
|
+
totalXP: exports_external.number().int().min(0, "Total XP cannot be negative").default(0)
|
|
77551
|
+
}).omit({ createdAt: true, updatedAt: true });
|
|
77552
|
+
var SelectUserLevelSchema = createSelectSchema(userLevels);
|
|
77553
|
+
var UpdateUserLevelSchema = createUpdateSchema(userLevels).omit({
|
|
77554
|
+
userId: true,
|
|
77555
|
+
createdAt: true
|
|
77556
|
+
});
|
|
77557
|
+
var InsertLevelConfigSchema = createInsertSchema(levelConfigs, {
|
|
77558
|
+
level: exports_external.number().int().min(1, "Level must be at least 1"),
|
|
77559
|
+
xpRequired: exports_external.number().int().min(0, "XP required cannot be negative"),
|
|
77560
|
+
creditsReward: exports_external.number().int().min(0, "Credits reward cannot be negative").default(0)
|
|
77561
|
+
}).omit({ id: true, createdAt: true });
|
|
77562
|
+
var SelectLevelConfigSchema = createSelectSchema(levelConfigs);
|
|
77563
|
+
var UpdateLevelConfigSchema = createUpdateSchema(levelConfigs).omit({
|
|
77564
|
+
id: true,
|
|
77565
|
+
createdAt: true
|
|
77566
|
+
});
|
|
77567
|
+
var XPActionInputSchema = exports_external.object({
|
|
77568
|
+
amount: exports_external.number().int("XP amount must be an integer").positive("XP amount must be positive")
|
|
77569
|
+
});
|
|
77511
77570
|
// src/database/path-manager.ts
|
|
77512
77571
|
import fs2 from "node:fs";
|
|
77513
77572
|
import { join as join2, isAbsolute, dirname } from "node:path";
|
|
@@ -77587,12 +77646,47 @@ async function seedDemoData(db) {
|
|
|
77587
77646
|
for (const inventory2 of SAMPLE_INVENTORY) {
|
|
77588
77647
|
await db.insert(inventoryItems).values(inventory2);
|
|
77589
77648
|
}
|
|
77649
|
+
const levelConfigsData = generateLevelConfigs();
|
|
77650
|
+
for (const config of levelConfigsData) {
|
|
77651
|
+
await db.insert(levelConfigs).values(config);
|
|
77652
|
+
}
|
|
77590
77653
|
} catch (error2) {
|
|
77591
77654
|
console.error("❌ Error seeding demo data:", error2);
|
|
77592
77655
|
throw error2;
|
|
77593
77656
|
}
|
|
77594
77657
|
return DEMO_USER;
|
|
77595
77658
|
}
|
|
77659
|
+
function generateLevelConfigs() {
|
|
77660
|
+
const configs = [];
|
|
77661
|
+
for (let level = 1;level <= 100; level++) {
|
|
77662
|
+
let xpRequired;
|
|
77663
|
+
if (level === 1) {
|
|
77664
|
+
xpRequired = 0;
|
|
77665
|
+
} else {
|
|
77666
|
+
const baseXp = Math.pow(level - 1, 1.8) * 100;
|
|
77667
|
+
let roundTo;
|
|
77668
|
+
if (level <= 10) {
|
|
77669
|
+
roundTo = 50;
|
|
77670
|
+
} else if (level <= 20) {
|
|
77671
|
+
roundTo = 100;
|
|
77672
|
+
} else if (level <= 50) {
|
|
77673
|
+
roundTo = 250;
|
|
77674
|
+
} else {
|
|
77675
|
+
roundTo = 500;
|
|
77676
|
+
}
|
|
77677
|
+
xpRequired = Math.round(baseXp / roundTo) * roundTo;
|
|
77678
|
+
}
|
|
77679
|
+
const baseReward = level === 1 ? 0 : 25 + (level - 1) * 25;
|
|
77680
|
+
const bonusReward = Math.floor((level - 1) / 10) * 50;
|
|
77681
|
+
const creditsReward = baseReward + bonusReward;
|
|
77682
|
+
configs.push({
|
|
77683
|
+
level,
|
|
77684
|
+
xpRequired,
|
|
77685
|
+
creditsReward
|
|
77686
|
+
});
|
|
77687
|
+
}
|
|
77688
|
+
return configs;
|
|
77689
|
+
}
|
|
77596
77690
|
async function seedCurrentProjectGame(db, project) {
|
|
77597
77691
|
const now2 = new Date;
|
|
77598
77692
|
try {
|
|
@@ -77628,7 +77722,7 @@ async function seedCurrentProjectGame(db, project) {
|
|
|
77628
77722
|
// package.json
|
|
77629
77723
|
var package_default = {
|
|
77630
77724
|
name: "@playcademy/sandbox",
|
|
77631
|
-
version: "0.1.0-beta.
|
|
77725
|
+
version: "0.1.0-beta.4",
|
|
77632
77726
|
description: "Local development server for Playcademy game development",
|
|
77633
77727
|
type: "module",
|
|
77634
77728
|
exports: {
|
|
@@ -77731,6 +77825,291 @@ async function getUserMe(ctx) {
|
|
|
77731
77825
|
}
|
|
77732
77826
|
}
|
|
77733
77827
|
|
|
77828
|
+
// ../data/src/constants.ts
|
|
77829
|
+
var ITEM_INTERNAL_NAMES = {
|
|
77830
|
+
PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
|
|
77831
|
+
PLAYCADEMY_XP: "PLAYCADEMY_XP",
|
|
77832
|
+
FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
|
|
77833
|
+
EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
|
|
77834
|
+
FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
|
|
77835
|
+
COMMON_SWORD: "COMMON_SWORD",
|
|
77836
|
+
SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
|
|
77837
|
+
SMALL_BACKPACK: "SMALL_BACKPACK"
|
|
77838
|
+
};
|
|
77839
|
+
var CURRENCIES = {
|
|
77840
|
+
PRIMARY: ITEM_INTERNAL_NAMES.PLAYCADEMY_CREDITS,
|
|
77841
|
+
XP: ITEM_INTERNAL_NAMES.PLAYCADEMY_XP
|
|
77842
|
+
};
|
|
77843
|
+
var BADGES = {
|
|
77844
|
+
FOUNDING_MEMBER: ITEM_INTERNAL_NAMES.FOUNDING_MEMBER_BADGE,
|
|
77845
|
+
EARLY_ADOPTER: ITEM_INTERNAL_NAMES.EARLY_ADOPTER_BADGE,
|
|
77846
|
+
FIRST_GAME: ITEM_INTERNAL_NAMES.FIRST_GAME_BADGE
|
|
77847
|
+
};
|
|
77848
|
+
|
|
77849
|
+
// ../api-core/src/utils/levels.ts
|
|
77850
|
+
var levelConfigCache = null;
|
|
77851
|
+
async function getLevelConfig(db, level) {
|
|
77852
|
+
if (level < 1) {
|
|
77853
|
+
throw ApiError.badRequest("Level must be at least 1");
|
|
77854
|
+
}
|
|
77855
|
+
if (levelConfigCache?.has(level)) {
|
|
77856
|
+
return levelConfigCache.get(level) || null;
|
|
77857
|
+
}
|
|
77858
|
+
try {
|
|
77859
|
+
const [config] = await db.select().from(levelConfigs).where(eq(levelConfigs.level, level)).limit(1);
|
|
77860
|
+
if (levelConfigCache && config) {
|
|
77861
|
+
levelConfigCache.set(level, config);
|
|
77862
|
+
}
|
|
77863
|
+
return config || null;
|
|
77864
|
+
} catch (error2) {
|
|
77865
|
+
logger2.error(`Error fetching level config for level ${level}:`, error2);
|
|
77866
|
+
throw ApiError.internal("Internal server error", error2);
|
|
77867
|
+
}
|
|
77868
|
+
}
|
|
77869
|
+
async function calculateXPToNextLevel(db, currentLevel, currentXp) {
|
|
77870
|
+
try {
|
|
77871
|
+
const nextLevelConfig = await getLevelConfig(db, currentLevel + 1);
|
|
77872
|
+
if (!nextLevelConfig) {
|
|
77873
|
+
return 0;
|
|
77874
|
+
}
|
|
77875
|
+
return Math.max(0, nextLevelConfig.xpRequired - currentXp);
|
|
77876
|
+
} catch (error2) {
|
|
77877
|
+
logger2.error(`Error calculating XP to next level:`, error2);
|
|
77878
|
+
throw ApiError.internal("Internal server error", error2);
|
|
77879
|
+
}
|
|
77880
|
+
}
|
|
77881
|
+
async function checkLevelUp(tx, currentLevel, newXp) {
|
|
77882
|
+
let level = currentLevel;
|
|
77883
|
+
let remainingXp = newXp;
|
|
77884
|
+
let totalCreditsAwarded = 0;
|
|
77885
|
+
let leveledUp = false;
|
|
77886
|
+
while (true) {
|
|
77887
|
+
if (level >= MAX_LEVEL) {
|
|
77888
|
+
break;
|
|
77889
|
+
}
|
|
77890
|
+
const nextLevelConfig2 = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
|
|
77891
|
+
const [nextLevel2] = nextLevelConfig2;
|
|
77892
|
+
if (!nextLevel2) {
|
|
77893
|
+
break;
|
|
77894
|
+
}
|
|
77895
|
+
if (remainingXp >= nextLevel2.xpRequired) {
|
|
77896
|
+
level += 1;
|
|
77897
|
+
remainingXp -= nextLevel2.xpRequired;
|
|
77898
|
+
totalCreditsAwarded += nextLevel2.creditsReward;
|
|
77899
|
+
leveledUp = true;
|
|
77900
|
+
} else {
|
|
77901
|
+
break;
|
|
77902
|
+
}
|
|
77903
|
+
}
|
|
77904
|
+
const nextLevelConfig = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
|
|
77905
|
+
const [nextLevel] = nextLevelConfig;
|
|
77906
|
+
const xpToNextLevel = nextLevel ? Math.max(0, nextLevel.xpRequired - remainingXp) : 0;
|
|
77907
|
+
return {
|
|
77908
|
+
newLevel: level,
|
|
77909
|
+
remainingXp,
|
|
77910
|
+
leveledUp,
|
|
77911
|
+
creditsAwarded: totalCreditsAwarded,
|
|
77912
|
+
xpToNextLevel
|
|
77913
|
+
};
|
|
77914
|
+
}
|
|
77915
|
+
|
|
77916
|
+
// ../api-core/src/utils/validation.ts
|
|
77917
|
+
function formatValidationErrors(error2) {
|
|
77918
|
+
const flattened = error2.flatten();
|
|
77919
|
+
const fieldErrors = Object.entries(flattened.fieldErrors).map(([field, errors2]) => `${field}: ${errors2?.join(", ") || "Invalid"}`).join("; ");
|
|
77920
|
+
const formErrors = flattened.formErrors.join("; ");
|
|
77921
|
+
const allErrors = [fieldErrors, formErrors].filter(Boolean).join("; ");
|
|
77922
|
+
return allErrors || "Validation failed";
|
|
77923
|
+
}
|
|
77924
|
+
|
|
77925
|
+
// ../api-core/src/levels/index.ts
|
|
77926
|
+
var AddXPSchema = XPActionInputSchema;
|
|
77927
|
+
async function getUserLevel(ctx) {
|
|
77928
|
+
const user = ctx.user;
|
|
77929
|
+
if (!user) {
|
|
77930
|
+
throw ApiError.unauthorized("Valid session or bearer token required");
|
|
77931
|
+
}
|
|
77932
|
+
try {
|
|
77933
|
+
const db = getDatabase();
|
|
77934
|
+
let [userLevel] = await db.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
|
|
77935
|
+
if (!userLevel) {
|
|
77936
|
+
const [newUserLevel] = await db.insert(userLevels).values({
|
|
77937
|
+
userId: user.id,
|
|
77938
|
+
currentLevel: 1,
|
|
77939
|
+
currentXp: 0,
|
|
77940
|
+
totalXP: 0
|
|
77941
|
+
}).returning();
|
|
77942
|
+
if (!newUserLevel) {
|
|
77943
|
+
throw ApiError.internal("Failed to create user level record");
|
|
77944
|
+
}
|
|
77945
|
+
userLevel = newUserLevel;
|
|
77946
|
+
}
|
|
77947
|
+
return userLevel;
|
|
77948
|
+
} catch (error2) {
|
|
77949
|
+
if (error2 instanceof ApiError) {
|
|
77950
|
+
throw error2;
|
|
77951
|
+
}
|
|
77952
|
+
logger2.error(`Error fetching user level for user ${user.id}:`, error2);
|
|
77953
|
+
throw ApiError.internal("Internal server error", error2);
|
|
77954
|
+
}
|
|
77955
|
+
}
|
|
77956
|
+
async function addXP(ctx, amount) {
|
|
77957
|
+
const user = ctx.user;
|
|
77958
|
+
if (!user) {
|
|
77959
|
+
throw ApiError.unauthorized("Valid session or bearer token required");
|
|
77960
|
+
}
|
|
77961
|
+
if (amount <= 0) {
|
|
77962
|
+
throw ApiError.badRequest("XP amount must be positive");
|
|
77963
|
+
}
|
|
77964
|
+
try {
|
|
77965
|
+
const db = getDatabase();
|
|
77966
|
+
const result = await db.transaction(async (tx) => {
|
|
77967
|
+
let [userLevel] = await tx.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
|
|
77968
|
+
if (!userLevel) {
|
|
77969
|
+
const [newUserLevel] = await tx.insert(userLevels).values({
|
|
77970
|
+
userId: user.id,
|
|
77971
|
+
currentLevel: 1,
|
|
77972
|
+
currentXp: 0,
|
|
77973
|
+
totalXP: 0
|
|
77974
|
+
}).returning();
|
|
77975
|
+
if (!newUserLevel) {
|
|
77976
|
+
throw ApiError.internal("Failed to create user level record");
|
|
77977
|
+
}
|
|
77978
|
+
userLevel = newUserLevel;
|
|
77979
|
+
}
|
|
77980
|
+
const newCurrentXp = userLevel.currentXp + amount;
|
|
77981
|
+
const newTotalXP = userLevel.totalXP + amount;
|
|
77982
|
+
const levelUpResult = await checkLevelUp(tx, userLevel.currentLevel, newCurrentXp);
|
|
77983
|
+
const [updatedUserLevel] = await tx.update(userLevels).set({
|
|
77984
|
+
currentLevel: levelUpResult.newLevel,
|
|
77985
|
+
currentXp: levelUpResult.remainingXp,
|
|
77986
|
+
totalXP: newTotalXP,
|
|
77987
|
+
lastLevelUpAt: levelUpResult.leveledUp ? new Date : userLevel.lastLevelUpAt
|
|
77988
|
+
}).where(eq(userLevels.userId, user.id)).returning();
|
|
77989
|
+
if (!updatedUserLevel) {
|
|
77990
|
+
throw ApiError.internal("Failed to update user level");
|
|
77991
|
+
}
|
|
77992
|
+
let creditsAwarded = 0;
|
|
77993
|
+
if (levelUpResult.leveledUp) {
|
|
77994
|
+
const creditsToAward = levelUpResult.creditsAwarded;
|
|
77995
|
+
logger2.debug("User leveled up", {
|
|
77996
|
+
userId: user.id,
|
|
77997
|
+
oldLevel: userLevel.currentLevel,
|
|
77998
|
+
newLevel: levelUpResult.newLevel,
|
|
77999
|
+
xpAdded: amount,
|
|
78000
|
+
totalXP: newTotalXP,
|
|
78001
|
+
creditsAwarded: creditsToAward
|
|
78002
|
+
});
|
|
78003
|
+
if (creditsToAward > 0) {
|
|
78004
|
+
const [creditsItem] = await tx.select({ id: items.id }).from(items).where(eq(items.internalName, CURRENCIES.PRIMARY)).limit(1);
|
|
78005
|
+
if (!creditsItem) {
|
|
78006
|
+
throw ApiError.internal(`${CURRENCIES.PRIMARY} item not found`);
|
|
78007
|
+
}
|
|
78008
|
+
await tx.insert(inventoryItems).values({
|
|
78009
|
+
userId: user.id,
|
|
78010
|
+
itemId: creditsItem.id,
|
|
78011
|
+
quantity: creditsToAward
|
|
78012
|
+
}).onConflictDoUpdate({
|
|
78013
|
+
target: [
|
|
78014
|
+
inventoryItems.userId,
|
|
78015
|
+
inventoryItems.itemId
|
|
78016
|
+
],
|
|
78017
|
+
set: {
|
|
78018
|
+
quantity: sql`${inventoryItems.quantity} + ${creditsToAward}`
|
|
78019
|
+
}
|
|
78020
|
+
});
|
|
78021
|
+
}
|
|
78022
|
+
creditsAwarded = creditsToAward;
|
|
78023
|
+
} else {
|
|
78024
|
+
logger2.debug("XP added to user", {
|
|
78025
|
+
userId: user.id,
|
|
78026
|
+
level: userLevel.currentLevel,
|
|
78027
|
+
xpAdded: amount,
|
|
78028
|
+
totalXP: newTotalXP
|
|
78029
|
+
});
|
|
78030
|
+
}
|
|
78031
|
+
return {
|
|
78032
|
+
totalXP: newTotalXP,
|
|
78033
|
+
newLevel: levelUpResult.newLevel,
|
|
78034
|
+
leveledUp: levelUpResult.leveledUp,
|
|
78035
|
+
creditsAwarded,
|
|
78036
|
+
xpToNextLevel: levelUpResult.xpToNextLevel
|
|
78037
|
+
};
|
|
78038
|
+
});
|
|
78039
|
+
return result;
|
|
78040
|
+
} catch (error2) {
|
|
78041
|
+
if (error2 instanceof ApiError) {
|
|
78042
|
+
throw error2;
|
|
78043
|
+
}
|
|
78044
|
+
logger2.error(`Error adding XP for user ${user.id}:`, error2);
|
|
78045
|
+
throw ApiError.internal("Internal server error", error2);
|
|
78046
|
+
}
|
|
78047
|
+
}
|
|
78048
|
+
async function addXPFromRequest(ctx) {
|
|
78049
|
+
let amount;
|
|
78050
|
+
try {
|
|
78051
|
+
const requestBody = await ctx.request.json();
|
|
78052
|
+
const parsed = AddXPSchema.parse(requestBody);
|
|
78053
|
+
amount = parsed.amount;
|
|
78054
|
+
} catch (error2) {
|
|
78055
|
+
if (error2 instanceof ZodError) {
|
|
78056
|
+
throw ApiError.badRequest(`Validation failed: ${formatValidationErrors(error2)}`);
|
|
78057
|
+
}
|
|
78058
|
+
logger2.error("Failed to parse request body:", error2);
|
|
78059
|
+
throw ApiError.badRequest("Invalid JSON body");
|
|
78060
|
+
}
|
|
78061
|
+
return await addXP(ctx, amount);
|
|
78062
|
+
}
|
|
78063
|
+
async function getAllLevelConfigs(_ctx) {
|
|
78064
|
+
try {
|
|
78065
|
+
const db = getDatabase();
|
|
78066
|
+
const configs = await db.select().from(levelConfigs).orderBy(levelConfigs.level);
|
|
78067
|
+
return configs;
|
|
78068
|
+
} catch (error2) {
|
|
78069
|
+
logger2.error("Error fetching all level configs:", error2);
|
|
78070
|
+
throw ApiError.internal("Internal server error", error2);
|
|
78071
|
+
}
|
|
78072
|
+
}
|
|
78073
|
+
async function getUserLevelProgress(ctx) {
|
|
78074
|
+
const userLevel = await getUserLevel(ctx);
|
|
78075
|
+
try {
|
|
78076
|
+
const db = getDatabase();
|
|
78077
|
+
const xpToNextLevel = await calculateXPToNextLevel(db, userLevel.currentLevel, userLevel.currentXp);
|
|
78078
|
+
return {
|
|
78079
|
+
level: userLevel.currentLevel,
|
|
78080
|
+
currentXp: userLevel.currentXp,
|
|
78081
|
+
xpToNextLevel,
|
|
78082
|
+
totalXP: userLevel.totalXP
|
|
78083
|
+
};
|
|
78084
|
+
} catch (error2) {
|
|
78085
|
+
if (error2 instanceof ApiError) {
|
|
78086
|
+
throw error2;
|
|
78087
|
+
}
|
|
78088
|
+
logger2.error(`Error fetching user level progress:`, error2);
|
|
78089
|
+
throw ApiError.internal("Internal server error", error2);
|
|
78090
|
+
}
|
|
78091
|
+
}
|
|
78092
|
+
async function getLevelConfigByPath(ctx) {
|
|
78093
|
+
const levelParam = ctx.params.level;
|
|
78094
|
+
if (!levelParam) {
|
|
78095
|
+
throw ApiError.badRequest("Level parameter is required");
|
|
78096
|
+
}
|
|
78097
|
+
const level = parseInt(levelParam, 10);
|
|
78098
|
+
if (isNaN(level) || level < 1) {
|
|
78099
|
+
throw ApiError.badRequest("Level must be a positive integer");
|
|
78100
|
+
}
|
|
78101
|
+
try {
|
|
78102
|
+
const db = getDatabase();
|
|
78103
|
+
return await getLevelConfig(db, level);
|
|
78104
|
+
} catch (error2) {
|
|
78105
|
+
if (error2 instanceof ApiError) {
|
|
78106
|
+
throw error2;
|
|
78107
|
+
}
|
|
78108
|
+
logger2.error(`Error fetching level config for level ${level}:`, error2);
|
|
78109
|
+
throw ApiError.internal("Internal server error", error2);
|
|
78110
|
+
}
|
|
78111
|
+
}
|
|
78112
|
+
|
|
77734
78113
|
// src/routes/users.ts
|
|
77735
78114
|
var usersRouter = new Hono2;
|
|
77736
78115
|
usersRouter.get("/me", async (c2) => {
|
|
@@ -77752,18 +78131,66 @@ usersRouter.get("/me", async (c2) => {
|
|
|
77752
78131
|
return c2.json({ error: message }, 500);
|
|
77753
78132
|
}
|
|
77754
78133
|
});
|
|
78134
|
+
usersRouter.get("/level", async (c2) => {
|
|
78135
|
+
const ctx = {
|
|
78136
|
+
user: c2.get("user"),
|
|
78137
|
+
params: {},
|
|
78138
|
+
url: new URL(c2.req.url),
|
|
78139
|
+
request: c2.req.raw
|
|
78140
|
+
};
|
|
78141
|
+
try {
|
|
78142
|
+
const levelData = await getUserLevel(ctx);
|
|
78143
|
+
return c2.json(levelData);
|
|
78144
|
+
} catch (error2) {
|
|
78145
|
+
if (error2 instanceof ApiError) {
|
|
78146
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
78147
|
+
}
|
|
78148
|
+
console.error("Error in getUserLevel:", error2);
|
|
78149
|
+
const message = error2 instanceof Error ? error2.message : "Internal server error";
|
|
78150
|
+
return c2.json({ error: message }, 500);
|
|
78151
|
+
}
|
|
78152
|
+
});
|
|
78153
|
+
usersRouter.get("/level/progress", async (c2) => {
|
|
78154
|
+
const ctx = {
|
|
78155
|
+
user: c2.get("user"),
|
|
78156
|
+
params: {},
|
|
78157
|
+
url: new URL(c2.req.url),
|
|
78158
|
+
request: c2.req.raw
|
|
78159
|
+
};
|
|
78160
|
+
try {
|
|
78161
|
+
const progressData = await getUserLevelProgress(ctx);
|
|
78162
|
+
return c2.json(progressData);
|
|
78163
|
+
} catch (error2) {
|
|
78164
|
+
if (error2 instanceof ApiError) {
|
|
78165
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
78166
|
+
}
|
|
78167
|
+
console.error("Error in getUserLevelProgress:", error2);
|
|
78168
|
+
const message = error2 instanceof Error ? error2.message : "Internal server error";
|
|
78169
|
+
return c2.json({ error: message }, 500);
|
|
78170
|
+
}
|
|
78171
|
+
});
|
|
78172
|
+
usersRouter.post("/xp/add", async (c2) => {
|
|
78173
|
+
const ctx = {
|
|
78174
|
+
user: c2.get("user"),
|
|
78175
|
+
params: {},
|
|
78176
|
+
url: new URL(c2.req.url),
|
|
78177
|
+
request: c2.req.raw
|
|
78178
|
+
};
|
|
78179
|
+
try {
|
|
78180
|
+
const result = await addXPFromRequest(ctx);
|
|
78181
|
+
return c2.json(result);
|
|
78182
|
+
} catch (error2) {
|
|
78183
|
+
if (error2 instanceof ApiError) {
|
|
78184
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
78185
|
+
}
|
|
78186
|
+
console.error("Error in addXPFromRequest:", error2);
|
|
78187
|
+
const message = error2 instanceof Error ? error2.message : "Internal server error";
|
|
78188
|
+
return c2.json({ error: message }, 500);
|
|
78189
|
+
}
|
|
78190
|
+
});
|
|
77755
78191
|
// src/routes/health.ts
|
|
77756
78192
|
var healthRouter = new Hono2;
|
|
77757
78193
|
healthRouter.get("/", (c2) => c2.json({ status: "ok", timestamp: new Date().toISOString() }));
|
|
77758
|
-
// ../api-core/src/utils/validation.ts
|
|
77759
|
-
function formatValidationErrors(error2) {
|
|
77760
|
-
const flattened = error2.flatten();
|
|
77761
|
-
const fieldErrors = Object.entries(flattened.fieldErrors).map(([field, errors2]) => `${field}: ${errors2?.join(", ") || "Invalid"}`).join("; ");
|
|
77762
|
-
const formErrors = flattened.formErrors.join("; ");
|
|
77763
|
-
const allErrors = [fieldErrors, formErrors].filter(Boolean).join("; ");
|
|
77764
|
-
return allErrors || "Validation failed";
|
|
77765
|
-
}
|
|
77766
|
-
|
|
77767
78194
|
// ../api-core/src/inventory/index.ts
|
|
77768
78195
|
async function getUserInventory(ctx) {
|
|
77769
78196
|
const user = ctx.user;
|
|
@@ -81993,6 +82420,46 @@ devRouter.delete("/keys/:keyId", async (c2) => {
|
|
|
81993
82420
|
return c2.json({ error: message2 }, 500);
|
|
81994
82421
|
}
|
|
81995
82422
|
});
|
|
82423
|
+
// src/routes/levels.ts
|
|
82424
|
+
var levelsRouter = new Hono2;
|
|
82425
|
+
levelsRouter.get("/config", async (c2) => {
|
|
82426
|
+
const ctx = {
|
|
82427
|
+
user: c2.get("user"),
|
|
82428
|
+
params: {},
|
|
82429
|
+
url: new URL(c2.req.url),
|
|
82430
|
+
request: c2.req.raw
|
|
82431
|
+
};
|
|
82432
|
+
try {
|
|
82433
|
+
const configs = await getAllLevelConfigs(ctx);
|
|
82434
|
+
return c2.json(configs);
|
|
82435
|
+
} catch (error2) {
|
|
82436
|
+
if (error2 instanceof ApiError) {
|
|
82437
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
82438
|
+
}
|
|
82439
|
+
console.error("Error in getAllLevelConfigs:", error2);
|
|
82440
|
+
const message2 = error2 instanceof Error ? error2.message : "Internal server error";
|
|
82441
|
+
return c2.json({ error: message2 }, 500);
|
|
82442
|
+
}
|
|
82443
|
+
});
|
|
82444
|
+
levelsRouter.get("/config/:level", async (c2) => {
|
|
82445
|
+
const ctx = {
|
|
82446
|
+
user: c2.get("user"),
|
|
82447
|
+
params: { level: c2.req.param("level") },
|
|
82448
|
+
url: new URL(c2.req.url),
|
|
82449
|
+
request: c2.req.raw
|
|
82450
|
+
};
|
|
82451
|
+
try {
|
|
82452
|
+
const config = await getLevelConfigByPath(ctx);
|
|
82453
|
+
return c2.json(config);
|
|
82454
|
+
} catch (error2) {
|
|
82455
|
+
if (error2 instanceof ApiError) {
|
|
82456
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
82457
|
+
}
|
|
82458
|
+
console.error("Error in getLevelConfigByPath:", error2);
|
|
82459
|
+
const message2 = error2 instanceof Error ? error2.message : "Internal server error";
|
|
82460
|
+
return c2.json({ error: message2 }, 500);
|
|
82461
|
+
}
|
|
82462
|
+
});
|
|
81996
82463
|
// src/server.ts
|
|
81997
82464
|
async function startServer(options) {
|
|
81998
82465
|
const { port, verbose, project } = options;
|
|
@@ -82019,6 +82486,7 @@ async function startServer(options) {
|
|
|
82019
82486
|
app.route("/api/maps", mapsRouter);
|
|
82020
82487
|
app.route("/api/shop-listings", shopListingsRouter);
|
|
82021
82488
|
app.route("/api/dev", devRouter);
|
|
82489
|
+
app.route("/api/levels", levelsRouter);
|
|
82022
82490
|
return serve({ fetch: app.fetch, port });
|
|
82023
82491
|
}
|
|
82024
82492
|
var version3 = package_default.version;
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
import type { Item } from '@playcademy/data/schemas';
|
|
2
|
-
export declare const DEMO_USER:
|
|
3
|
-
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
4
|
-
name: string;
|
|
5
|
-
username: string;
|
|
6
|
-
email: string;
|
|
7
|
-
emailVerified: boolean;
|
|
8
|
-
image: null;
|
|
9
|
-
role: "player";
|
|
10
|
-
developerStatus: "none";
|
|
11
|
-
createdAt: Date;
|
|
12
|
-
updatedAt: Date;
|
|
13
|
-
};
|
|
1
|
+
import type { Item, User } from '@playcademy/data/schemas';
|
|
2
|
+
export declare const DEMO_USER: User;
|
|
14
3
|
export declare const DEMO_TOKEN = "demo-token";
|
|
15
4
|
export declare const PLAYCADEMY_CREDITS_ID: `${string}-${string}-${string}-${string}-${string}`;
|
|
16
5
|
export declare const SAMPLE_ITEMS: Omit<Item, 'createdAt'>[];
|
|
17
6
|
export declare const SAMPLE_INVENTORY: {
|
|
18
7
|
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
19
|
-
userId:
|
|
8
|
+
userId: string;
|
|
20
9
|
itemId: `${string}-${string}-${string}-${string}-${string}`;
|
|
21
10
|
quantity: number;
|
|
22
11
|
}[];
|
package/dist/database/seed.d.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { setupDatabase } from '.';
|
|
2
2
|
import type { ProjectInfo } from '../types';
|
|
3
3
|
export declare function seedDemoData(db: Awaited<ReturnType<typeof setupDatabase>>): Promise<{
|
|
4
|
-
id:
|
|
4
|
+
id: string;
|
|
5
5
|
name: string;
|
|
6
|
-
username: string;
|
|
6
|
+
username: string | null;
|
|
7
7
|
email: string;
|
|
8
8
|
emailVerified: boolean;
|
|
9
|
-
image: null;
|
|
10
|
-
role: "player";
|
|
11
|
-
developerStatus: "none";
|
|
9
|
+
image: string | null;
|
|
10
|
+
role: "admin" | "player" | "developer";
|
|
11
|
+
developerStatus: "none" | "pending" | "approved";
|
|
12
12
|
createdAt: Date;
|
|
13
13
|
updatedAt: Date;
|
|
14
14
|
}>;
|
|
15
|
+
export declare function generateLevelConfigs(): {
|
|
16
|
+
level: number;
|
|
17
|
+
xpRequired: number;
|
|
18
|
+
creditsReward: number;
|
|
19
|
+
}[];
|
|
15
20
|
export declare function seedCurrentProjectGame(db: Awaited<ReturnType<typeof setupDatabase>>, project: ProjectInfo): Promise<{
|
|
16
21
|
id: string;
|
|
17
22
|
createdAt: Date | null;
|
package/dist/routes/index.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -13023,7 +13023,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
13023
13023
|
}
|
|
13024
13024
|
};
|
|
13025
13025
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
13026
|
-
this.debug("%s
|
|
13026
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
13027
13027
|
if (escaping) {
|
|
13028
13028
|
if (c2 === "/") {
|
|
13029
13029
|
return false;
|
|
@@ -21546,7 +21546,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
|
|
|
21546
21546
|
IS_LINE_JUNK = function(line2, pat = /^\s*#?\s*$/) {
|
|
21547
21547
|
return pat.test(line2);
|
|
21548
21548
|
};
|
|
21549
|
-
IS_CHARACTER_JUNK = function(ch, ws = "
|
|
21549
|
+
IS_CHARACTER_JUNK = function(ch, ws = " ") {
|
|
21550
21550
|
return indexOf.call(ws, ch) >= 0;
|
|
21551
21551
|
};
|
|
21552
21552
|
_formatRangeUnified = function(start2, stop2) {
|
|
@@ -31136,7 +31136,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
31136
31136
|
}
|
|
31137
31137
|
};
|
|
31138
31138
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
31139
|
-
this.debug("%s
|
|
31139
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
31140
31140
|
if (escaping) {
|
|
31141
31141
|
if (c2 === "/") {
|
|
31142
31142
|
return false;
|
|
@@ -50934,8 +50934,8 @@ var DEMO_USER = {
|
|
|
50934
50934
|
email: "demo@playcademy.com",
|
|
50935
50935
|
emailVerified: true,
|
|
50936
50936
|
image: null,
|
|
50937
|
-
role: "
|
|
50938
|
-
developerStatus: "
|
|
50937
|
+
role: "developer",
|
|
50938
|
+
developerStatus: "approved",
|
|
50939
50939
|
createdAt: now,
|
|
50940
50940
|
updatedAt: now
|
|
50941
50941
|
};
|
|
@@ -70763,6 +70763,8 @@ __export(exports_schemas, {
|
|
|
70763
70763
|
verification: () => verification,
|
|
70764
70764
|
users: () => users,
|
|
70765
70765
|
userRoleEnum: () => userRoleEnum,
|
|
70766
|
+
userLevelsRelations: () => userLevelsRelations,
|
|
70767
|
+
userLevels: () => userLevels,
|
|
70766
70768
|
shopListingsRelations: () => shopListingsRelations,
|
|
70767
70769
|
shopListings: () => shopListings,
|
|
70768
70770
|
sessions: () => sessions,
|
|
@@ -70770,6 +70772,7 @@ __export(exports_schemas, {
|
|
|
70770
70772
|
maps: () => maps,
|
|
70771
70773
|
mapElementsRelations: () => mapElementsRelations,
|
|
70772
70774
|
mapElements: () => mapElements,
|
|
70775
|
+
levelConfigs: () => levelConfigs,
|
|
70773
70776
|
itemsRelations: () => itemsRelations,
|
|
70774
70777
|
items: () => items,
|
|
70775
70778
|
itemTypeEnum: () => itemTypeEnum,
|
|
@@ -70786,11 +70789,14 @@ __export(exports_schemas, {
|
|
|
70786
70789
|
currenciesRelations: () => currenciesRelations,
|
|
70787
70790
|
currencies: () => currencies,
|
|
70788
70791
|
accounts: () => accounts,
|
|
70792
|
+
XPActionInputSchema: () => XPActionInputSchema,
|
|
70789
70793
|
VersionSchema: () => VersionSchema,
|
|
70790
70794
|
UpsertGameMetadataSchema: () => UpsertGameMetadataSchema,
|
|
70791
70795
|
UpdateUserSchema: () => UpdateUserSchema,
|
|
70796
|
+
UpdateUserLevelSchema: () => UpdateUserLevelSchema,
|
|
70792
70797
|
UpdateShopListingSchema: () => UpdateShopListingSchema,
|
|
70793
70798
|
UpdateMapElementSchema: () => UpdateMapElementSchema,
|
|
70799
|
+
UpdateLevelConfigSchema: () => UpdateLevelConfigSchema,
|
|
70794
70800
|
UpdateItemSchema: () => UpdateItemSchema,
|
|
70795
70801
|
UpdateInventoryItemSchema: () => UpdateInventoryItemSchema,
|
|
70796
70802
|
UpdateGameStateSchema: () => UpdateGameStateSchema,
|
|
@@ -70801,10 +70807,12 @@ __export(exports_schemas, {
|
|
|
70801
70807
|
StartSessionInputSchema: () => StartSessionInputSchema,
|
|
70802
70808
|
SelectVerificationSchema: () => SelectVerificationSchema,
|
|
70803
70809
|
SelectUserSchema: () => SelectUserSchema,
|
|
70810
|
+
SelectUserLevelSchema: () => SelectUserLevelSchema,
|
|
70804
70811
|
SelectShopListingSchema: () => SelectShopListingSchema,
|
|
70805
70812
|
SelectSessionSchema: () => SelectSessionSchema,
|
|
70806
70813
|
SelectMapSchema: () => SelectMapSchema,
|
|
70807
70814
|
SelectMapElementSchema: () => SelectMapElementSchema,
|
|
70815
|
+
SelectLevelConfigSchema: () => SelectLevelConfigSchema,
|
|
70808
70816
|
SelectItemSchema: () => SelectItemSchema,
|
|
70809
70817
|
SelectInventoryItemSchema: () => SelectInventoryItemSchema,
|
|
70810
70818
|
SelectGameStateSchema: () => SelectGameStateSchema,
|
|
@@ -70816,13 +70824,16 @@ __export(exports_schemas, {
|
|
|
70816
70824
|
ProcessZipSchema: () => ProcessZipSchema,
|
|
70817
70825
|
MapElementMetadataZodSchema: () => MapElementMetadataZodSchema,
|
|
70818
70826
|
ManifestV1Schema: () => ManifestV1Schema,
|
|
70827
|
+
MAX_LEVEL: () => MAX_LEVEL,
|
|
70819
70828
|
ItemMetadataSchema: () => ItemMetadataSchema,
|
|
70820
70829
|
InventoryActionInputSchema: () => InventoryActionInputSchema,
|
|
70821
70830
|
InsertVerificationSchema: () => InsertVerificationSchema,
|
|
70822
70831
|
InsertUserSchema: () => InsertUserSchema,
|
|
70832
|
+
InsertUserLevelSchema: () => InsertUserLevelSchema,
|
|
70823
70833
|
InsertShopListingSchema: () => InsertShopListingSchema,
|
|
70824
70834
|
InsertSessionSchema: () => InsertSessionSchema,
|
|
70825
70835
|
InsertMapElementSchema: () => InsertMapElementSchema,
|
|
70836
|
+
InsertLevelConfigSchema: () => InsertLevelConfigSchema,
|
|
70826
70837
|
InsertItemSchema: () => InsertItemSchema,
|
|
70827
70838
|
InsertInventoryItemSchema: () => InsertInventoryItemSchema,
|
|
70828
70839
|
InsertGameStateSchema: () => InsertGameStateSchema,
|
|
@@ -75598,6 +75609,54 @@ var InsertShopListingSchema = createInsertSchema(shopListings, {
|
|
|
75598
75609
|
}).omit({ id: true, createdAt: true, updatedAt: true });
|
|
75599
75610
|
var SelectShopListingSchema = createSelectSchema(shopListings);
|
|
75600
75611
|
var UpdateShopListingSchema = InsertShopListingSchema.partial().extend({});
|
|
75612
|
+
// ../data/src/schemas/levels.ts
|
|
75613
|
+
var MAX_LEVEL = 100;
|
|
75614
|
+
var userLevels = pgTable("user_levels", {
|
|
75615
|
+
userId: text("user_id").primaryKey().references(() => users.id, { onDelete: "cascade" }),
|
|
75616
|
+
currentLevel: integer("current_level").notNull().default(1),
|
|
75617
|
+
currentXp: integer("current_xp").notNull().default(0),
|
|
75618
|
+
totalXP: integer("total_xp").notNull().default(0),
|
|
75619
|
+
lastLevelUpAt: timestamp("last_level_up_at", { withTimezone: true }),
|
|
75620
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
75621
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().$onUpdate(() => new Date)
|
|
75622
|
+
});
|
|
75623
|
+
var levelConfigs = pgTable("level_configs", {
|
|
75624
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
75625
|
+
level: integer("level").notNull().unique(),
|
|
75626
|
+
xpRequired: integer("xp_required").notNull(),
|
|
75627
|
+
creditsReward: integer("credits_reward").notNull().default(0),
|
|
75628
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
75629
|
+
}, (table) => [uniqueIndex("unique_level_config_idx").on(table.level)]);
|
|
75630
|
+
var userLevelsRelations = relations(userLevels, ({ one }) => ({
|
|
75631
|
+
user: one(users, {
|
|
75632
|
+
fields: [userLevels.userId],
|
|
75633
|
+
references: [users.id]
|
|
75634
|
+
})
|
|
75635
|
+
}));
|
|
75636
|
+
var InsertUserLevelSchema = createInsertSchema(userLevels, {
|
|
75637
|
+
userId: exports_external.string().min(1, "User ID is required"),
|
|
75638
|
+
currentLevel: exports_external.number().int().min(1, "Level must be at least 1").default(1),
|
|
75639
|
+
currentXp: exports_external.number().int().min(0, "XP cannot be negative").default(0),
|
|
75640
|
+
totalXP: exports_external.number().int().min(0, "Total XP cannot be negative").default(0)
|
|
75641
|
+
}).omit({ createdAt: true, updatedAt: true });
|
|
75642
|
+
var SelectUserLevelSchema = createSelectSchema(userLevels);
|
|
75643
|
+
var UpdateUserLevelSchema = createUpdateSchema(userLevels).omit({
|
|
75644
|
+
userId: true,
|
|
75645
|
+
createdAt: true
|
|
75646
|
+
});
|
|
75647
|
+
var InsertLevelConfigSchema = createInsertSchema(levelConfigs, {
|
|
75648
|
+
level: exports_external.number().int().min(1, "Level must be at least 1"),
|
|
75649
|
+
xpRequired: exports_external.number().int().min(0, "XP required cannot be negative"),
|
|
75650
|
+
creditsReward: exports_external.number().int().min(0, "Credits reward cannot be negative").default(0)
|
|
75651
|
+
}).omit({ id: true, createdAt: true });
|
|
75652
|
+
var SelectLevelConfigSchema = createSelectSchema(levelConfigs);
|
|
75653
|
+
var UpdateLevelConfigSchema = createUpdateSchema(levelConfigs).omit({
|
|
75654
|
+
id: true,
|
|
75655
|
+
createdAt: true
|
|
75656
|
+
});
|
|
75657
|
+
var XPActionInputSchema = exports_external.object({
|
|
75658
|
+
amount: exports_external.number().int("XP amount must be an integer").positive("XP amount must be positive")
|
|
75659
|
+
});
|
|
75601
75660
|
// src/database/path-manager.ts
|
|
75602
75661
|
import fs2 from "node:fs";
|
|
75603
75662
|
import { join as join2, isAbsolute, dirname } from "node:path";
|
|
@@ -75677,12 +75736,47 @@ async function seedDemoData(db) {
|
|
|
75677
75736
|
for (const inventory2 of SAMPLE_INVENTORY) {
|
|
75678
75737
|
await db.insert(inventoryItems).values(inventory2);
|
|
75679
75738
|
}
|
|
75739
|
+
const levelConfigsData = generateLevelConfigs();
|
|
75740
|
+
for (const config of levelConfigsData) {
|
|
75741
|
+
await db.insert(levelConfigs).values(config);
|
|
75742
|
+
}
|
|
75680
75743
|
} catch (error2) {
|
|
75681
75744
|
console.error("❌ Error seeding demo data:", error2);
|
|
75682
75745
|
throw error2;
|
|
75683
75746
|
}
|
|
75684
75747
|
return DEMO_USER;
|
|
75685
75748
|
}
|
|
75749
|
+
function generateLevelConfigs() {
|
|
75750
|
+
const configs = [];
|
|
75751
|
+
for (let level = 1;level <= 100; level++) {
|
|
75752
|
+
let xpRequired;
|
|
75753
|
+
if (level === 1) {
|
|
75754
|
+
xpRequired = 0;
|
|
75755
|
+
} else {
|
|
75756
|
+
const baseXp = Math.pow(level - 1, 1.8) * 100;
|
|
75757
|
+
let roundTo;
|
|
75758
|
+
if (level <= 10) {
|
|
75759
|
+
roundTo = 50;
|
|
75760
|
+
} else if (level <= 20) {
|
|
75761
|
+
roundTo = 100;
|
|
75762
|
+
} else if (level <= 50) {
|
|
75763
|
+
roundTo = 250;
|
|
75764
|
+
} else {
|
|
75765
|
+
roundTo = 500;
|
|
75766
|
+
}
|
|
75767
|
+
xpRequired = Math.round(baseXp / roundTo) * roundTo;
|
|
75768
|
+
}
|
|
75769
|
+
const baseReward = level === 1 ? 0 : 25 + (level - 1) * 25;
|
|
75770
|
+
const bonusReward = Math.floor((level - 1) / 10) * 50;
|
|
75771
|
+
const creditsReward = baseReward + bonusReward;
|
|
75772
|
+
configs.push({
|
|
75773
|
+
level,
|
|
75774
|
+
xpRequired,
|
|
75775
|
+
creditsReward
|
|
75776
|
+
});
|
|
75777
|
+
}
|
|
75778
|
+
return configs;
|
|
75779
|
+
}
|
|
75686
75780
|
async function seedCurrentProjectGame(db, project) {
|
|
75687
75781
|
const now2 = new Date;
|
|
75688
75782
|
try {
|
|
@@ -75718,7 +75812,7 @@ async function seedCurrentProjectGame(db, project) {
|
|
|
75718
75812
|
// package.json
|
|
75719
75813
|
var package_default = {
|
|
75720
75814
|
name: "@playcademy/sandbox",
|
|
75721
|
-
version: "0.1.0-beta.
|
|
75815
|
+
version: "0.1.0-beta.4",
|
|
75722
75816
|
description: "Local development server for Playcademy game development",
|
|
75723
75817
|
type: "module",
|
|
75724
75818
|
exports: {
|
|
@@ -75821,6 +75915,291 @@ async function getUserMe(ctx) {
|
|
|
75821
75915
|
}
|
|
75822
75916
|
}
|
|
75823
75917
|
|
|
75918
|
+
// ../data/src/constants.ts
|
|
75919
|
+
var ITEM_INTERNAL_NAMES = {
|
|
75920
|
+
PLAYCADEMY_CREDITS: "PLAYCADEMY_CREDITS",
|
|
75921
|
+
PLAYCADEMY_XP: "PLAYCADEMY_XP",
|
|
75922
|
+
FOUNDING_MEMBER_BADGE: "FOUNDING_MEMBER_BADGE",
|
|
75923
|
+
EARLY_ADOPTER_BADGE: "EARLY_ADOPTER_BADGE",
|
|
75924
|
+
FIRST_GAME_BADGE: "FIRST_GAME_BADGE",
|
|
75925
|
+
COMMON_SWORD: "COMMON_SWORD",
|
|
75926
|
+
SMALL_HEALTH_POTION: "SMALL_HEALTH_POTION",
|
|
75927
|
+
SMALL_BACKPACK: "SMALL_BACKPACK"
|
|
75928
|
+
};
|
|
75929
|
+
var CURRENCIES = {
|
|
75930
|
+
PRIMARY: ITEM_INTERNAL_NAMES.PLAYCADEMY_CREDITS,
|
|
75931
|
+
XP: ITEM_INTERNAL_NAMES.PLAYCADEMY_XP
|
|
75932
|
+
};
|
|
75933
|
+
var BADGES = {
|
|
75934
|
+
FOUNDING_MEMBER: ITEM_INTERNAL_NAMES.FOUNDING_MEMBER_BADGE,
|
|
75935
|
+
EARLY_ADOPTER: ITEM_INTERNAL_NAMES.EARLY_ADOPTER_BADGE,
|
|
75936
|
+
FIRST_GAME: ITEM_INTERNAL_NAMES.FIRST_GAME_BADGE
|
|
75937
|
+
};
|
|
75938
|
+
|
|
75939
|
+
// ../api-core/src/utils/levels.ts
|
|
75940
|
+
var levelConfigCache = null;
|
|
75941
|
+
async function getLevelConfig(db, level) {
|
|
75942
|
+
if (level < 1) {
|
|
75943
|
+
throw ApiError.badRequest("Level must be at least 1");
|
|
75944
|
+
}
|
|
75945
|
+
if (levelConfigCache?.has(level)) {
|
|
75946
|
+
return levelConfigCache.get(level) || null;
|
|
75947
|
+
}
|
|
75948
|
+
try {
|
|
75949
|
+
const [config] = await db.select().from(levelConfigs).where(eq(levelConfigs.level, level)).limit(1);
|
|
75950
|
+
if (levelConfigCache && config) {
|
|
75951
|
+
levelConfigCache.set(level, config);
|
|
75952
|
+
}
|
|
75953
|
+
return config || null;
|
|
75954
|
+
} catch (error2) {
|
|
75955
|
+
logger2.error(`Error fetching level config for level ${level}:`, error2);
|
|
75956
|
+
throw ApiError.internal("Internal server error", error2);
|
|
75957
|
+
}
|
|
75958
|
+
}
|
|
75959
|
+
async function calculateXPToNextLevel(db, currentLevel, currentXp) {
|
|
75960
|
+
try {
|
|
75961
|
+
const nextLevelConfig = await getLevelConfig(db, currentLevel + 1);
|
|
75962
|
+
if (!nextLevelConfig) {
|
|
75963
|
+
return 0;
|
|
75964
|
+
}
|
|
75965
|
+
return Math.max(0, nextLevelConfig.xpRequired - currentXp);
|
|
75966
|
+
} catch (error2) {
|
|
75967
|
+
logger2.error(`Error calculating XP to next level:`, error2);
|
|
75968
|
+
throw ApiError.internal("Internal server error", error2);
|
|
75969
|
+
}
|
|
75970
|
+
}
|
|
75971
|
+
async function checkLevelUp(tx, currentLevel, newXp) {
|
|
75972
|
+
let level = currentLevel;
|
|
75973
|
+
let remainingXp = newXp;
|
|
75974
|
+
let totalCreditsAwarded = 0;
|
|
75975
|
+
let leveledUp = false;
|
|
75976
|
+
while (true) {
|
|
75977
|
+
if (level >= MAX_LEVEL) {
|
|
75978
|
+
break;
|
|
75979
|
+
}
|
|
75980
|
+
const nextLevelConfig2 = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
|
|
75981
|
+
const [nextLevel2] = nextLevelConfig2;
|
|
75982
|
+
if (!nextLevel2) {
|
|
75983
|
+
break;
|
|
75984
|
+
}
|
|
75985
|
+
if (remainingXp >= nextLevel2.xpRequired) {
|
|
75986
|
+
level += 1;
|
|
75987
|
+
remainingXp -= nextLevel2.xpRequired;
|
|
75988
|
+
totalCreditsAwarded += nextLevel2.creditsReward;
|
|
75989
|
+
leveledUp = true;
|
|
75990
|
+
} else {
|
|
75991
|
+
break;
|
|
75992
|
+
}
|
|
75993
|
+
}
|
|
75994
|
+
const nextLevelConfig = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
|
|
75995
|
+
const [nextLevel] = nextLevelConfig;
|
|
75996
|
+
const xpToNextLevel = nextLevel ? Math.max(0, nextLevel.xpRequired - remainingXp) : 0;
|
|
75997
|
+
return {
|
|
75998
|
+
newLevel: level,
|
|
75999
|
+
remainingXp,
|
|
76000
|
+
leveledUp,
|
|
76001
|
+
creditsAwarded: totalCreditsAwarded,
|
|
76002
|
+
xpToNextLevel
|
|
76003
|
+
};
|
|
76004
|
+
}
|
|
76005
|
+
|
|
76006
|
+
// ../api-core/src/utils/validation.ts
|
|
76007
|
+
function formatValidationErrors(error2) {
|
|
76008
|
+
const flattened = error2.flatten();
|
|
76009
|
+
const fieldErrors = Object.entries(flattened.fieldErrors).map(([field, errors2]) => `${field}: ${errors2?.join(", ") || "Invalid"}`).join("; ");
|
|
76010
|
+
const formErrors = flattened.formErrors.join("; ");
|
|
76011
|
+
const allErrors = [fieldErrors, formErrors].filter(Boolean).join("; ");
|
|
76012
|
+
return allErrors || "Validation failed";
|
|
76013
|
+
}
|
|
76014
|
+
|
|
76015
|
+
// ../api-core/src/levels/index.ts
|
|
76016
|
+
var AddXPSchema = XPActionInputSchema;
|
|
76017
|
+
async function getUserLevel(ctx) {
|
|
76018
|
+
const user = ctx.user;
|
|
76019
|
+
if (!user) {
|
|
76020
|
+
throw ApiError.unauthorized("Valid session or bearer token required");
|
|
76021
|
+
}
|
|
76022
|
+
try {
|
|
76023
|
+
const db = getDatabase();
|
|
76024
|
+
let [userLevel] = await db.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
|
|
76025
|
+
if (!userLevel) {
|
|
76026
|
+
const [newUserLevel] = await db.insert(userLevels).values({
|
|
76027
|
+
userId: user.id,
|
|
76028
|
+
currentLevel: 1,
|
|
76029
|
+
currentXp: 0,
|
|
76030
|
+
totalXP: 0
|
|
76031
|
+
}).returning();
|
|
76032
|
+
if (!newUserLevel) {
|
|
76033
|
+
throw ApiError.internal("Failed to create user level record");
|
|
76034
|
+
}
|
|
76035
|
+
userLevel = newUserLevel;
|
|
76036
|
+
}
|
|
76037
|
+
return userLevel;
|
|
76038
|
+
} catch (error2) {
|
|
76039
|
+
if (error2 instanceof ApiError) {
|
|
76040
|
+
throw error2;
|
|
76041
|
+
}
|
|
76042
|
+
logger2.error(`Error fetching user level for user ${user.id}:`, error2);
|
|
76043
|
+
throw ApiError.internal("Internal server error", error2);
|
|
76044
|
+
}
|
|
76045
|
+
}
|
|
76046
|
+
async function addXP(ctx, amount) {
|
|
76047
|
+
const user = ctx.user;
|
|
76048
|
+
if (!user) {
|
|
76049
|
+
throw ApiError.unauthorized("Valid session or bearer token required");
|
|
76050
|
+
}
|
|
76051
|
+
if (amount <= 0) {
|
|
76052
|
+
throw ApiError.badRequest("XP amount must be positive");
|
|
76053
|
+
}
|
|
76054
|
+
try {
|
|
76055
|
+
const db = getDatabase();
|
|
76056
|
+
const result = await db.transaction(async (tx) => {
|
|
76057
|
+
let [userLevel] = await tx.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
|
|
76058
|
+
if (!userLevel) {
|
|
76059
|
+
const [newUserLevel] = await tx.insert(userLevels).values({
|
|
76060
|
+
userId: user.id,
|
|
76061
|
+
currentLevel: 1,
|
|
76062
|
+
currentXp: 0,
|
|
76063
|
+
totalXP: 0
|
|
76064
|
+
}).returning();
|
|
76065
|
+
if (!newUserLevel) {
|
|
76066
|
+
throw ApiError.internal("Failed to create user level record");
|
|
76067
|
+
}
|
|
76068
|
+
userLevel = newUserLevel;
|
|
76069
|
+
}
|
|
76070
|
+
const newCurrentXp = userLevel.currentXp + amount;
|
|
76071
|
+
const newTotalXP = userLevel.totalXP + amount;
|
|
76072
|
+
const levelUpResult = await checkLevelUp(tx, userLevel.currentLevel, newCurrentXp);
|
|
76073
|
+
const [updatedUserLevel] = await tx.update(userLevels).set({
|
|
76074
|
+
currentLevel: levelUpResult.newLevel,
|
|
76075
|
+
currentXp: levelUpResult.remainingXp,
|
|
76076
|
+
totalXP: newTotalXP,
|
|
76077
|
+
lastLevelUpAt: levelUpResult.leveledUp ? new Date : userLevel.lastLevelUpAt
|
|
76078
|
+
}).where(eq(userLevels.userId, user.id)).returning();
|
|
76079
|
+
if (!updatedUserLevel) {
|
|
76080
|
+
throw ApiError.internal("Failed to update user level");
|
|
76081
|
+
}
|
|
76082
|
+
let creditsAwarded = 0;
|
|
76083
|
+
if (levelUpResult.leveledUp) {
|
|
76084
|
+
const creditsToAward = levelUpResult.creditsAwarded;
|
|
76085
|
+
logger2.debug("User leveled up", {
|
|
76086
|
+
userId: user.id,
|
|
76087
|
+
oldLevel: userLevel.currentLevel,
|
|
76088
|
+
newLevel: levelUpResult.newLevel,
|
|
76089
|
+
xpAdded: amount,
|
|
76090
|
+
totalXP: newTotalXP,
|
|
76091
|
+
creditsAwarded: creditsToAward
|
|
76092
|
+
});
|
|
76093
|
+
if (creditsToAward > 0) {
|
|
76094
|
+
const [creditsItem] = await tx.select({ id: items.id }).from(items).where(eq(items.internalName, CURRENCIES.PRIMARY)).limit(1);
|
|
76095
|
+
if (!creditsItem) {
|
|
76096
|
+
throw ApiError.internal(`${CURRENCIES.PRIMARY} item not found`);
|
|
76097
|
+
}
|
|
76098
|
+
await tx.insert(inventoryItems).values({
|
|
76099
|
+
userId: user.id,
|
|
76100
|
+
itemId: creditsItem.id,
|
|
76101
|
+
quantity: creditsToAward
|
|
76102
|
+
}).onConflictDoUpdate({
|
|
76103
|
+
target: [
|
|
76104
|
+
inventoryItems.userId,
|
|
76105
|
+
inventoryItems.itemId
|
|
76106
|
+
],
|
|
76107
|
+
set: {
|
|
76108
|
+
quantity: sql`${inventoryItems.quantity} + ${creditsToAward}`
|
|
76109
|
+
}
|
|
76110
|
+
});
|
|
76111
|
+
}
|
|
76112
|
+
creditsAwarded = creditsToAward;
|
|
76113
|
+
} else {
|
|
76114
|
+
logger2.debug("XP added to user", {
|
|
76115
|
+
userId: user.id,
|
|
76116
|
+
level: userLevel.currentLevel,
|
|
76117
|
+
xpAdded: amount,
|
|
76118
|
+
totalXP: newTotalXP
|
|
76119
|
+
});
|
|
76120
|
+
}
|
|
76121
|
+
return {
|
|
76122
|
+
totalXP: newTotalXP,
|
|
76123
|
+
newLevel: levelUpResult.newLevel,
|
|
76124
|
+
leveledUp: levelUpResult.leveledUp,
|
|
76125
|
+
creditsAwarded,
|
|
76126
|
+
xpToNextLevel: levelUpResult.xpToNextLevel
|
|
76127
|
+
};
|
|
76128
|
+
});
|
|
76129
|
+
return result;
|
|
76130
|
+
} catch (error2) {
|
|
76131
|
+
if (error2 instanceof ApiError) {
|
|
76132
|
+
throw error2;
|
|
76133
|
+
}
|
|
76134
|
+
logger2.error(`Error adding XP for user ${user.id}:`, error2);
|
|
76135
|
+
throw ApiError.internal("Internal server error", error2);
|
|
76136
|
+
}
|
|
76137
|
+
}
|
|
76138
|
+
async function addXPFromRequest(ctx) {
|
|
76139
|
+
let amount;
|
|
76140
|
+
try {
|
|
76141
|
+
const requestBody = await ctx.request.json();
|
|
76142
|
+
const parsed = AddXPSchema.parse(requestBody);
|
|
76143
|
+
amount = parsed.amount;
|
|
76144
|
+
} catch (error2) {
|
|
76145
|
+
if (error2 instanceof ZodError) {
|
|
76146
|
+
throw ApiError.badRequest(`Validation failed: ${formatValidationErrors(error2)}`);
|
|
76147
|
+
}
|
|
76148
|
+
logger2.error("Failed to parse request body:", error2);
|
|
76149
|
+
throw ApiError.badRequest("Invalid JSON body");
|
|
76150
|
+
}
|
|
76151
|
+
return await addXP(ctx, amount);
|
|
76152
|
+
}
|
|
76153
|
+
async function getAllLevelConfigs(_ctx) {
|
|
76154
|
+
try {
|
|
76155
|
+
const db = getDatabase();
|
|
76156
|
+
const configs = await db.select().from(levelConfigs).orderBy(levelConfigs.level);
|
|
76157
|
+
return configs;
|
|
76158
|
+
} catch (error2) {
|
|
76159
|
+
logger2.error("Error fetching all level configs:", error2);
|
|
76160
|
+
throw ApiError.internal("Internal server error", error2);
|
|
76161
|
+
}
|
|
76162
|
+
}
|
|
76163
|
+
async function getUserLevelProgress(ctx) {
|
|
76164
|
+
const userLevel = await getUserLevel(ctx);
|
|
76165
|
+
try {
|
|
76166
|
+
const db = getDatabase();
|
|
76167
|
+
const xpToNextLevel = await calculateXPToNextLevel(db, userLevel.currentLevel, userLevel.currentXp);
|
|
76168
|
+
return {
|
|
76169
|
+
level: userLevel.currentLevel,
|
|
76170
|
+
currentXp: userLevel.currentXp,
|
|
76171
|
+
xpToNextLevel,
|
|
76172
|
+
totalXP: userLevel.totalXP
|
|
76173
|
+
};
|
|
76174
|
+
} catch (error2) {
|
|
76175
|
+
if (error2 instanceof ApiError) {
|
|
76176
|
+
throw error2;
|
|
76177
|
+
}
|
|
76178
|
+
logger2.error(`Error fetching user level progress:`, error2);
|
|
76179
|
+
throw ApiError.internal("Internal server error", error2);
|
|
76180
|
+
}
|
|
76181
|
+
}
|
|
76182
|
+
async function getLevelConfigByPath(ctx) {
|
|
76183
|
+
const levelParam = ctx.params.level;
|
|
76184
|
+
if (!levelParam) {
|
|
76185
|
+
throw ApiError.badRequest("Level parameter is required");
|
|
76186
|
+
}
|
|
76187
|
+
const level = parseInt(levelParam, 10);
|
|
76188
|
+
if (isNaN(level) || level < 1) {
|
|
76189
|
+
throw ApiError.badRequest("Level must be a positive integer");
|
|
76190
|
+
}
|
|
76191
|
+
try {
|
|
76192
|
+
const db = getDatabase();
|
|
76193
|
+
return await getLevelConfig(db, level);
|
|
76194
|
+
} catch (error2) {
|
|
76195
|
+
if (error2 instanceof ApiError) {
|
|
76196
|
+
throw error2;
|
|
76197
|
+
}
|
|
76198
|
+
logger2.error(`Error fetching level config for level ${level}:`, error2);
|
|
76199
|
+
throw ApiError.internal("Internal server error", error2);
|
|
76200
|
+
}
|
|
76201
|
+
}
|
|
76202
|
+
|
|
75824
76203
|
// src/routes/users.ts
|
|
75825
76204
|
var usersRouter = new Hono2;
|
|
75826
76205
|
usersRouter.get("/me", async (c2) => {
|
|
@@ -75842,18 +76221,66 @@ usersRouter.get("/me", async (c2) => {
|
|
|
75842
76221
|
return c2.json({ error: message }, 500);
|
|
75843
76222
|
}
|
|
75844
76223
|
});
|
|
76224
|
+
usersRouter.get("/level", async (c2) => {
|
|
76225
|
+
const ctx = {
|
|
76226
|
+
user: c2.get("user"),
|
|
76227
|
+
params: {},
|
|
76228
|
+
url: new URL(c2.req.url),
|
|
76229
|
+
request: c2.req.raw
|
|
76230
|
+
};
|
|
76231
|
+
try {
|
|
76232
|
+
const levelData = await getUserLevel(ctx);
|
|
76233
|
+
return c2.json(levelData);
|
|
76234
|
+
} catch (error2) {
|
|
76235
|
+
if (error2 instanceof ApiError) {
|
|
76236
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
76237
|
+
}
|
|
76238
|
+
console.error("Error in getUserLevel:", error2);
|
|
76239
|
+
const message = error2 instanceof Error ? error2.message : "Internal server error";
|
|
76240
|
+
return c2.json({ error: message }, 500);
|
|
76241
|
+
}
|
|
76242
|
+
});
|
|
76243
|
+
usersRouter.get("/level/progress", async (c2) => {
|
|
76244
|
+
const ctx = {
|
|
76245
|
+
user: c2.get("user"),
|
|
76246
|
+
params: {},
|
|
76247
|
+
url: new URL(c2.req.url),
|
|
76248
|
+
request: c2.req.raw
|
|
76249
|
+
};
|
|
76250
|
+
try {
|
|
76251
|
+
const progressData = await getUserLevelProgress(ctx);
|
|
76252
|
+
return c2.json(progressData);
|
|
76253
|
+
} catch (error2) {
|
|
76254
|
+
if (error2 instanceof ApiError) {
|
|
76255
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
76256
|
+
}
|
|
76257
|
+
console.error("Error in getUserLevelProgress:", error2);
|
|
76258
|
+
const message = error2 instanceof Error ? error2.message : "Internal server error";
|
|
76259
|
+
return c2.json({ error: message }, 500);
|
|
76260
|
+
}
|
|
76261
|
+
});
|
|
76262
|
+
usersRouter.post("/xp/add", async (c2) => {
|
|
76263
|
+
const ctx = {
|
|
76264
|
+
user: c2.get("user"),
|
|
76265
|
+
params: {},
|
|
76266
|
+
url: new URL(c2.req.url),
|
|
76267
|
+
request: c2.req.raw
|
|
76268
|
+
};
|
|
76269
|
+
try {
|
|
76270
|
+
const result = await addXPFromRequest(ctx);
|
|
76271
|
+
return c2.json(result);
|
|
76272
|
+
} catch (error2) {
|
|
76273
|
+
if (error2 instanceof ApiError) {
|
|
76274
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
76275
|
+
}
|
|
76276
|
+
console.error("Error in addXPFromRequest:", error2);
|
|
76277
|
+
const message = error2 instanceof Error ? error2.message : "Internal server error";
|
|
76278
|
+
return c2.json({ error: message }, 500);
|
|
76279
|
+
}
|
|
76280
|
+
});
|
|
75845
76281
|
// src/routes/health.ts
|
|
75846
76282
|
var healthRouter = new Hono2;
|
|
75847
76283
|
healthRouter.get("/", (c2) => c2.json({ status: "ok", timestamp: new Date().toISOString() }));
|
|
75848
|
-
// ../api-core/src/utils/validation.ts
|
|
75849
|
-
function formatValidationErrors(error2) {
|
|
75850
|
-
const flattened = error2.flatten();
|
|
75851
|
-
const fieldErrors = Object.entries(flattened.fieldErrors).map(([field, errors2]) => `${field}: ${errors2?.join(", ") || "Invalid"}`).join("; ");
|
|
75852
|
-
const formErrors = flattened.formErrors.join("; ");
|
|
75853
|
-
const allErrors = [fieldErrors, formErrors].filter(Boolean).join("; ");
|
|
75854
|
-
return allErrors || "Validation failed";
|
|
75855
|
-
}
|
|
75856
|
-
|
|
75857
76284
|
// ../api-core/src/inventory/index.ts
|
|
75858
76285
|
async function getUserInventory(ctx) {
|
|
75859
76286
|
const user = ctx.user;
|
|
@@ -80083,6 +80510,46 @@ devRouter.delete("/keys/:keyId", async (c2) => {
|
|
|
80083
80510
|
return c2.json({ error: message2 }, 500);
|
|
80084
80511
|
}
|
|
80085
80512
|
});
|
|
80513
|
+
// src/routes/levels.ts
|
|
80514
|
+
var levelsRouter = new Hono2;
|
|
80515
|
+
levelsRouter.get("/config", async (c2) => {
|
|
80516
|
+
const ctx = {
|
|
80517
|
+
user: c2.get("user"),
|
|
80518
|
+
params: {},
|
|
80519
|
+
url: new URL(c2.req.url),
|
|
80520
|
+
request: c2.req.raw
|
|
80521
|
+
};
|
|
80522
|
+
try {
|
|
80523
|
+
const configs = await getAllLevelConfigs(ctx);
|
|
80524
|
+
return c2.json(configs);
|
|
80525
|
+
} catch (error2) {
|
|
80526
|
+
if (error2 instanceof ApiError) {
|
|
80527
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
80528
|
+
}
|
|
80529
|
+
console.error("Error in getAllLevelConfigs:", error2);
|
|
80530
|
+
const message2 = error2 instanceof Error ? error2.message : "Internal server error";
|
|
80531
|
+
return c2.json({ error: message2 }, 500);
|
|
80532
|
+
}
|
|
80533
|
+
});
|
|
80534
|
+
levelsRouter.get("/config/:level", async (c2) => {
|
|
80535
|
+
const ctx = {
|
|
80536
|
+
user: c2.get("user"),
|
|
80537
|
+
params: { level: c2.req.param("level") },
|
|
80538
|
+
url: new URL(c2.req.url),
|
|
80539
|
+
request: c2.req.raw
|
|
80540
|
+
};
|
|
80541
|
+
try {
|
|
80542
|
+
const config = await getLevelConfigByPath(ctx);
|
|
80543
|
+
return c2.json(config);
|
|
80544
|
+
} catch (error2) {
|
|
80545
|
+
if (error2 instanceof ApiError) {
|
|
80546
|
+
return c2.json({ error: error2.message }, error2.statusCode);
|
|
80547
|
+
}
|
|
80548
|
+
console.error("Error in getLevelConfigByPath:", error2);
|
|
80549
|
+
const message2 = error2 instanceof Error ? error2.message : "Internal server error";
|
|
80550
|
+
return c2.json({ error: message2 }, 500);
|
|
80551
|
+
}
|
|
80552
|
+
});
|
|
80086
80553
|
// src/server.ts
|
|
80087
80554
|
async function startServer(options) {
|
|
80088
80555
|
const { port, verbose, project } = options;
|
|
@@ -80109,6 +80576,7 @@ async function startServer(options) {
|
|
|
80109
80576
|
app.route("/api/maps", mapsRouter);
|
|
80110
80577
|
app.route("/api/shop-listings", shopListingsRouter);
|
|
80111
80578
|
app.route("/api/dev", devRouter);
|
|
80579
|
+
app.route("/api/levels", levelsRouter);
|
|
80112
80580
|
return serve({ fetch: app.fetch, port });
|
|
80113
80581
|
}
|
|
80114
80582
|
var version3 = package_default.version;
|