Currency, storefronts, purchasing (#12)

And a few other minor things, but primarily, the balance table exists and also consumable/inventory table.
This commit is contained in:
devin
2026-07-15 14:10:49 -04:00
committed by GitHub
parent 8314e54439
commit c33919bc67
29 changed files with 2204 additions and 524 deletions
+57 -2
View File
@@ -55,6 +55,26 @@ interface RoomRole {
InvitedRole: number
}
/**
* Room roles that confer owner-level management of a room: Creator (255) and
* CoOwner (30). The reference gates its room-admin actions on this set. (Host and
* Moderator are lower tiers and are deliberately excluded.)
*/
const MANAGE_ROLES: ReadonlySet<number> = new Set([Role.Creator, Role.CoOwner])
/**
* Whether an account may manage a room — its creator, or the holder of a
* Creator/CoOwner role on the room's `Roles`. This is the owner-or-co-owner gate
* the reference applies to room-admin actions (editing room data, viewing a room's
* live instances). Shared so the `rooms` and `match` workers apply the same check
* rather than each re-deriving the role set.
*/
export function canManageRoom(room: Room, accountId: number): boolean {
if (room.CreatorAccountId === accountId) return true
const roles = Array.isArray(room.Roles) ? (room.Roles as RoomRole[]) : []
return roles.some((r) => r.AccountId === accountId && MANAGE_ROLES.has(r.Role))
}
/**
* Clone an existing room into a new one owned by `accountId`. Copies the source
* room's content (scene/subrooms/settings), assigning a fresh RoomId, the given
@@ -86,7 +106,7 @@ export async function cloneRoom(
// any co-owners, e.g. the seeded base-room roles for accounts 1/2) must NOT
// carry over, or the clone would still list the template's owner as owner.
const roles: RoomRole[] = [
{ AccountId: accountId, Role: Role.Owner, LastChangedByAccountId: null, InvitedRole: 0 },
{ AccountId: accountId, Role: Role.Creator, LastChangedByAccountId: null, InvitedRole: 0 },
]
const cloned: Room = {
@@ -132,6 +152,41 @@ export async function setRoomImage(db: D1Database, roomId: number, imageName: st
.run()
}
/**
* Set a target account's room `Role` — updating their existing `Roles` entry or
* appending a new one — and stamp `LastChangedByAccountId` with the editor. The
* caller supplies the already-loaded room (after its owner/co-owner check) to avoid
* a re-read; the whole room JSON is rewritten. Returns the updated room.
*/
export async function setRoomRole(
db: D1Database,
roomId: number,
targetAccountId: number,
role: number,
changedByAccountId: number,
room: Room
): Promise<Room> {
const roles = Array.isArray(room.Roles) ? (room.Roles as RoomRole[]) : []
const existing = roles.find((r) => r.AccountId === targetAccountId)
if (existing) {
existing.Role = role
existing.LastChangedByAccountId = changedByAccountId
} else {
roles.push({
AccountId: targetAccountId,
Role: role,
LastChangedByAccountId: changedByAccountId,
InvitedRole: 0,
})
}
const updated: Room = { ...room, Roles: roles }
await db
.prepare('UPDATE room SET data = ?2 WHERE room_id = ?1')
.bind(roomId, JSON.stringify(updated))
.run()
return updated
}
/**
* Mutually-exclusive "main" room tags. The UI presents these as radio buttons, so
* setting one clears any other main tag. Compared case-insensitively.
@@ -810,7 +865,7 @@ export async function getOrCreateDormRoom(db: D1Database, accountId: number): Pr
Name: `@${username}'s Dorm`,
CreatorAccountId: accountId,
IsDorm: true,
Roles: [{ AccountId: accountId, Role: Role.Owner, LastChangedByAccountId: null, InvitedRole: 0 }],
Roles: [{ AccountId: accountId, Role: Role.Creator, LastChangedByAccountId: null, InvitedRole: 0 }],
SubRooms: [{ ...templateSub, CreatorAccountId: accountId }],
CreatedAt: new Date().toISOString(),
}