diff --git a/.husky/pre-commit b/.husky/pre-commit index 44d21ba..28afa76 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,6 @@ #!/bin/sh +export PATH="$HOME/.nvm/versions/node/v20.11.0/bin:$PATH" + . "$(dirname "$0")/_/husky.sh" yarn lint-staged --verbose \ No newline at end of file diff --git a/README.md b/README.md index 9109e28..ff10a8f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
-# 🏗🔴 Scaffold-OP +# 🏗🔴 Ceptor-Games-Scaffold

Documentation | diff --git a/package.json b/package.json index 3e026f1..511ebdb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "workspaces": { "packages": [ "packages/hardhat", - "packages/nextjs" + "packages/nextjs", + "packages/backend" ] }, "scripts": { @@ -36,7 +37,14 @@ }, "packageManager": "yarn@3.2.3", "devDependencies": { + "@types/react": "^18", + "@types/react-dom": "^18", "husky": "^8.0.1", "lint-staged": "^13.0.3" + }, + "dependencies": { + "next-auth": "^4.24.7", + "react": "^18.3.1", + "react-dom": "^18.3.1" } } diff --git a/packages/backend/README.md b/packages/backend/README.md new file mode 100644 index 0000000..18803d0 --- /dev/null +++ b/packages/backend/README.md @@ -0,0 +1 @@ +# backend diff --git a/packages/backend/package.json b/packages/backend/package.json new file mode 100644 index 0000000..efdcc4f --- /dev/null +++ b/packages/backend/package.json @@ -0,0 +1,17 @@ +{ + "name": "backend", + "packageManager": "yarn@3.2.3", + "dependencies": { + "express": "^4.19.2", + "mongoose": "^8.3.5" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/mongoose": "^5.11.97", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5" + }, + "scripts": { + "start": "ts-node-dev --respawn --transpile-only src/server.ts" + } +} diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts new file mode 100644 index 0000000..d6196ac --- /dev/null +++ b/packages/backend/src/app.ts @@ -0,0 +1,13 @@ +import express from 'express'; +import campaignRoutes from './routes/campaignRoutes'; +import characterRoutes from './routes/characterRoutes'; +import sessionRoutes from './routes/sessionRoutes'; + +const app = express(); + +app.use(express.json()); +app.use('/campaigns', campaignRoutes); +app.use('/characters', characterRoutes); +app.use('/sessions', sessionRoutes); + +export default app; \ No newline at end of file diff --git a/packages/backend/src/controllers/campaignController.ts b/packages/backend/src/controllers/campaignController.ts new file mode 100644 index 0000000..1087227 --- /dev/null +++ b/packages/backend/src/controllers/campaignController.ts @@ -0,0 +1,112 @@ +import { Request, Response } from 'express'; +import { Campaign } from '../models/Campaign'; + +export const createCampaign = async (req: Request, res: Response) => { + const { worldId, ccId } = req.body; + try { + const world = await World.findById(worldId); + if (!world) { + return res.status(404).json({ message: 'World not found' }); + } + if (world.ccId !== ccId && !world.permissions.includes(ccId)) { + return res.status(403).json({ message: 'Not authorized to create campaign in this world' }); + } + const newCampaign = new Campaign(req.body); + await newCampaign.save(); + res.status(201).json(newCampaign); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +export const getCampaignById = async (req: Request, res: Response) => { + try { + const campaign = await Campaign.findOne({ externalId: req.params.id }); + if (!campaign) { + return res.status(404).json({ message: 'Campaign not found' }); + } + res.status(200).json(campaign); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const updateCampaign = async (req: Request, res: Response) => { + try { + const updatedCampaign = await Campaign.findOneAndUpdate({ externalId: req.params.id }, req.body, { new: true }); + if (!updatedCampaign) { + return res.status(404).json({ message: 'Campaign not found' }); + } + res.status(200).json(updatedCampaign); + } catch (error) { + res.status(400).json({ message: error.message }); + } + +export const deleteCampaign = async (req: Request, res: Response) => { + try { + const deletedCampaign = await Campaign.findOneAndDelete({ externalId: req.params.id }); + if (!deletedCampaign) { + return res.status(404).json({ message: 'Campaign not found' }); + } + res.status(200).json({ message: 'Campaign deleted successfully' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } + +export const listCampaigns = async (req: Request, res: Response) => { + const { name, worldId, ccId, numCharacters, frequency } = req.query; + try { + const query = {}; + if (name) query.name = name; + if (worldId) query.worldId = worldId; + if (ccId) query.ccId = ccId; + if (numCharacters) query.numCharacters = numCharacters; + if (frequency) query.frequency = frequency; + + const campaigns = await Campaign.find(query); + res.status(200).json(campaigns); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const listCampaignsByWorld = async (req: Request, res: Response) => { + const { worldExternalId } = req.query; + + try { + // Find the world by its externalId + const world = await World.findOne({ externalId: worldExternalId }); + if (!world) { + return res.status(404).json({ message: 'World not found' }); + } + + // Find campaigns associated with the world's _id + const campaigns = await Campaign.find({ worldId: world._id }); + + // Send the list of campaigns in the response + res.status(200).json(campaigns); + } catch (error) { + // Handle any errors that occur during the process + res.status(500).json({ message: error.message }); + } +}; + +// Example mock data for campaigns +const mockCampaigns = [ + { + _id: '1', + name: 'The Quest for the Holy Grail', + description: 'Embark on an epic quest to find the legendary Holy Grail.', + externalId: 'campaign-1', + worldId: '1', + // ... other properties ... + }, + // ... more mock campaigns ... +]; + +// Serve mock data in your API route +export const listCampaigns = async (req: Request, res: Response) => { + const { worldId } = req.query; + const filteredCampaigns = mockCampaigns.filter(campaign => campaign.worldId === worldId); + res.status(200).json(filteredCampaigns); +}; \ No newline at end of file diff --git a/packages/backend/src/controllers/characterController.ts b/packages/backend/src/controllers/characterController.ts new file mode 100644 index 0000000..79ddfbc --- /dev/null +++ b/packages/backend/src/controllers/characterController.ts @@ -0,0 +1,57 @@ +import { Request, Response } from 'express'; +import { Character } from '../models/Character'; + +export const createCharacter = async (req: Request, res: Response) => { + try { + const newCharacter = new Character(req.body); + await newCharacter.save(); + res.status(201).json(newCharacter); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +export const getCharacterById = async (req: Request, res: Response) => { + try { + const character = await Character.findById(req.params.id); + if (!character) { + return res.status(404).json({ message: 'Character not found' }); + } + res.status(200).json(character); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const updateCharacter = async (req: Request, res: Response) => { + try { + const updatedCharacter = await Character.findByIdAndUpdate(req.params.id, req.body, { new: true }); + if (!updatedCharacter) { + return res.status(404).json({ message: 'Character not found' }); + } + res.status(200).json(updatedCharacter); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +export const deleteCharacter = async (req: Request, res: Response) => { + try { + const deletedCharacter = await Character.findByIdAndDelete(req.params.id); + if (!deletedCharacter) { + return res.status(404).json({ message: 'Character not found' }); + } + res.status(200).json({ message: 'Character deleted successfully' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const listCharacters = async (req: Request, res: Response) => { + try { + const characters = await Character.find({}); + res.status(200).json(characters); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; \ No newline at end of file diff --git a/packages/backend/src/controllers/notificationController.ts b/packages/backend/src/controllers/notificationController.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/controllers/sessionController.ts b/packages/backend/src/controllers/sessionController.ts new file mode 100644 index 0000000..70b07d8 --- /dev/null +++ b/packages/backend/src/controllers/sessionController.ts @@ -0,0 +1,69 @@ +import { Request, Response } from 'express'; +import { Session } from '../models/Session'; + +export const createSession = async (req: Request, res: Response) => { + try { + const newSession = new Session(req.body); + await newSession.save(); + res.status(201).json(newSession); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +export const getSessionById = async (req: Request, res: Response) => { + try { + const session = await Session.findById(req.params.id); + if (!session) { + return res.status(404).json({ message: 'Session not found' }); + } + res.status(200).json(session); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const updateSession = async (req: Request, res: Response) => { + try { + const updatedSession = await Session.findByIdAndUpdate(req.params.id, req.body, { new: true }); + if (!updatedSession) { + return res.status(404).json({ message: 'Session not found' }); + } + res.status(200).json(updatedSession); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +export const deleteSession = async (req: Request, res: Response) => { + try { + const deletedSession = await Session.findByIdAndDelete(req.params.id); + if (!deletedSession) { + return res.status(404).json({ message: 'Session not found' }); + } + res.status(200).json({ message: 'Session deleted successfully' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const listSessions = async (req: Request, res: Response) => { + try { + const sessions = await Session.find({}); + res.status(200).json(sessions); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const getSessionDetails = async (req: Request, res: Response) => { + try { + const session = await Session.findOne({ externalId: req.params.externalId }).populate('characters'); + if (!session) { + return res.status(404).json({ message: 'Session not found' }); + } + res.status(200).json(session); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; \ No newline at end of file diff --git a/packages/backend/src/controllers/worldController.ts b/packages/backend/src/controllers/worldController.ts new file mode 100644 index 0000000..793bc5d --- /dev/null +++ b/packages/backend/src/controllers/worldController.ts @@ -0,0 +1,108 @@ +import { Request, Response } from 'express'; +import { World } from '../models/World'; + +export const createWorld = async (req: Request, res: Response) => { + try { + const newWorld = new World(req.body); + await newWorld.save(); + res.status(201).json(newWorld); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +export const getWorldById = async (req: Request, res: Response) => { + try { + const world = await World.findById(req.params.id); + if (!world) { + return res.status(404).json({ message: 'World not found' }); + } + res.status(200).json(world); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const updateWorld = async (req: Request, res: Response) => { + try { + const updatedWorld = await World.findByIdAndUpdate(req.params.id, req.body, { new: true }); + if (!updatedWorld) { + return res.status(404).json({ message: 'World not found' }); + } + res.status(200).json(updatedWorld); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}; + +export const deleteWorld = async (req: Request, res: Response) => { + try { + const deletedWorld = await World.findByIdAndDelete(req.params.id); + if (!deletedWorld) { + return res.status(404).json({ message: 'World not found' }); + } + res.status(200).json({ message: 'World deleted successfully' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const listWorlds = async (req: Request, res: Response) => { + const { search, ccId, vibe, page = 1, limit = 10 } = req.query; + try { + let query: any = {}; + if (search) { + query.name = { $regex: search, $options: 'i' }; + } + if (ccId) { + query.ccId = ccId; + } + if (vibe) { + query.vibe = vibe; + } + const worlds = await World.find(query) + .skip((page - 1) * limit) + .limit(limit); + const total = await World.countDocuments(query); + res.status(200).json({ worlds, total, page, limit }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +// Example mock data for worlds +const mockWorlds = [ + { + _id: '1', + ccid: 'admin01' + name: 'Fantasy Land', + description: 'A world full of magic and dragons.', + vibe: 'chill', + externalId: 'world-1', + // ... other properties ... + }, + { + _id: '42', + ccid: 'admin01' + name: 'Techno Land', + description: 'A world full of technology and gadgets.', + vibe: 'intense', + externalId: 'world-2', + // ... other properties ... + }, + { + _id: '70', + ccid: 'admin01' + name: 'Dimension Travlers Land', + description: 'A world full of things unimaginable.', + vibe: 'unknown', + externalId: 'world-3', + // ... other properties ... + }, + // ... more mock worlds ... +]; + +// Serve mock data in your API route +export const listWorlds = async (req: Request, res: Response) => { + res.status(200).json(mockWorlds); +}; \ No newline at end of file diff --git a/packages/backend/src/models/Campaign.ts b/packages/backend/src/models/Campaign.ts new file mode 100644 index 0000000..9df4430 --- /dev/null +++ b/packages/backend/src/models/Campaign.ts @@ -0,0 +1,19 @@ +import { Schema, model } from 'mongoose'; + +const campaignSchema = new Schema({ + name: { type: String, required: true }, + description: { type: String, required: true }, + worldId: { type: Schema.Types.ObjectId, ref: 'World', required: true }, + ccId: { type: String, required: true }, + externalId: { type: String, required: true, unique: true, index: true }, + numCharacters: { type: Number, required: true }, + numGmMadeCharacters: { type: Number, required: true }, + numPlayerMadeCharacters: { type: Number, required: true }, + sessionZero: { type: Boolean, required: true }, + frequency: { type: String, enum: ['one_shot', 'regular_cadence', 'custom_cadence'], required: true }, + scheduledSessions: [{ type: Date }], + permissions: [{ type: String }], + notifications: [{ type: Schema.Types.ObjectId, ref: 'Notification' }], +}); + +export const Campaign = model('Campaign', campaignSchema); \ No newline at end of file diff --git a/packages/backend/src/models/Character.ts b/packages/backend/src/models/Character.ts new file mode 100644 index 0000000..3972582 --- /dev/null +++ b/packages/backend/src/models/Character.ts @@ -0,0 +1,17 @@ +import { Schema, model } from 'mongoose'; + +const characterSchema = new Schema({ + campaignId: { type: Schema.Types.ObjectId, ref: 'Campaign', required: true }, + ccId: { type: String, required: true }, + externalId: { type: String, required: true, unique: true, index: true }, + name: { type: String, required: true }, + class: { type: String, required: true }, + race: { type: String, required: true }, + description: { type: String, required: true }, + createdBy: { type: String, required: true }, + isGmMade: { type: Boolean, required: true }, + isCampaignLocked: { type: Boolean, required: true }, + permissions: [{ type: String }], +}); + +export const Character = model('Character', characterSchema); \ No newline at end of file diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/models/Session.ts b/packages/backend/src/models/Session.ts new file mode 100644 index 0000000..cafeeb4 --- /dev/null +++ b/packages/backend/src/models/Session.ts @@ -0,0 +1,13 @@ +import { Schema, model } from 'mongoose'; + +const sessionSchema = new Schema({ + campaignId: { type: Schema.Types.ObjectId, ref: 'Campaign', required: true }, + externalId: { type: String, required: true, unique: true, index: true }, + sessionNumber: { type: Number, required: true }, + date: { type: Date, required: true }, + description: { type: String, required: false }, + characters: [{ type: Schema.Types.ObjectId, ref: 'Character' }], + permissions: [{ type: String }], +}); + +export const Session = model('Session', sessionSchema); \ No newline at end of file diff --git a/packages/backend/src/models/World.ts b/packages/backend/src/models/World.ts new file mode 100644 index 0000000..c8435b8 --- /dev/null +++ b/packages/backend/src/models/World.ts @@ -0,0 +1,12 @@ +import { Schema, model } from 'mongoose'; + +const worldSchema = new Schema({ + name: { type: String, required: true }, + description: { type: String, required: true }, + vibe: { type: String, required: true }, + ccId: { type: String, required: true }, + externalId: { type: String, required: true, unique: true, index: true }, + permissions: [{ type: String }], +}); + +export const World = model('World', worldSchema); \ No newline at end of file diff --git a/packages/backend/src/routes/campaignRoutes.ts b/packages/backend/src/routes/campaignRoutes.ts new file mode 100644 index 0000000..09e8873 --- /dev/null +++ b/packages/backend/src/routes/campaignRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { createCampaign, getCampaignById, updateCampaign, deleteCampaign, listCampaigns } from '../controllers/campaignController'; + +const router = Router(); + +router.post('/', createCampaign); +router.get('/:id', getCampaignById); +router.put('/:id', updateCampaign); +router.delete('/:id', deleteCampaign); +router.get('/', listCampaigns); + +export default router; \ No newline at end of file diff --git a/packages/backend/src/routes/characterRoutes.ts b/packages/backend/src/routes/characterRoutes.ts new file mode 100644 index 0000000..66f810f --- /dev/null +++ b/packages/backend/src/routes/characterRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { createCharacter, getCharacterById, updateCharacter, deleteCharacter, listCharacters } from '../controllers/characterController'; + +const router = Router(); + +router.post('/', createCharacter); +router.get('/:id', getCharacterById); +router.put('/:id', updateCharacter); +router.delete('/:id', deleteCharacter); +router.get('/', listCharacters); + +export default router; \ No newline at end of file diff --git a/packages/backend/src/routes/notificationRoutes.ts b/packages/backend/src/routes/notificationRoutes.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/backend/src/routes/sessionRoutes.ts b/packages/backend/src/routes/sessionRoutes.ts new file mode 100644 index 0000000..d63e956 --- /dev/null +++ b/packages/backend/src/routes/sessionRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { createSession, getSessionById, updateSession, deleteSession, listSessions } from '../controllers/sessionController'; + +const router = Router(); + +router.post('/', createSession); +router.get('/:id', getSessionById); +router.put('/:id', updateSession); +router.delete('/:id', deleteSession); +router.get('/', listSessions); + +export default router; \ No newline at end of file diff --git a/packages/backend/src/routes/worldRoutes.ts b/packages/backend/src/routes/worldRoutes.ts new file mode 100644 index 0000000..ec67950 --- /dev/null +++ b/packages/backend/src/routes/worldRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { createWorld, getWorldById, updateWorld, deleteWorld, listWorlds } from '../controllers/worldController'; + +const router = Router(); + +router.post('/', createWorld); +router.get('/:id', getWorldById); +router.put('/:id', updateWorld); +router.delete('/:id', deleteWorld); +router.get('/', listWorlds); + +export default router; \ No newline at end of file diff --git a/packages/backend/src/server.ts b/packages/backend/src/server.ts new file mode 100644 index 0000000..07fe2ab --- /dev/null +++ b/packages/backend/src/server.ts @@ -0,0 +1,6 @@ +import app from './app'; + +const port = process.env.PORT || 3000; +app.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); +}); \ No newline at end of file diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json new file mode 100644 index 0000000..66c1106 --- /dev/null +++ b/packages/backend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/hardhat/contracts/BuyMeACeptor.sol b/packages/hardhat/contracts/BuyMeACeptor.sol new file mode 100644 index 0000000..fcbb20f --- /dev/null +++ b/packages/hardhat/contracts/BuyMeACeptor.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +/** + * @title World + * @dev World struct + */ +struct World { + string vibe; + string gameMasterName; + string gameMasterTwitterHandle; + string description; + uint256 time; + address gameMasterAddress; +} + +/** + * @title BuyMeACeptorWorld + * @dev BuyMeACeptorWorld contract to accept donations and for our users to create a world for us + */ +contract BuyMeACeptor{ + address payable public owner; + uint256 public price; + World[] public worlds; + + error InsufficientFunds(); + error InvalidArguments(string message); + error OnlyOwner(); + + event BuyMeACeptorWorldEvent(address indexed buyer, uint256 price); + event NewWorld(address indexed gameMasterAddress, uint256 time, string vibe, string gameMasterName, string gameMasterTwitterHandle, string description); + + constructor() { + owner = payable(msg.sender); + price = 0.0001 ether; + } + + /** + * WRITE FUNCTIONS ************* + */ + + /** + * @dev Function to buy a world + * @param gameMasterName The name of the game master + * @param gameMasterTwitterHandle The Twitter handle of the game master + * @param description The description of the world + * (Note: Using calldata for gas efficiency) + */ + function buyWorld(string calldata vibe, string calldata gameMasterName, string calldata gameMasterTwitterHandle, string calldata description) public payable { + if (msg.value < price) { + revert InsufficientFunds(); + } + + emit BuyMeACeptorWorldEvent(msg.sender, msg.value); + + if (bytes(gameMasterName).length == 0 && bytes(description).length == 0) { + revert InvalidArguments("Invalid gameMasterName or description"); + } + + worlds.push(World(vibe, gameMasterName, gameMasterTwitterHandle, description, block.timestamp, msg.sender)); + + emit NewWorld(msg.sender, block.timestamp, vibe, gameMasterName, gameMasterTwitterHandle, description); + } + + /** + * @dev Function to remove a world + * @param index The index of the world + */ + function removeWorld(uint256 index) public { + if (index >= worlds.length) { + revert InvalidArguments("Invalid index"); + } + + World memory world = worlds[index]; + + // if operation isnt sent from the same game master or the owner, then not allowed + if (world.gameMasterAddress != msg.sender && msg.sender != owner) { + revert InvalidArguments("Operation not allowed"); + } + + World memory indexWorld = worlds[index]; + worlds[index] = worlds[worlds.length - 1]; + worlds[worlds.length - 1] = indexWorld; + worlds.pop(); + } + + /** + * @dev Function to modify a world description + * @param index The index of the world + * @param description The description of the world + */ + function modifyWorldDescription(uint256 index, string memory description) public { + if (index >= worlds.length) { + revert InvalidArguments("Invalid index"); + } + + World memory world = worlds[index]; + + if (world.gameMasterAddress != msg.sender || msg.sender != owner) { + revert InvalidArguments("Operation not allowed"); + } + + worlds[index].description = description; + } + + /** + * @dev Function to withdraw the balance + */ + function withdrawTips() public { + if (msg.sender != owner) { + revert OnlyOwner(); + } + + if (address(this).balance == 0) { + revert InsufficientFunds(); + } + + (bool sent,) = owner.call{value: address(this).balance}(""); + require(sent, "Failed to send Ether"); + } + + /** + * READ FUNCTIONS ************* + */ + + /** + * @dev Function to get the worlds + */ + function getWorlds() public view returns (World[] memory) { + return worlds; + } + + /** + * @dev Recieve function to accept ether + */ + receive() external payable {} +} diff --git a/packages/hardhat/contracts/BuyMeACoffee.sol b/packages/hardhat/contracts/BuyMeACoffee.sol deleted file mode 100644 index 2e09998..0000000 --- a/packages/hardhat/contracts/BuyMeACoffee.sol +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.0 <0.9.0; -/** - * ---------------------------------------------------------------------------------------------------------------- - * ---------██████╗ ██╗ ██╗██╗██╗ ██████╗ ██████╗ ███╗ ██╗ ██████╗██╗ ██╗ █████╗ ██╗███╗ ██╗----- - * ---------██╔══██╗██║ ██║██║██║ ██╔══██╗ ██╔═══██╗████╗ ██║██╔════╝██║ ██║██╔══██╗██║████╗ ██║----- - * ---------██████╔╝██║ ██║██║██║ ██║ ██║█████╗██║ ██║██╔██╗ ██║██║ ███████║███████║██║██╔██╗ ██║----- - * ---------██╔══██╗██║ ██║██║██║ ██║ ██║╚════╝██║ ██║██║╚██╗██║██║ ██╔══██║██╔══██║██║██║╚██╗██║----- - * ---------██████╔╝╚██████╔╝██║███████╗██████╔╝ ╚██████╔╝██║ ╚████║╚██████╗██║ ██║██║ ██║██║██║ ╚████║----- - * ---------╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝----- - * ---------------------------------------------------------------------------------------------------------------- - * https://github.com/coinbase/build-onchain-apps - * - * Disclaimer: The provided Solidity contracts are intended solely for educational purposes and are - * not warranted for any specific use. They have not been audited and may contain vulnerabilities, hence should - * not be deployed in production environments. Users are advised to seek professional review and conduct a - * comprehensive security audit before any real-world application to mitigate risks of financial loss or other - * consequences. The author(s) disclaim all liability for any damages arising from the use of these contracts. - * Use at your own risk, acknowledging the inherent risks of smart contract technology on the blockchain. - * - */ - -/** - * @title Memos - * @dev Memo struct - */ -struct Memo { - uint numCoffees; - string userName; - string twitterHandle; - string message; - uint256 time; - address userAddress; -} - -/** - * @title BuyMeACoffee - * @dev BuyMeACoffee contract to accept donations and for our users to leave a memo for us - */ -contract BuyMeACoffee { - address payable public owner; - uint256 public price; - Memo[] public memos; - - error InsufficientFunds(); - error InvalidArguments(string message); - error OnlyOwner(); - - event BuyMeACoffeeEvent(address indexed buyer, uint256 price); - event NewMemo(address indexed userAddress, uint256 time, uint numCoffees, string userName, string twitterHandle, string message); - - constructor() { - owner = payable(msg.sender); - price = 0.0001 ether; - } - - /** - * WRITE FUNCTIONS ************* - */ - - /** - * @dev Function to buy a coffee - * @param userName The name of the user - * @param twitterHandle The Twitter handle of the user - * @param message The message of the user - * (Note: Using calldata for gas efficiency) - */ - function buyCoffee(uint numCoffees, string calldata userName, string calldata twitterHandle, string calldata message) public payable { - if (msg.value < price*numCoffees) { - revert InsufficientFunds(); - } - - emit BuyMeACoffeeEvent(msg.sender, msg.value); - - if (bytes(userName).length == 0 && bytes(message).length == 0) { - revert InvalidArguments("Invalid userName or message"); - } - - memos.push(Memo(numCoffees, userName, twitterHandle, message, block.timestamp, msg.sender)); - - emit NewMemo(msg.sender, block.timestamp, numCoffees, userName, twitterHandle, message); - } - - /** - * @dev Function to remove a memo - * @param index The index of the memo - */ - function removeMemo(uint256 index) public { - if (index >= memos.length) { - revert InvalidArguments("Invalid index"); - } - - Memo memory memo = memos[index]; - - // if operation isnt sent from the same user or the owner, then not allowed - if (memo.userAddress != msg.sender && msg.sender != owner) { - revert InvalidArguments("Operation not allowed"); - } - - Memo memory indexMemo = memos[index]; - memos[index] = memos[memos.length - 1]; - memos[memos.length - 1] = indexMemo; - memos.pop(); - } - - /** - * @dev Function to modify a memo - * @param index The index of the memo - * @param message The message of the memo - */ - function modifyMemoMessage(uint256 index, string memory message) public { - if (index >= memos.length) { - revert InvalidArguments("Invalid index"); - } - - Memo memory memo = memos[index]; - - if (memo.userAddress != msg.sender || msg.sender != owner) { - revert InvalidArguments("Operation not allowed"); - } - - memos[index].message = message; - } - - /** - * @dev Function to withdraw the balance - */ - function withdrawTips() public { - if (msg.sender != owner) { - revert OnlyOwner(); - } - - if (address(this).balance == 0) { - revert InsufficientFunds(); - } - - (bool sent,) = owner.call{value: address(this).balance}(""); - require(sent, "Failed to send Ether"); - } - - /** - * READ FUNCTIONS ************* - */ - - /** - * @dev Function to get the memos - */ - function getMemos() public view returns (Memo[] memory) { - return memos; - } - - /** - * @dev Recieve function to accept ether - */ - receive() external payable {} -} diff --git a/packages/hardhat/contracts/CeptorCS.txt b/packages/hardhat/contracts/CeptorCS.txt new file mode 100644 index 0000000..01ea2ed --- /dev/null +++ b/packages/hardhat/contracts/CeptorCS.txt @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol"; + +contract CeptorCharacterSheets is ERC721URIStorage, VRFConsumerBaseV2, Ownable { + struct Stats { + uint8 strength; + uint8 dexterity; + uint8 constitution; + uint8 intelligence; + uint8 wisdom; + uint8 charisma; + uint8 luck; + } + + struct Character { + Stats stats; + string name; + uint swapsLeft; + } + + mapping(address => uint256) public ownerToTokenId; + mapping(uint256 => Character) public tokenIdToCharacter; + + uint256 public tokenIdCounter; + bytes32 public keyHash; + uint256 public fee; + uint64 public subscriptionId; + + event CharacterCreated(uint256 indexed tokenId, address owner); + event StatsSwapped(uint256 indexed tokenId, address owner); + + constructor( + address vrfCoordinator, + address linkToken, + bytes32 _keyHash, + uint64 _subscriptionId + ) + VRFConsumerBaseV2(vrfCoordinator) + ERC721("CeptorCharacterSheets", "CCS") + { + keyHash = _keyHash; + fee = 0.1 * 10**18; // Chainlink VRF fee + subscriptionId = _subscriptionId; + tokenIdCounter = 1; + } + + function createCharacter(string memory name) external { + require(ownerToTokenId[msg.sender] == 0, "Owner already has a character"); + uint256 tokenId = tokenIdCounter++; + ownerToTokenId[msg.sender] = tokenId; + tokenIdToCharacter[tokenId] = Character({ + name: name, + stats: Stats(0, 0, 0, 0, 0, 0, 0), + swapsLeft: 3 + }); + _safeMint(msg.sender, tokenId); + emit CharacterCreated(tokenId, msg.sender); + requestStats(tokenId); + } + + function requestStats(uint256 tokenId) internal { + require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK"); + requestRandomWords(keyHash, subscriptionId, 3, fee, 1); + } + + function fulfillRandomWords(uint256, uint256[] memory randomWords) internal override { + uint256 tokenId = ownerToTokenId[msg.sender]; + Character storage character = tokenIdToCharacter[tokenId]; + character.stats.strength = uint8(randomWords[0] % 16 + 3); + character.stats.dexterity = uint8(randomWords[1] % 16 + 3); + character.stats.constitution = uint8(randomWords[2] % 16 + 3); + character.stats.intelligence = uint8(randomWords[3] % 16 + 3); + character.stats.wisdom = uint8(randomWords[4] % 16 + 3); + character.stats.charisma = uint8(randomWords[5] % 16 + 3); + character.stats.luck = uint8(randomWords[6] % 16 + 3); + } + + function swapStats(uint256 tokenId) external { + require(ownerOf(tokenId) == msg.sender, "Not the owner"); + Character storage character = tokenIdToCharacter[tokenId]; + require(character.swapsLeft > 0, "No swaps left"); + character.swapsLeft--; + requestStats(tokenId); + emit StatsSwapped(tokenId, msg.sender); + } +} diff --git a/packages/hardhat/contracts/CeptorCharacterGenerator.sol b/packages/hardhat/contracts/CeptorCharacterGenerator.sol new file mode 100644 index 0000000..95fadec --- /dev/null +++ b/packages/hardhat/contracts/CeptorCharacterGenerator.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol"; +import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol"; +import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; + +contract DnDCharacterGenerator is VRFConsumerBaseV2, ConfirmedOwner { + VRFCoordinatorV2Interface COORDINATOR; + uint64 s_subscriptionId; + bytes32 keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c; + // forge-test gas report & gas limit plugin on hardhat + uint32 callbackGasLimit = 100000; + uint16 requestConfirmations = 3; + uint32 numWords = 7; // 6 ability scores + 1 for class + + struct Character { + uint256[6] abilities; + string class; + string name; + string alignment; + string background; + uint8 swaps; + } + + mapping(uint256 => address) requestToSender; + mapping(address => Character) public characters; + + event CharacterCreated(address owner, uint256 requestId); + event CharacterUpdated(address owner, string name, string alignment, string background); + event ScoresSwapped(address owner); + event RequestFulfilled(uint256 requestId, uint256[] randomWords); + + constructor(uint64 subscriptionId) VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625) + ConfirmedOwner(msg.sender) { + COORDINATOR = VRFCoordinatorV2Interface(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625); + s_subscriptionId = subscriptionId; + } + + function createCharacter() external onlyOwner { + require(characters[msg.sender].abilities[0] == 0, "Character already created"); + uint256 requestId = COORDINATOR.requestRandomWords( + keyHash, + s_subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords + ); + requestToSender[requestId] = msg.sender; + emit CharacterCreated(msg.sender, requestId); + } + + function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override { + address owner = requestToSender[requestId]; + uint256[6] memory abilities; + for (uint i = 0; i < 6; ++i) { // ++i saves 2 gas + abilities[i] = (randomWords[i] % 16) + 3; // Score range: 3-18 + } + characters[owner] = Character({ + abilities: abilities, + class: getClass(randomWords[6]), + name: "", + alignment: "", + background: "", + swaps: 0 + }); + emit RequestFulfilled(requestId, randomWords); + } + + function updateCharacterDetails(string calldata name, string calldata alignment, string calldata background) external { + require(characters[msg.sender].abilities[0] != 0, "Character not created"); + characters[msg.sender].name = name; + characters[msg.sender].alignment = alignment; + characters[msg.sender].background = background; + emit CharacterUpdated(msg.sender, name, alignment, background); + } + + function swapScores(uint8 index1, uint8 index2) external { + require(characters[msg.sender].swaps < 3, "Max swaps reached"); + require(index1 < 6 && index2 < 6, "Invalid index"); + + (characters[msg.sender].abilities[index1], characters[msg.sender].abilities[index2]) = + (characters[msg.sender].abilities[index2], characters[msg.sender].abilities[index1]); + characters[msg.sender].swaps++; + emit ScoresSwapped(msg.sender); + } + + function getClass(uint256 randomNumber) private pure returns (string memory) { + string[12] memory classes = ["Barbarian", "Bard", "Cleric", "Druid", "Fighter", "Monk", "Paladin", "Ranger", "Rogue", "Sorcerer", "Warlock", "Wizard"]; + return classes[randomNumber % classes.length]; + } +} diff --git a/packages/hardhat/contracts/CharacterSheets.txt b/packages/hardhat/contracts/CharacterSheets.txt new file mode 100644 index 0000000..0eb0de6 --- /dev/null +++ b/packages/hardhat/contracts/CharacterSheets.txt @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +// An example of a consumer contract that relies on a subscription for funding. +pragma solidity ^0.8.7; + +// Useful for debugging. Remove when deploying to a live network. +import "hardhat/console.sol"; + +// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc) +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol"; +import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol"; +import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; + +/** + * Request testnet LINK and ETH here: https://faucets.chain.link/ + * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/ + */ + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ + +contract VRFv2Consumer is VRFConsumerBaseV2, ConfirmedOwner { + event RequestSent(uint256 requestId, uint32 numWords); + event RequestFulfilled(uint256 requestId, uint256[] randomWords); + + struct RequestStatus { + bool fulfilled; // whether the request has been successfully fulfilled + bool exists; // whether a requestId exists + uint256[] randomWords; + } + mapping(uint256 => RequestStatus) + public s_requests; /* requestId --> requestStatus */ + VRFCoordinatorV2Interface COORDINATOR; + + // Your subscription ID. + uint64 s_subscriptionId; + + // past requests Id. + uint256[] public requestIds; + uint256 public lastRequestId; + + // The gas lane to use, which specifies the maximum gas price to bump to. + // For a list of available gas lanes on each network, + // see https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#configurations + bytes32 keyHash = + 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c; + + // Depends on the number of requested values that you want sent to the + // fulfillRandomWords() function. Storing each word costs about 20,000 gas, + // so 100,000 is a safe default for this example contract. Test and adjust + // this limit based on the network that you select, the size of the request, + // and the processing of the callback request in the fulfillRandomWords() + // function. + uint32 callbackGasLimit = 100000; + + // The default is 3, but you can set this higher. + uint16 requestConfirmations = 3; + + // For this example, retrieve 2 random values in one request. + // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS. + uint32 numWords = 2; + + /** + * HARDCODED FOR SEPOLIA + * COORDINATOR: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625 + */ + constructor( + uint64 subscriptionId + ) + VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625) + ConfirmedOwner(msg.sender) + { + COORDINATOR = VRFCoordinatorV2Interface( + 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625 + ); + s_subscriptionId = subscriptionId; + } + + // Assumes the subscription is funded sufficiently. + function requestRandomWords() + external + onlyOwner + returns (uint256 requestId) + { + // Will revert if subscription is not set and funded. + requestId = COORDINATOR.requestRandomWords( + keyHash, + s_subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords + ); + s_requests[requestId] = RequestStatus({ + randomWords: new uint256[](0), + exists: true, + fulfilled: false + }); + requestIds.push(requestId); + lastRequestId = requestId; + emit RequestSent(requestId, numWords); + return requestId; + } + + function fulfillRandomWords( + uint256 _requestId, + uint256[] memory _randomWords + ) internal override { + require(s_requests[_requestId].exists, "request not found"); + s_requests[_requestId].fulfilled = true; + s_requests[_requestId].randomWords = _randomWords; + emit RequestFulfilled(_requestId, _randomWords); + } + + function getRequestStatus( + uint256 _requestId + ) external view returns (bool fulfilled, uint256[] memory randomWords) { + require(s_requests[_requestId].exists, "request not found"); + RequestStatus memory request = s_requests[_requestId]; + return (request.fulfilled, request.randomWords); + } +} diff --git a/packages/hardhat/contracts/DynamicHooty.sol b/packages/hardhat/contracts/DynamicHooty.sol new file mode 100644 index 0000000..39bbe4c --- /dev/null +++ b/packages/hardhat/contracts/DynamicHooty.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +// An example of a consumer contract that relies on a subscription for funding. +pragma solidity ^0.8.7; + +import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/vrf/interfaces/VRFCoordinatorV2Interface.sol"; +import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol"; +import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol"; + +/** + * Request testnet LINK and ETH here: https://faucets.chain.link/ + * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/ + */ + +/** + * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. + * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. + * DO NOT USE THIS CODE IN PRODUCTION. + */ + +contract VRFv2Consumer is VRFConsumerBaseV2, ConfirmedOwner { + event RequestSent(uint256 requestId, uint32 numWords); + event RequestFulfilled(uint256 requestId, uint256[] randomWords); + + struct RequestStatus { + bool fulfilled; // whether the request has been successfully fulfilled + bool exists; // whether a requestId exists + uint256[] randomWords; + } + mapping(uint256 => RequestStatus) + public s_requests; /* requestId --> requestStatus */ + VRFCoordinatorV2Interface COORDINATOR; + + // Your subscription ID. + uint64 s_subscriptionId; + + // past requests Id. + uint256[] public requestIds; + uint256 public lastRequestId; + + // The gas lane to use, which specifies the maximum gas price to bump to. + // For a list of available gas lanes on each network, + // see https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/#configurations + bytes32 keyHash = + 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c; + + // Depends on the number of requested values that you want sent to the + // fulfillRandomWords() function. Storing each word costs about 20,000 gas, + // so 100,000 is a safe default for this example contract. Test and adjust + // this limit based on the network that you select, the size of the request, + // and the processing of the callback request in the fulfillRandomWords() + // function. + uint32 callbackGasLimit = 100000; + + // The default is 3, but you can set this higher. + uint16 requestConfirmations = 3; + + // For this example, retrieve 2 random values in one request. + // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS. + uint32 numWords = 2; + + /** + * HARDCODED FOR SEPOLIA + * COORDINATOR: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625 + */ + constructor( + uint64 subscriptionId + ) + VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625) + ConfirmedOwner(msg.sender) + { + COORDINATOR = VRFCoordinatorV2Interface( + 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625 + ); + s_subscriptionId = subscriptionId; + } + + // Assumes the subscription is funded sufficiently. + function requestRandomWords() + external + onlyOwner + returns (uint256 requestId) + { + // Will revert if subscription is not set and funded. + requestId = COORDINATOR.requestRandomWords( + keyHash, + s_subscriptionId, + requestConfirmations, + callbackGasLimit, + numWords + ); + s_requests[requestId] = RequestStatus({ + randomWords: new uint256[](0), + exists: true, + fulfilled: false + }); + requestIds.push(requestId); + lastRequestId = requestId; + emit RequestSent(requestId, numWords); + return requestId; + } + + function fulfillRandomWords( + uint256 _requestId, + uint256[] memory _randomWords + ) internal override { + require(s_requests[_requestId].exists, "request not found"); + s_requests[_requestId].fulfilled = true; + s_requests[_requestId].randomWords = _randomWords; + emit RequestFulfilled(_requestId, _randomWords); + } + + function getRequestStatus( + uint256 _requestId + ) external view returns (bool fulfilled, uint256[] memory randomWords) { + require(s_requests[_requestId].exists, "request not found"); + RequestStatus memory request = s_requests[_requestId]; + return (request.fulfilled, request.randomWords); + } +} diff --git a/packages/hardhat/contracts/YourContract.sol b/packages/hardhat/contracts/YourContract.sol index 3d364a0..ccd9a12 100644 --- a/packages/hardhat/contracts/YourContract.sol +++ b/packages/hardhat/contracts/YourContract.sol @@ -70,7 +70,15 @@ contract YourContract { // emit: keyword used to trigger an event emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, msg.value); } +// Mapping from address to number +mapping(address => uint) public userNumbers; +event NumberUpdated(address indexed user, uint number); +// Function to store a number +function storeNumber(uint _number) public { + userNumbers[msg.sender] = _number; + emit NumberUpdated(msg.sender, _number); + } /** * Function that allows the owner to withdraw all the Ether in the contract * The function can only be called by the owner of the contract as defined by the isOwner modifier diff --git a/packages/hardhat/contracts/readme.md b/packages/hardhat/contracts/readme.md new file mode 100644 index 0000000..93eb659 --- /dev/null +++ b/packages/hardhat/contracts/readme.md @@ -0,0 +1,14 @@ +# Games Contracts + +1. [Game World Generator](BuyMeACeptor.sol) - A contract that generates a game world based on a user's vibe and number of players. The world is generated with a visual of the planet, scenarios, locations, descriptions, maps, denizens, secrets, goals, and players. Each world has its own blockchain. Creating a World costs 10 gameTokens. when creating a game, i want to have my own world or play with others. each world should be locked to a blockchain. 10 gT to make a world. 5 gT to join one as a GM, 2 gT to join as player. + +Inside worlds, there are games +inside games there are schedules +inside schedules there are sessions +(and we verify who shows up) + +1. [Character Generator](CeptorCharacterGenerator.sol) - A contract that generates a character for a user in the game world. The character is generated with abilities, class, name, alignment, and background. Each character has its own unique attributes. Creating a Character is only allowed by the owner of the contract. This is a mistake, and will be replaced in upgrade to VRF2.5 + +2. Is the World Generator deploying a World contract? Yes. Is the World contract tracking all its games, or deploying each game as its own contract which tracks the sessions. Verifiable Truth. + +3. NPC Generator - Unlike the PCG which is usable by any Verified Credential having hooty in their hey hey. The NPCG is a contract that generates a non-player character for a user in the game world. The character is generated with abilities, class, name, alignment, hometown, and background. Each character has its own unique attributes. Creating a Character is only allowed by the owner of the contract. VRF2.5 because reusable code choices. \ No newline at end of file diff --git a/packages/hardhat/deploy/01_deploy_buy_me_a_coffee.ts b/packages/hardhat/deploy/01_deploy_buy_me_a_ceptor.ts similarity index 75% rename from packages/hardhat/deploy/01_deploy_buy_me_a_coffee.ts rename to packages/hardhat/deploy/01_deploy_buy_me_a_ceptor.ts index 474d826..fd61c54 100644 --- a/packages/hardhat/deploy/01_deploy_buy_me_a_coffee.ts +++ b/packages/hardhat/deploy/01_deploy_buy_me_a_ceptor.ts @@ -3,12 +3,12 @@ import { DeployFunction } from "hardhat-deploy/types"; import { Contract } from "ethers"; /** - * Deploys a contract named "BuyMeACoffee" using the deployer account and + * Deploys a contract named "BuyMeACeptor" using the deployer account and * constructor arguments set to the deployer address * * @param hre HardhatRuntimeEnvironment object. */ -const deployBuyMeACoffee: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { +const deployBuyMeACeptor: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { /* On localhost, the deployer account is the one that comes with Hardhat, which is already funded. @@ -22,7 +22,7 @@ const deployBuyMeACoffee: DeployFunction = async function (hre: HardhatRuntimeEn const { deployer } = await hre.getNamedAccounts(); const { deploy } = hre.deployments; - await deploy("BuyMeACoffee", { + await deploy("BuyMeACeptor", { from: deployer, // Contract constructor arguments log: true, @@ -32,12 +32,12 @@ const deployBuyMeACoffee: DeployFunction = async function (hre: HardhatRuntimeEn }); // Get the deployed contract to interact with it after deploying. - const buyMeACoffeeContract = await hre.ethers.getContract("BuyMeACoffee", deployer); - console.log("👋 Buy this person a coffee!", await buyMeACoffeeContract.owner()); + const buyMeACeptorContract = await hre.ethers.getContract("BuyMeACeptor", deployer); + console.log("👋 Buy this person a Ceptor!", await buyMeACeptorContract.owner()); }; -export default deployBuyMeACoffee; +export default deployBuyMeACeptor; // Tags are useful if you have multiple deploy files and only want to run one of them. -// e.g. yarn deploy --tags BuyMeACoffee -deployBuyMeACoffee.tags = ["BuyMeACoffee"]; +// e.g. yarn deploy --tags BuyMeACeptor +deployBuyMeACeptor.tags = ["BuyMeACeptor"]; diff --git a/packages/hardhat/package.json b/packages/hardhat/package.json index 918b5d0..c5a9a2a 100644 --- a/packages/hardhat/package.json +++ b/packages/hardhat/package.json @@ -50,6 +50,7 @@ "typescript": "^5.1.6" }, "dependencies": { + "@chainlink/contracts": "^1.1.0", "@openzeppelin/contracts": "^4.8.1", "@typechain/ethers-v6": "^0.5.1", "dotenv": "^16.0.3", diff --git a/packages/nextjs/app/games/page.tsx b/packages/nextjs/app/games/page.tsx new file mode 100644 index 0000000..42ab391 --- /dev/null +++ b/packages/nextjs/app/games/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import type { NextPage } from "next"; +import { useAccount } from "wagmi"; +import CharacterCard from "~~/components/ceptor/CharacterCard"; +import characters from "~~/components/ceptor/CharacterData"; +import { Address } from "~~/components/scaffold-eth"; + +const Games: NextPage = () => { + const { address: connectedAddress } = useAccount(); + + return ( + <> +
+
+

