Template
Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
SESSION_SECRET=dev
|
||||||
|
|
||||||
|
CLIENT_ENDPOINT=http://localhost:3000
|
||||||
|
API_ENDPOINT=http://localhost:8765
|
||||||
|
API_PREFIX=/api
|
||||||
|
DISCORD_CLIENTID=
|
||||||
|
DISCORD_CLIENTSECRET=
|
||||||
|
|
||||||
|
PG_ENDPOINT=postgres://127.0.0.1:concept
|
||||||
|
PG_PASSWORD=postgres
|
||||||
|
PG_USER=test
|
||||||
|
|
||||||
|
DEV_MODE=true
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn', // or "error"
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
+401
@@ -0,0 +1,401 @@
|
|||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff:
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
|
||||||
|
# Sensitive or high-churn files:
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.xml
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
|
||||||
|
# Gradle:
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-debug/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin:
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
## File-based project format:
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
## Plugin-specific files:
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
### VisualStudio template
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Visual Studio 2015 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
**/Properties/launchSettings.json
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# CodeRush
|
||||||
|
.cr/
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
### macOS template
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
=======
|
||||||
|
# Local
|
||||||
|
.env
|
||||||
|
dist
|
||||||
|
.webpack
|
||||||
|
.serverless/**/*.zip
|
||||||
|
mongodb-keyfile
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch via NPM",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": ["run", "start:dev", "-b", "swc"],
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**",
|
||||||
|
"${workspaceFolder}/node_modules/**/*.js",
|
||||||
|
"${workspaceFolder}/lib/**/*.js"
|
||||||
|
],
|
||||||
|
"type": "node",
|
||||||
|
"console": "internalConsole",
|
||||||
|
"outputCapture": "std",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"envFile": "${workspaceFolder}/.env"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:lts-alpine AS build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npx nest build
|
||||||
|
|
||||||
|
# étape de production
|
||||||
|
FROM node:lts-alpine AS production-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build-stage /app/dist/ /app
|
||||||
|
COPY --from=build-stage /app/node_modules/ /app/node_modules
|
||||||
|
EXPOSE 8765
|
||||||
|
CMD ["node", "main"]
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
volumes:
|
||||||
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=test
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true,
|
||||||
|
"plugins": ["@nestjs/swagger"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+10770
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "nest-typescript-starter",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Nest TypeScript starter repository",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.3.2",
|
||||||
|
"@nestjs/config": "^3.2.2",
|
||||||
|
"@nestjs/core": "^10.3.2",
|
||||||
|
"@nestjs/event-emitter": "^2.0.4",
|
||||||
|
"@nestjs/mapped-types": "^2.0.5",
|
||||||
|
"@nestjs/platform-express": "^10.3.2",
|
||||||
|
"@nestjs/swagger": "^7.3.1",
|
||||||
|
"@nestjs/typeorm": "^10.0.2",
|
||||||
|
"connect-mongo": "^5.1.0",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
|
"mongoose-autopopulate": "^1.1.0",
|
||||||
|
"reflect-metadata": "^0.2.1",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^0.3.20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.3.1",
|
||||||
|
"@nestjs/schematics": "^10.1.0",
|
||||||
|
"@nestjs/testing": "^10.3.2",
|
||||||
|
"@swc/cli": "^0.3.9",
|
||||||
|
"@swc/core": "^1.4.0",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/express-session": "^1.18.0",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^20.11.16",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^6.3.4",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getHello', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
const appController = app.get(AppController);
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiTags('admin')
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
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 { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
ConfigModule.forRoot(),
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type:"postgres",
|
||||||
|
url: process.env.PG_ENDPOINT,
|
||||||
|
password: process.env.PG_PASSWORD,
|
||||||
|
username: process.env.PG_USER,
|
||||||
|
synchronize: process.env.DEV_MODE=="true"
|
||||||
|
}),
|
||||||
|
GamesModule,
|
||||||
|
UsersModule,
|
||||||
|
],
|
||||||
|
// controllers: [AppController],
|
||||||
|
// providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Session,
|
||||||
|
Sse,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { LobbyService } from '../services/games.lobby.service';
|
||||||
|
import { AuthGuard } from '../guards/auth.guard';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { StartingDeckDto } from '../dto/update-game.dto';
|
||||||
|
import { Game } from '../schemas/game.schema';
|
||||||
|
import mongoose, { Document } from 'mongoose';
|
||||||
|
import { GamePipe, GameStartedPipe } from '../pipe/game.pipe';
|
||||||
|
|
||||||
|
import { Observable, fromEvent, map } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { GameService } from '../services/games.service';
|
||||||
|
import { getCurrentTeam, getPlayer, isInTeam, WithTransaction } from '../utils';
|
||||||
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
|
import { ReadTeamDto } from '../dto/read-games.dto';
|
||||||
|
|
||||||
|
@ApiTags('game')
|
||||||
|
@Controller('games/:id')
|
||||||
|
export class GamesController {
|
||||||
|
constructor(
|
||||||
|
private readonly lobbyService: LobbyService,
|
||||||
|
private readonly gameService: GameService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('changeStartingDeck')
|
||||||
|
@ApiOperation({ summary: 'Pick a different starting deck' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async changeStartingDeck(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
@Body() startingDeckDto: StartingDeckDto,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot do that after the game has started',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const player = getPlayer(session, game);
|
||||||
|
await this.lobbyService.changeStartingDeck(startingDeckDto, player, game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('endTurn')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async endTurn(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Param('id', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentPlayer.discardMarkers || currentPlayer.fuckUMarkers) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You must consume all your discard markers before ending your turn',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.gameService.endTurn(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('leave')
|
||||||
|
@ApiOperation({ summary: "Leave a game that hasn't already started" })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async leave(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return await this.gameService.leaveGame(game, session.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('surrender')
|
||||||
|
@ApiOperation({ summary: "Leave a game that hasn't already started" })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async surrender(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (!game.started) {
|
||||||
|
throw new HttpException('Game has not started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return await this.gameService.leaveGame(game, session.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('kick/:playerId')
|
||||||
|
@ApiOperation({ summary: 'Kick a player' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async kick(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
@Param('playerId') playerId: string,
|
||||||
|
) {
|
||||||
|
if (session.user.userId != game.owner.userId) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You do do not own this game',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await this.gameService.kickPlayer(game, session.user, playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sse('/subscribe')
|
||||||
|
subscribe(
|
||||||
|
@Param('id') id: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@Param('id', GamePipe) _game: Document<Game> & Game,
|
||||||
|
): Observable<{ data: string }> {
|
||||||
|
return fromEvent(this.eventEmitter, 'sse.game.' + id).pipe(
|
||||||
|
map((payload: any) => {
|
||||||
|
return {
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { LobbyController } from './games.lobby.controller';
|
||||||
|
import { LobbyService } from '../services/games.lobby.service';
|
||||||
|
|
||||||
|
describe('GamesController', () => {
|
||||||
|
let controller: LobbyController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [LobbyController],
|
||||||
|
providers: [LobbyService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<LobbyController>(LobbyController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
UseGuards,
|
||||||
|
Session,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Sse,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { LobbyService } from '../services/games.lobby.service';
|
||||||
|
import { AuthGuard } from '../guards/auth.guard';
|
||||||
|
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { ReadGameDto } from '../dto/read-games.dto';
|
||||||
|
import { UpdateGameDto } from '../dto/update-game.dto';
|
||||||
|
import { GamePipe } from '../pipe/game.pipe';
|
||||||
|
import { Game } from '../schemas/game.schema';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
import { Observable, fromEvent, map } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
@ApiTags('lobby')
|
||||||
|
@Controller('games')
|
||||||
|
export class LobbyController {
|
||||||
|
constructor(
|
||||||
|
private readonly lobbyService: LobbyService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create game' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Game successfully created.',
|
||||||
|
type: ReadGameDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'You can only create one game at a time',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 403, description: 'Forbidden.' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async create(@Session() session: Record<string, any>) {
|
||||||
|
const games = await this.lobbyService.findByOwner(session.user._id);
|
||||||
|
if (games) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only create one game at a time',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await this.lobbyService.create(session.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List all games' })
|
||||||
|
findAll() {
|
||||||
|
const games = this.lobbyService.findAll();
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sse('/subscribe')
|
||||||
|
subscribe(): Observable<{ data: string }> {
|
||||||
|
return fromEvent(this.eventEmitter, 'sse.lobby').pipe(
|
||||||
|
map((payload) => {
|
||||||
|
return {
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get game information' })
|
||||||
|
async findOne(@Param('id') id: string, @Param('id', GamePipe) game: Game) {
|
||||||
|
return new ReadGameDto(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Edit game settings' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Body() updateGameDto: UpdateGameDto,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.owner._id.toString() != session.user._id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only edit games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.lobbyService.update(game, updateGameDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Delete game' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async remove(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.owner._id.toString() != session.user._id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only delete games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const deleted = await this.lobbyService.remove(game);
|
||||||
|
if (deleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/start')
|
||||||
|
@ApiOperation({ summary: 'Start game' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Game successfully started.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'Bad Request',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 403,
|
||||||
|
description: 'You can only start games you own',
|
||||||
|
})
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async start(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (game.teams.length < 2 && !process.env.DEV_MODE) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Not enough teams to start the game',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (game.owner._id.toString() != session.user._id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only start games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.lobbyService.start(game);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/join')
|
||||||
|
@ApiOperation({ summary: 'Join game' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async join(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
game.teams
|
||||||
|
.flatMap((t) => t.players)
|
||||||
|
.some((p) => p.user.userId == session.user.userId)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You are already in this game',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await this.lobbyService.joinGame(game, session.user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Session,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '../guards/auth.guard';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Game, Player } from '../schemas/game.schema';
|
||||||
|
import { Document, Model } from 'mongoose';
|
||||||
|
import { GameStartedPipe } from '../pipe/game.pipe';
|
||||||
|
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { TurnService } from '../services/turn.service';
|
||||||
|
import {
|
||||||
|
getPlayer,
|
||||||
|
getCurrentTeam,
|
||||||
|
isInTeam,
|
||||||
|
findUser,
|
||||||
|
findPlayer,
|
||||||
|
} from '../utils';
|
||||||
|
import { CardPipe } from '../pipe/card.pipe';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { EffectPipe } from '../pipe/effect.pipe';
|
||||||
|
import { Effect } from 'src/cards/schemas/effect.schema';
|
||||||
|
import { GameService } from '../services/games.service';
|
||||||
|
import { CardEffects, CardType } from 'src/cards/schemas/cards.types';
|
||||||
|
import { UseTokenDTO } from '../dto/use-token-dto';
|
||||||
|
import { DamagePlayerDTO } from '../dto/damage-player-dto';
|
||||||
|
import { DamageChampionDTO } from '../dto/damage-champion-dto';
|
||||||
|
import { equal } from 'assert';
|
||||||
|
|
||||||
|
@ApiTags('turn')
|
||||||
|
@Controller('games/:gameId/turn')
|
||||||
|
export class TurnController {
|
||||||
|
constructor(
|
||||||
|
private readonly turnService: TurnService,
|
||||||
|
private readonly gameService: GameService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('lockCard/:cardId')
|
||||||
|
@ApiOperation({ summary: 'Lock a card' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async lockCard(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('cardId') _cardId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Param('cardId', CardPipe) card: Document<Card> & Card,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentPlayer.discardMarkers || currentPlayer.fuckUMarkers) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot lock a card while having discard markers',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (game.currentTurn.cardsLocked.some((c) => c._id.equals(card._id))) {
|
||||||
|
throw new HttpException('Card already locked', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (!currentPlayer.board.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Card not found in player board',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.turnService.lockCard(game, currentPlayer, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('buyCard/:cardId')
|
||||||
|
@ApiOperation({ summary: 'Buys a card from the market' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async buyCard(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('cardId') _cardId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Param('cardId', CardPipe) card: Document<Card> & Card,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentPlayer.discardMarkers || currentPlayer.fuckUMarkers) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot buy a card while having discard markers',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!game.market.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Card not in current game market',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (game.currentTurn.gold < card.cost) {
|
||||||
|
throw new HttpException('Not enough gold', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
await this.turnService.buyGeneric(
|
||||||
|
game,
|
||||||
|
card,
|
||||||
|
game.market,
|
||||||
|
currentPlayer.discard,
|
||||||
|
);
|
||||||
|
await this.gameService.fillMarket(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('buyGem')
|
||||||
|
@ApiOperation({ summary: 'Buys a gem from the market' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async buyGem(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentPlayer.discardMarkers || currentPlayer.fuckUMarkers) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot buy a card while having discard markers',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (game.fireGems.cards.length < 1) {
|
||||||
|
throw new HttpException('No Gems left', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
const targetCard = game.fireGems.cards[0];
|
||||||
|
await this.turnService.buyGeneric(
|
||||||
|
game,
|
||||||
|
targetCard,
|
||||||
|
game.fireGems,
|
||||||
|
currentPlayer.discard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('lockEffect/:effectId')
|
||||||
|
@ApiOperation({ summary: 'Lock an effect' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async lockEffect(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('effectId') _effectId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Param('effectId', EffectPipe) effect: Document<Effect> & Effect,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentPlayer.discardMarkers || currentPlayer.fuckUMarkers) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot lock an effect while having discard markers',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (game.currentTurn.lockedEffects.some((e) => e._id.equals(effect._id))) {
|
||||||
|
throw new HttpException('Effect already locked', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
const card = (await effect.populate('card')).card;
|
||||||
|
if (!currentPlayer.board.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Card not found in player board',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!game.currentTurn.cardsLocked.some((c) => c._id.equals(card._id))) {
|
||||||
|
throw new HttpException('Card not locked', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.turnService.lockEffect(game, currentPlayer, effect, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('makeDiscard/:playerId')
|
||||||
|
@ApiOperation({ summary: 'Use a token to make discard a player' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async makeDiscard(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('playerId') _playerId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPlayer = findPlayer(_playerId, game);
|
||||||
|
if (isInTeam(currentTeam, targetPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot target players in the same team as yours',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
targetPlayer.fuckUMarkers + targetPlayer.discardMarkers >=
|
||||||
|
targetPlayer.hand.cards.length
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot add more discard markers to this player',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const targetToken = game.currentTurn.tokens.find(
|
||||||
|
(t) => t.effect == CardEffects.MAKE_DISCARD,
|
||||||
|
);
|
||||||
|
if (!targetToken) {
|
||||||
|
throw new HttpException('Token not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
await this.turnService.addFuckUMarker(game, targetToken, targetPlayer);
|
||||||
|
const index = game.currentTurn.tokens.findIndex((t) =>
|
||||||
|
t._id.equals(targetToken._id),
|
||||||
|
);
|
||||||
|
game.currentTurn.tokens.splice(index, 1);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.useToken',
|
||||||
|
tokenId: targetToken._id.toHexString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('useToken/:tokenId')
|
||||||
|
@ApiOperation({ summary: 'Use a token' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async useToken(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('tokenId') _tokenId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Param('tokenId', EffectPipe) token: Document<Effect> & Effect,
|
||||||
|
@Body() useTokenDto: UseTokenDTO,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!game.currentTurn.tokens.some((t) => t._id.equals(token._id))) {
|
||||||
|
throw new HttpException('Token not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enemyTargetTokens = [
|
||||||
|
CardEffects.STUN,
|
||||||
|
CardEffects.CONTROL_OPPOSING_CHAMPION_PASSIVE,
|
||||||
|
];
|
||||||
|
|
||||||
|
const marketTokens = [
|
||||||
|
CardEffects.STACK_NEXT_ACTION_BOUGHT,
|
||||||
|
CardEffects.STACK_NEXT_CARD_BOUGHT,
|
||||||
|
CardEffects.PLAY_NEXT_CARD_BOUGHT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const discardTokens = [
|
||||||
|
CardEffects.RESTACK_DISCARDED_ACTION,
|
||||||
|
CardEffects.RESTACK_DISCARDED_CARD,
|
||||||
|
CardEffects.RESTACK_DISCARDED_CHAMPION,
|
||||||
|
CardEffects.SACRIFICE,
|
||||||
|
];
|
||||||
|
|
||||||
|
let targetCard: Card;
|
||||||
|
let targetPlayer = currentPlayer;
|
||||||
|
if (enemyTargetTokens.includes(token.effect)) {
|
||||||
|
targetPlayer = findPlayer(useTokenDto.playerId, game);
|
||||||
|
targetCard = targetPlayer.board.cards.find((c) =>
|
||||||
|
c._id.equals(useTokenDto.cardId),
|
||||||
|
);
|
||||||
|
} else if (marketTokens.includes(token.effect)) {
|
||||||
|
targetCard =
|
||||||
|
game.market.cards.find((c) => c._id.equals(useTokenDto.cardId)) ??
|
||||||
|
game.fireGems.cards.find((c) => c._id.equals(useTokenDto.cardId));
|
||||||
|
} else if (discardTokens.includes(token.effect)) {
|
||||||
|
targetCard = targetPlayer.discard.cards.find((c) =>
|
||||||
|
c._id.equals(useTokenDto.cardId),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
targetCard = currentPlayer.board.cards.find((c) =>
|
||||||
|
c._id.equals(useTokenDto.cardId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetCard && token.effect == CardEffects.SACRIFICE) {
|
||||||
|
targetCard = currentPlayer.board.cards.find((c) =>
|
||||||
|
c._id.equals(useTokenDto.cardId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.effect != CardEffects.DRAW_AND_DISCARD) {
|
||||||
|
if (currentPlayer.discardMarkers || currentPlayer.fuckUMarkers) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot use a token while having discard markers',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!targetCard) {
|
||||||
|
throw new HttpException('Card not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.turnService.useToken(game, targetPlayer, token, targetCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('damagePlayer/:playerId')
|
||||||
|
@ApiOperation({ summary: 'Consume damage points to damage a player' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async damagePlayer(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('playerId') playerId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Body() damagePlayerDTO: DamagePlayerDTO,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (damagePlayerDTO.amount > game.currentTurn.damage) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot deal more damage than what your current damage points',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPlayer = findPlayer(playerId, game);
|
||||||
|
if (targetPlayer.board.cards.some((c) => c.guard)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot target this player while there is a guard',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.turnService.damagePlayer(
|
||||||
|
game,
|
||||||
|
damagePlayerDTO.amount,
|
||||||
|
currentPlayer,
|
||||||
|
targetPlayer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('damageChampion/:cardId')
|
||||||
|
@ApiOperation({ summary: 'Consume damage points to stun a champion' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async damageChampion(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('cardId') _cardId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Param('cardId', CardPipe) card: Document<Card> & Card,
|
||||||
|
@Body() damageChampion: DamageChampionDTO,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (card.defense > game.currentTurn.damage) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot deal more damage than what your current damage points',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPlayer = findPlayer(damageChampion.playerId, game);
|
||||||
|
if (!targetPlayer) {
|
||||||
|
throw new HttpException('Player not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (card.cardType != CardType.CHAMPION) {
|
||||||
|
throw new HttpException('Card is not a champion', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetPlayer.board.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Champion not found in player board',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!card.guard) {
|
||||||
|
if (targetPlayer.board.cards.some((c) => c.guard)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot target this champion while there is a guard',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.turnService.damageChampion(game, targetPlayer, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('discardCard/:cardId')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Discard a card if you have fuckU or discard markers',
|
||||||
|
})
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async discardCard(
|
||||||
|
@Param('gameId') _gameId: string,
|
||||||
|
@Param('cardId') _cardId: string,
|
||||||
|
@Param('gameId', GameStartedPipe) game: Document<Game> & Game,
|
||||||
|
@Param('cardId', CardPipe) card: Document<Card> & Card,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
|
) {
|
||||||
|
const currentPlayer = getPlayer(session, game);
|
||||||
|
const currentTeam = getCurrentTeam(game);
|
||||||
|
if (!isInTeam(currentTeam, currentPlayer)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'It is not your turn to play',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPlayer.discardMarkers == 0 && currentPlayer.fuckUMarkers == 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You do not have any discard or fuckU markers',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentPlayer.board.cards.some((c) => c._id.equals(_cardId))) {
|
||||||
|
throw new HttpException('Card not found in board', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.turnService.discardCard(game, currentPlayer, card);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { LobbyService } from './services/games.lobby.service';
|
||||||
|
import { LobbyController } from './controllers/games.lobby.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';
|
||||||
|
import { GamesController } from './controllers/games.controller';
|
||||||
|
import { GameService } from './services/games.service';
|
||||||
|
import { TurnService } from './services/turn.service';
|
||||||
|
import { TurnController } from './controllers/turn.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
CardsModule,
|
||||||
|
UsersModule,
|
||||||
|
MongooseModule.forFeature([{ name: Game.name, schema: GameSchema }]),
|
||||||
|
],
|
||||||
|
controllers: [LobbyController, GamesController, TurnController],
|
||||||
|
providers: [LobbyService, GameService, TurnService],
|
||||||
|
})
|
||||||
|
export class GamesModule {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
const http = context.switchToHttp();
|
||||||
|
const request = http.getRequest<Request>();
|
||||||
|
if (!(request.session as Record<string, any>).user) {
|
||||||
|
throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
PipeTransform,
|
||||||
|
Injectable,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Game } from '../schemas/game.schema';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GamePipe implements PipeTransform {
|
||||||
|
constructor(@InjectRepository(Game) private gameRepository: Repository<Game>) {}
|
||||||
|
|
||||||
|
async transform(value: any) {
|
||||||
|
const game = await this.gameRepository.findBy({id: value});
|
||||||
|
if (!game) {
|
||||||
|
throw new HttpException('Game not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameStartedPipe implements PipeTransform {
|
||||||
|
constructor(@InjectRepository(Game) private gameRepository: Repository<Game>) {}
|
||||||
|
|
||||||
|
async transform(value: any) {
|
||||||
|
const game = await this.gameRepository.findBy({id: value});
|
||||||
|
if (!game) {
|
||||||
|
throw new HttpException('Game not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (!game.started) {
|
||||||
|
throw new HttpException(
|
||||||
|
'The game must be started',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Column, Entity, JoinTable, ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { Player } from './player.schema';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Game {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
seed: string
|
||||||
|
|
||||||
|
@OneToOne(type=>Player, {onDelete:"CASCADE"})
|
||||||
|
owner: Player
|
||||||
|
|
||||||
|
@OneToMany(type=>Player, player => player.game, {onDelete:"CASCADE"})
|
||||||
|
@JoinTable()
|
||||||
|
players: Player[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
|
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
import { Game } from './game.schema';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Unique(["user, game"])
|
||||||
|
export class Player{
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@ManyToOne(type=>User)
|
||||||
|
user: User
|
||||||
|
|
||||||
|
@ManyToOne(type=>Game)
|
||||||
|
game: Game
|
||||||
|
}
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Game, Player } from '../schemas/game.schema';
|
||||||
|
import mongoose, { Document } from 'mongoose';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import {
|
||||||
|
CardEffects,
|
||||||
|
CardRole,
|
||||||
|
CardType,
|
||||||
|
EffectType,
|
||||||
|
Per,
|
||||||
|
} from 'src/cards/schemas/cards.types';
|
||||||
|
import { Effect, EffectCondition } from 'src/cards/schemas/effect.schema';
|
||||||
|
import { TurnService } from './turn.service';
|
||||||
|
import { GameService } from './games.service';
|
||||||
|
import { ReadEffectDto } from 'src/cards/dto/effect.dto';
|
||||||
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
|
export function isAutoActivable(effect: Effect) {
|
||||||
|
if (
|
||||||
|
!effect.mutex &&
|
||||||
|
(!effect.condition ||
|
||||||
|
( effect.condition.effectId == EffectType.CHAMPION_ACTION && !effect.per))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToken(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
_player: Player,
|
||||||
|
_card: Card,
|
||||||
|
_gameService: GameService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
_session: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
game.currentTurn.tokens.push(effect);
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.addToken',
|
||||||
|
token: new ReadEffectDto(effect),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeToken(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
) {
|
||||||
|
game.currentTurn.tokens.splice(
|
||||||
|
game.currentTurn.tokens.findIndex((t) => t._id.equals(effect._id)),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.removeToken',
|
||||||
|
token: effect._id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const effectMappings = {
|
||||||
|
[CardEffects.GOLD]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
_player: Player,
|
||||||
|
_card: Card,
|
||||||
|
_gameService: GameService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
_session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
game.currentTurn.gold += effect.amount;
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'consumeEffect',
|
||||||
|
effect: effect._id.toHexString(),
|
||||||
|
});
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.updateGold',
|
||||||
|
gold: game.currentTurn.gold,
|
||||||
|
operation: effect.amount,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[CardEffects.DAMAGE]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
_player: Player,
|
||||||
|
_card: Card,
|
||||||
|
_gameService: GameService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
_session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
game.currentTurn.damage += effect.amount;
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'consumeEffect',
|
||||||
|
effect: effect._id.toHexString(),
|
||||||
|
});
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.updateDamage',
|
||||||
|
damage: game.currentTurn.damage,
|
||||||
|
operation: effect.amount,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[CardEffects.HEAL]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
_card: Card,
|
||||||
|
_gameService: GameService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
_session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
const team = game.teams.find((t) =>
|
||||||
|
t.players.some((p) => p._id.equals(player._id)),
|
||||||
|
);
|
||||||
|
team.health += effect.amount;
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'consumeEffect',
|
||||||
|
effect: effect._id.toHexString(),
|
||||||
|
});
|
||||||
|
eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'team.updateHealth',
|
||||||
|
team: team._id,
|
||||||
|
health: team.health,
|
||||||
|
operation: effect.amount,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[CardEffects.DRAW]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
_card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
_eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
await gameService.drawToBoard(effect.amount, player, game, session);
|
||||||
|
},
|
||||||
|
[CardEffects.PREPARE]: addToken,
|
||||||
|
[CardEffects.STUN]: addToken,
|
||||||
|
[CardEffects.SACRIFICE]: addToken,
|
||||||
|
[CardEffects.DRAW_AND_DISCARD]: addToken,
|
||||||
|
[CardEffects.MAKE_DISCARD]: addToken,
|
||||||
|
[CardEffects.RESTACK_DISCARDED_CHAMPION]: addToken,
|
||||||
|
[CardEffects.RESTACK_DISCARDED_CARD]: addToken,
|
||||||
|
[CardEffects.STACK_NEXT_ACTION_BOUGHT]: addToken,
|
||||||
|
[CardEffects.STACK_NEXT_CARD_BOUGHT]: addToken,
|
||||||
|
[CardEffects.PLAY_NEXT_CARD_BOUGHT]: addToken,
|
||||||
|
|
||||||
|
// [CardEffects.BUY_FOR_FREE]: addToken,
|
||||||
|
// [CardEffects.DISCARD_X_AND_DRAW_X]: addToken,
|
||||||
|
// [CardEffects.PREPARE_ANOTHER_CHAMPION]: addToken,
|
||||||
|
// [CardEffects.CHEAPER_CHAMPION]: addToken,
|
||||||
|
// [CardEffects.CHEAPER_CHAMPIONS_PASSIVE]: addToken,
|
||||||
|
// [CardEffects.CHEAPER_ACTION]: addToken,
|
||||||
|
// [CardEffects.CONTROL_OPPOSING_CHAMPION_PASSIVE]: addToken,
|
||||||
|
|
||||||
|
// [CardEffects.RESTACK_DISCARDED_ACTION]: addToken,
|
||||||
|
|
||||||
|
// [CardEffects.KEEP_IN_HAND]: addToken,
|
||||||
|
// [CardEffects.BUY_GEM_FOR_FREE]: addToken,
|
||||||
|
// [CardEffects.CHEAPER_SKILLS_PASSIVE]: addToken,
|
||||||
|
// [CardEffects.CHEAPER_CARD_IF_HIGHER_PRICE]: addToken,
|
||||||
|
// [CardEffects.PICK_FACTION]: addToken,
|
||||||
|
} as Record<
|
||||||
|
CardEffects,
|
||||||
|
(
|
||||||
|
game: Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => Promise<void>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const tokenMappings = {
|
||||||
|
[CardEffects.PREPARE]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
_effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
_gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
_eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
if (card.cardType !== CardType.CHAMPION) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Cannot Prepare : Card is not a champion',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const targetEffects = card.effects.filter(
|
||||||
|
(e) => e.condition.effectId == EffectType.CHAMPION_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!targetEffects.every((e) =>
|
||||||
|
game.currentTurn.lockedEffects.some((ee) => ee._id.equals(e._id)),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Cannot Prepare : champion effect not locked',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of targetEffects) {
|
||||||
|
await turnService.unlockEffect(game, player, e, card, session);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[CardEffects.STUN]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
if (card.cardType !== CardType.CHAMPION) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Cannot Stun : Card is not a champion',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await gameService.distributeCards(
|
||||||
|
[card],
|
||||||
|
player.board,
|
||||||
|
player.discard,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[CardEffects.SACRIFICE]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
if (game.currentTurn.cardsLocked.some((c) => c._id.equals(card._id))) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Cannot destroy : Card is locked',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (player.discard.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
await gameService.destroyCard(card, player.discard, game, session);
|
||||||
|
} else if (player.board.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
await gameService.destroyCard(card, player.board, game, session);
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'Card not found in Discard or Board',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[CardEffects.RESTACK_DISCARDED_CHAMPION]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
if (card.cardType != CardType.CHAMPION) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Cannot Restack : Card is not a champion',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await gameService.distributeCards(
|
||||||
|
[card],
|
||||||
|
player.discard,
|
||||||
|
player.stack,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[CardEffects.RESTACK_DISCARDED_CARD]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
await gameService.distributeCards(
|
||||||
|
[card],
|
||||||
|
player.discard,
|
||||||
|
player.stack,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[CardEffects.STACK_NEXT_ACTION_BOUGHT]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
if (card.cardType != CardType.ACTION) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Cannot stack : Card is not an action',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await turnService.buyGeneric(
|
||||||
|
game,
|
||||||
|
card,
|
||||||
|
game.market,
|
||||||
|
player.stack,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
await gameService.fillMarket(game, session);
|
||||||
|
},
|
||||||
|
[CardEffects.STACK_NEXT_CARD_BOUGHT]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
let fromContainer = game.market;
|
||||||
|
if (game.fireGems.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
fromContainer = game.fireGems;
|
||||||
|
}
|
||||||
|
|
||||||
|
await turnService.buyGeneric(
|
||||||
|
game,
|
||||||
|
card,
|
||||||
|
fromContainer,
|
||||||
|
player.stack,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
await gameService.fillMarket(game, session);
|
||||||
|
},
|
||||||
|
[CardEffects.PLAY_NEXT_CARD_BOUGHT]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
let fromContainer = game.market;
|
||||||
|
if (game.fireGems.cards.some((c) => c._id.equals(card._id))) {
|
||||||
|
fromContainer = game.fireGems;
|
||||||
|
}
|
||||||
|
|
||||||
|
await turnService.buyGeneric(
|
||||||
|
game,
|
||||||
|
card,
|
||||||
|
fromContainer,
|
||||||
|
player.board,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
await gameService.fillMarket(game, session);
|
||||||
|
},
|
||||||
|
[CardEffects.DRAW_AND_DISCARD]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
_card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
game.currentTurn.drawAndDiscardCurrentEffect &&
|
||||||
|
!game.currentTurn.drawAndDiscardCurrentEffect._id.equals(effect._id)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot use a token while having discard markers',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await gameService.drawToBoard(1, player, game, session);
|
||||||
|
await turnService.addDiscardMarker(game, player, session);
|
||||||
|
game.currentTurn.drawAndDiscardCurrentAmount++;
|
||||||
|
game.currentTurn.drawAndDiscardCurrentEffect = effect;
|
||||||
|
},
|
||||||
|
} as Record<
|
||||||
|
CardEffects,
|
||||||
|
(
|
||||||
|
game: Game,
|
||||||
|
effect: Effect,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
gameService: GameService,
|
||||||
|
turnService: TurnService,
|
||||||
|
eventEmitter: EventEmitter2,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => Promise<void>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const conditionsMappings = {
|
||||||
|
[EffectType.CHAMPION_ACTION]: async () => true,
|
||||||
|
[EffectType.FACTION_COMBO]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
condition: EffectCondition,
|
||||||
|
) => {
|
||||||
|
return game.currentTurn.cardsLocked.some(
|
||||||
|
(c) => c.faction == condition.faction && !c._id.equals(card._id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[EffectType.SUICIDE]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
condition: EffectCondition,
|
||||||
|
gameService: GameService,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => {
|
||||||
|
await gameService.destroyCard(card, player.board, game, session);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
// [EffectTypeSpecial.ACTION_AMOUNT]: {},
|
||||||
|
// [EffectTypeSpecial.CARD_AMOUNT]: {},
|
||||||
|
// [EffectTypeSpecial.CHAMPION_AMOUNT]: {},
|
||||||
|
// [EffectTypeSpecial.TOTAL_DAMAGE]: {},
|
||||||
|
} as Record<
|
||||||
|
string,
|
||||||
|
(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
condition: EffectCondition,
|
||||||
|
gameService: GameService,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) => Promise<boolean>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const perMapping = {
|
||||||
|
[Per.CARD_OF_SAME_FACTION]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
card: Card,
|
||||||
|
) => {
|
||||||
|
return game.currentTurn.cardsLocked.filter((c) => c.faction == card.faction)
|
||||||
|
.length;
|
||||||
|
},
|
||||||
|
[Per.OTHER_CARD_OF_SAME_FACTION]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
card: Card,
|
||||||
|
) => {
|
||||||
|
return game.currentTurn.cardsLocked.filter(
|
||||||
|
(c) => c.faction == card.faction && !c._id.equals(card._id),
|
||||||
|
).length;
|
||||||
|
},
|
||||||
|
[Per.CHAMPION]: async (game: Document<Game> & Game) => {
|
||||||
|
return game.currentTurn.cardsLocked.filter((c) => c.defense).length;
|
||||||
|
},
|
||||||
|
[Per.CHAMPION_OF_SAME_FACTION]: async (
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
card: Card,
|
||||||
|
) => {
|
||||||
|
return game.currentTurn.cardsLocked.filter(
|
||||||
|
(c) => c.faction == card.faction && c.defense,
|
||||||
|
).length;
|
||||||
|
},
|
||||||
|
[Per.OTHER_CHAMPION]: async (game: Document<Game> & Game, card: Card) => {
|
||||||
|
return game.currentTurn.cardsLocked.filter(
|
||||||
|
(c) => !c._id.equals(card._id) && c.defense,
|
||||||
|
).length;
|
||||||
|
},
|
||||||
|
[Per.OTHER_GUARD]: async (game: Document<Game> & Game, card: Card) => {
|
||||||
|
return game.currentTurn.cardsLocked.filter(
|
||||||
|
(c) => !c._id.equals(card._id) && c.guard,
|
||||||
|
).length;
|
||||||
|
},
|
||||||
|
[Per.STUNNED_CHAMPION]: async () => {
|
||||||
|
console.error('per_stunned_champion not implemented');
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
[Per.OTHER_KNIFE_PLAYED]: async (game: Document<Game> & Game, card: Card) => {
|
||||||
|
return game.currentTurn.cardsLocked.filter(
|
||||||
|
(c) => !c._id.equals(card._id) && c.cardId == 80,
|
||||||
|
).length;
|
||||||
|
},
|
||||||
|
} as Record<
|
||||||
|
string,
|
||||||
|
(game: Document<Game> & Game, card: Card) => Promise<number>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { LobbyService } from './games.lobby.service';
|
||||||
|
|
||||||
|
describe('GamesService', () => {
|
||||||
|
let service: LobbyService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [LobbyService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<LobbyService>(LobbyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { DatabaseObjectDto } from '../dto/database-object.dto';
|
||||||
|
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Game, Player, Team } from '../schemas/game.schema';
|
||||||
|
import mongoose, { Document, Model } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ReadContainerDto,
|
||||||
|
ReadGameDto,
|
||||||
|
ReadPlayerDto,
|
||||||
|
ReadTeamDto,
|
||||||
|
} from '../dto/read-games.dto';
|
||||||
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { CardRole } from 'src/cards/schemas/cards.types';
|
||||||
|
import { StartingDeckDto, UpdateGameDto } from '../dto/update-game.dto';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { GameService } from './games.service';
|
||||||
|
import { WithTransaction } from '../utils';
|
||||||
|
import { platform } from 'os';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LobbyService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(Game.name) private gameModel: Model<Game>,
|
||||||
|
@InjectModel(Card.name) private cardModel: Model<Card>,
|
||||||
|
@InjectConnection() private readonly connection: mongoose.Connection,
|
||||||
|
private readonly gamesService: GameService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async create(owner: User, session?: mongoose.mongo.ClientSession) {
|
||||||
|
const createdGame = new this.gameModel();
|
||||||
|
createdGame.owner = owner;
|
||||||
|
createdGame.packs = [
|
||||||
|
'pack1_base_deck',
|
||||||
|
'pack1_base_necros',
|
||||||
|
'pack1_base_wild',
|
||||||
|
'pack1_base_imperial',
|
||||||
|
'pack1_base_guild',
|
||||||
|
];
|
||||||
|
await this.joinGame(createdGame, owner, session);
|
||||||
|
const dto = new ReadGameDto(createdGame);
|
||||||
|
this.eventEmitter.emit('sse.lobby', { type: 'create', ...dto });
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async joinGame(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
user: User,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
game.teams.push({
|
||||||
|
players: [{ user } as Player],
|
||||||
|
} as Team);
|
||||||
|
|
||||||
|
const team = game.teams.at(-1);
|
||||||
|
// this.eventEmitter.emit('sse.lobby', {
|
||||||
|
// type: 'joinGame',
|
||||||
|
// team: new ReadTeamDto(team),
|
||||||
|
// });
|
||||||
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
|
type: 'joinGame',
|
||||||
|
team: new ReadTeamDto(team),
|
||||||
|
game: game._id.toHexString(),
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'joinGame',
|
||||||
|
team: new ReadTeamDto(team),
|
||||||
|
});
|
||||||
|
await game.save({ session });
|
||||||
|
|
||||||
|
return 'true!';
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<ReadGameDto[]> {
|
||||||
|
const games = await this.gameModel.find().exec();
|
||||||
|
return await Promise.all(
|
||||||
|
games.map(async (e) => {
|
||||||
|
return await new ReadGameDto(e);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByOwner(_id: string): Promise<ReadGameDto> {
|
||||||
|
const game = await this.gameModel.findOne({ owner: _id }).exec();
|
||||||
|
if (!game) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new ReadGameDto(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(game: Document<Game> & Game, updateGameDto: UpdateGameDto) {
|
||||||
|
game.packs = updateGameDto.packs;
|
||||||
|
const packs = await this.cardModel.distinct('pack');
|
||||||
|
if (updateGameDto.packs.every((e) => packs.includes(e))) {
|
||||||
|
await game.save();
|
||||||
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
|
type: 'update',
|
||||||
|
_id: game._id,
|
||||||
|
...updateGameDto,
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'update',
|
||||||
|
...updateGameDto,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new HttpException('Pack not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(game: Document<Game> & Game) {
|
||||||
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
|
type: 'delete',
|
||||||
|
_id: game._id,
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'delete',
|
||||||
|
});
|
||||||
|
return await game.deleteOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async start(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const random = Math.floor(Math.random() * game.teams.length);
|
||||||
|
game.currentTurn.currentTeam = game.teams[random]._id.toHexString();
|
||||||
|
await game.save({ session });
|
||||||
|
game.started = true;
|
||||||
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
|
type: 'start',
|
||||||
|
game: game._id,
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'start',
|
||||||
|
game: new ReadGameDto(game),
|
||||||
|
});
|
||||||
|
|
||||||
|
let playerIndex = 0;
|
||||||
|
for (const team of game.teams) {
|
||||||
|
for (const player of team.players) {
|
||||||
|
player.stack.cards = await this.cardModel
|
||||||
|
.find({
|
||||||
|
pack: { $in: game.packs },
|
||||||
|
role: { $in: [player.class, player.race] },
|
||||||
|
playerIndex,
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'populateContainer',
|
||||||
|
container: new ReadContainerDto(player.stack, true),
|
||||||
|
});
|
||||||
|
await this.gamesService.shuffleContainer(player.stack, game, session);
|
||||||
|
playerIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
game.fireGems.cards = await this.cardModel.find({
|
||||||
|
pack: { $in: game.packs },
|
||||||
|
role: CardRole.FIRE_GEM,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.marketStack.cards = await this.cardModel.find({
|
||||||
|
pack: { $in: game.packs },
|
||||||
|
role: CardRole.MARKET,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'populateContainer',
|
||||||
|
container: new ReadContainerDto(game.fireGems),
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'populateContainer',
|
||||||
|
container: new ReadContainerDto(game.marketStack, true),
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.gamesService.shuffleContainer(game.marketStack, game, session);
|
||||||
|
await this.gamesService.fillMarket(game, session);
|
||||||
|
for (const team of game.teams) {
|
||||||
|
for (const player of team.players) {
|
||||||
|
if (team._id.equals(game.currentTurn.currentTeam)) {
|
||||||
|
await this.gamesService.drawToHand(3, player, game, session);
|
||||||
|
} else {
|
||||||
|
await this.gamesService.drawToHand(5, player, game, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTeam = game.teams.find((t) =>
|
||||||
|
t._id.equals(game.currentTurn.currentTeam),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const player of currentTeam.players) {
|
||||||
|
await this.gamesService.distributeCards(
|
||||||
|
player.hand.cards,
|
||||||
|
player.hand,
|
||||||
|
player.board,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeStartingDeck(
|
||||||
|
data: StartingDeckDto,
|
||||||
|
player: Document<Player> & Player,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
const packs = await this.cardModel
|
||||||
|
.find({ pack: { $in: game.packs } })
|
||||||
|
.distinct('role');
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Object.values<string>(CardRole).includes(data.class) ||
|
||||||
|
!Object.values<string>(CardRole).includes(data.race) ||
|
||||||
|
data.race == CardRole.FIRE_GEM ||
|
||||||
|
data.race == CardRole.MARKET ||
|
||||||
|
data.class == CardRole.FIRE_GEM ||
|
||||||
|
data.class == CardRole.MARKET
|
||||||
|
) {
|
||||||
|
throw new HttpException('Invalid race or class', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
packs.includes(data.class as CardRole) &&
|
||||||
|
packs.includes(data.race as CardRole)
|
||||||
|
) {
|
||||||
|
player.class = data.class;
|
||||||
|
player.race = data.race;
|
||||||
|
await game.save();
|
||||||
|
this.eventEmitter.emit('sse.game.changeStartingDeck', {
|
||||||
|
_id: player._id,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new HttpException('Pack not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectModel, InjectConnection } from '@nestjs/mongoose';
|
||||||
|
import { Container, Game, Player, Team } from '../schemas/game.schema';
|
||||||
|
import mongoose, { Document, Model } from 'mongoose';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { WithTransaction, getCurrentTeam, shuffle } from '../utils';
|
||||||
|
import { TurnService } from './turn.service';
|
||||||
|
import { CardRole } from 'src/cards/schemas/cards.types';
|
||||||
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
|
import { ReadTeamDto } from '../dto/read-games.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameService {
|
||||||
|
constructor(
|
||||||
|
@Inject(forwardRef(() => TurnService))
|
||||||
|
private readonly turnService: TurnService,
|
||||||
|
@InjectModel(Game.name) private gameModel: Model<Game>,
|
||||||
|
@InjectModel(Card.name) private cardModel: Model<Card>,
|
||||||
|
@InjectConnection() private readonly connection: mongoose.Connection,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async endTurn(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
let currentTeam = getCurrentTeam(game);
|
||||||
|
|
||||||
|
for (const player of currentTeam.players) {
|
||||||
|
if (player.discardMarkers > 0) {
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'player.removeDiscardMarker',
|
||||||
|
operation: -player.discardMarkers,
|
||||||
|
discardMarkers: 0,
|
||||||
|
playerId: player._id.toHexString(),
|
||||||
|
});
|
||||||
|
player.discardMarkers = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.fuckUMarkers > 0) {
|
||||||
|
player.fuckUMarkers--;
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'player.removeFuckUMarker',
|
||||||
|
operation: -player.fuckUMarkers,
|
||||||
|
fuckUMarkers: 0,
|
||||||
|
playerId: player._id.toHexString(),
|
||||||
|
});
|
||||||
|
player.fuckUMarkers = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.distributeCards(
|
||||||
|
player.board.cards.filter((c) => !c.defense),
|
||||||
|
player.board,
|
||||||
|
player.discard,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
await this.drawToHand(5, player, game, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = game.teams.indexOf(currentTeam);
|
||||||
|
|
||||||
|
game.currentTurn.currentTeam =
|
||||||
|
game.teams[(index + 1) % game.teams.length]._id.toHexString();
|
||||||
|
|
||||||
|
game.currentTurn.cardsLocked = [];
|
||||||
|
game.currentTurn.lockedEffects = [];
|
||||||
|
game.currentTurn.tokens = [];
|
||||||
|
game.currentTurn.damage = 0;
|
||||||
|
game.currentTurn.gold = 0;
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'endTurn',
|
||||||
|
currentTurn: game.currentTurn,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentTeam = getCurrentTeam(game);
|
||||||
|
|
||||||
|
for (const player of currentTeam.players) {
|
||||||
|
for (const card of player.board.cards) {
|
||||||
|
if (card.defense) {
|
||||||
|
await this.turnService.lockCard(game, player, card, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const player of currentTeam.players) {
|
||||||
|
await this.distributeCards(
|
||||||
|
player.hand.cards,
|
||||||
|
player.hand,
|
||||||
|
player.board,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async drawToBoard(
|
||||||
|
amount,
|
||||||
|
player: Player,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
await this.drawCards(
|
||||||
|
amount,
|
||||||
|
player.stack,
|
||||||
|
player.board,
|
||||||
|
player.discard,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async drawToHand(
|
||||||
|
amount,
|
||||||
|
player: Player,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
await this.drawCards(
|
||||||
|
amount,
|
||||||
|
player.stack,
|
||||||
|
player.hand,
|
||||||
|
player.discard,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
private async drawCards(
|
||||||
|
amount: number,
|
||||||
|
from: Container,
|
||||||
|
to: Container,
|
||||||
|
discard: Container,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const shallow = [...from.cards];
|
||||||
|
shallow.reverse();
|
||||||
|
if (amount <= from.cards.length) {
|
||||||
|
await this.distributeCards(
|
||||||
|
shallow.slice(0, amount),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
await game.save({ session });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const toDraw = from.cards.length;
|
||||||
|
await this.distributeCards(
|
||||||
|
shallow.slice(0, toDraw),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
if (discard.cards.length == 0) {
|
||||||
|
await game.save({ session });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// restack all discarded
|
||||||
|
await this.distributeCards(discard.cards, discard, from, game, session);
|
||||||
|
await this.shuffleContainer(from, game, session);
|
||||||
|
await this.drawCards(amount - toDraw, from, to, discard, game, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async fillMarket(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
if (game.market.cards.length < 5) {
|
||||||
|
const amount = Math.min(
|
||||||
|
game.marketStack.cards.length,
|
||||||
|
5 - game.market.cards.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.distributeCards(
|
||||||
|
game.marketStack.cards.slice(0, amount),
|
||||||
|
game.marketStack,
|
||||||
|
game.market,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const missingIndexes = game.market.cards.reduce(function (a, e, i) {
|
||||||
|
if (e === null) a.push(i);
|
||||||
|
return a;
|
||||||
|
}, []);
|
||||||
|
if (missingIndexes.length > 0) {
|
||||||
|
console.error('not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async shuffleContainer(
|
||||||
|
container: Container,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
container.cards = shuffle(container.cards);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'shuffleContainer',
|
||||||
|
container: container._id.toHexString(),
|
||||||
|
});
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async distributeCards(
|
||||||
|
cards: Card[],
|
||||||
|
from: Container,
|
||||||
|
to: Container,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
if (cards.some((c) => !from.cards.some((cc) => cc._id.equals(c._id)))) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Card not found in container',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const shallow = [...cards];
|
||||||
|
for (const c of shallow) {
|
||||||
|
const index = from.cards.findIndex((cc) => cc?._id.equals(c._id));
|
||||||
|
from.cards.splice(index, 1);
|
||||||
|
to.cards.push(c);
|
||||||
|
}
|
||||||
|
await game.save({ session });
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'distributeCards',
|
||||||
|
from: from._id.toHexString(),
|
||||||
|
to: to._id.toHexString(),
|
||||||
|
cards: shallow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async destroyCard(
|
||||||
|
card: Card,
|
||||||
|
from: Container,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
if (card.role == CardRole.FIRE_GEM) {
|
||||||
|
await this.distributeCards([card], from, game.fireGems, game, session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = from.cards.findIndex((cc) => cc?._id.equals(card._id));
|
||||||
|
from.cards.splice(index, 1);
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'destroyCard',
|
||||||
|
from: from._id.toHexString(),
|
||||||
|
card: card._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async killTeam(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
team: Team,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const index = game.teams.findIndex((t) => t._id.equals(team._id));
|
||||||
|
game.teams.splice(index, 1);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'killTeam',
|
||||||
|
teamId: team._id,
|
||||||
|
});
|
||||||
|
await game.save({ session });
|
||||||
|
if (game.teams.length <= 1) {
|
||||||
|
await this.endGame(game, game.teams[0], session);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async leaveGame(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
user: User,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const team = game.teams.find((t) =>
|
||||||
|
t.players.find((p) => p.user._id.equals(user._id)),
|
||||||
|
);
|
||||||
|
if (team == undefined) {
|
||||||
|
throw new HttpException('User not in game', HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
if (game.owner._id.equals(user._id)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You cannot leave a game you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const player = team.players.find((p) => p.user._id.equals(user._id));
|
||||||
|
await this.kickPlayer(game, game.owner, player._id.toHexString(), session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async kickPlayer(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
user: User,
|
||||||
|
kickedPlayerId: string,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const team = game.teams.find((t) =>
|
||||||
|
t.players.find((p) => p._id.equals(kickedPlayerId)),
|
||||||
|
);
|
||||||
|
if (team == undefined) {
|
||||||
|
throw new HttpException('User not in game', HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
const index = team.players.findIndex((p) => p.user._id.equals(user._id));
|
||||||
|
team.players.splice(index, 1);
|
||||||
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
|
type: 'leaveTeam',
|
||||||
|
playerId: kickedPlayerId,
|
||||||
|
team: new ReadTeamDto(team),
|
||||||
|
game: game._id.toHexString(),
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'leaveTeam',
|
||||||
|
team: new ReadTeamDto(team),
|
||||||
|
playerId: kickedPlayerId,
|
||||||
|
});
|
||||||
|
let gameExists = true;
|
||||||
|
if (team.players.length == 0) {
|
||||||
|
if (!game.started) {
|
||||||
|
const teamIndex = game.teams.indexOf(team);
|
||||||
|
game.teams.splice(teamIndex, 1);
|
||||||
|
} else {
|
||||||
|
gameExists = await this.killTeam(game, team, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gameExists) {
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async endGame(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
winner: Team,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'endGame',
|
||||||
|
winner: winner._id.toHexString(),
|
||||||
|
});
|
||||||
|
await game.deleteOne({ session });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Container, Game, Player } from '../schemas/game.schema';
|
||||||
|
import mongoose, { Document, Model } from 'mongoose';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Effect } from 'src/cards/schemas/effect.schema';
|
||||||
|
import { CardEffects } from 'src/cards/schemas/cards.types';
|
||||||
|
import { getPlayerTeam, WithTransaction } from '../utils';
|
||||||
|
import {
|
||||||
|
conditionsMappings,
|
||||||
|
effectMappings,
|
||||||
|
isAutoActivable,
|
||||||
|
perMapping,
|
||||||
|
tokenMappings,
|
||||||
|
} from './effect.functions';
|
||||||
|
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { GameService } from './games.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TurnService {
|
||||||
|
constructor(
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
@Inject(forwardRef(() => GameService))
|
||||||
|
private readonly gamesService: GameService,
|
||||||
|
@InjectModel(Game.name) private gameModel: Model<Game>,
|
||||||
|
@InjectConnection() private readonly connection: mongoose.Connection,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async lockCard(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
game.currentTurn.cardsLocked.push(card);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'lockCard',
|
||||||
|
card: card._id.toHexString(),
|
||||||
|
});
|
||||||
|
for (const effect of card.effects) {
|
||||||
|
if (isAutoActivable(effect)) {
|
||||||
|
await this.lockEffect(game, player, effect, card, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async unlockEffect(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
effect: Effect,
|
||||||
|
card: Card,
|
||||||
|
session: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const index = game.currentTurn.lockedEffects.findIndex((ee) =>
|
||||||
|
ee._id.equals(effect._id),
|
||||||
|
);
|
||||||
|
game.currentTurn.lockedEffects.splice(index, 1);
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'unlockEffect',
|
||||||
|
effect: effect._id.toHexString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const sub of effect.subEffects) {
|
||||||
|
await this.unlockEffect(game, player, sub, card, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of [effect, ...effect.subEffects]) {
|
||||||
|
if (isAutoActivable(e)) {
|
||||||
|
await this.lockEffect(game, player, e, card, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async lockEffect(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
effect: Effect,
|
||||||
|
card: Card,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const effectRunner = effectMappings[effect.effect];
|
||||||
|
if (!effectRunner) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Effect ${effect.effect} not implemented`,
|
||||||
|
HttpStatus.NOT_IMPLEMENTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Process effect conditions
|
||||||
|
if (effect.condition) {
|
||||||
|
const effectCondition = conditionsMappings[effect.condition.effectId];
|
||||||
|
if (!effectCondition) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Condition ${effect.condition.effectId} not implemented`,
|
||||||
|
HttpStatus.NOT_IMPLEMENTED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const conditionSuccess = await effectCondition(
|
||||||
|
game,
|
||||||
|
player,
|
||||||
|
card,
|
||||||
|
effect.condition,
|
||||||
|
this.gamesService,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!conditionSuccess) {
|
||||||
|
throw new HttpException(
|
||||||
|
`Condition ${effect.condition.effectId} not met`,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const effectUUID = effect._id.toHexString();
|
||||||
|
if (game.currentTurn.lockedEffects.some((e) => e._id.equals(effectUUID))) {
|
||||||
|
console.error(`Effect ${effectUUID} already used this turn`);
|
||||||
|
throw new HttpException(
|
||||||
|
`Effect ${effectUUID} already used this turn`,
|
||||||
|
HttpStatus.CONFLICT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'lockEffect',
|
||||||
|
effect: effectUUID,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process effect mutexes
|
||||||
|
|
||||||
|
if (effect.mutex) {
|
||||||
|
for (const e of card.effects) {
|
||||||
|
if (e.mutex == effect.mutex) {
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'lockEffect',
|
||||||
|
effect: e._id.toHexString(),
|
||||||
|
});
|
||||||
|
game.currentTurn.lockedEffects.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process effect Per
|
||||||
|
game.currentTurn.lockedEffects.push(effect);
|
||||||
|
let times = 1;
|
||||||
|
if (effect.per) {
|
||||||
|
times = await perMapping[effect.per](game, card);
|
||||||
|
}
|
||||||
|
for (let t = 0; t < times; t++) {
|
||||||
|
await effectRunner(
|
||||||
|
game,
|
||||||
|
effect,
|
||||||
|
player,
|
||||||
|
card,
|
||||||
|
this.gamesService,
|
||||||
|
this.eventEmitter,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// process subeffects
|
||||||
|
if (effect.subEffects) {
|
||||||
|
for (const subeffect of effect.subEffects) {
|
||||||
|
await this.lockEffect(game, player, subeffect, effect.card, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async buyGeneric(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
card: Card,
|
||||||
|
fromContainer: Container,
|
||||||
|
toContainer: Container,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
if (!card.cost) {
|
||||||
|
throw new TypeError('Card cost is null or undefined');
|
||||||
|
}
|
||||||
|
// const targetChangingEffects = [CardEffects.STACK_NEXT_CARD_BOUGHT];
|
||||||
|
// if (card.cardType == CardType.ACTION) {
|
||||||
|
// targetChangingEffects.push(CardEffects.STACK_NEXT_ACTION_BOUGHT);
|
||||||
|
// }
|
||||||
|
// const targetChangingToken = game.currentTurn.tokens.find((e) =>
|
||||||
|
// targetChangingEffects.includes(e.effect),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const forFreeEffects = [CardEffects.BUY_FOR_FREE];
|
||||||
|
// if (card.role == CardRole.FIRE_GEM) {
|
||||||
|
// forFreeEffects.push(CardEffects.BUY_GEM_FOR_FREE);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const forFreeToken = game.currentTurn.tokens.find((e) =>
|
||||||
|
// forFreeEffects.includes(e.effect),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const reducedPriceEffects = [];
|
||||||
|
|
||||||
|
// if (card.cardType == CardType.ACTION) {
|
||||||
|
// reducedPriceEffects.push(CardEffects.CHEAPER_ACTION);
|
||||||
|
// }
|
||||||
|
// if (card.cardType == CardType.CHAMPION) {
|
||||||
|
// reducedPriceEffects.push(CardEffects.CHEAPER_CHAMPION);
|
||||||
|
// reducedPriceEffects.push(CardEffects.CHEAPER_CHAMPIONS_PASSIVE);
|
||||||
|
// }
|
||||||
|
// // if(card.cost)
|
||||||
|
// CardEffects.CHEAPER_CARD_IF_HIGHER_PRICE
|
||||||
|
|
||||||
|
// const reducedPriceToken = game.currentTurn.tokens.find((e) =>
|
||||||
|
// forFreeEffects.includes(e.effect),
|
||||||
|
// );
|
||||||
|
|
||||||
|
const cost = card.cost;
|
||||||
|
// if (forFreeToken) {
|
||||||
|
// cost = 0;
|
||||||
|
// removeToken(game, targetChangingToken, this.eventEmitter);
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (game.currentTurn.gold < cost) {
|
||||||
|
throw new HttpException('Not enough gold', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
// if (targetChangingToken) {
|
||||||
|
// targetContainer = player.stack;
|
||||||
|
// removeToken(game, targetChangingToken, this.eventEmitter);
|
||||||
|
// }
|
||||||
|
|
||||||
|
await this.gamesService.distributeCards(
|
||||||
|
[card],
|
||||||
|
fromContainer,
|
||||||
|
toContainer,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cost != 0) {
|
||||||
|
game.currentTurn.gold -= cost;
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.updateGold',
|
||||||
|
gold: game.currentTurn.gold,
|
||||||
|
operation: -cost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async addFuckUMarker(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
token: Effect,
|
||||||
|
target: Player,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
target.hand.cards.length <=
|
||||||
|
target.fuckUMarkers + target.discardMarkers
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
"This player doesn't have enough cards in the hand",
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
target.fuckUMarkers++;
|
||||||
|
const index = game.currentTurn.tokens.findIndex((t) =>
|
||||||
|
t._id.equals(token._id),
|
||||||
|
);
|
||||||
|
if (index == -1) {
|
||||||
|
throw new Error('Token not found');
|
||||||
|
}
|
||||||
|
game.currentTurn.tokens.splice(index, 1);
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'player.addFuckUMarker',
|
||||||
|
operation: 1,
|
||||||
|
fuckUMarkers: target.fuckUMarkers,
|
||||||
|
playerId: target._id.toHexString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async addDiscardMarker(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
target: Player,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const unlockedCards = target.board.cards.filter((c) => !c.isLocked(game));
|
||||||
|
if (
|
||||||
|
target.hand.cards.length + unlockedCards.length <=
|
||||||
|
target.fuckUMarkers + target.discardMarkers
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
"This player doesn't have enough cards in the hand",
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
target.discardMarkers++;
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'player.addDiscardMarker',
|
||||||
|
operation: 1,
|
||||||
|
discardMarkers: target.discardMarkers,
|
||||||
|
playerId: target._id.toHexString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async useToken(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
token: Effect,
|
||||||
|
target: Card,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const handler = tokenMappings[token.effect];
|
||||||
|
if (!handler) {
|
||||||
|
throw new Error('Effect not implemented');
|
||||||
|
}
|
||||||
|
await handler(
|
||||||
|
game,
|
||||||
|
token,
|
||||||
|
player,
|
||||||
|
target,
|
||||||
|
this.gamesService,
|
||||||
|
this,
|
||||||
|
this.eventEmitter,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
token.effect != CardEffects.DRAW_AND_DISCARD ||
|
||||||
|
(token.effect == CardEffects.DRAW_AND_DISCARD &&
|
||||||
|
token.times <= game.currentTurn.drawAndDiscardCurrentAmount)
|
||||||
|
) {
|
||||||
|
const index = game.currentTurn.tokens.findIndex((t) =>
|
||||||
|
t._id.equals(token._id),
|
||||||
|
);
|
||||||
|
game.currentTurn.tokens.splice(index, 1);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.useToken',
|
||||||
|
tokenId: token._id.toHexString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// process subeffects
|
||||||
|
if (token.subEffects) {
|
||||||
|
for (const subeffect of token.subEffects) {
|
||||||
|
await this.lockEffect(game, player, subeffect, token.card, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async damagePlayer(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
amount: number,
|
||||||
|
player: Player,
|
||||||
|
targetPlayer: Player,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
const team = getPlayerTeam(game, targetPlayer);
|
||||||
|
team.health -= amount;
|
||||||
|
game.currentTurn.damage -= amount;
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.updateDamage',
|
||||||
|
damage: game.currentTurn.damage,
|
||||||
|
operation: -amount,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'team.updateHealth',
|
||||||
|
team: team._id,
|
||||||
|
health: team.health,
|
||||||
|
operation: -amount,
|
||||||
|
});
|
||||||
|
await game.save({ session });
|
||||||
|
if (team.health <= 0) {
|
||||||
|
await this.gamesService.killTeam(game, team, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async damageChampion(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
targetPlayer: Player,
|
||||||
|
targetCard: Card,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
game.currentTurn.damage -= targetCard.defense;
|
||||||
|
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.updateDamage',
|
||||||
|
damage: game.currentTurn.damage,
|
||||||
|
operation: -targetCard.defense,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.gamesService.distributeCards(
|
||||||
|
[targetCard],
|
||||||
|
targetPlayer.board,
|
||||||
|
targetPlayer.discard,
|
||||||
|
game,
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
|
||||||
|
@WithTransaction
|
||||||
|
async discardCard(
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
player: Player,
|
||||||
|
card: Card,
|
||||||
|
session?: mongoose.mongo.ClientSession,
|
||||||
|
) {
|
||||||
|
await this.gamesService.distributeCards(
|
||||||
|
[card],
|
||||||
|
player.board,
|
||||||
|
player.discard,
|
||||||
|
game,
|
||||||
|
);
|
||||||
|
if (player.discardMarkers > 0) {
|
||||||
|
player.discardMarkers--;
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'player.removeDiscardMarker',
|
||||||
|
operation: -1,
|
||||||
|
discardMarkers: player.discardMarkers,
|
||||||
|
playerId: player._id.toHexString(),
|
||||||
|
});
|
||||||
|
} else if (player.fuckUMarkers > 0) {
|
||||||
|
player.fuckUMarkers--;
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'player.removeFuckUMarker',
|
||||||
|
operation: -1,
|
||||||
|
fuckUMarkers: player.fuckUMarkers,
|
||||||
|
playerId: player._id.toHexString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
game.currentTurn.drawAndDiscardCurrentAmount = 0;
|
||||||
|
if (game.currentTurn.drawAndDiscardCurrentEffect) {
|
||||||
|
const tokenIndex = game.currentTurn.tokens.findIndex((t) =>
|
||||||
|
t._id.equals(game.currentTurn.drawAndDiscardCurrentEffect._id),
|
||||||
|
);
|
||||||
|
game.currentTurn.tokens.splice(tokenIndex, 1);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'currentTurn.useToken',
|
||||||
|
tokenId: game.currentTurn.drawAndDiscardCurrentEffect._id.toHexString(),
|
||||||
|
});
|
||||||
|
game.currentTurn.drawAndDiscardCurrentEffect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await game.save({ session });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { HttpStatus } from '@nestjs/common/enums';
|
||||||
|
import { HttpException } from '@nestjs/common/exceptions';
|
||||||
|
import mongoose, { Document } from 'mongoose';
|
||||||
|
import { Game, Player, Team } from './schemas/game.schema';
|
||||||
|
import { ReadUserDto } from './dto/read-games.dto';
|
||||||
|
|
||||||
|
export function shuffle(a) {
|
||||||
|
let j, x, i;
|
||||||
|
for (i = a.length - 1; i > 0; i--) {
|
||||||
|
j = Math.floor(Math.random() * (i + 1));
|
||||||
|
x = a[i];
|
||||||
|
a[i] = a[j];
|
||||||
|
a[j] = x;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentTeam(game: Document<Game> & Game) {
|
||||||
|
return game.teams.find((t) => t._id.equals(game.currentTurn.currentTeam));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlayer(session: Record<string, any>, game: Game) {
|
||||||
|
const user = session.user as ReadUserDto;
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return findUser(user._id, game);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findUser(playerId: string, game: Game) {
|
||||||
|
for (const team of game.teams) {
|
||||||
|
for (const player of team.players) {
|
||||||
|
if (player.user._id.equals(playerId)) {
|
||||||
|
return player as Document<Player> & Player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new HttpException('User not found in game', HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findPlayer(playerId: string, game: Game) {
|
||||||
|
for (const team of game.teams) {
|
||||||
|
for (const player of team.players) {
|
||||||
|
if (player._id.equals(playerId)) {
|
||||||
|
return player as Document<Player> & Player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new HttpException('Player not found in game', HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInTeam(team: Team, player: Player) {
|
||||||
|
if (team.players.some((p) => p._id.equals(player._id))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlayerTeam(game: Game, player: Player) {
|
||||||
|
return game.teams.find((t) =>
|
||||||
|
t.players.some((p) => p._id.equals(player._id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function WithTransaction(
|
||||||
|
target: any,
|
||||||
|
propertyKey: string,
|
||||||
|
descriptor: PropertyDescriptor,
|
||||||
|
) {
|
||||||
|
const originalFunc = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = function (...args: any[]) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (args.at(-1) instanceof mongoose.mongo.ClientSession) {
|
||||||
|
const data = await originalFunc.apply(this, args).catch((e) => {
|
||||||
|
if (e instanceof HttpException) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
console.log(e);
|
||||||
|
reject(
|
||||||
|
new HttpException(
|
||||||
|
'Another request is processing',
|
||||||
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
resolve(data);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
await this.connection
|
||||||
|
.transaction(async (session) => {
|
||||||
|
const data = await originalFunc.apply(this, [...args, session]);
|
||||||
|
resolve(data);
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof HttpException) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
reject(
|
||||||
|
new HttpException(
|
||||||
|
'Another request is processing',
|
||||||
|
HttpStatus.TOO_MANY_REQUESTS,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import * as session from 'express-session';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
import MongoStore from 'connect-mongo';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.enableCors({ origin: 'http://localhost:3000', credentials: true });
|
||||||
|
app.setGlobalPrefix(process.env.API_PREFIX);
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Heron Game')
|
||||||
|
.setDescription('Heron')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('api', app, document);
|
||||||
|
|
||||||
|
mongoose.set('transactionAsyncLocalStorage', true);
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session.default({
|
||||||
|
secret: process.env.SESSION_SECRET,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
name: 'heron.session',
|
||||||
|
store: MongoStore.create({
|
||||||
|
collectionName: 'sessions',
|
||||||
|
dbName: 'heron',
|
||||||
|
mongoUrl: process.env.MONGO_ENDPOINT,
|
||||||
|
mongoOptions: {
|
||||||
|
auth: {
|
||||||
|
password: process.env.MONGO_PASSWORD,
|
||||||
|
username: process.env.MONGO_USER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen(8765);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export class CreateUserDto {
|
||||||
|
method: 'discord' | 'password';
|
||||||
|
username?: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
constructor(userId: string, username: string, avatar: string) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.username = username;
|
||||||
|
this.avatar = avatar;
|
||||||
|
}
|
||||||
|
@ApiProperty()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
import { Entity, Column, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class User {
|
||||||
|
@PrimaryColumn()
|
||||||
|
userId: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
avatar: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
Session,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Redirect,
|
||||||
|
} 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')
|
||||||
|
@Redirect(process.env.CLIENT_ENDPOINT)
|
||||||
|
async login(
|
||||||
|
@Query('code') code: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.usersService.createDiscordToken({ code }, session);
|
||||||
|
await this.usersService.discordLogin(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/logout')
|
||||||
|
@Redirect(process.env.CLIENT_ENDPOINT)
|
||||||
|
async logout(@Session() session: Record<string, any>): Promise<void> {
|
||||||
|
await this.usersService.discordLogout(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.discordLogin(session);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
import { User } from './schemas/user.entity';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([User])],
|
||||||
|
controllers: [UsersController],
|
||||||
|
providers: [UsersService],
|
||||||
|
})
|
||||||
|
export class UsersModule {
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { LoginDto } from './dto/login.dto';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import { User } from './schemas/user.entity';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(@InjectRepository(User) private usersRepository: Repository<User>) {}
|
||||||
|
|
||||||
|
async createDiscordToken(
|
||||||
|
{
|
||||||
|
code,
|
||||||
|
refresh_token,
|
||||||
|
}: {
|
||||||
|
code?: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
},
|
||||||
|
session: Record<string, any>,
|
||||||
|
): Promise<void> {
|
||||||
|
let body;
|
||||||
|
if (code) {
|
||||||
|
body = new URLSearchParams({
|
||||||
|
client_id: process.env.DISCORD_CLIENTID,
|
||||||
|
client_secret: process.env.DISCORD_CLIENTSECRET,
|
||||||
|
code,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri:
|
||||||
|
process.env.API_ENDPOINT + process.env.API_PREFIX + '/login',
|
||||||
|
});
|
||||||
|
} else if (refresh_token) {
|
||||||
|
body = new URLSearchParams({
|
||||||
|
client_id: process.env.DISCORD_CLIENTID,
|
||||||
|
client_secret: process.env.DISCORD_CLIENTSECRET,
|
||||||
|
refresh_token,
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
'Invalid internal call',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const req = await fetch('https://discord.com/api/oauth2/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (!req.ok) {
|
||||||
|
const data = await req.json();
|
||||||
|
if (data.error == 'invalid_grant') {
|
||||||
|
throw new HttpException(
|
||||||
|
'Invalid code in request',
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(data);
|
||||||
|
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 expires = new Date(Date.now() + data.expires_in);
|
||||||
|
session.tokenData = {
|
||||||
|
access_token: data.access_token,
|
||||||
|
token_type: data.token_type,
|
||||||
|
refresh_token: data.refresh_token,
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeDiscordToken(refresh_token: string) {
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
client_id: process.env.DISCORD_CLIENTID,
|
||||||
|
client_secret: process.env.DISCORD_CLIENTSECRET,
|
||||||
|
token: refresh_token,
|
||||||
|
token_type_hint: 'refresh_token',
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = await fetch('https://discord.com/api/oauth2/token/revoke', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async discordLogin(
|
||||||
|
session: Record<string, any>,
|
||||||
|
recursive: boolean = false,
|
||||||
|
): Promise<LoginDto> {
|
||||||
|
const req2 = await fetch('https://discord.com/api/users/@me', {
|
||||||
|
headers: {
|
||||||
|
authorization:
|
||||||
|
session.tokenData.token_type + ' ' + session.tokenData.access_token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!req2.ok) {
|
||||||
|
const error = await req2.json();
|
||||||
|
if (error.error == 'invalid_grant' && !recursive) {
|
||||||
|
await this.createDiscordToken(
|
||||||
|
{
|
||||||
|
refresh_token: session.tokenData.refresh_token,
|
||||||
|
},
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
return this.discordLogin(session);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
throw new HttpException(
|
||||||
|
"Couldn't fetch Discord api",
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data2 = await req2.json();
|
||||||
|
const dto = new LoginDto(data2.id, data2.global_name, data2.avatar);
|
||||||
|
const user = await this.getOrCreateUser(dto.userId, dto);
|
||||||
|
session.user = user;
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrCreateUser(userId: string, data?: LoginDto) {
|
||||||
|
let user = await this.usersRepository.findOneBy({ userId });
|
||||||
|
if (!user) {
|
||||||
|
user = this.usersRepository.create({ ...data, userId });
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Object.assign(user, data);
|
||||||
|
}
|
||||||
|
this.usersRepository.save(user)
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async discordLogout(session: Record<string, any>) {
|
||||||
|
this.revokeDiscordToken(session.tokenData.access_token);
|
||||||
|
delete session.user;
|
||||||
|
delete session.tokenData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
|
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
import { Word } from './word.schema';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Topic{
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
name: string
|
||||||
|
|
||||||
|
@ManyToMany(type=>Word, word=>word.topics)
|
||||||
|
words: Word[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
|
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
import { Topic } from './topic.schema';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Word{
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
value: string
|
||||||
|
|
||||||
|
@ManyToOne(type=>User)
|
||||||
|
owner: User
|
||||||
|
|
||||||
|
@ManyToMany(type=>Topic, topic => topic.words)
|
||||||
|
@JoinTable()
|
||||||
|
topics: Topic[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as request from 'supertest';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const moduleFixture = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": "\\.e2e-spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user