@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 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\t%s %s %j", pattern, i3, re3, c2);
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 = " \t") {
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\t%s %s %j", pattern, i3, re3, c2);
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: "player",
52848
- developerStatus: "none",
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.2",
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;
@@ -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: `${string}-${string}-${string}-${string}-${string}`;
8
+ userId: string;
20
9
  itemId: `${string}-${string}-${string}-${string}-${string}`;
21
10
  quantity: number;
22
11
  }[];
@@ -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: `${string}-${string}-${string}-${string}-${string}`;
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;
@@ -9,3 +9,4 @@ export { currenciesRouter } from './currencies';
9
9
  export { mapRouter, mapsRouter } from './maps';
10
10
  export { shopListingsRouter } from './shop-listings';
11
11
  export { devRouter } from './dev';
12
+ export { levelsRouter } from './levels';
@@ -0,0 +1,3 @@
1
+ import { Hono } from 'hono';
2
+ import type { HonoEnv } from '../types';
3
+ export declare const levelsRouter: Hono<HonoEnv, import("hono/types").BlankSchema, "/">;
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\t%s %s %j", pattern, i3, re3, c2);
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 = " \t") {
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\t%s %s %j", pattern, i3, re3, c2);
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: "player",
50938
- developerStatus: "none",
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.2",
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {