Skip to content

Commit

Permalink
feat: add character and game commands
Browse files Browse the repository at this point in the history
  • Loading branch information
SomethingSexy committed Nov 12, 2024
1 parent 345e0ec commit b2fe88c
Show file tree
Hide file tree
Showing 50 changed files with 1,040 additions and 190 deletions.
453 changes: 442 additions & 11 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"workspaces": [
"packages/bot",
"packages/desktop"
"packages/browser-vite"
],
"scripts": {
"build": "npm run build -ws",
Expand Down
8 changes: 5 additions & 3 deletions packages/bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"pretty:fix": "npx prettier . --write --ignore-unknown",
"release": "npx semantic-release",
"start": "node --env-file=.env ./dist/index.js",
"test": "exit 0"
"test": "npx vitest run"
},
"husky": {
"hooks": {
Expand Down Expand Up @@ -41,7 +41,8 @@
"discord.js": "^14.16.3",
"fs-extra": "^10.0.0",
"joi": "^17.4.0",
"neverthrow": "^8.1.1"
"neverthrow": "^8.1.1",
"uuid": "^11.0.3"
},
"devDependencies": {
"@swc-node/jest": "^1.2.1",
Expand All @@ -54,6 +55,7 @@
"nodemon": "^3.1.7",
"prettier": "^2.2.1",
"pretty-quick": "^1.10.0",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"vitest": "^2.1.4"
}
}
124 changes: 48 additions & 76 deletions packages/bot/src/entity/character.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,57 @@
import { type Result, err, ok } from 'neverthrow'
import hapi, { ObjectSchema } from 'joi'

const { object, string } = hapi
export interface IAttribute {
name: string
value: 0 | 1 | 2 | 3 | 4 | 5
}

export interface ISkill {
name: string
value: 0 | 1 | 2 | 3 | 4 | 5
}

export interface IStats {
health: number
}

export interface IVampireStats extends IStats {
willpower: number
hunger: number
humanity: number
import Ajv, { type Schema } from 'ajv'
import { type Validate, validate } from './validator.js'
import { type Result } from 'neverthrow'
import { v4 } from 'uuid'

const ajv = new Ajv()

const characterSchema: Schema = {
type: 'object',
properties: {
name: {
type: 'string',
},
referenceId: {
type: 'string',
},
referenceType: {
type: 'string',
enum: ['discord'],
},
},
required: ['name'],
}

export type IHumanStats = IStats
const validateCharacter = ajv.compile(characterSchema)
const validateCharacterEntity = validate(validateCharacter)

export interface IVampireCharacteristics {
sire: string
predator: string
clan: string
generation: string
}

export type Splat = 'vampire' | 'human'

export interface ICharacter<Stats extends IStats> {
export interface Character {
id: string
chronicleId: string
name: string
concept: string
ambition: string
desire: string
splat: Splat
// TODO: Do we want these explicit?
attributes: {
[name: string]: IAttribute
}
// TODO: Do we want these explicit?
skills: {
[name: string]: ISkill
}
stats: Stats
created: string
modified: string
referenceId: string
referenceType: string
}

export type Vampire = ICharacter<IVampireStats>

export type Human = ICharacter<IHumanStats>

export type Character = Vampire | Human

// This is only what is required to create, we will probably want another validation
// for locking a character in?
export const Validation = object({
name: string().required(),
concept: string(),
ambition: string(),
desire: string(),
splat: string().valid('vampire', 'human').required(),
export type CreateCharacterEntity = Pick<
Character,
'id' | 'name' | 'referenceId' | 'referenceType'
>

export type CreateCharacterRequest = Pick<
CreateCharacterEntity,
'name' | 'referenceId' | 'referenceType'
>

const addId = (character: CreateCharacterRequest): CreateCharacterEntity => ({
...character,
id: character.referenceId
? `${character.referenceType}:${character.referenceId}`
: // TODO: WE need to remove this from here and pass it in
`uuid:${v4()}`,
})

export type CreateCharacterEntity = Pick<Vampire | Human, 'name' | 'splat'>

export const makeCreateCharacterEntity =
(schema: ObjectSchema) =>
(c: CreateCharacterEntity): Result<CreateCharacterEntity, string> => {
const { error, value } = schema.validate(c)
return error ? err(error.message) : ok(value)
}
const makeCharacterEntity =
(validate: Validate) =>
(character: CreateCharacterRequest): Result<CreateCharacterEntity, string> =>
validate(character).map(addId)

/**
* Creates a Character entity. A character is any player or non-player character in the game.
*/
export const createCharacterEntity = makeCreateCharacterEntity(Validation)
export const characterEntity = makeCharacterEntity(validateCharacterEntity)
28 changes: 13 additions & 15 deletions packages/bot/src/entity/chronicle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Ajv, { Schema } from 'ajv'
import { type Result, err, ok } from 'neverthrow'
import { type Validate, validate } from './validator.js'
import { type Result } from 'neverthrow'

const ajv = new Ajv()

Expand Down Expand Up @@ -73,25 +74,22 @@ export type GetChronicleRequest = Pick<
'referenceId' | 'referenceType'
>

export const makeCreateChronicleEntity = (
c: CreateChronicleRequest
): Result<CreateChronicleEntity, string> => {
const valid = validateChronicle(c)
const validateChronicleEntity = validate(validateChronicle)

const chronicleWithId = {
...c,
id: buildChronicleId(c),
}
export const makeChronicleEntity =
(validator: Validate) =>
(c: CreateChronicleRequest): Result<CreateChronicleEntity, string> =>
validator(c).map(addId)

return !valid
? err(validateChronicle.errors.map((e) => e.message).join())
: ok(chronicleWithId)
}

export const createChronicleEntity = makeCreateChronicleEntity
export const chronicleEntity = makeChronicleEntity(validateChronicleEntity)

export const buildChronicleId = ({
referenceId,
referenceType,
}: Pick<Chronicle, 'referenceId' | 'referenceType'>): string =>
`${referenceType}:${referenceId}`

const addId = (chronicle: CreateChronicleRequest): CreateChronicleEntity => ({
...chronicle,
id: buildChronicleId(chronicle),
})
22 changes: 12 additions & 10 deletions packages/bot/src/entity/validator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Schema } from 'joi'
import { type Result, err, ok } from 'neverthrow'
import { type ValidateFunction } from 'ajv'

export const validator = (schema: Schema) => (payload: object) => {
const { error } = schema.validate(payload)
if (error) {
const message = error.details.map((el) => el.message).join('\n')
return {
error: message,
}
export type Validate = <T>(payload: T) => Result<T, string>

export const validate =
(validatior: ValidateFunction): Validate =>
(payload) => {
const valid = validatior(payload)

return !valid
? err(validatior.errors.map((e) => e.message).join())
: ok(payload)
}
return true
}
45 changes: 45 additions & 0 deletions packages/bot/src/framework/discord/commands/character/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Need to add ability to make the character public or private
// We will probably want separata commands for setting skill, attribute, discipline-power
// Basically setting one of these to 0-5, 0 would be remove.
// Need live tracking for setting, health, willpower, etc.
// Can we use subcommands to handle setting a skill, attriute,or power?
// If a character is "locked", only an admin should be able to change the character or exp would need to be spent
import { type Gateways } from '../../../../gateway/index.js'
import { type ICommand } from '../../types.js'
import { SlashCommandBuilder } from 'discord.js'
import { characterMessage } from '../../messages/character.js'
import { createCharacter } from '../../../../use-case/create-character.js'

const command: ICommand = {
command: new SlashCommandBuilder()
.setName('add-character')
.setDescription('Add a Player Character or Non-Player Character')
.addStringOption((option) =>
option
.setName('name')
.setDescription('Name of character')
.setRequired(true)
)
.addUserOption((option) =>
option
.setName('target')
.setDescription('Add user target if a Player Character.')
.setRequired(false)
),
execute(interaction, gateway: Gateways) {
// Using a TypeGuard here but not sure it should ever get that far?>
// Might be better to make ICommand a generic
if (!interaction.isChatInputCommand()) {
return
}

const target = interaction.options.getUser('target')

return createCharacter(gateway)({
name: interaction.options.getString('name'),
referenceId: target ? target.id : null,
}).map(characterMessage)
},
}

export default command
Empty file.
Empty file.
Empty file.
Empty file.
22 changes: 22 additions & 0 deletions packages/bot/src/framework/discord/commands/character/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type Gateways } from '../../../../gateway/index.js'
import { type ICommand } from '../../types'
import { SlashCommandBuilder } from 'discord.js'
import { charactersMessage } from '../../messages/character.js'
import { listCharacters } from '../../../../use-case/list-characters.js'

const command: ICommand = {
command: new SlashCommandBuilder()
.setName('list-characters')
.setDescription('Retrieves a list of characters.'),
execute(interaction, gateways: Gateways) {
// Using a TypeGuard here but not sure it should ever get that far?>
// Might be better to make ICommand a generic
if (!interaction.isChatInputCommand()) {
return
}

return listCharacters(gateways)().map(charactersMessage)
},
}

export default command
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Can we use subcommands to handle setting a skill, attriute,or power?
// If a character is "locked", only an admin should be able to change the character or exp would need to be spent
import { type ICommand } from '../../types'
import { SlashCommandBuilder } from 'discord.js'
import { okAsync } from 'neverthrow'
import { v4 } from 'uuid'

const command: ICommand = {
command: new SlashCommandBuilder()
.setName('set-character-property')
.setDescription('Set an attribute, skill, or power on a character')
.addSubcommand((subcommand) =>
subcommand
.setName('skill')
.setDescription('Set the skill of a character')
.addUserOption((option) =>
option.setName('target').setDescription('The user')
)
)
.addSubcommand((subcommand) =>
subcommand
.setName('attribute')
.setDescription('Set the attribute of a character')
.addStringOption((option) =>
option
.setName('character')
.setDescription('Character to apply the attribute change to')
.setAutocomplete(true)
)
)
.addSubcommand((subcommand) =>
subcommand.setName('power').setDescription('Set the power of a character')
),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
execute(interaction, _) {
// Using a TypeGuard here but not sure it should ever get that far?>
// Might be better to make ICommand a generic
if (!interaction.isChatInputCommand()) {
return
}

const subcommand = interaction.options.getSubcommand()

// if (subcommand === 'skill') {
// } else if (subcommand === 'attribute') {
// } else if (subcommand === 'power') {
// }

return okAsync(
`Valid command ${subcommand} was passed. ${interaction.options.getString(
'character'
)}`
)
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
autocomplete(interaction, _) {
if (!interaction.isAutocomplete()) {
return
}
const focusedValue = interaction.options.getFocused()
console.log('Search value', focusedValue)
return [
{ name: 'Foo', value: v4() },
{ name: 'Bar', value: v4() },
]
},
}

export default command
4 changes: 2 additions & 2 deletions packages/bot/src/framework/discord/commands/game/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ChronicleGateway } from '../../../../gateway/chronicle/types'
import { type Gateways } from '../../../../gateway/index.js'
import { type ICommand } from '../../types'
import { SlashCommandBuilder } from 'discord.js'
import { chronicleMessage } from '../../messages/chronicle.js'
Expand Down Expand Up @@ -28,7 +28,7 @@ const command: ICommand = {
.setRequired(true)
.addChoices({ name: 'v5', value: 'v5' })
),
execute(interaction, chronicleGateway: ChronicleGateway) {
execute(interaction, chronicleGateway: Gateways) {
// Using a TypeGuard here but not sure it should ever get that far?>
// Might be better to make ICommand a generic
if (!interaction.isChatInputCommand()) {
Expand Down
Loading

0 comments on commit b2fe88c

Please sign in to comment.