@playcademy/sandbox 0.1.0-beta.2 → 0.1.0-beta.4

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,12 +77519,60 @@ 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
+ totalXpEarned: integer("total_xp_earned").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
+ totalXpEarned: exports_external.number().int().min(0, "Total XP earned 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";
77514
77573
 
77515
77574
  class DatabasePathManager {
77516
- static DEFAULT_DB_SUBPATH = join2("@playcademy", "vite-plugin", ".playcademy", "sandbox.db");
77575
+ static DEFAULT_DB_SUBPATH = join2("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
77517
77576
  static findNodeModulesPath() {
77518
77577
  let currentDir = process.cwd();
77519
77578
  while (currentDir !== dirname(currentDir)) {
@@ -77587,12 +77646,35 @@ 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
+ const xpRequired = level === 1 ? 0 : Math.floor(50 * Math.pow(level - 1, 1.7));
77663
+ let creditsReward = 0;
77664
+ if (level > 1) {
77665
+ creditsReward = 25 + level * 25;
77666
+ if (level % 10 === 1 && level > 1) {
77667
+ creditsReward += 75;
77668
+ }
77669
+ }
77670
+ configs.push({
77671
+ level,
77672
+ xpRequired,
77673
+ creditsReward
77674
+ });
77675
+ }
77676
+ return configs;
77677
+ }
77596
77678
  async function seedCurrentProjectGame(db, project) {
77597
77679
  const now2 = new Date;
77598
77680
  try {
@@ -77628,7 +77710,7 @@ async function seedCurrentProjectGame(db, project) {
77628
77710
  // package.json
77629
77711
  var package_default = {
77630
77712
  name: "@playcademy/sandbox",
77631
- version: "0.1.0-beta.1",
77713
+ version: "0.1.0-beta.3",
77632
77714
  description: "Local development server for Playcademy game development",
77633
77715
  type: "module",
77634
77716
  exports: {
@@ -77657,8 +77739,6 @@ var package_default = {
77657
77739
  dependencies: {
77658
77740
  "@electric-sql/pglite": "^0.3.2",
77659
77741
  "@hono/node-server": "^1.14.2",
77660
- "@playcademy/api-core": "workspace:*",
77661
- "@playcademy/data": "workspace:*",
77662
77742
  commander: "^12.1.0",
77663
77743
  "drizzle-kit": "^0.31.0",
77664
77744
  "drizzle-orm": "^0.42.0",
@@ -77667,6 +77747,8 @@ var package_default = {
77667
77747
  picocolors: "^1.1.1"
77668
77748
  },
77669
77749
  devDependencies: {
77750
+ "@playcademy/api-core": "workspace:*",
77751
+ "@playcademy/data": "workspace:*",
77670
77752
  "@types/bun": "latest"
77671
77753
  },
77672
77754
  peerDependencies: {
@@ -77731,6 +77813,270 @@ async function getUserMe(ctx) {
77731
77813
  }
77732
77814
  }
77733
77815
 
77816
+ // ../api-core/src/utils/levels.ts
77817
+ var levelConfigCache = null;
77818
+ async function getLevelConfig(db, level) {
77819
+ if (level < 1) {
77820
+ throw ApiError.badRequest("Level must be at least 1");
77821
+ }
77822
+ if (levelConfigCache?.has(level)) {
77823
+ return levelConfigCache.get(level) || null;
77824
+ }
77825
+ try {
77826
+ const [config] = await db.select().from(levelConfigs).where(eq(levelConfigs.level, level)).limit(1);
77827
+ if (levelConfigCache && config) {
77828
+ levelConfigCache.set(level, config);
77829
+ }
77830
+ return config || null;
77831
+ } catch (error2) {
77832
+ logger2.error(`Error fetching level config for level ${level}:`, error2);
77833
+ throw ApiError.internal("Internal server error", error2);
77834
+ }
77835
+ }
77836
+ async function calculateXPToNextLevel(db, currentLevel, currentXp) {
77837
+ try {
77838
+ const nextLevelConfig = await getLevelConfig(db, currentLevel + 1);
77839
+ if (!nextLevelConfig) {
77840
+ return 0;
77841
+ }
77842
+ return Math.max(0, nextLevelConfig.xpRequired - currentXp);
77843
+ } catch (error2) {
77844
+ logger2.error(`Error calculating XP to next level:`, error2);
77845
+ throw ApiError.internal("Internal server error", error2);
77846
+ }
77847
+ }
77848
+ async function checkLevelUp(tx, currentLevel, newXp) {
77849
+ let level = currentLevel;
77850
+ let remainingXp = newXp;
77851
+ let totalCreditsAwarded = 0;
77852
+ let leveledUp = false;
77853
+ while (true) {
77854
+ if (level >= MAX_LEVEL) {
77855
+ break;
77856
+ }
77857
+ const nextLevelConfig2 = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
77858
+ const [nextLevel2] = nextLevelConfig2;
77859
+ if (!nextLevel2) {
77860
+ break;
77861
+ }
77862
+ if (remainingXp >= nextLevel2.xpRequired) {
77863
+ level += 1;
77864
+ remainingXp -= nextLevel2.xpRequired;
77865
+ totalCreditsAwarded += nextLevel2.creditsReward;
77866
+ leveledUp = true;
77867
+ } else {
77868
+ break;
77869
+ }
77870
+ }
77871
+ const nextLevelConfig = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
77872
+ const [nextLevel] = nextLevelConfig;
77873
+ const xpToNextLevel = nextLevel ? Math.max(0, nextLevel.xpRequired - remainingXp) : 0;
77874
+ return {
77875
+ newLevel: level,
77876
+ remainingXp,
77877
+ leveledUp,
77878
+ creditsAwarded: totalCreditsAwarded,
77879
+ xpToNextLevel
77880
+ };
77881
+ }
77882
+
77883
+ // ../api-core/src/utils/validation.ts
77884
+ function formatValidationErrors(error2) {
77885
+ const flattened = error2.flatten();
77886
+ const fieldErrors = Object.entries(flattened.fieldErrors).map(([field, errors2]) => `${field}: ${errors2?.join(", ") || "Invalid"}`).join("; ");
77887
+ const formErrors = flattened.formErrors.join("; ");
77888
+ const allErrors = [fieldErrors, formErrors].filter(Boolean).join("; ");
77889
+ return allErrors || "Validation failed";
77890
+ }
77891
+
77892
+ // ../api-core/src/levels/index.ts
77893
+ var AddXPSchema = XPActionInputSchema;
77894
+ async function getUserLevel(ctx) {
77895
+ const user = ctx.user;
77896
+ if (!user) {
77897
+ throw ApiError.unauthorized("Valid session or bearer token required");
77898
+ }
77899
+ try {
77900
+ const db = getDatabase();
77901
+ let [userLevel] = await db.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
77902
+ if (!userLevel) {
77903
+ const [newUserLevel] = await db.insert(userLevels).values({
77904
+ userId: user.id,
77905
+ currentLevel: 1,
77906
+ currentXp: 0,
77907
+ totalXpEarned: 0
77908
+ }).returning();
77909
+ if (!newUserLevel) {
77910
+ throw ApiError.internal("Failed to create user level record");
77911
+ }
77912
+ userLevel = newUserLevel;
77913
+ }
77914
+ return userLevel;
77915
+ } catch (error2) {
77916
+ if (error2 instanceof ApiError) {
77917
+ throw error2;
77918
+ }
77919
+ logger2.error(`Error fetching user level for user ${user.id}:`, error2);
77920
+ throw ApiError.internal("Internal server error", error2);
77921
+ }
77922
+ }
77923
+ async function addXP(ctx, amount) {
77924
+ const user = ctx.user;
77925
+ if (!user) {
77926
+ throw ApiError.unauthorized("Valid session or bearer token required");
77927
+ }
77928
+ if (amount <= 0) {
77929
+ throw ApiError.badRequest("XP amount must be positive");
77930
+ }
77931
+ try {
77932
+ const db = getDatabase();
77933
+ const result = await db.transaction(async (tx) => {
77934
+ let [userLevel] = await tx.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
77935
+ if (!userLevel) {
77936
+ const [newUserLevel] = await tx.insert(userLevels).values({
77937
+ userId: user.id,
77938
+ currentLevel: 1,
77939
+ currentXp: 0,
77940
+ totalXpEarned: 0
77941
+ }).returning();
77942
+ if (!newUserLevel) {
77943
+ throw ApiError.internal("Failed to create user level record");
77944
+ }
77945
+ userLevel = newUserLevel;
77946
+ }
77947
+ const newCurrentXp = userLevel.currentXp + amount;
77948
+ const newTotalXpEarned = userLevel.totalXpEarned + amount;
77949
+ const levelUpResult = await checkLevelUp(tx, userLevel.currentLevel, newCurrentXp);
77950
+ const [updatedUserLevel] = await tx.update(userLevels).set({
77951
+ currentLevel: levelUpResult.newLevel,
77952
+ currentXp: levelUpResult.remainingXp,
77953
+ totalXpEarned: newTotalXpEarned,
77954
+ lastLevelUpAt: levelUpResult.leveledUp ? new Date : userLevel.lastLevelUpAt
77955
+ }).where(eq(userLevels.userId, user.id)).returning();
77956
+ if (!updatedUserLevel) {
77957
+ throw ApiError.internal("Failed to update user level");
77958
+ }
77959
+ let creditsAwarded = 0;
77960
+ if (levelUpResult.leveledUp) {
77961
+ const creditsToAward = levelUpResult.creditsAwarded;
77962
+ logger2.debug("User leveled up", {
77963
+ userId: user.id,
77964
+ oldLevel: userLevel.currentLevel,
77965
+ newLevel: levelUpResult.newLevel,
77966
+ xpAdded: amount,
77967
+ totalXpEarned: newTotalXpEarned,
77968
+ creditsAwarded: creditsToAward
77969
+ });
77970
+ if (creditsToAward > 0) {
77971
+ const [creditsItem] = await tx.select({ id: items.id }).from(items).where(eq(items.internalName, "PLAYCADEMY_CREDITS")).limit(1);
77972
+ if (!creditsItem) {
77973
+ throw ApiError.internal("PLAYCADEMY_CREDITS item not found");
77974
+ }
77975
+ await tx.insert(inventoryItems).values({
77976
+ userId: user.id,
77977
+ itemId: creditsItem.id,
77978
+ quantity: creditsToAward
77979
+ }).onConflictDoUpdate({
77980
+ target: [
77981
+ inventoryItems.userId,
77982
+ inventoryItems.itemId
77983
+ ],
77984
+ set: {
77985
+ quantity: sql`${inventoryItems.quantity} + ${creditsToAward}`
77986
+ }
77987
+ });
77988
+ }
77989
+ creditsAwarded = creditsToAward;
77990
+ } else {
77991
+ logger2.debug("XP added to user", {
77992
+ userId: user.id,
77993
+ level: userLevel.currentLevel,
77994
+ xpAdded: amount,
77995
+ totalXpEarned: newTotalXpEarned
77996
+ });
77997
+ }
77998
+ return {
77999
+ totalXpEarned: newTotalXpEarned,
78000
+ newLevel: levelUpResult.newLevel,
78001
+ leveledUp: levelUpResult.leveledUp,
78002
+ creditsAwarded,
78003
+ xpToNextLevel: levelUpResult.xpToNextLevel
78004
+ };
78005
+ });
78006
+ return result;
78007
+ } catch (error2) {
78008
+ if (error2 instanceof ApiError) {
78009
+ throw error2;
78010
+ }
78011
+ logger2.error(`Error adding XP for user ${user.id}:`, error2);
78012
+ throw ApiError.internal("Internal server error", error2);
78013
+ }
78014
+ }
78015
+ async function addXPFromRequest(ctx) {
78016
+ let amount;
78017
+ try {
78018
+ const requestBody = await ctx.request.json();
78019
+ const parsed = AddXPSchema.parse(requestBody);
78020
+ amount = parsed.amount;
78021
+ } catch (error2) {
78022
+ if (error2 instanceof ZodError) {
78023
+ throw ApiError.badRequest(`Validation failed: ${formatValidationErrors(error2)}`);
78024
+ }
78025
+ logger2.error("Failed to parse request body:", error2);
78026
+ throw ApiError.badRequest("Invalid JSON body");
78027
+ }
78028
+ return await addXP(ctx, amount);
78029
+ }
78030
+ async function getAllLevelConfigs(_ctx) {
78031
+ try {
78032
+ const db = getDatabase();
78033
+ const configs = await db.select().from(levelConfigs).orderBy(levelConfigs.level);
78034
+ return configs;
78035
+ } catch (error2) {
78036
+ logger2.error("Error fetching all level configs:", error2);
78037
+ throw ApiError.internal("Internal server error", error2);
78038
+ }
78039
+ }
78040
+ async function getUserLevelProgress(ctx) {
78041
+ const userLevel = await getUserLevel(ctx);
78042
+ try {
78043
+ const db = getDatabase();
78044
+ const xpToNextLevel = await calculateXPToNextLevel(db, userLevel.currentLevel, userLevel.currentXp);
78045
+ return {
78046
+ level: userLevel.currentLevel,
78047
+ currentXp: userLevel.currentXp,
78048
+ xpToNextLevel,
78049
+ totalXpEarned: userLevel.totalXpEarned
78050
+ };
78051
+ } catch (error2) {
78052
+ if (error2 instanceof ApiError) {
78053
+ throw error2;
78054
+ }
78055
+ logger2.error(`Error fetching user level progress:`, error2);
78056
+ throw ApiError.internal("Internal server error", error2);
78057
+ }
78058
+ }
78059
+ async function getLevelConfigByPath(ctx) {
78060
+ const levelParam = ctx.params.level;
78061
+ if (!levelParam) {
78062
+ throw ApiError.badRequest("Level parameter is required");
78063
+ }
78064
+ const level = parseInt(levelParam, 10);
78065
+ if (isNaN(level) || level < 1) {
78066
+ throw ApiError.badRequest("Level must be a positive integer");
78067
+ }
78068
+ try {
78069
+ const db = getDatabase();
78070
+ return await getLevelConfig(db, level);
78071
+ } catch (error2) {
78072
+ if (error2 instanceof ApiError) {
78073
+ throw error2;
78074
+ }
78075
+ logger2.error(`Error fetching level config for level ${level}:`, error2);
78076
+ throw ApiError.internal("Internal server error", error2);
78077
+ }
78078
+ }
78079
+
77734
78080
  // src/routes/users.ts
77735
78081
  var usersRouter = new Hono2;
77736
78082
  usersRouter.get("/me", async (c2) => {
@@ -77752,18 +78098,66 @@ usersRouter.get("/me", async (c2) => {
77752
78098
  return c2.json({ error: message }, 500);
77753
78099
  }
77754
78100
  });
78101
+ usersRouter.get("/level", async (c2) => {
78102
+ const ctx = {
78103
+ user: c2.get("user"),
78104
+ params: {},
78105
+ url: new URL(c2.req.url),
78106
+ request: c2.req.raw
78107
+ };
78108
+ try {
78109
+ const levelData = await getUserLevel(ctx);
78110
+ return c2.json(levelData);
78111
+ } catch (error2) {
78112
+ if (error2 instanceof ApiError) {
78113
+ return c2.json({ error: error2.message }, error2.statusCode);
78114
+ }
78115
+ console.error("Error in getUserLevel:", error2);
78116
+ const message = error2 instanceof Error ? error2.message : "Internal server error";
78117
+ return c2.json({ error: message }, 500);
78118
+ }
78119
+ });
78120
+ usersRouter.get("/level/progress", async (c2) => {
78121
+ const ctx = {
78122
+ user: c2.get("user"),
78123
+ params: {},
78124
+ url: new URL(c2.req.url),
78125
+ request: c2.req.raw
78126
+ };
78127
+ try {
78128
+ const progressData = await getUserLevelProgress(ctx);
78129
+ return c2.json(progressData);
78130
+ } catch (error2) {
78131
+ if (error2 instanceof ApiError) {
78132
+ return c2.json({ error: error2.message }, error2.statusCode);
78133
+ }
78134
+ console.error("Error in getUserLevelProgress:", error2);
78135
+ const message = error2 instanceof Error ? error2.message : "Internal server error";
78136
+ return c2.json({ error: message }, 500);
78137
+ }
78138
+ });
78139
+ usersRouter.post("/xp/add", async (c2) => {
78140
+ const ctx = {
78141
+ user: c2.get("user"),
78142
+ params: {},
78143
+ url: new URL(c2.req.url),
78144
+ request: c2.req.raw
78145
+ };
78146
+ try {
78147
+ const result = await addXPFromRequest(ctx);
78148
+ return c2.json(result);
78149
+ } catch (error2) {
78150
+ if (error2 instanceof ApiError) {
78151
+ return c2.json({ error: error2.message }, error2.statusCode);
78152
+ }
78153
+ console.error("Error in addXPFromRequest:", error2);
78154
+ const message = error2 instanceof Error ? error2.message : "Internal server error";
78155
+ return c2.json({ error: message }, 500);
78156
+ }
78157
+ });
77755
78158
  // src/routes/health.ts
77756
78159
  var healthRouter = new Hono2;
77757
78160
  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
78161
  // ../api-core/src/inventory/index.ts
77768
78162
  async function getUserInventory(ctx) {
77769
78163
  const user = ctx.user;
@@ -81993,6 +82387,46 @@ devRouter.delete("/keys/:keyId", async (c2) => {
81993
82387
  return c2.json({ error: message2 }, 500);
81994
82388
  }
81995
82389
  });
82390
+ // src/routes/levels.ts
82391
+ var levelsRouter = new Hono2;
82392
+ levelsRouter.get("/config", async (c2) => {
82393
+ const ctx = {
82394
+ user: c2.get("user"),
82395
+ params: {},
82396
+ url: new URL(c2.req.url),
82397
+ request: c2.req.raw
82398
+ };
82399
+ try {
82400
+ const configs = await getAllLevelConfigs(ctx);
82401
+ return c2.json(configs);
82402
+ } catch (error2) {
82403
+ if (error2 instanceof ApiError) {
82404
+ return c2.json({ error: error2.message }, error2.statusCode);
82405
+ }
82406
+ console.error("Error in getAllLevelConfigs:", error2);
82407
+ const message2 = error2 instanceof Error ? error2.message : "Internal server error";
82408
+ return c2.json({ error: message2 }, 500);
82409
+ }
82410
+ });
82411
+ levelsRouter.get("/config/:level", async (c2) => {
82412
+ const ctx = {
82413
+ user: c2.get("user"),
82414
+ params: { level: c2.req.param("level") },
82415
+ url: new URL(c2.req.url),
82416
+ request: c2.req.raw
82417
+ };
82418
+ try {
82419
+ const config = await getLevelConfigByPath(ctx);
82420
+ return c2.json(config);
82421
+ } catch (error2) {
82422
+ if (error2 instanceof ApiError) {
82423
+ return c2.json({ error: error2.message }, error2.statusCode);
82424
+ }
82425
+ console.error("Error in getLevelConfigByPath:", error2);
82426
+ const message2 = error2 instanceof Error ? error2.message : "Internal server error";
82427
+ return c2.json({ error: message2 }, 500);
82428
+ }
82429
+ });
81996
82430
  // src/server.ts
81997
82431
  async function startServer(options) {
81998
82432
  const { port, verbose, project } = options;
@@ -82019,6 +82453,7 @@ async function startServer(options) {
82019
82453
  app.route("/api/maps", mapsRouter);
82020
82454
  app.route("/api/shop-listings", shopListingsRouter);
82021
82455
  app.route("/api/dev", devRouter);
82456
+ app.route("/api/levels", levelsRouter);
82022
82457
  return serve({ fetch: app.fetch, port });
82023
82458
  }
82024
82459
  var version3 = package_default.version;
@@ -82060,14 +82495,19 @@ async function findAvailablePort(startPort = 4321) {
82060
82495
 
82061
82496
  // src/cli.ts
82062
82497
  var program2 = new Command;
82063
- program2.name("playcademy-sandbox").description("Local development server for Playcademy game development").version("0.1.0").option("-p, --port <number>", "Port to run the server on", "4321").option("-v, --verbose", "Enable verbose logging", true).action(async (options) => {
82498
+ program2.name("playcademy-sandbox").description("Local development server for Playcademy game development").version("0.1.0").option("-p, --port <number>", "Port to run the server on", "4321").option("-v, --verbose", "Enable verbose logging", true).option("--project-name <name>", "Name of the current project").option("--project-slug <slug>", "Slug of the current project").action(async (options) => {
82064
82499
  try {
82065
82500
  const requestedPort = parseInt(options.port);
82066
82501
  const availablePort = await findAvailablePort(requestedPort);
82067
82502
  const serverOptions = {
82068
82503
  port: availablePort,
82069
82504
  seed: options.seed,
82070
- verbose: options.verbose
82505
+ verbose: options.verbose,
82506
+ project: options.projectName && options.projectSlug ? {
82507
+ slug: options.projectSlug,
82508
+ displayName: options.projectName,
82509
+ version: "1.0.0"
82510
+ } : undefined
82071
82511
  };
82072
82512
  const server = await startServer(serverOptions);
82073
82513
  console.log("");
@@ -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,12 +75609,60 @@ 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
+ totalXpEarned: integer("total_xp_earned").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
+ totalXpEarned: exports_external.number().int().min(0, "Total XP earned 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";
75604
75663
 
75605
75664
  class DatabasePathManager {
75606
- static DEFAULT_DB_SUBPATH = join2("@playcademy", "vite-plugin", ".playcademy", "sandbox.db");
75665
+ static DEFAULT_DB_SUBPATH = join2("@playcademy", "vite-plugin", "node_modules", ".playcademy", "sandbox.db");
75607
75666
  static findNodeModulesPath() {
75608
75667
  let currentDir = process.cwd();
75609
75668
  while (currentDir !== dirname(currentDir)) {
@@ -75677,12 +75736,35 @@ 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
+ const xpRequired = level === 1 ? 0 : Math.floor(50 * Math.pow(level - 1, 1.7));
75753
+ let creditsReward = 0;
75754
+ if (level > 1) {
75755
+ creditsReward = 25 + level * 25;
75756
+ if (level % 10 === 1 && level > 1) {
75757
+ creditsReward += 75;
75758
+ }
75759
+ }
75760
+ configs.push({
75761
+ level,
75762
+ xpRequired,
75763
+ creditsReward
75764
+ });
75765
+ }
75766
+ return configs;
75767
+ }
75686
75768
  async function seedCurrentProjectGame(db, project) {
75687
75769
  const now2 = new Date;
75688
75770
  try {
@@ -75718,7 +75800,7 @@ async function seedCurrentProjectGame(db, project) {
75718
75800
  // package.json
75719
75801
  var package_default = {
75720
75802
  name: "@playcademy/sandbox",
75721
- version: "0.1.0-beta.1",
75803
+ version: "0.1.0-beta.3",
75722
75804
  description: "Local development server for Playcademy game development",
75723
75805
  type: "module",
75724
75806
  exports: {
@@ -75747,8 +75829,6 @@ var package_default = {
75747
75829
  dependencies: {
75748
75830
  "@electric-sql/pglite": "^0.3.2",
75749
75831
  "@hono/node-server": "^1.14.2",
75750
- "@playcademy/api-core": "workspace:*",
75751
- "@playcademy/data": "workspace:*",
75752
75832
  commander: "^12.1.0",
75753
75833
  "drizzle-kit": "^0.31.0",
75754
75834
  "drizzle-orm": "^0.42.0",
@@ -75757,6 +75837,8 @@ var package_default = {
75757
75837
  picocolors: "^1.1.1"
75758
75838
  },
75759
75839
  devDependencies: {
75840
+ "@playcademy/api-core": "workspace:*",
75841
+ "@playcademy/data": "workspace:*",
75760
75842
  "@types/bun": "latest"
75761
75843
  },
75762
75844
  peerDependencies: {
@@ -75821,6 +75903,270 @@ async function getUserMe(ctx) {
75821
75903
  }
75822
75904
  }
75823
75905
 
75906
+ // ../api-core/src/utils/levels.ts
75907
+ var levelConfigCache = null;
75908
+ async function getLevelConfig(db, level) {
75909
+ if (level < 1) {
75910
+ throw ApiError.badRequest("Level must be at least 1");
75911
+ }
75912
+ if (levelConfigCache?.has(level)) {
75913
+ return levelConfigCache.get(level) || null;
75914
+ }
75915
+ try {
75916
+ const [config] = await db.select().from(levelConfigs).where(eq(levelConfigs.level, level)).limit(1);
75917
+ if (levelConfigCache && config) {
75918
+ levelConfigCache.set(level, config);
75919
+ }
75920
+ return config || null;
75921
+ } catch (error2) {
75922
+ logger2.error(`Error fetching level config for level ${level}:`, error2);
75923
+ throw ApiError.internal("Internal server error", error2);
75924
+ }
75925
+ }
75926
+ async function calculateXPToNextLevel(db, currentLevel, currentXp) {
75927
+ try {
75928
+ const nextLevelConfig = await getLevelConfig(db, currentLevel + 1);
75929
+ if (!nextLevelConfig) {
75930
+ return 0;
75931
+ }
75932
+ return Math.max(0, nextLevelConfig.xpRequired - currentXp);
75933
+ } catch (error2) {
75934
+ logger2.error(`Error calculating XP to next level:`, error2);
75935
+ throw ApiError.internal("Internal server error", error2);
75936
+ }
75937
+ }
75938
+ async function checkLevelUp(tx, currentLevel, newXp) {
75939
+ let level = currentLevel;
75940
+ let remainingXp = newXp;
75941
+ let totalCreditsAwarded = 0;
75942
+ let leveledUp = false;
75943
+ while (true) {
75944
+ if (level >= MAX_LEVEL) {
75945
+ break;
75946
+ }
75947
+ const nextLevelConfig2 = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
75948
+ const [nextLevel2] = nextLevelConfig2;
75949
+ if (!nextLevel2) {
75950
+ break;
75951
+ }
75952
+ if (remainingXp >= nextLevel2.xpRequired) {
75953
+ level += 1;
75954
+ remainingXp -= nextLevel2.xpRequired;
75955
+ totalCreditsAwarded += nextLevel2.creditsReward;
75956
+ leveledUp = true;
75957
+ } else {
75958
+ break;
75959
+ }
75960
+ }
75961
+ const nextLevelConfig = await tx.select().from(levelConfigs).where(eq(levelConfigs.level, level + 1)).limit(1);
75962
+ const [nextLevel] = nextLevelConfig;
75963
+ const xpToNextLevel = nextLevel ? Math.max(0, nextLevel.xpRequired - remainingXp) : 0;
75964
+ return {
75965
+ newLevel: level,
75966
+ remainingXp,
75967
+ leveledUp,
75968
+ creditsAwarded: totalCreditsAwarded,
75969
+ xpToNextLevel
75970
+ };
75971
+ }
75972
+
75973
+ // ../api-core/src/utils/validation.ts
75974
+ function formatValidationErrors(error2) {
75975
+ const flattened = error2.flatten();
75976
+ const fieldErrors = Object.entries(flattened.fieldErrors).map(([field, errors2]) => `${field}: ${errors2?.join(", ") || "Invalid"}`).join("; ");
75977
+ const formErrors = flattened.formErrors.join("; ");
75978
+ const allErrors = [fieldErrors, formErrors].filter(Boolean).join("; ");
75979
+ return allErrors || "Validation failed";
75980
+ }
75981
+
75982
+ // ../api-core/src/levels/index.ts
75983
+ var AddXPSchema = XPActionInputSchema;
75984
+ async function getUserLevel(ctx) {
75985
+ const user = ctx.user;
75986
+ if (!user) {
75987
+ throw ApiError.unauthorized("Valid session or bearer token required");
75988
+ }
75989
+ try {
75990
+ const db = getDatabase();
75991
+ let [userLevel] = await db.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
75992
+ if (!userLevel) {
75993
+ const [newUserLevel] = await db.insert(userLevels).values({
75994
+ userId: user.id,
75995
+ currentLevel: 1,
75996
+ currentXp: 0,
75997
+ totalXpEarned: 0
75998
+ }).returning();
75999
+ if (!newUserLevel) {
76000
+ throw ApiError.internal("Failed to create user level record");
76001
+ }
76002
+ userLevel = newUserLevel;
76003
+ }
76004
+ return userLevel;
76005
+ } catch (error2) {
76006
+ if (error2 instanceof ApiError) {
76007
+ throw error2;
76008
+ }
76009
+ logger2.error(`Error fetching user level for user ${user.id}:`, error2);
76010
+ throw ApiError.internal("Internal server error", error2);
76011
+ }
76012
+ }
76013
+ async function addXP(ctx, amount) {
76014
+ const user = ctx.user;
76015
+ if (!user) {
76016
+ throw ApiError.unauthorized("Valid session or bearer token required");
76017
+ }
76018
+ if (amount <= 0) {
76019
+ throw ApiError.badRequest("XP amount must be positive");
76020
+ }
76021
+ try {
76022
+ const db = getDatabase();
76023
+ const result = await db.transaction(async (tx) => {
76024
+ let [userLevel] = await tx.select().from(userLevels).where(eq(userLevels.userId, user.id)).limit(1);
76025
+ if (!userLevel) {
76026
+ const [newUserLevel] = await tx.insert(userLevels).values({
76027
+ userId: user.id,
76028
+ currentLevel: 1,
76029
+ currentXp: 0,
76030
+ totalXpEarned: 0
76031
+ }).returning();
76032
+ if (!newUserLevel) {
76033
+ throw ApiError.internal("Failed to create user level record");
76034
+ }
76035
+ userLevel = newUserLevel;
76036
+ }
76037
+ const newCurrentXp = userLevel.currentXp + amount;
76038
+ const newTotalXpEarned = userLevel.totalXpEarned + amount;
76039
+ const levelUpResult = await checkLevelUp(tx, userLevel.currentLevel, newCurrentXp);
76040
+ const [updatedUserLevel] = await tx.update(userLevels).set({
76041
+ currentLevel: levelUpResult.newLevel,
76042
+ currentXp: levelUpResult.remainingXp,
76043
+ totalXpEarned: newTotalXpEarned,
76044
+ lastLevelUpAt: levelUpResult.leveledUp ? new Date : userLevel.lastLevelUpAt
76045
+ }).where(eq(userLevels.userId, user.id)).returning();
76046
+ if (!updatedUserLevel) {
76047
+ throw ApiError.internal("Failed to update user level");
76048
+ }
76049
+ let creditsAwarded = 0;
76050
+ if (levelUpResult.leveledUp) {
76051
+ const creditsToAward = levelUpResult.creditsAwarded;
76052
+ logger2.debug("User leveled up", {
76053
+ userId: user.id,
76054
+ oldLevel: userLevel.currentLevel,
76055
+ newLevel: levelUpResult.newLevel,
76056
+ xpAdded: amount,
76057
+ totalXpEarned: newTotalXpEarned,
76058
+ creditsAwarded: creditsToAward
76059
+ });
76060
+ if (creditsToAward > 0) {
76061
+ const [creditsItem] = await tx.select({ id: items.id }).from(items).where(eq(items.internalName, "PLAYCADEMY_CREDITS")).limit(1);
76062
+ if (!creditsItem) {
76063
+ throw ApiError.internal("PLAYCADEMY_CREDITS item not found");
76064
+ }
76065
+ await tx.insert(inventoryItems).values({
76066
+ userId: user.id,
76067
+ itemId: creditsItem.id,
76068
+ quantity: creditsToAward
76069
+ }).onConflictDoUpdate({
76070
+ target: [
76071
+ inventoryItems.userId,
76072
+ inventoryItems.itemId
76073
+ ],
76074
+ set: {
76075
+ quantity: sql`${inventoryItems.quantity} + ${creditsToAward}`
76076
+ }
76077
+ });
76078
+ }
76079
+ creditsAwarded = creditsToAward;
76080
+ } else {
76081
+ logger2.debug("XP added to user", {
76082
+ userId: user.id,
76083
+ level: userLevel.currentLevel,
76084
+ xpAdded: amount,
76085
+ totalXpEarned: newTotalXpEarned
76086
+ });
76087
+ }
76088
+ return {
76089
+ totalXpEarned: newTotalXpEarned,
76090
+ newLevel: levelUpResult.newLevel,
76091
+ leveledUp: levelUpResult.leveledUp,
76092
+ creditsAwarded,
76093
+ xpToNextLevel: levelUpResult.xpToNextLevel
76094
+ };
76095
+ });
76096
+ return result;
76097
+ } catch (error2) {
76098
+ if (error2 instanceof ApiError) {
76099
+ throw error2;
76100
+ }
76101
+ logger2.error(`Error adding XP for user ${user.id}:`, error2);
76102
+ throw ApiError.internal("Internal server error", error2);
76103
+ }
76104
+ }
76105
+ async function addXPFromRequest(ctx) {
76106
+ let amount;
76107
+ try {
76108
+ const requestBody = await ctx.request.json();
76109
+ const parsed = AddXPSchema.parse(requestBody);
76110
+ amount = parsed.amount;
76111
+ } catch (error2) {
76112
+ if (error2 instanceof ZodError) {
76113
+ throw ApiError.badRequest(`Validation failed: ${formatValidationErrors(error2)}`);
76114
+ }
76115
+ logger2.error("Failed to parse request body:", error2);
76116
+ throw ApiError.badRequest("Invalid JSON body");
76117
+ }
76118
+ return await addXP(ctx, amount);
76119
+ }
76120
+ async function getAllLevelConfigs(_ctx) {
76121
+ try {
76122
+ const db = getDatabase();
76123
+ const configs = await db.select().from(levelConfigs).orderBy(levelConfigs.level);
76124
+ return configs;
76125
+ } catch (error2) {
76126
+ logger2.error("Error fetching all level configs:", error2);
76127
+ throw ApiError.internal("Internal server error", error2);
76128
+ }
76129
+ }
76130
+ async function getUserLevelProgress(ctx) {
76131
+ const userLevel = await getUserLevel(ctx);
76132
+ try {
76133
+ const db = getDatabase();
76134
+ const xpToNextLevel = await calculateXPToNextLevel(db, userLevel.currentLevel, userLevel.currentXp);
76135
+ return {
76136
+ level: userLevel.currentLevel,
76137
+ currentXp: userLevel.currentXp,
76138
+ xpToNextLevel,
76139
+ totalXpEarned: userLevel.totalXpEarned
76140
+ };
76141
+ } catch (error2) {
76142
+ if (error2 instanceof ApiError) {
76143
+ throw error2;
76144
+ }
76145
+ logger2.error(`Error fetching user level progress:`, error2);
76146
+ throw ApiError.internal("Internal server error", error2);
76147
+ }
76148
+ }
76149
+ async function getLevelConfigByPath(ctx) {
76150
+ const levelParam = ctx.params.level;
76151
+ if (!levelParam) {
76152
+ throw ApiError.badRequest("Level parameter is required");
76153
+ }
76154
+ const level = parseInt(levelParam, 10);
76155
+ if (isNaN(level) || level < 1) {
76156
+ throw ApiError.badRequest("Level must be a positive integer");
76157
+ }
76158
+ try {
76159
+ const db = getDatabase();
76160
+ return await getLevelConfig(db, level);
76161
+ } catch (error2) {
76162
+ if (error2 instanceof ApiError) {
76163
+ throw error2;
76164
+ }
76165
+ logger2.error(`Error fetching level config for level ${level}:`, error2);
76166
+ throw ApiError.internal("Internal server error", error2);
76167
+ }
76168
+ }
76169
+
75824
76170
  // src/routes/users.ts
75825
76171
  var usersRouter = new Hono2;
75826
76172
  usersRouter.get("/me", async (c2) => {
@@ -75842,18 +76188,66 @@ usersRouter.get("/me", async (c2) => {
75842
76188
  return c2.json({ error: message }, 500);
75843
76189
  }
75844
76190
  });
76191
+ usersRouter.get("/level", async (c2) => {
76192
+ const ctx = {
76193
+ user: c2.get("user"),
76194
+ params: {},
76195
+ url: new URL(c2.req.url),
76196
+ request: c2.req.raw
76197
+ };
76198
+ try {
76199
+ const levelData = await getUserLevel(ctx);
76200
+ return c2.json(levelData);
76201
+ } catch (error2) {
76202
+ if (error2 instanceof ApiError) {
76203
+ return c2.json({ error: error2.message }, error2.statusCode);
76204
+ }
76205
+ console.error("Error in getUserLevel:", error2);
76206
+ const message = error2 instanceof Error ? error2.message : "Internal server error";
76207
+ return c2.json({ error: message }, 500);
76208
+ }
76209
+ });
76210
+ usersRouter.get("/level/progress", async (c2) => {
76211
+ const ctx = {
76212
+ user: c2.get("user"),
76213
+ params: {},
76214
+ url: new URL(c2.req.url),
76215
+ request: c2.req.raw
76216
+ };
76217
+ try {
76218
+ const progressData = await getUserLevelProgress(ctx);
76219
+ return c2.json(progressData);
76220
+ } catch (error2) {
76221
+ if (error2 instanceof ApiError) {
76222
+ return c2.json({ error: error2.message }, error2.statusCode);
76223
+ }
76224
+ console.error("Error in getUserLevelProgress:", error2);
76225
+ const message = error2 instanceof Error ? error2.message : "Internal server error";
76226
+ return c2.json({ error: message }, 500);
76227
+ }
76228
+ });
76229
+ usersRouter.post("/xp/add", async (c2) => {
76230
+ const ctx = {
76231
+ user: c2.get("user"),
76232
+ params: {},
76233
+ url: new URL(c2.req.url),
76234
+ request: c2.req.raw
76235
+ };
76236
+ try {
76237
+ const result = await addXPFromRequest(ctx);
76238
+ return c2.json(result);
76239
+ } catch (error2) {
76240
+ if (error2 instanceof ApiError) {
76241
+ return c2.json({ error: error2.message }, error2.statusCode);
76242
+ }
76243
+ console.error("Error in addXPFromRequest:", error2);
76244
+ const message = error2 instanceof Error ? error2.message : "Internal server error";
76245
+ return c2.json({ error: message }, 500);
76246
+ }
76247
+ });
75845
76248
  // src/routes/health.ts
75846
76249
  var healthRouter = new Hono2;
75847
76250
  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
76251
  // ../api-core/src/inventory/index.ts
75858
76252
  async function getUserInventory(ctx) {
75859
76253
  const user = ctx.user;
@@ -80083,6 +80477,46 @@ devRouter.delete("/keys/:keyId", async (c2) => {
80083
80477
  return c2.json({ error: message2 }, 500);
80084
80478
  }
80085
80479
  });
80480
+ // src/routes/levels.ts
80481
+ var levelsRouter = new Hono2;
80482
+ levelsRouter.get("/config", async (c2) => {
80483
+ const ctx = {
80484
+ user: c2.get("user"),
80485
+ params: {},
80486
+ url: new URL(c2.req.url),
80487
+ request: c2.req.raw
80488
+ };
80489
+ try {
80490
+ const configs = await getAllLevelConfigs(ctx);
80491
+ return c2.json(configs);
80492
+ } catch (error2) {
80493
+ if (error2 instanceof ApiError) {
80494
+ return c2.json({ error: error2.message }, error2.statusCode);
80495
+ }
80496
+ console.error("Error in getAllLevelConfigs:", error2);
80497
+ const message2 = error2 instanceof Error ? error2.message : "Internal server error";
80498
+ return c2.json({ error: message2 }, 500);
80499
+ }
80500
+ });
80501
+ levelsRouter.get("/config/:level", async (c2) => {
80502
+ const ctx = {
80503
+ user: c2.get("user"),
80504
+ params: { level: c2.req.param("level") },
80505
+ url: new URL(c2.req.url),
80506
+ request: c2.req.raw
80507
+ };
80508
+ try {
80509
+ const config = await getLevelConfigByPath(ctx);
80510
+ return c2.json(config);
80511
+ } catch (error2) {
80512
+ if (error2 instanceof ApiError) {
80513
+ return c2.json({ error: error2.message }, error2.statusCode);
80514
+ }
80515
+ console.error("Error in getLevelConfigByPath:", error2);
80516
+ const message2 = error2 instanceof Error ? error2.message : "Internal server error";
80517
+ return c2.json({ error: message2 }, 500);
80518
+ }
80519
+ });
80086
80520
  // src/server.ts
80087
80521
  async function startServer(options) {
80088
80522
  const { port, verbose, project } = options;
@@ -80109,6 +80543,7 @@ async function startServer(options) {
80109
80543
  app.route("/api/maps", mapsRouter);
80110
80544
  app.route("/api/shop-listings", shopListingsRouter);
80111
80545
  app.route("/api/dev", devRouter);
80546
+ app.route("/api/levels", levelsRouter);
80112
80547
  return serve({ fetch: app.fetch, port });
80113
80548
  }
80114
80549
  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.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,8 +29,6 @@
29
29
  "dependencies": {
30
30
  "@electric-sql/pglite": "^0.3.2",
31
31
  "@hono/node-server": "^1.14.2",
32
- "@playcademy/api-core": "0.1.0",
33
- "@playcademy/data": "0.0.1",
34
32
  "commander": "^12.1.0",
35
33
  "drizzle-kit": "^0.31.0",
36
34
  "drizzle-orm": "^0.42.0",
@@ -39,6 +37,8 @@
39
37
  "picocolors": "^1.1.1"
40
38
  },
41
39
  "devDependencies": {
40
+ "@playcademy/api-core": "0.1.0",
41
+ "@playcademy/data": "0.0.1",
42
42
  "@types/bun": "latest"
43
43
  },
44
44
  "peerDependencies": {