Initial commit

This commit is contained in:
2024-06-05 21:13:55 -04:00
parent 7c21aea7e3
commit 30e9a3efcb
27 changed files with 1131 additions and 88 deletions
+15 -2
View File
@@ -1,10 +1,23 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GamesModule } from './games/games.module';
import { UsersModule } from './users/users.module';
import { CardsService } from './cards/cards.service';
@Module({
imports: [],
imports: [
ConfigModule.forRoot(),
MongooseModule.forRoot('mongodb://localhost:27017', {
auth: { password: 'example', username: 'root' },
dbName: 'heron',
}),
GamesModule,
UsersModule,
],
controllers: [AppController],
providers: [AppService],
providers: [AppService, CardsService],
})
export class AppModule {}
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Card, CardSchema } from './schemas/cards.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: Card.name, schema: CardSchema }]),
],
exports: [MongooseModule],
})
export class CardsModule {
schema = CardSchema;
}
+18
View File
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CardsService } from './cards.service';
describe('CardsService', () => {
let service: CardsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CardsService],
}).compile();
service = module.get<CardsService>(CardsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+26
View File
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import path from 'node:path';
import fs from 'node:fs/promises';
@Injectable()
export class CardsService {
async loadCards(): Promise<void> {
const cardPath = path.join(path.resolve(), 'cards');
const files = await fs.readdir(cardPath);
const processed = await Promise.all(
files.map(async (f) => {
return [
path.parse(f).name,
(
await fs
.readFile(path.join(cardPath, f))
.then((e) => JSON.parse(e.toString()))
).cards,
];
}),
);
const packs = Object.fromEntries(processed);
console.log(packs);
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Schema, SchemaFactory } from '@nestjs/mongoose';
import { CardFaction, CardRole, CardType, EffectParser } from './cards.types';
@Schema()
export class Card {
cardId: number;
role: CardRole;
cardType: CardType;
faction: CardFaction;
name: string;
initAmount: number;
cost?: number;
defense?: number;
guard?: boolean;
effects: EffectParser[];
}
export const CardSchema = SchemaFactory.createForClass(Card);
+102
View File
@@ -0,0 +1,102 @@
/**
*
*/
export enum CardRole {
PERSONAL = 'personal',
MARKET = 'market',
FIRE_GEM = 'fire_gem',
}
/**
* Playable card types
*/
export enum CardType {
HERO = 'hero',
HERO_ABILITY = 'hero_ability',
CURRENCY = 'currency',
WEAPON = 'weapon',
CHAMPION = 'champion',
ACTION = 'action',
ITEM = 'item',
}
/**
* Playable card factions
*/
export enum CardFaction {
BASE = 'base',
BLUE = 'blue',
RED = 'red',
GREEN = 'green',
YELLOW = 'yellow',
}
export enum CardEffects {
GOLD = 'gold',
DAMAGE = 'damage',
HEAL = 'heal',
PREPARE = 'prepare',
STUN = 'stun',
SACRIFICE = 'sacrifice',
DRAW = 'draw',
DRAW_AND_DISCARD = 'draw_and_discard',
MAKE_DISCARD = 'make_discard',
RESTACK_DISCARDED_CHAMPION = 'restack_discarded_champion',
RESTACK_DISCARDED_CARD = 'restack_discarded_card',
STACK_NEXT_ACTION_BOUGHT = 'stack_next_action_bought',
STACK_NEXT_CARD_BOUGHT = 'stack_next_card_bought',
PLAY_NEXT_CARD_BOUGHT = 'play_next_card_bought',
}
export enum Per {
CHAMPION = 'champion',
OTHER_CHAMPION = 'other_champion',
OTHER_GUARD = 'other_guard',
CARD_OF_SAME_FACTION = 'card_of_same_faction',
OTHER_CARD_OF_SAME_FACTION = 'other_card_of_same_faction',
}
export enum EffectType {
CHAMPION_ACTION = 'champion_action',
SUICIDE = 'suicide',
FACTION_COMBO = 'faction_combo',
}
export enum EffectTypeSpecial {
CHAMPION_AMOUNT = 'champion_amount',
TOTAL_DAMAGE = 'total_damage',
CARD_AMOUNT = 'card_amount',
ACTION_AMOUNT = 'action_amount',
}
export type EffectTypeVerbose = {
id: EffectType | EffectTypeSpecial;
faction?: string;
amount?: number;
card_id?: number;
};
export type EffectParser = {
effect: CardEffects;
amount: number;
times?: number;
sub_effects?: EffectParser[];
effect_type?: EffectType | EffectTypeVerbose;
per?: Per;
mutex?: number;
};
export type CardParser = {
id: number;
role: CardRole;
card_type: CardType;
faction: CardFaction;
name: string;
init_amount: number;
cost?: number;
amount: number;
defense?: number;
guard?: boolean;
effects: EffectParser[];
};
+1
View File
@@ -0,0 +1 @@
export class CreateGameDto {}
+4
View File
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateGameDto } from './create-game.dto';
export class UpdateGameDto extends PartialType(CreateGameDto) {}
+20
View File
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GamesController } from './games.controller';
import { GamesService } from './games.service';
describe('GamesController', () => {
let controller: GamesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GamesController],
providers: [GamesService],
}).compile();
controller = module.get<GamesController>(GamesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+42
View File
@@ -0,0 +1,42 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { GamesService } from './games.service';
import { CreateGameDto } from './dto/create-game.dto';
import { UpdateGameDto } from './dto/update-game.dto';
@Controller('games')
export class GamesController {
constructor(private readonly gamesService: GamesService) {}
@Post()
create() {
return this.gamesService.create();
}
@Get()
findAll() {
return this.gamesService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.gamesService.findOne(id);
}
// @Patch(':id')
// update(@Param('id') id: string, @Body() updateGameDto: UpdateGameDto) {
// return this.gamesService.update(+id, updateGameDto);
// }
@Delete(':id')
remove(@Param('id') id: string) {
return this.gamesService.remove(+id);
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { GamesService } from './games.service';
import { GamesController } from './games.controller';
import { Game, GameSchema } from './schemas/game.schema';
import { MongooseModule } from '@nestjs/mongoose';
import { UsersModule } from 'src/users/users.module';
import { CardsModule } from 'src/cards/cards.module';
@Module({
imports: [
CardsModule,
UsersModule,
MongooseModule.forFeature([{ name: Game.name, schema: GameSchema }]),
],
controllers: [GamesController],
providers: [GamesService],
})
export class GamesModule {}
+18
View File
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GamesService } from './games.service';
describe('GamesService', () => {
let service: GamesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GamesService],
}).compile();
service = module.get<GamesService>(GamesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+31
View File
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { CreateGameDto } from './dto/create-game.dto';
import { UpdateGameDto } from './dto/update-game.dto';
import { InjectModel } from '@nestjs/mongoose';
import { Game } from './schemas/game.schema';
import { Model } from 'mongoose';
@Injectable()
export class GamesService {
constructor(@InjectModel(Game.name) private gameModel: Model<Game>) {}
async create(): Promise<Game> {
const createdGame = new this.gameModel();
return createdGame.save();
}
findAll() {
return this.gameModel.find();
}
findOne(id: string) {
return this.gameModel.find({ _id: id }).exec();
}
// update(id: number, updateGameDto: UpdateGameDto) {
// return `This action updates a #${id} game`;
// }
remove(id: number) {
return `This action removes a #${id} game`;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { HydratedDocument } from 'mongoose';
import { Card } from 'src/cards/schemas/cards.schema';
import { User } from 'src/users/schemas/user.entity';
export type GameDocument = HydratedDocument<Game>;
@Schema()
class GameObject {}
@Schema()
export class Team extends GameObject {}
export const TeamSchema = SchemaFactory.createForClass(Team);
@Schema()
export class Container extends GameObject {
@Prop([{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }])
cards: Card[];
}
export const ContainerSchema = SchemaFactory.createForClass(Container);
@Schema()
export class Game extends GameObject {
@Prop([Team])
teams: Team[];
@Prop({ type: ContainerSchema, default: {} })
market: Container;
@Prop({ type: ContainerSchema, default: {} })
marketStack: Container;
@Prop({ type: ContainerSchema, default: {} })
fireGems: Container;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Game.teams' })
currentTurn?: Team;
@Prop({ default: false })
started: boolean;
@Prop({ default: false })
private ended: boolean;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
owner: User;
}
export const GameSchema = SchemaFactory.createForClass(Game);
+21 -1
View File
@@ -1,8 +1,28 @@
import { NestFactory } from '@nestjs/core';
import * as session from 'express-session';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
const config = new DocumentBuilder()
.setTitle('Heron Game')
.setDescription('Heron')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
app.use(
session({
secret: 'my-secret',
resave: false,
saveUninitialized: false,
name: 'heron.session',
}),
);
await app.listen(8765);
}
bootstrap();
+5
View File
@@ -0,0 +1,5 @@
export class CreateUserDto {
method: 'discord' | 'password';
username?: string;
password: string;
}
+20
View File
@@ -0,0 +1,20 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { ApiProperty } from '@nestjs/swagger';
export class LoginDto extends PartialType(CreateUserDto) {
constructor(id: string, global_name: string, avatar: string) {
super();
this.id = id;
this.global_name = global_name;
this.avatar = avatar;
}
@ApiProperty()
id: string;
@ApiProperty()
global_name: string;
@ApiProperty()
avatar: string;
}
+9
View File
@@ -0,0 +1,9 @@
import { Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
@Schema()
export class User {}
export const UserSchema = SchemaFactory.createForClass(User);
+20
View File
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+46
View File
@@ -0,0 +1,46 @@
import {
Controller,
Get,
Query,
Session,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { LoginDto } from './dto/login.dto';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('auth')
@Controller()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get('/login')
async login(
@Query('code') code: string,
@Session() session: Record<string, any>,
) {
const tokenData = await this.usersService.saveDiscordToken(code);
session.tokenData = tokenData;
const user = await this.usersService.useDiscordToken(
tokenData.token_type,
tokenData.token,
);
return user;
}
@Get('/whoami')
async remove(@Session() session: Record<string, any>): Promise<LoginDto> {
if (!session.tokenData) {
throw new HttpException(
"Session doesn't contains auth information",
HttpStatus.UNAUTHORIZED,
);
}
const user = await this.usersService.useDiscordToken(
session.tokenData.token_type,
session.tokenData.token,
);
return user;
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './schemas/user.entity';
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
controllers: [UsersController],
providers: [UsersService],
exports: [MongooseModule],
})
export class UsersModule {
schema = UserSchema;
}
+18
View File
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+63
View File
@@ -0,0 +1,63 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { LoginDto } from './dto/login.dto';
import { URLSearchParams } from 'url';
@Injectable()
export class UsersService {
async saveDiscordToken(code: string) {
const req = await fetch('https://discord.com/api/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENTID,
client_secret: process.env.DISCORD_CLIENTSECRET,
code: code,
grant_type: 'authorization_code',
redirect_uri: process.env.API_ENDPOINT + '/login',
scope: 'identify',
}),
});
if (!req.ok) {
console.error(await req.json());
throw new HttpException(
"Couldn't fetch Discord api",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const data = await req.json();
if (!data.token_type || !data.access_token) {
console.error(data);
throw new HttpException(
'Invalid Discord API response',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const token_type = data.token_type;
const token = data.access_token;
return { token, token_type };
}
async useDiscordToken(token_type: string, token: string) {
const req2 = await fetch('https://discord.com/api/users/@me', {
headers: {
authorization: token_type + ' ' + token,
},
});
if (!req2.ok) {
throw new HttpException(
"Couldn't fetch Discord api",
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const data2 = await req2.json();
return new LoginDto(data2.id, data2.global_name, data2.avatar);
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}