+ Welcome to + Games-Scaffold 🏗🔴 +

+
+

Connected Address:

+
+
+
+ {characters.map((character, index) => ( + + ))} +
+
+
+ + ); +}; + +export default Games; diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index 8243e2f..144078c 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -15,28 +15,22 @@ const Home: NextPage = () => {

Welcome to - Scaffold-OP 🏗🔴 + D&D Campaign Scheduler 🐉🗓️

Connected Address:

-

- Get started by editing{" "} - - packages/nextjs/app/page.tsx - -

-

- Edit your smart contract{" "} - - YourContract.sol - {" "} - in{" "} - - packages/hardhat/contracts - -

+
+ +
+ Create a Campaign +
+
+ View Worlds +
+
+ View Campaigns
@@ -54,9 +48,9 @@ const Home: NextPage = () => {

- Explore your local transactions with the{" "} - - Block Explorer + Explore your local characters with the{" "} + + Games {" "} tab.

diff --git a/packages/nextjs/components/CampaignForm.tsx b/packages/nextjs/components/CampaignForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/components/CampaignList.tsx b/packages/nextjs/components/CampaignList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/components/CharacterForm.tsx b/packages/nextjs/components/CharacterForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/components/CharacterList.tsx b/packages/nextjs/components/CharacterList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 3489729..7bf157c 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -5,7 +5,7 @@ import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; import sunny from "./assets/sunny.svg"; -import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline"; +import { Bars3Icon, BugAntIcon, PuzzlePieceIcon } from "@heroicons/react/24/outline"; import { DappConsoleButton, FaucetButton, @@ -30,6 +30,11 @@ export const menuLinks: HeaderMenuLink[] = [ href: "/debug", icon: , }, + { + label: "Games Hooty", + href: "/games", + icon: , + }, ]; export const HeaderMenuLinks = () => { diff --git a/packages/nextjs/components/ceptor/CharacterCard.tsx b/packages/nextjs/components/ceptor/CharacterCard.tsx new file mode 100644 index 0000000..2405dbe --- /dev/null +++ b/packages/nextjs/components/ceptor/CharacterCard.tsx @@ -0,0 +1,52 @@ +// components/ceptor/CharacterCard.tsx +import React from "react"; + +interface Character { + name: string; + classInfo: string; + race: string; + level: number; + background: string; + alignment: string; + abilities: Record; + combatStats: { + ac: number; + hitPoints: number; + initiative: number; + speed: string; + }; +} + +interface CharacterCardProps { + character: Character; +} + +const CharacterCard: React.FC = ({ character }) => { + return ( +
+

+ {character.name} - Level {character.level} {character.classInfo} +

+

+ {character.race} | {character.background} | {character.alignment} +

+
+

Abilities:

+ {Object.entries(character.abilities).map(([ability, value]) => ( +

+ {ability}: {value} +

+ ))} +
+
+

Combat Stats:

+

AC: {character.combatStats.ac}

+

Hit Points: {character.combatStats.hitPoints}

+

Initiative: {character.combatStats.initiative}

+

Speed: {character.combatStats.speed}

+
+
+ ); +}; + +export default CharacterCard; diff --git a/packages/nextjs/components/ceptor/CharacterData.ts b/packages/nextjs/components/ceptor/CharacterData.ts new file mode 100644 index 0000000..e535db9 --- /dev/null +++ b/packages/nextjs/components/ceptor/CharacterData.ts @@ -0,0 +1,82 @@ +interface Character { + name: string; + race: string; + classInfo: string; + level: number; + background: string; + alignment: string; + player: string; + abilities: { + strength: number; + dexterity: number; + constitution: number; + intelligence: number; + wisdom: number; + charisma: number; + }; + proficiencies: { + armor: string[]; + weapons: string[]; + tools: string[]; + savingThrows: string[]; + skills: string[]; + }; + featuresTraits: string[]; + equipment: string[]; + combatStats: { + ac: number; + initiative: number; + speed: string; + hitPoints: number; + }; +} + +const characters: Character[] = [ + { + name: "猪八戒", + race: "Zhu Bajie", + classInfo: "Fighter", + level: 1, + background: "Folk Hero", + alignment: "Neutral Good", + player: "Allan Ma", + abilities: { + strength: 15, + dexterity: 10, + constitution: 16, + intelligence: 8, + wisdom: 13, + charisma: 12, + }, + proficiencies: { + armor: ["All armor", "shields"], + weapons: ["Simple weapons", "martial weapons"], + tools: ["Brewer's supplies"], + savingThrows: ["Strength", "Constitution"], + skills: ["Athletics", "Perception"], + }, + featuresTraits: [ + "Pig's Resilience: Resistance to poison damage and advantage on saving throws against poison.", + "Noble Heart: Proficiency in Insight.", + "Forager: Can find food and fresh water for himself and up to five others each day in suitable environments.", + "Fighting Style (Defense): +1 bonus to AC while wearing armor.", + "Second Wind: Use a bonus action to regain hit points equal to 1d10 + your fighter level once per short or long rest.", + ], + equipment: [ + "Guandao (1d12 slashing damage, heavy, two-handed)", + "Chain mail (AC 16)", + "Adventurer's pack", + "Flask of strong liquor", + "Signet of his celestial origin", + "Set of common clothes", + ], + combatStats: { + ac: 17, + initiative: 0, + speed: "30 feet", + hitPoints: 13, + }, + }, +]; + +export default characters; diff --git a/packages/nextjs/components/sessionForm.tsx b/packages/nextjs/components/sessionForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/components/sessionList.tsx b/packages/nextjs/components/sessionList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/components/worldForm.tsx b/packages/nextjs/components/worldForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/components/worldList.tsx b/packages/nextjs/components/worldList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 008d4eb..da0a5b3 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -4,6 +4,761 @@ */ import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; -const deployedContracts = {} as const; +const deployedContracts = { + 31337: { + BuyMeACeptor: { + address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + abi: [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "InsufficientFunds", + type: "error", + }, + { + inputs: [ + { + internalType: "string", + name: "message", + type: "string", + }, + ], + name: "InvalidArguments", + type: "error", + }, + { + inputs: [], + name: "OnlyOwner", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "buyer", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "price", + type: "uint256", + }, + ], + name: "BuyMeACeptorWorldEvent", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "gameMasterAddress", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "time", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "vibe", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "gameMasterName", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "gameMasterTwitterHandle", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "description", + type: "string", + }, + ], + name: "NewWorld", + type: "event", + }, + { + inputs: [ + { + internalType: "string", + name: "vibe", + type: "string", + }, + { + internalType: "string", + name: "gameMasterName", + type: "string", + }, + { + internalType: "string", + name: "gameMasterTwitterHandle", + type: "string", + }, + { + internalType: "string", + name: "description", + type: "string", + }, + ], + name: "buyWorld", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "getWorlds", + outputs: [ + { + components: [ + { + internalType: "string", + name: "vibe", + type: "string", + }, + { + internalType: "string", + name: "gameMasterName", + type: "string", + }, + { + internalType: "string", + name: "gameMasterTwitterHandle", + type: "string", + }, + { + internalType: "string", + name: "description", + type: "string", + }, + { + internalType: "uint256", + name: "time", + type: "uint256", + }, + { + internalType: "address", + name: "gameMasterAddress", + type: "address", + }, + ], + internalType: "struct World[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "string", + name: "description", + type: "string", + }, + ], + name: "modifyWorldDescription", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address payable", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "price", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "removeWorld", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "withdrawTips", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "worlds", + outputs: [ + { + internalType: "string", + name: "vibe", + type: "string", + }, + { + internalType: "string", + name: "gameMasterName", + type: "string", + }, + { + internalType: "string", + name: "gameMasterTwitterHandle", + type: "string", + }, + { + internalType: "string", + name: "description", + type: "string", + }, + { + internalType: "uint256", + name: "time", + type: "uint256", + }, + { + internalType: "address", + name: "gameMasterAddress", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, + ], + inheritedFunctions: {}, + }, + BuyMeACoffee: { + address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + abi: [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "InsufficientFunds", + type: "error", + }, + { + inputs: [ + { + internalType: "string", + name: "message", + type: "string", + }, + ], + name: "InvalidArguments", + type: "error", + }, + { + inputs: [], + name: "OnlyOwner", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "buyer", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "price", + type: "uint256", + }, + ], + name: "BuyMeACoffeeEvent", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "userAddress", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "time", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "numCoffees", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "userName", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "twitterHandle", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "message", + type: "string", + }, + ], + name: "NewMemo", + type: "event", + }, + { + inputs: [ + { + internalType: "uint256", + name: "numCoffees", + type: "uint256", + }, + { + internalType: "string", + name: "userName", + type: "string", + }, + { + internalType: "string", + name: "twitterHandle", + type: "string", + }, + { + internalType: "string", + name: "message", + type: "string", + }, + ], + name: "buyCoffee", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "getMemos", + outputs: [ + { + components: [ + { + internalType: "uint256", + name: "numCoffees", + type: "uint256", + }, + { + internalType: "string", + name: "userName", + type: "string", + }, + { + internalType: "string", + name: "twitterHandle", + type: "string", + }, + { + internalType: "string", + name: "message", + type: "string", + }, + { + internalType: "uint256", + name: "time", + type: "uint256", + }, + { + internalType: "address", + name: "userAddress", + type: "address", + }, + ], + internalType: "struct Memo[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "memos", + outputs: [ + { + internalType: "uint256", + name: "numCoffees", + type: "uint256", + }, + { + internalType: "string", + name: "userName", + type: "string", + }, + { + internalType: "string", + name: "twitterHandle", + type: "string", + }, + { + internalType: "string", + name: "message", + type: "string", + }, + { + internalType: "uint256", + name: "time", + type: "uint256", + }, + { + internalType: "address", + name: "userAddress", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "string", + name: "message", + type: "string", + }, + ], + name: "modifyMemoMessage", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address payable", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "price", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "removeMemo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "withdrawTips", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, + ], + inheritedFunctions: {}, + }, + YourContract: { + address: "0x5FbDB2315678afecb367f032d93F642f64180aa3", + abi: [ + { + inputs: [ + { + internalType: "address", + name: "_owner", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "greetingSetter", + type: "address", + }, + { + indexed: false, + internalType: "string", + name: "newGreeting", + type: "string", + }, + { + indexed: false, + internalType: "bool", + name: "premium", + type: "bool", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "GreetingChange", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "number", + type: "uint256", + }, + ], + name: "NumberUpdated", + type: "event", + }, + { + inputs: [], + name: "greeting", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "premium", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "_newGreeting", + type: "string", + }, + ], + name: "setGreeting", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_number", + type: "uint256", + }, + ], + name: "storeNumber", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "totalCounter", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "userGreetingCounter", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "userNumbers", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, + ], + inheritedFunctions: {}, + }, + }, +} as const; export default deployedContracts satisfies GenericContractsDeclaration; diff --git a/packages/nextjs/next-env.d.ts b/packages/nextjs/next-env.d.ts index 4f11a03..fd36f94 100644 --- a/packages/nextjs/next-env.d.ts +++ b/packages/nextjs/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/nextjs/pages/campaigns/campaignDetails.tsx b/packages/nextjs/pages/campaigns/campaignDetails.tsx new file mode 100644 index 0000000..79f394d --- /dev/null +++ b/packages/nextjs/pages/campaigns/campaignDetails.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import { Campaign } from "../../types/types"; + +const CampaignDetails = () => { + const router = useRouter(); + const { externalId } = router.query; + const [campaign, setCampaign] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + setLoading(true); + const fetchCampaignDetails = async () => { + if (!externalId) { + setLoading(false); + return; + } + try { + const response = await fetch(`/api/campaigns/${externalId}`); + if (!response.ok) { + throw new Error("Failed to fetch campaign details"); + } + const data = await response.json(); + setCampaign(data); + } catch (error) { + if (error instanceof Error) { + setError(error.message); + } else { + setError("An unexpected error occurred"); + } + } finally { + setLoading(false); + } + }; + + fetchCampaignDetails(); + }, [externalId]); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + if (!campaign) return
No campaign found
; + + return ( +
+

{campaign.name}

+

{campaign.description}

+

{campaign.worldId}

+

{campaign.numCharacters}

+

{campaign.frequency}

+

{campaign.scheduledSessions.map(session => session.toLocaleString()).join(", ")}

+
+ ); +}; + +export default CampaignDetails; diff --git a/packages/nextjs/pages/campaigns/createCampaign.tsx b/packages/nextjs/pages/campaigns/createCampaign.tsx new file mode 100644 index 0000000..836e2a2 --- /dev/null +++ b/packages/nextjs/pages/campaigns/createCampaign.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import { User, World } from "../../types/types"; +import { useSession } from "next-auth/react"; + +const CreateCampaign = () => { + const { data: sessionData } = useSession(); + const user = sessionData?.user as User | undefined; // Cast session data to your User type + const router = useRouter(); + const [worlds, setWorlds] = useState([]); + + const [formState, setFormState] = useState({ + name: "", + description: "", + worldId: "", + ccId: user?.ccId || "", // Initially empty, will be set when the component mounts and we have the session data + numCharacters: 0, + numGmMadeCharacters: 0, + numPlayerMadeCharacters: 0, + sessionZero: false, + frequency: "one_shot", + scheduledSessions: [], + notifications: [], + }); + + useEffect(() => { + // When the component mounts, set the ccId if the user is authenticated + if (user?.ccId) { + setFormState(prevState => ({ + ...prevState, + ccId: user.ccId || "", // Provide a default empty string if user.ccId is undefined + })); + } + // Fetch worlds and check if the user is the owner or has permissions + fetch("/api/worlds") + .then(response => response.json()) + .then((worldsData: World[]) => { + // Add explicit type annotation here + const userWorlds = worldsData.filter( + (world: World) => world.ccId === user?.ccId || world.permissions?.includes(user?.ccId ?? ""), + ); + setWorlds(userWorlds); + }) + .catch(error => { + // Handle errors more specifically here + console.error("Failed to fetch worlds:", error); + // Consider setting an error state and displaying it to the user + }); + }, [user]); + + useEffect(() => { + // Fetch worlds from the backend and set them in state + fetch("/api/worlds") + .then(response => response.json()) + .then(setWorlds) + .catch(console.error); // Handle errors appropriately + }, []); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormState(prev => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await fetch("/api/campaigns", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formState), + }); + if (!response.ok) throw new Error("Campaign creation failed"); + // Redirect to a success page or the campaign details page + router.push("/campaigns/success"); + } catch (error) { + console.error("Failed to create campaign:", error); + } + }; + + return ( +
+ {worlds.length > 0 ? ( +
+ +