@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 +460 -20
- package/dist/constants.d.ts +3 -14
- package/dist/database/seed.d.ts +10 -5
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/levels.d.ts +3 -0
- package/dist/server.js +453 -18
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -13024,7 +13024,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
13024
13024
|
}
|
|
13025
13025
|
};
|
|
13026
13026
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
13027
|
-
this.debug("%s
|
|
13027
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
13028
13028
|
if (escaping) {
|
|
13029
13029
|
if (c2 === "/") {
|
|
13030
13030
|
return false;
|
|
@@ -21547,7 +21547,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
|
|
|
21547
21547
|
IS_LINE_JUNK = function(line2, pat = /^\s*#?\s*$/) {
|
|
21548
21548
|
return pat.test(line2);
|
|
21549
21549
|
};
|
|
21550
|
-
IS_CHARACTER_JUNK = function(ch, ws = "
|
|
21550
|
+
IS_CHARACTER_JUNK = function(ch, ws = " ") {
|
|
21551
21551
|
return indexOf.call(ws, ch) >= 0;
|
|
21552
21552
|
};
|
|
21553
21553
|
_formatRangeUnified = function(start2, stop2) {
|
|
@@ -31137,7 +31137,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
31137
31137
|
}
|
|
31138
31138
|
};
|
|
31139
31139
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
31140
|
-
this.debug("%s
|
|
31140
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
31141
31141
|
if (escaping) {
|
|
31142
31142
|
if (c2 === "/") {
|
|
31143
31143
|
return false;
|
|
@@ -52844,8 +52844,8 @@ var DEMO_USER = {
|
|
|
52844
52844
|
email: "demo@playcademy.com",
|
|
52845
52845
|
emailVerified: true,
|
|
52846
52846
|
image: null,
|
|
52847
|
-
role: "
|
|
52848
|
-
developerStatus: "
|
|
52847
|
+
role: "developer",
|
|
52848
|
+
developerStatus: "approved",
|
|
52849
52849
|
createdAt: now,
|
|
52850
52850
|
updatedAt: now
|
|
52851
52851
|
};
|
|
@@ -72673,6 +72673,8 @@ __export(exports_schemas, {
|
|
|
72673
72673
|
verification: () => verification,
|
|
72674
72674
|
users: () => users,
|
|
72675
72675
|
userRoleEnum: () => userRoleEnum,
|
|
72676
|
+
userLevelsRelations: () => userLevelsRelations,
|
|
72677
|
+
userLevels: () => userLevels,
|
|
72676
72678
|
shopListingsRelations: () => shopListingsRelations,
|
|
72677
72679
|
shopListings: () => shopListings,
|
|
72678
72680
|
sessions: () => sessions,
|
|
@@ -72680,6 +72682,7 @@ __export(exports_schemas, {
|
|
|
72680
72682
|
maps: () => maps,
|
|
72681
72683
|
mapElementsRelations: () => mapElementsRelations,
|
|
72682
72684
|
mapElements: () => mapElements,
|
|
72685
|
+
levelConfigs: () => levelConfigs,
|
|
72683
72686
|
itemsRelations: () => itemsRelations,
|
|
72684
72687
|
items: () => items,
|
|
72685
72688
|
itemTypeEnum: () => itemTypeEnum,
|
|
@@ -72696,11 +72699,14 @@ __export(exports_schemas, {
|
|
|
72696
72699
|
currenciesRelations: () => currenciesRelations,
|
|
72697
72700
|
currencies: () => currencies,
|
|
72698
72701
|
accounts: () => accounts,
|
|
72702
|
+
XPActionInputSchema: () => XPActionInputSchema,
|
|
72699
72703
|
VersionSchema: () => VersionSchema,
|
|
72700
72704
|
UpsertGameMetadataSchema: () => UpsertGameMetadataSchema,
|
|
72701
72705
|
UpdateUserSchema: () => UpdateUserSchema,
|
|
72706
|
+
UpdateUserLevelSchema: () => UpdateUserLevelSchema,
|
|
72702
72707
|
UpdateShopListingSchema: () => UpdateShopListingSchema,
|
|
72703
72708
|
UpdateMapElementSchema: () => UpdateMapElementSchema,
|
|
72709
|
+
UpdateLevelConfigSchema: () => UpdateLevelConfigSchema,
|
|
72704
72710
|
UpdateItemSchema: () => UpdateItemSchema,
|
|
72705
72711
|
UpdateInventoryItemSchema: () => UpdateInventoryItemSchema,
|
|
72706
72712
|
UpdateGameStateSchema: () => UpdateGameStateSchema,
|
|
@@ -72711,10 +72717,12 @@ __export(exports_schemas, {
|
|
|
72711
72717
|
StartSessionInputSchema: () => StartSessionInputSchema,
|
|
72712
72718
|
SelectVerificationSchema: () => SelectVerificationSchema,
|
|
72713
72719
|
SelectUserSchema: () => SelectUserSchema,
|
|
72720
|
+
SelectUserLevelSchema: () => SelectUserLevelSchema,
|
|
72714
72721
|
SelectShopListingSchema: () => SelectShopListingSchema,
|
|
72715
72722
|
SelectSessionSchema: () => SelectSessionSchema,
|
|
72716
72723
|
SelectMapSchema: () => SelectMapSchema,
|
|
72717
72724
|
SelectMapElementSchema: () => SelectMapElementSchema,
|
|
72725
|
+
SelectLevelConfigSchema: () => SelectLevelConfigSchema,
|
|
72718
72726
|
SelectItemSchema: () => SelectItemSchema,
|
|
72719
72727
|
SelectInventoryItemSchema: () => SelectInventoryItemSchema,
|
|
72720
72728
|
SelectGameStateSchema: () => SelectGameStateSchema,
|
|
@@ -72726,13 +72734,16 @@ __export(exports_schemas, {
|
|
|
72726
72734
|
ProcessZipSchema: () => ProcessZipSchema,
|
|
72727
72735
|
MapElementMetadataZodSchema: () => MapElementMetadataZodSchema,
|
|
72728
72736
|
ManifestV1Schema: () => ManifestV1Schema,
|
|
72737
|
+
MAX_LEVEL: () => MAX_LEVEL,
|
|
72729
72738
|
ItemMetadataSchema: () => ItemMetadataSchema,
|
|
72730
72739
|
InventoryActionInputSchema: () => InventoryActionInputSchema,
|
|
72731
72740
|
InsertVerificationSchema: () => InsertVerificationSchema,
|
|
72732
72741
|
InsertUserSchema: () => InsertUserSchema,
|
|
72742
|
+
InsertUserLevelSchema: () => InsertUserLevelSchema,
|
|
72733
72743
|
InsertShopListingSchema: () => InsertShopListingSchema,
|
|
72734
72744
|
InsertSessionSchema: () => InsertSessionSchema,
|
|
72735
72745
|
InsertMapElementSchema: () => InsertMapElementSchema,
|
|
72746
|
+
InsertLevelConfigSchema: () => InsertLevelConfigSchema,
|
|
72736
72747
|
InsertItemSchema: () => InsertItemSchema,
|
|
72737
72748
|
InsertInventoryItemSchema: () => InsertInventoryItemSchema,
|
|
72738
72749
|
InsertGameStateSchema: () => InsertGameStateSchema,
|
|
@@ -77508,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.
|
|
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("");
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
import type { Item } from '@playcademy/data/schemas';
|
|
2
|
-
export declare const DEMO_USER:
|
|
3
|
-
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
4
|
-
name: string;
|
|
5
|
-
username: string;
|
|
6
|
-
email: string;
|
|
7
|
-
emailVerified: boolean;
|
|
8
|
-
image: null;
|
|
9
|
-
role: "player";
|
|
10
|
-
developerStatus: "none";
|
|
11
|
-
createdAt: Date;
|
|
12
|
-
updatedAt: Date;
|
|
13
|
-
};
|
|
1
|
+
import type { Item, User } from '@playcademy/data/schemas';
|
|
2
|
+
export declare const DEMO_USER: User;
|
|
14
3
|
export declare const DEMO_TOKEN = "demo-token";
|
|
15
4
|
export declare const PLAYCADEMY_CREDITS_ID: `${string}-${string}-${string}-${string}-${string}`;
|
|
16
5
|
export declare const SAMPLE_ITEMS: Omit<Item, 'createdAt'>[];
|
|
17
6
|
export declare const SAMPLE_INVENTORY: {
|
|
18
7
|
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
19
|
-
userId:
|
|
8
|
+
userId: string;
|
|
20
9
|
itemId: `${string}-${string}-${string}-${string}-${string}`;
|
|
21
10
|
quantity: number;
|
|
22
11
|
}[];
|
package/dist/database/seed.d.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { setupDatabase } from '.';
|
|
2
2
|
import type { ProjectInfo } from '../types';
|
|
3
3
|
export declare function seedDemoData(db: Awaited<ReturnType<typeof setupDatabase>>): Promise<{
|
|
4
|
-
id:
|
|
4
|
+
id: string;
|
|
5
5
|
name: string;
|
|
6
|
-
username: string;
|
|
6
|
+
username: string | null;
|
|
7
7
|
email: string;
|
|
8
8
|
emailVerified: boolean;
|
|
9
|
-
image: null;
|
|
10
|
-
role: "player";
|
|
11
|
-
developerStatus: "none";
|
|
9
|
+
image: string | null;
|
|
10
|
+
role: "admin" | "player" | "developer";
|
|
11
|
+
developerStatus: "none" | "pending" | "approved";
|
|
12
12
|
createdAt: Date;
|
|
13
13
|
updatedAt: Date;
|
|
14
14
|
}>;
|
|
15
|
+
export declare function generateLevelConfigs(): {
|
|
16
|
+
level: number;
|
|
17
|
+
xpRequired: number;
|
|
18
|
+
creditsReward: number;
|
|
19
|
+
}[];
|
|
15
20
|
export declare function seedCurrentProjectGame(db: Awaited<ReturnType<typeof setupDatabase>>, project: ProjectInfo): Promise<{
|
|
16
21
|
id: string;
|
|
17
22
|
createdAt: Date | null;
|
package/dist/routes/index.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -13023,7 +13023,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
13023
13023
|
}
|
|
13024
13024
|
};
|
|
13025
13025
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
13026
|
-
this.debug("%s
|
|
13026
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
13027
13027
|
if (escaping) {
|
|
13028
13028
|
if (c2 === "/") {
|
|
13029
13029
|
return false;
|
|
@@ -21546,7 +21546,7 @@ Is ${source_default.bold.blue(this.base.name)} schema created or renamed from an
|
|
|
21546
21546
|
IS_LINE_JUNK = function(line2, pat = /^\s*#?\s*$/) {
|
|
21547
21547
|
return pat.test(line2);
|
|
21548
21548
|
};
|
|
21549
|
-
IS_CHARACTER_JUNK = function(ch, ws = "
|
|
21549
|
+
IS_CHARACTER_JUNK = function(ch, ws = " ") {
|
|
21550
21550
|
return indexOf.call(ws, ch) >= 0;
|
|
21551
21551
|
};
|
|
21552
21552
|
_formatRangeUnified = function(start2, stop2) {
|
|
@@ -31136,7 +31136,7 @@ globstar while`, file, fr, pattern, pr3, swallowee);
|
|
|
31136
31136
|
}
|
|
31137
31137
|
};
|
|
31138
31138
|
for (let i3 = 0, c2;i3 < pattern.length && (c2 = pattern.charAt(i3)); i3++) {
|
|
31139
|
-
this.debug("%s
|
|
31139
|
+
this.debug("%s %s %s %j", pattern, i3, re3, c2);
|
|
31140
31140
|
if (escaping) {
|
|
31141
31141
|
if (c2 === "/") {
|
|
31142
31142
|
return false;
|
|
@@ -50934,8 +50934,8 @@ var DEMO_USER = {
|
|
|
50934
50934
|
email: "demo@playcademy.com",
|
|
50935
50935
|
emailVerified: true,
|
|
50936
50936
|
image: null,
|
|
50937
|
-
role: "
|
|
50938
|
-
developerStatus: "
|
|
50937
|
+
role: "developer",
|
|
50938
|
+
developerStatus: "approved",
|
|
50939
50939
|
createdAt: now,
|
|
50940
50940
|
updatedAt: now
|
|
50941
50941
|
};
|
|
@@ -70763,6 +70763,8 @@ __export(exports_schemas, {
|
|
|
70763
70763
|
verification: () => verification,
|
|
70764
70764
|
users: () => users,
|
|
70765
70765
|
userRoleEnum: () => userRoleEnum,
|
|
70766
|
+
userLevelsRelations: () => userLevelsRelations,
|
|
70767
|
+
userLevels: () => userLevels,
|
|
70766
70768
|
shopListingsRelations: () => shopListingsRelations,
|
|
70767
70769
|
shopListings: () => shopListings,
|
|
70768
70770
|
sessions: () => sessions,
|
|
@@ -70770,6 +70772,7 @@ __export(exports_schemas, {
|
|
|
70770
70772
|
maps: () => maps,
|
|
70771
70773
|
mapElementsRelations: () => mapElementsRelations,
|
|
70772
70774
|
mapElements: () => mapElements,
|
|
70775
|
+
levelConfigs: () => levelConfigs,
|
|
70773
70776
|
itemsRelations: () => itemsRelations,
|
|
70774
70777
|
items: () => items,
|
|
70775
70778
|
itemTypeEnum: () => itemTypeEnum,
|
|
@@ -70786,11 +70789,14 @@ __export(exports_schemas, {
|
|
|
70786
70789
|
currenciesRelations: () => currenciesRelations,
|
|
70787
70790
|
currencies: () => currencies,
|
|
70788
70791
|
accounts: () => accounts,
|
|
70792
|
+
XPActionInputSchema: () => XPActionInputSchema,
|
|
70789
70793
|
VersionSchema: () => VersionSchema,
|
|
70790
70794
|
UpsertGameMetadataSchema: () => UpsertGameMetadataSchema,
|
|
70791
70795
|
UpdateUserSchema: () => UpdateUserSchema,
|
|
70796
|
+
UpdateUserLevelSchema: () => UpdateUserLevelSchema,
|
|
70792
70797
|
UpdateShopListingSchema: () => UpdateShopListingSchema,
|
|
70793
70798
|
UpdateMapElementSchema: () => UpdateMapElementSchema,
|
|
70799
|
+
UpdateLevelConfigSchema: () => UpdateLevelConfigSchema,
|
|
70794
70800
|
UpdateItemSchema: () => UpdateItemSchema,
|
|
70795
70801
|
UpdateInventoryItemSchema: () => UpdateInventoryItemSchema,
|
|
70796
70802
|
UpdateGameStateSchema: () => UpdateGameStateSchema,
|
|
@@ -70801,10 +70807,12 @@ __export(exports_schemas, {
|
|
|
70801
70807
|
StartSessionInputSchema: () => StartSessionInputSchema,
|
|
70802
70808
|
SelectVerificationSchema: () => SelectVerificationSchema,
|
|
70803
70809
|
SelectUserSchema: () => SelectUserSchema,
|
|
70810
|
+
SelectUserLevelSchema: () => SelectUserLevelSchema,
|
|
70804
70811
|
SelectShopListingSchema: () => SelectShopListingSchema,
|
|
70805
70812
|
SelectSessionSchema: () => SelectSessionSchema,
|
|
70806
70813
|
SelectMapSchema: () => SelectMapSchema,
|
|
70807
70814
|
SelectMapElementSchema: () => SelectMapElementSchema,
|
|
70815
|
+
SelectLevelConfigSchema: () => SelectLevelConfigSchema,
|
|
70808
70816
|
SelectItemSchema: () => SelectItemSchema,
|
|
70809
70817
|
SelectInventoryItemSchema: () => SelectInventoryItemSchema,
|
|
70810
70818
|
SelectGameStateSchema: () => SelectGameStateSchema,
|
|
@@ -70816,13 +70824,16 @@ __export(exports_schemas, {
|
|
|
70816
70824
|
ProcessZipSchema: () => ProcessZipSchema,
|
|
70817
70825
|
MapElementMetadataZodSchema: () => MapElementMetadataZodSchema,
|
|
70818
70826
|
ManifestV1Schema: () => ManifestV1Schema,
|
|
70827
|
+
MAX_LEVEL: () => MAX_LEVEL,
|
|
70819
70828
|
ItemMetadataSchema: () => ItemMetadataSchema,
|
|
70820
70829
|
InventoryActionInputSchema: () => InventoryActionInputSchema,
|
|
70821
70830
|
InsertVerificationSchema: () => InsertVerificationSchema,
|
|
70822
70831
|
InsertUserSchema: () => InsertUserSchema,
|
|
70832
|
+
InsertUserLevelSchema: () => InsertUserLevelSchema,
|
|
70823
70833
|
InsertShopListingSchema: () => InsertShopListingSchema,
|
|
70824
70834
|
InsertSessionSchema: () => InsertSessionSchema,
|
|
70825
70835
|
InsertMapElementSchema: () => InsertMapElementSchema,
|
|
70836
|
+
InsertLevelConfigSchema: () => InsertLevelConfigSchema,
|
|
70826
70837
|
InsertItemSchema: () => InsertItemSchema,
|
|
70827
70838
|
InsertInventoryItemSchema: () => InsertInventoryItemSchema,
|
|
70828
70839
|
InsertGameStateSchema: () => InsertGameStateSchema,
|
|
@@ -75598,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.
|
|
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.
|
|
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": {
|