Compare commits
89
Commits
test_monorepo
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
196150bedc | ||
|
|
74b4e955df | ||
|
|
1aee0f7881 | ||
|
|
42e68f90ad | ||
|
|
0ce8293b89 | ||
|
|
494ed3f3f1 | ||
|
|
78bcc95e2e | ||
|
|
6a6bdba08b | ||
|
|
39e8eab6cb | ||
|
|
b4b00e1983 | ||
|
|
3e4129c3f1 | ||
|
|
533400b1b9 | ||
|
|
cdfaac773a | ||
|
|
e7b751c3f1 | ||
|
|
dd9b236547 | ||
|
|
d40995e1d9 | ||
|
|
28b78e0872 | ||
|
|
21a9fc54f3 | ||
|
|
74174ca343 | ||
|
|
1b2e062bb6 | ||
|
|
e49408db2a | ||
|
|
e6f6798f34 | ||
|
|
2266cad0f8 | ||
|
|
c9c974d191 | ||
|
|
fb16369384 | ||
|
|
5adf8db676 | ||
|
|
c99f97323d | ||
|
|
cf3a208591 | ||
|
|
1537e8a137 | ||
|
|
9a9f41c784 | ||
|
|
e236654ecf | ||
|
|
f693406df6 | ||
|
|
708f14fdb6 | ||
|
|
918bb997b9 | ||
|
|
b449cf3a11 | ||
|
|
fb68d9be22 | ||
|
|
6b819bbce5 | ||
|
|
d742633e6d | ||
|
|
e0784a670c | ||
|
|
c42a97bb5f | ||
|
|
ddfff44c12 | ||
|
|
554588fb42 | ||
|
|
99238636e6 | ||
|
|
5928655bc1 | ||
|
|
bf8ad0a78c | ||
|
|
d87c177d50 | ||
|
|
ff23cd27b8 | ||
|
|
3d67f8bb55 | ||
|
|
6e7ca3f998 | ||
|
|
473963b58b | ||
|
|
31184c1cd4 | ||
|
|
a430d45e2a | ||
|
|
a6833473de | ||
|
|
9aecd4dfb4 | ||
|
|
c7c84b4fd3 | ||
|
|
3ad136b803 | ||
|
|
3b460b5f04 | ||
|
|
66c8261438 | ||
|
|
3ba5793755 | ||
|
|
c9afc111cc | ||
|
|
f320ce58a2 | ||
|
|
afb6fbb1b1 | ||
|
|
e965ed9816 | ||
|
|
94becec429 | ||
|
|
fda68b690d | ||
|
|
fa29486af6 | ||
|
|
a38f72fa46 | ||
|
|
a5f5fb00ba | ||
|
|
04e76ddd9d | ||
|
|
85b1cee1b9 | ||
|
|
21da4d14e6 | ||
|
|
be6e765d76 | ||
|
|
23070ee19a | ||
|
|
2fb1364282 | ||
|
|
9120943a1a | ||
|
|
bd6158c0bc | ||
|
|
3687d597c4 | ||
|
|
45f14f0b72 | ||
|
|
905b401aab | ||
|
|
4bc5407bf2 | ||
|
|
e084835249 | ||
|
|
cea888573c | ||
|
|
9495cff295 | ||
|
|
c3d3a05f6e | ||
|
|
a0c730299c | ||
|
|
778f189a21 | ||
|
|
5ec1173f15 | ||
|
|
409e6513e3 | ||
|
|
e59f9a9659 |
@@ -0,0 +1,17 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "vue-eslint-parser",
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist_server
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@sammo/api_def",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./gateway": "./dist/gateway.js",
|
||||
"./def": "./dist/def/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"author": "Hide_D <hided62@gmail.com>",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sammo/crypto": "workspace:^",
|
||||
"@sammo/secure_token": "workspace:^",
|
||||
"@sammo/util": "workspace:^",
|
||||
"@strpc/def": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const hello = "hello";
|
||||
@@ -0,0 +1,62 @@
|
||||
import { GET, POST, type DefAPINamespace } from "@strpc/def";
|
||||
|
||||
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
|
||||
import type {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
InvalidResponse, ValidResponse
|
||||
} from "@strpc/def";
|
||||
import type {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
ArgDeleteAPI, ArgGetAPI, ArgHeadAPI, ArgPatchAPI, ArgPostAPI, ArgPutAPI,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
EmptyDeleteAPI, EmptyGetAPI, EmptyHeadAPI, EmptyPatchAPI, EmptyPostAPI, EmptyPutAPI,
|
||||
} from "@strpc/def/types";
|
||||
|
||||
export type LoginResponse = {
|
||||
result: true,
|
||||
nextToken: [number, string] | undefined,
|
||||
}
|
||||
|
||||
export type LoginFailed = {
|
||||
result: false,
|
||||
reqOTP: boolean,
|
||||
reason: string,
|
||||
}
|
||||
|
||||
|
||||
export type AutoLoginNonceResponse = {
|
||||
result: true,
|
||||
loginNonce: string,
|
||||
};
|
||||
|
||||
export type AutoLoginResponse = {
|
||||
result: true,
|
||||
nextToken: [number, string] | undefined,
|
||||
}
|
||||
|
||||
|
||||
export type AutoLoginFailed = {
|
||||
result: false,
|
||||
silent: boolean,
|
||||
reason: string,
|
||||
}
|
||||
|
||||
export const structure = {
|
||||
Login: {
|
||||
LoginByID: POST<{
|
||||
id: string,
|
||||
password: string,
|
||||
}, LoginResponse, LoginFailed>(),
|
||||
LoginByToken: POST<{
|
||||
hashedToken: string,
|
||||
token_id: number,
|
||||
}, AutoLoginResponse, AutoLoginFailed>(),
|
||||
ReqNonce: GET<AutoLoginNonceResponse, AutoLoginFailed>(),
|
||||
},
|
||||
GetGameLoginToken: GET<{
|
||||
result: true,
|
||||
gameLoginToken: string,
|
||||
userID: number,
|
||||
}>(),
|
||||
|
||||
} satisfies DefAPINamespace;
|
||||
@@ -0,0 +1,33 @@
|
||||
import type {
|
||||
DefAPINamespace,
|
||||
} from "@strpc/def";
|
||||
import { GET, POST } from '@strpc/def';
|
||||
|
||||
//굳이 할 필요는 없지만, d.ts가 깔끔해짐
|
||||
import type {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
InvalidResponse, ValidResponse
|
||||
} from "@strpc/def";
|
||||
import type {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
ArgDeleteAPI, ArgGetAPI, ArgHeadAPI, ArgPatchAPI, ArgPostAPI, ArgPutAPI,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
EmptyDeleteAPI, EmptyGetAPI, EmptyHeadAPI, EmptyPatchAPI, EmptyPostAPI, EmptyPutAPI,
|
||||
} from "@strpc/def/types";
|
||||
|
||||
|
||||
export type {
|
||||
InferResponse,
|
||||
InferError,
|
||||
InferQuery,
|
||||
} from "@strpc/def";
|
||||
export type * as def from './def/index.js';
|
||||
|
||||
export const structure = {
|
||||
daemon: {
|
||||
status: GET<ValidResponse & { isRunning: boolean, lastExecuted?: string }>(),
|
||||
start: POST<ValidResponse>(),
|
||||
stop: POST<ValidResponse>(),
|
||||
restart: POST<ValidResponse>(),
|
||||
}
|
||||
} satisfies DefAPINamespace;
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
|
||||
"path": "../util"
|
||||
},
|
||||
{
|
||||
"path": "../secure_token"
|
||||
},
|
||||
{
|
||||
"path": "../../@strpc/def"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@sammo/client",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc",
|
||||
"build-only": "vite build",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"author": "Hide_D <hided62@gmail.com>",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sammo/game_logic": "workspace:^",
|
||||
"@sammo/util": "workspace:^",
|
||||
"@strpc/client_ky": "workspace:^",
|
||||
"@strpc/def": "workspace:^",
|
||||
"@trpc/client": "^11.6.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootstrap-vue-next": "^0.40.6",
|
||||
"bson": "^6.10.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"ky": "^1.11.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.7.0",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tslib": "^2.8.1",
|
||||
"vite": "^7.1.9",
|
||||
"vue-tsc": "^3.1.1",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
|
||||
|
||||
<div class="wrapper">
|
||||
<HelloWorld msg="You did it!" />
|
||||
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in <code>README.md</code>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
||||
Discord server, or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also subscribe to
|
||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
||||
the official
|
||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
twitter account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router/index.js'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import TheWelcome from '../components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../tsconfig.vite.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../util"
|
||||
},
|
||||
{
|
||||
"path": "../secure_token"
|
||||
},
|
||||
{
|
||||
"path": "../../@strpc/def"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@util': fileURLToPath(new URL('./server/util', import.meta.url)),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
root: false,
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off'
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@sammo/crypto",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./AES": "./dist/AES.js",
|
||||
"./ECDSA": "./dist/ECDSA.js",
|
||||
"./ECDHe": "./dist/ECDHe.js",
|
||||
"./ECDHe_AES": "./dist/ECDHe_AES.js",
|
||||
"./ECKey": "./dist/ECKey.js",
|
||||
"./PBKDF2": "./dist/PBKDF2.js",
|
||||
"./SHA2": "./dist/SHA2.js",
|
||||
"./RawTypes": "./dist/RawTypes.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"test": "vitest"
|
||||
},
|
||||
"author": "Hide_D <hided62@gmail.com>",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sammo/util": "workspace:^",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"bson": "^6.10.4",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^9.37.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bson": "^5.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./AES.js";
|
||||
|
||||
type TestType = {
|
||||
key: Buffer<ArrayBuffer>,
|
||||
IV: Buffer<ArrayBuffer>,
|
||||
PT: Buffer<ArrayBuffer>,
|
||||
AAD?: Buffer<ArrayBuffer>,
|
||||
CT: Buffer<ArrayBuffer>,
|
||||
failed?: boolean,
|
||||
};
|
||||
|
||||
type RawTestType = {
|
||||
key: string,
|
||||
IV: string,
|
||||
PT: string,
|
||||
AAD?: string,
|
||||
CT: string,
|
||||
failed?: boolean,
|
||||
}
|
||||
|
||||
function convertItem(obj: RawTestType): TestType {
|
||||
return {
|
||||
key: Buffer.from(obj.key, 'hex'),
|
||||
IV: Buffer.from(obj.IV, 'hex'),
|
||||
PT: Buffer.from(obj.PT, 'hex'),
|
||||
AAD: obj.AAD ? Buffer.from(obj.AAD, 'hex') : undefined,
|
||||
CT: Buffer.from(obj.CT, 'hex'),
|
||||
failed: obj.failed,
|
||||
}
|
||||
}
|
||||
|
||||
const tests: TestType[] = [
|
||||
{
|
||||
key: '92e11dcdaa866f5ce790fd24501f92509aacf4cb8b1339d50c9c1240935dd08b',
|
||||
IV : 'ac93a1a6145299bde902f21a',
|
||||
PT : '2d71bcfa914e4ac045b2aa60955fad24',
|
||||
AAD: '1e0889016f67601c8ebea4943bc23ad6',
|
||||
CT : '8995ae2e6df3dbf96fac7b7137bae67feca5aa77d51d4a0a14d9c51e1da474ab',
|
||||
failed: false,
|
||||
},
|
||||
{
|
||||
key: 'b52c505a37d78eda5dd34f20c22540ea1b58963cf8e5bf8ffa85f9f2492505b4',
|
||||
IV : '516c33929df5a3284ff463d7',
|
||||
PT : '',
|
||||
AAD: '',
|
||||
CT : 'bdc1ac884d332457a1d2664f168c76f0',
|
||||
failed: false,
|
||||
},
|
||||
{
|
||||
key: '886cff5f3e6b8d0e1ad0a38fcdb26de97e8acbe79f6bed66959a598fa5047d65',
|
||||
IV : '3a8efa1cd74bbab5448f9945',
|
||||
PT : '',
|
||||
AAD: '519fee519d25c7a304d6c6aa1897ee1eb8c59655',
|
||||
CT : 'f6d47505ec96c98a42dc3ae719877b87',
|
||||
failed: false,
|
||||
},
|
||||
{
|
||||
key: '460fc864972261c2560e1eb88761ff1c992b982497bd2ac36c04071cbb8e5d99',
|
||||
IV : '8a4a16b9e210eb68bcb6f58d',
|
||||
PT : '99e4e926ffe927f691893fb79a96b067',
|
||||
AAD: '',
|
||||
CT : '133fc15751621b5f325c7ff71ce08324ec4e87e0cf74a13618d0b68636ba9fa7',
|
||||
failed: false,
|
||||
},
|
||||
].map(convertItem)
|
||||
|
||||
|
||||
|
||||
|
||||
test.each(tests)('encrypt(%#)', async (item) => {
|
||||
for (const item of tests) {
|
||||
|
||||
if(item.failed){
|
||||
expect(() => AES_GCM_Encrypt(item.key, item.IV, item.PT, item.AAD)).rejects.toThrow('Invalid');
|
||||
return;
|
||||
}
|
||||
const ct = await AES_GCM_Encrypt(item.key, item.IV, item.PT, item.AAD);
|
||||
expect(ct).toEqual(item.CT.buffer);
|
||||
}
|
||||
})
|
||||
|
||||
test.each(tests)('decrypt(%#)', async (item) => {
|
||||
for (const item of tests) {
|
||||
|
||||
if(item.failed){
|
||||
expect(() => AES_GCM_Decrypt(item.key, item.IV, item.CT, item.AAD)).rejects.toThrow('Invalid');
|
||||
return;
|
||||
}
|
||||
const pt = await AES_GCM_Decrypt(item.key, item.IV, item.CT, item.AAD);
|
||||
expect(pt).toEqual(item.PT.buffer);
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
|
||||
export async function AES_GCM_Encrypt(key: BufferSource | CryptoKey, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-GCM",
|
||||
}, false, ["encrypt"]);
|
||||
|
||||
const ciphertext = await subtle.encrypt({
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
additionalData: aad
|
||||
}, keyObj, msg);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
|
||||
export async function AES_GCM_Decrypt(key: BufferSource | CryptoKey, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-GCM",
|
||||
}, false, ["decrypt"]);
|
||||
|
||||
const plaintext = await subtle.decrypt({
|
||||
name: "AES-GCM",
|
||||
iv: iv,
|
||||
additionalData: aad
|
||||
}, keyObj, ciphertext);
|
||||
//아마도 tag 검증 실패시 예외가 발생할 것으로 예상
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
export async function AES_CBC_Encrypt(key: BufferSource | CryptoKey, iv: BufferSource, msg: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-CBC",
|
||||
}, false, ["encrypt"]);
|
||||
|
||||
const ciphertext = await subtle.encrypt({
|
||||
name: "AES-CBC",
|
||||
iv,
|
||||
}, keyObj, msg);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
|
||||
export async function AES_CBC_Decrypt(key: BufferSource | CryptoKey, iv: BufferSource, ciphertext: BufferSource): Promise<ArrayBuffer> {
|
||||
const keyObj = key instanceof CryptoKey ? key : await subtle.importKey("raw", key, {
|
||||
name: "AES-CBC",
|
||||
}, false, ["decrypt"]);
|
||||
|
||||
const plaintext = await subtle.decrypt({
|
||||
name: "AES-CBC",
|
||||
iv: iv,
|
||||
}, keyObj, ciphertext);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { wrapBuffer } from "@sammo/util";
|
||||
import { ECDSA_sign, ECDSA_verify } from "./ECDSA.js";
|
||||
import { verifyKeyFromSignKey } from "./ECKey.js";
|
||||
import type { ECDHe_P384_PublicKey, ECDHe_P384_KeyPair, ECDSA_PKCS8_P384_SignKey, ECDHe_P384_PrivateKey, ECDHe_P384_PublicKeyInfo, ECDHe_P384_LiteKeyPair } from "./RawTypes.js";
|
||||
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export const curveName: EcKeyGenParams | EcKeyImportParams = {
|
||||
name: 'ECDH',
|
||||
namedCurve: 'P-384'
|
||||
}
|
||||
|
||||
export async function genECDHeKey(signKey: ECDSA_PKCS8_P384_SignKey): Promise<ECDHe_P384_KeyPair> {
|
||||
const keyPairP = subtle.generateKey(curveName, true, ['deriveKey', 'deriveBits']);
|
||||
const verifyKeyP = verifyKeyFromSignKey(signKey);
|
||||
|
||||
const rawKeyPair = await keyPairP;
|
||||
const publicKey = wrapBuffer<ECDHe_P384_PublicKey>(await subtle.exportKey('spki', rawKeyPair.publicKey));
|
||||
const privateKey = wrapBuffer<ECDHe_P384_PrivateKey>(await subtle.exportKey('pkcs8', rawKeyPair.privateKey));
|
||||
|
||||
const signatureP = ECDSA_sign(signKey, privateKey);
|
||||
|
||||
return {
|
||||
publicInfo: {
|
||||
publicKey,
|
||||
verifyKey: await verifyKeyP,
|
||||
sign: await signatureP
|
||||
},
|
||||
privateKey,
|
||||
}
|
||||
}
|
||||
|
||||
export async function genECDHeLiteKey(): Promise<ECDHe_P384_LiteKeyPair> {
|
||||
const keyPair = await subtle.generateKey(curveName, true, ['deriveKey', 'deriveBits']);
|
||||
const publicKey = wrapBuffer<ECDHe_P384_PublicKey>(await subtle.exportKey('spki', keyPair.publicKey));
|
||||
const privateKey = wrapBuffer<ECDHe_P384_PrivateKey>(await subtle.exportKey('pkcs8', keyPair.privateKey));
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
privateKey
|
||||
};
|
||||
}
|
||||
|
||||
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, keySizeBit?: number): Promise<ArrayBuffer>;
|
||||
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, keySizeBit?: number): Promise<ArrayBuffer>;
|
||||
|
||||
export async function deriveKey(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, keySizeBit = 256): Promise<ArrayBuffer> {
|
||||
|
||||
const rawPublicKey = other instanceof Uint8Array ? other : other.publicKey;
|
||||
const rawPrivateKey = me.privateKey;
|
||||
const publicKeyP = subtle.importKey('spki', rawPublicKey, curveName, true, ['deriveBits']);
|
||||
const privateKeyP = subtle.importKey('pkcs8', rawPrivateKey, curveName, true, ['deriveBits']);
|
||||
|
||||
if (!(other instanceof Uint8Array) && !(await ECDSA_verify(other.verifyKey, other.publicKey, other.sign))) {
|
||||
throw new Error("Invalid signature");
|
||||
}
|
||||
|
||||
return await subtle.deriveBits({
|
||||
name: 'ECDH',
|
||||
public: await publicKeyP
|
||||
|
||||
}, await privateKeyP, keySizeBit);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { AES_GCM_Decrypt, AES_GCM_Encrypt } from "./AES.js";
|
||||
import { deriveKey } from "./ECDHe.js";
|
||||
import type { ECDHe_P384_KeyPair, ECDHe_P384_LiteKeyPair, ECDHe_P384_PublicKey, ECDHe_P384_PublicKeyInfo } from "./RawTypes.js";
|
||||
|
||||
export type EncryptedItem = {
|
||||
ct: Uint8Array;
|
||||
}
|
||||
|
||||
const keySizeBit = 256;
|
||||
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
|
||||
export async function ECDHe_AES_GCM_Encrypt(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, iv: BufferSource, msg: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>{
|
||||
let key: ArrayBuffer;
|
||||
if('publicInfo' in me){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else if('publicKey' in other){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else{
|
||||
throw new Error("Invalid argument: may be trying to do ECDH, not ECDHe.");
|
||||
}
|
||||
|
||||
return AES_GCM_Encrypt(key, iv, msg, aad);
|
||||
}
|
||||
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair | ECDHe_P384_KeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo, me: ECDHe_P384_LiteKeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>;
|
||||
|
||||
|
||||
export async function ECDHe_AES_GCM_Decrypt(other: ECDHe_P384_PublicKeyInfo | ECDHe_P384_PublicKey, me: ECDHe_P384_KeyPair | ECDHe_P384_LiteKeyPair, iv: BufferSource, ciphertext: BufferSource, aad?: BufferSource): Promise<ArrayBuffer>{
|
||||
let key: ArrayBuffer;
|
||||
if('publicInfo' in me){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else if('publicKey' in other){
|
||||
key = await deriveKey(other, me, keySizeBit);
|
||||
}
|
||||
else{
|
||||
throw new Error("Invalid argument: may be trying to do ECDH, not ECDHe.");
|
||||
}
|
||||
|
||||
return AES_GCM_Decrypt(key, iv, ciphertext, aad);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { ECDSA_sign_bson, ECDSA_verify_bson } from "./ECDSA.js";
|
||||
import { genKey } from "./ECKey.js";
|
||||
import { randomBytes } from "./utils.js";
|
||||
test('basic sign', async () => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
const misVerifyKey = Buffer.alloc(verifyKey.length);
|
||||
verifyKey.copy(misVerifyKey);
|
||||
misVerifyKey[misVerifyKey.length - 2] ^= 1; //1bit
|
||||
|
||||
const msg = randomBytes(100);
|
||||
const misMsg = Buffer.alloc(msg.length);
|
||||
msg.copy(misMsg);
|
||||
misMsg[1] ^= 1;
|
||||
|
||||
const signature = await ECDSA_sign_bson(signKey, msg);
|
||||
expect(signature.length).toEqual(96);
|
||||
|
||||
const misSignature = Buffer.alloc(signature.length);
|
||||
signature.copy(misSignature);
|
||||
misSignature[misSignature.length - 2] ^= 3;
|
||||
|
||||
const t0P = ECDSA_verify_bson(verifyKey, msg, signature);
|
||||
const f1P = ECDSA_verify_bson(misVerifyKey, msg, signature);
|
||||
const f2P = ECDSA_verify_bson(verifyKey, misMsg, signature);
|
||||
const f3P = ECDSA_verify_bson(verifyKey, msg, misSignature);
|
||||
|
||||
expect(await t0P).toEqual(true);
|
||||
expect(await f1P).toEqual(false);
|
||||
expect(await f2P).toEqual(false);
|
||||
expect(await f3P).toEqual(false);
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import { BSON } from "bson";
|
||||
import { type Bsonifiable, bsonify, type Jsonifiable, jsonify } from "@sammo/util";
|
||||
import { importSignKey, importVerifyKey } from "./ECKey.js";
|
||||
import type { ECDSASignature, ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey, ECDSASignatureBSON, ECDSASignatureJSON, ECDSASignatureRaw } from "./RawTypes.js";
|
||||
import { isBufferSource } from './types.js';
|
||||
|
||||
// 간편하게 저장하기 위해서 그냥! 매번 복잡한 일을 하도록 하자
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
const hashName: EcdsaParams = {
|
||||
name: 'ECDSA',
|
||||
hash: 'SHA-512'
|
||||
}
|
||||
|
||||
export async function ECDSA_sign(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource | Uint8Array): Promise<ECDSASignatureRaw> {
|
||||
const subtleSignKeyP = importSignKey(sign_key);
|
||||
if (msg instanceof Uint8Array){
|
||||
if(!(msg.buffer instanceof ArrayBuffer)){
|
||||
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
|
||||
nmsg.set(msg);
|
||||
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, nmsg));
|
||||
}
|
||||
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
|
||||
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, msg as Uint8Array<ArrayBuffer>));
|
||||
}
|
||||
return Buffer.from(await subtle.sign(hashName, await subtleSignKeyP, msg));
|
||||
}
|
||||
|
||||
|
||||
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureBSON>
|
||||
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource | Uint8Array): Promise<ECDSASignatureRaw>
|
||||
export async function ECDSA_sign_bson(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable | BufferSource | Uint8Array): Promise<ECDSASignatureRaw | ECDSASignatureBSON> {
|
||||
if (!isBufferSource(msg)) {
|
||||
const nmsg = BSON.serialize(bsonify(msg as Record<string, string>));
|
||||
return await ECDSA_sign(sign_key, nmsg) as ECDSASignatureBSON;
|
||||
}
|
||||
return ECDSA_sign(sign_key, msg);
|
||||
}
|
||||
|
||||
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Bsonifiable): Promise<ECDSASignatureJSON>
|
||||
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: BufferSource | Uint8Array): Promise<ECDSASignatureRaw>
|
||||
export async function ECDSA_sign_json(sign_key: ECDSA_PKCS8_P384_SignKey, msg: Jsonifiable | BufferSource | Uint8Array): Promise<ECDSASignatureRaw | ECDSASignatureJSON> {
|
||||
if (!isBufferSource(msg)) {
|
||||
const nmsg = Buffer.from(JSON.stringify(jsonify(msg as Record<string, string>)), 'utf-8') as Uint8Array<ArrayBuffer>;
|
||||
return await ECDSA_sign(sign_key, nmsg) as ECDSASignatureJSON;
|
||||
}
|
||||
return ECDSA_sign(sign_key, msg);
|
||||
}
|
||||
|
||||
export async function ECDSA_verify(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource | Uint8Array, sign: ECDSASignature): Promise<boolean> {
|
||||
try {
|
||||
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
|
||||
const subtleVerifyKeyP = importVerifyKey(verify_key);
|
||||
if (msg instanceof Uint8Array){
|
||||
if(!(msg.buffer instanceof ArrayBuffer)){
|
||||
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
|
||||
nmsg.set(msg);
|
||||
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, nmsg);
|
||||
}
|
||||
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, msg as Uint8Array<ArrayBuffer>);
|
||||
}
|
||||
return await subtle.verify(hashName, await subtleVerifyKeyP, sign, msg);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureBSON): Promise<boolean>;
|
||||
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource | Uint8Array, sign: ECDSASignatureRaw): Promise<boolean>;
|
||||
export async function ECDSA_verify_bson(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable | BufferSource | Uint8Array, sign: ECDSASignatureBSON | ECDSASignatureRaw): Promise<boolean> {
|
||||
|
||||
try {
|
||||
if (msg instanceof Uint8Array){
|
||||
return ECDSA_verify(verify_key, msg, sign);
|
||||
}
|
||||
if (!isBufferSource(msg)) {
|
||||
msg = BSON.serialize(bsonify(msg as Record<string, string>));
|
||||
}
|
||||
return ECDSA_verify(verify_key, msg, sign);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Bsonifiable, sign: ECDSASignatureJSON): Promise<boolean>;
|
||||
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: BufferSource | Uint8Array, sign: ECDSASignatureRaw): Promise<boolean>;
|
||||
export async function ECDSA_verify_json(verify_key: ECDSA_P384_VerifyKey, msg: Jsonifiable | BufferSource | Uint8Array, sign: ECDSASignatureJSON | ECDSASignatureRaw): Promise<boolean> {
|
||||
|
||||
try {
|
||||
if (msg instanceof Uint8Array){
|
||||
return ECDSA_verify(verify_key, msg, sign);
|
||||
}
|
||||
if (!isBufferSource(msg)) {
|
||||
const x = JSON.stringify(jsonify(msg as Record<string, string>));
|
||||
return ECDSA_verify(verify_key, Buffer.from(x, 'utf-8'), sign);
|
||||
}
|
||||
return ECDSA_verify(verify_key, msg, sign);
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { genKey, verifyKeyFromSignKey, pemFromVerifyKey, importVerifyKey, importSignKey, pemFromSignKey } from "./ECKey.js";
|
||||
import { TypeLength } from "./TypeLength.js";
|
||||
import { decodePEM } from "./utils.js";
|
||||
|
||||
test('gen_key', async () => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
expect(signKey.length).toEqual(TypeLength.ECDSA_PKCS8_P384_SignKey);
|
||||
expect(verifyKey.length).toEqual(TypeLength.ECDSA_SPKI_P384_VerifyKey);
|
||||
|
||||
const pemVerifyKey = await pemFromVerifyKey(verifyKey);
|
||||
const verifyKey2 = decodePEM(pemVerifyKey, 'PUBLIC KEY')[0];
|
||||
expect(verifyKey).toEqual(verifyKey2);
|
||||
|
||||
});
|
||||
|
||||
test('convert', async () => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
const newVerifyKey = await verifyKeyFromSignKey(signKey);
|
||||
|
||||
expect(verifyKey).toEqual(newVerifyKey);
|
||||
})
|
||||
|
||||
test('import sign_key', async() => {
|
||||
const [signKey, verifyKey] = await genKey();
|
||||
|
||||
const pemSignKey = pemFromSignKey(signKey);
|
||||
const signKey2 = decodePEM(pemSignKey, 'EC PRIVATE KEY')[0];
|
||||
|
||||
const signKeyObj = await importSignKey(signKey, true);
|
||||
const signKeyObj2 = await importSignKey(signKey2, true);
|
||||
|
||||
expect(signKeyObj).toEqual(signKeyObj2);
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
import type { ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey } from "./RawTypes.js";
|
||||
import { encodePEM } from "./utils.js";
|
||||
|
||||
// 간편하게 저장하기 위해서 그냥! 매번 복잡한 일을 하도록 하자
|
||||
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export const curveName: EcKeyGenParams | EcKeyImportParams = {
|
||||
name: 'ECDSA',
|
||||
namedCurve: 'P-384'
|
||||
}
|
||||
|
||||
export async function importSignKey(signKey: ECDSA_PKCS8_P384_SignKey, allowExport?: boolean): Promise<CryptoKey> {
|
||||
return await subtle.importKey('pkcs8', signKey, curveName, allowExport ?? false, ['sign']);
|
||||
}
|
||||
|
||||
export async function importVerifyKeyRaw(verifyKey: ECDSA_P384_VerifyKey, allowExport?: boolean): Promise<CryptoKey> {
|
||||
return await subtle.importKey('raw', verifyKey, curveName, allowExport ?? false, ['verify'])
|
||||
}
|
||||
|
||||
export async function exportVerifyKeyRaw(verifyKey: ECDSA_P384_VerifyKey): Promise<Buffer>{
|
||||
const key = await importVerifyKeyRaw(verifyKey);
|
||||
return Buffer.from(await subtle.exportKey('raw', key));
|
||||
}
|
||||
|
||||
export async function importVerifyKey(verifyKey: ECDSA_P384_VerifyKey, allowExport?: boolean): Promise<CryptoKey> {
|
||||
return await subtle.importKey('spki', verifyKey, curveName, allowExport ?? false, ['verify'])
|
||||
}
|
||||
|
||||
|
||||
export async function genKey(): Promise<[ECDSA_PKCS8_P384_SignKey, ECDSA_P384_VerifyKey]> {
|
||||
const keyPair = await subtle.generateKey(curveName, true, ['sign', 'verify']);
|
||||
const signKey = subtle.exportKey('pkcs8', keyPair.privateKey);
|
||||
const verifyKey = subtle.exportKey('spki', keyPair.publicKey);
|
||||
return [Buffer.from(await signKey), Buffer.from(await verifyKey)];
|
||||
}
|
||||
|
||||
export async function pemFromVerifyKey(verifyKey: ECDSA_P384_VerifyKey): Promise<string> {
|
||||
const subtleVerifyKey = await importVerifyKey(verifyKey, true);
|
||||
const spkiRaw = await subtle.exportKey('spki', subtleVerifyKey)
|
||||
return encodePEM(Buffer.from(spkiRaw), 'PUBLIC KEY');
|
||||
}
|
||||
|
||||
export function pemFromSignKey(signKey: ECDSA_PKCS8_P384_SignKey): string {
|
||||
return encodePEM(signKey, 'EC PRIVATE KEY');
|
||||
}
|
||||
|
||||
export async function verifyKeyFromSignKey(signKey: ECDSA_PKCS8_P384_SignKey): Promise<ECDSA_P384_VerifyKey> {
|
||||
const subtleSignKey = await importSignKey(signKey, true);
|
||||
const jwk = await subtle.exportKey('jwk', subtleSignKey);
|
||||
delete jwk.d;
|
||||
delete jwk.dp;
|
||||
delete jwk.dq;
|
||||
delete jwk.q;
|
||||
delete jwk.qi;
|
||||
jwk.key_ops = ["verify"];
|
||||
|
||||
const subtleVerifyKey = await subtle.importKey('jwk', jwk, curveName, true, ['verify']);
|
||||
const verifyKey = await subtle.exportKey('spki', subtleVerifyKey);
|
||||
return Buffer.from(verifyKey);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import type { RNG } from "./RNG.js";
|
||||
import { sha512 } from './SHA2.js';
|
||||
import { convertBytesLikeToUint8Array } from "@sammo/util/converter";
|
||||
import type { BytesLike } from "@sammo/util";
|
||||
import { delay } from "@sammo/util";
|
||||
|
||||
const maxRngSupportBit = 53;
|
||||
const maxInt = 0x1f_ffff_ffff_ffff; // NOTE: b 0, 10000110011, 11...11
|
||||
const maxIntMore1 = 0x20_0000_0000_0000n; //NOTE: b 0, 10000110100, 00...00
|
||||
const maxIntMore1f = Number(maxIntMore1);
|
||||
export const bufferByteSize = 512 / 8; //SHA512
|
||||
|
||||
const intBitMapMask = new Map([
|
||||
[0x1n, 1],
|
||||
[0x3n, 2],
|
||||
[0x7n, 3],
|
||||
[0xfn, 4],
|
||||
[0x1fn, 5],
|
||||
[0x3fn, 6],
|
||||
[0x7fn, 7],
|
||||
[0xffn, 8],
|
||||
[0x1ffn, 9],
|
||||
[0x3ffn, 10],
|
||||
[0x7ffn, 11],
|
||||
[0xfffn, 12],
|
||||
[0x1fffn, 13],
|
||||
[0x3fffn, 14],
|
||||
[0x7fffn, 15],
|
||||
[0xffffn, 16],
|
||||
[0x1ffffn, 17],
|
||||
[0x3ffffn, 18],
|
||||
[0x7ffffn, 19],
|
||||
[0xfffffn, 20],
|
||||
[0x1fffffn, 21],
|
||||
[0x3fffffn, 22],
|
||||
[0x7fffffn, 23],
|
||||
[0xffffffn, 24],
|
||||
[0x1ffffffn, 25],
|
||||
[0x3ffffffn, 26],
|
||||
[0x7ffffffn, 27],
|
||||
[0xfffffffn, 28],
|
||||
[0x1fffffffn, 29],
|
||||
[0x3fffffffn, 30],
|
||||
[0x7fffffffn, 31],
|
||||
[0xffffffffn, 32],
|
||||
[0x1ffffffffn, 33],
|
||||
[0x3ffffffffn, 34],
|
||||
[0x7ffffffffn, 35],
|
||||
[0xfffffffffn, 36],
|
||||
[0x1fffffffffn, 37],
|
||||
[0x3fffffffffn, 38],
|
||||
[0x7fffffffffn, 39],
|
||||
[0xffffffffffn, 40],
|
||||
[0x1ffffffffffn, 41],
|
||||
[0x3ffffffffffn, 42],
|
||||
[0x7ffffffffffn, 43],
|
||||
[0xfffffffffffn, 44],
|
||||
[0x1fffffffffffn, 45],
|
||||
[0x3fffffffffffn, 46],
|
||||
[0x7fffffffffffn, 47],
|
||||
[0xffffffffffffn, 48],
|
||||
[0x1ffffffffffffn, 49],
|
||||
[0x3ffffffffffffn, 50],
|
||||
[0x7ffffffffffffn, 51],
|
||||
[0xfffffffffffffn, 52],
|
||||
[0x1fffffffffffffn, 53],
|
||||
]);
|
||||
|
||||
function calcBitMask(n: bigint): bigint {
|
||||
n |= n >> 1n;
|
||||
n |= n >> 2n;
|
||||
n |= n >> 4n;
|
||||
n |= n >> 8n;
|
||||
n |= n >> 16n;
|
||||
n |= n >> 32n;
|
||||
|
||||
return n;
|
||||
}
|
||||
export class LiteHashDRBG implements RNG {
|
||||
|
||||
protected buffer!: ArrayBuffer;
|
||||
protected bufferIdx!: number;
|
||||
protected hq: DataView<ArrayBuffer>;
|
||||
protected hqIdxPos: number;
|
||||
|
||||
protected ready: Promise<void>;
|
||||
|
||||
public constructor(protected seed: BytesLike, protected stateIdx = 0, bufferIdx = 0) {
|
||||
if (bufferIdx < 0) {
|
||||
throw new Error(`bufferIdx ${bufferIdx} < 0`);
|
||||
}
|
||||
if (bufferIdx >= bufferByteSize) {
|
||||
throw new Error(`bufferidx ${bufferIdx} >= ${bufferByteSize}`);
|
||||
}
|
||||
if (stateIdx < 0) {
|
||||
throw new Error(`stateIdx ${stateIdx} < 0`);
|
||||
}
|
||||
|
||||
const seedU8 = convertBytesLikeToUint8Array(seed);
|
||||
const hqBuffer = new ArrayBuffer(seedU8.byteLength + 4);
|
||||
const hqU8 = new Uint8Array(hqBuffer);
|
||||
|
||||
hqU8.set(seedU8, 0);
|
||||
this.hq = new DataView(hqBuffer);
|
||||
this.hqIdxPos = seedU8.byteLength;
|
||||
|
||||
this.ready = this.genNextBlock();
|
||||
this.bufferIdx = bufferIdx;
|
||||
}
|
||||
|
||||
protected async genNextBlock(): Promise<void> {
|
||||
this.bufferIdx = 0;
|
||||
this.hq.setUint32(this.hqIdxPos, this.stateIdx, true);
|
||||
this.stateIdx += 1;
|
||||
const digest = await sha512(this.hq.buffer);
|
||||
this.buffer = digest;
|
||||
}
|
||||
|
||||
public getMaxInt(): number {
|
||||
return maxInt;
|
||||
}
|
||||
|
||||
public async nextBytes(bytes: number, baseBytes?: number): Promise<Uint8Array<ArrayBuffer>> {
|
||||
bytes |= 0;
|
||||
if (bytes <= 0) {
|
||||
throw new Error(`${bytes} <= 0`);
|
||||
}
|
||||
|
||||
const ticket = this.ready;
|
||||
|
||||
let waiter: Promise<Uint8Array<ArrayBuffer> | undefined> = Promise.resolve(undefined);
|
||||
|
||||
let nextBlockWait: (() => void) | null = (() => { throw 'something wrong'; });
|
||||
|
||||
this.ready = new Promise((resolve, reject) => {
|
||||
waiter = (async () => {
|
||||
await ticket;
|
||||
nextBlockWait = resolve;
|
||||
|
||||
if (this.bufferIdx + bytes <= bufferByteSize) {
|
||||
if (baseBytes === undefined || bytes >= baseBytes) {
|
||||
const result = this.buffer.slice(this.bufferIdx, this.bufferIdx + bytes);
|
||||
this.bufferIdx += bytes;
|
||||
if (this.bufferIdx === bufferByteSize) {
|
||||
nextBlockWait = null;
|
||||
this.genNextBlock().then(resolve, reject);
|
||||
}
|
||||
return new Uint8Array(result);
|
||||
}
|
||||
|
||||
const resultBuffer = new ArrayBuffer(Math.max(bytes, baseBytes ?? 0));
|
||||
const result = new Uint8Array(resultBuffer);
|
||||
result.set(new Uint8Array(this.buffer, this.bufferIdx, bytes));
|
||||
this.bufferIdx += bytes;
|
||||
if (this.bufferIdx === bufferByteSize) {
|
||||
nextBlockWait = null;
|
||||
this.genNextBlock().then(resolve, reject);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const resultBuffer = new ArrayBuffer(baseBytes ? Math.max(bytes, baseBytes) : bytes);
|
||||
const result = new Uint8Array(resultBuffer);
|
||||
|
||||
result.set(new Uint8Array(this.buffer, this.bufferIdx));
|
||||
let offset = bufferByteSize - this.bufferIdx;
|
||||
let remain = bytes - offset;
|
||||
|
||||
while (remain > bufferByteSize) {
|
||||
await this.genNextBlock();
|
||||
result.set(new Uint8Array(this.buffer), offset);
|
||||
offset += bufferByteSize;
|
||||
remain -= bufferByteSize;
|
||||
}
|
||||
|
||||
if (remain === 0) {
|
||||
nextBlockWait = null;
|
||||
this.genNextBlock().then(resolve, reject);
|
||||
return result;
|
||||
}
|
||||
|
||||
await this.genNextBlock();
|
||||
result.set(new Uint8Array(this.buffer, 0, remain), offset);
|
||||
this.bufferIdx = remain;
|
||||
return result;
|
||||
})();
|
||||
|
||||
});
|
||||
|
||||
//이 코드를 통해 Promise 내부가 실행된다
|
||||
await delay(0);
|
||||
|
||||
const nextBlock = await waiter;
|
||||
if (nextBlockWait) {
|
||||
nextBlockWait();
|
||||
}
|
||||
if(!nextBlock){ throw new Error("Internal error: nextBlock is undefined"); }
|
||||
return nextBlock;
|
||||
}
|
||||
|
||||
public async nextBits(bits: number, baseBytes?: number): Promise<Uint8Array<ArrayBuffer>> {
|
||||
await this.ready;
|
||||
|
||||
bits |= 0;
|
||||
const bytes = (bits + 7) >> 3;
|
||||
const headBits = bits & 0x7;
|
||||
|
||||
const result = await this.nextBytes(bytes, baseBytes);
|
||||
if (headBits === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result[bytes - 1] &= 0xff >> (8 - headBits);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async _nextInt(bits: number): Promise<bigint> {
|
||||
const buffer = await this.nextBits(bits, 8);
|
||||
const dataView = new DataView(buffer.buffer);
|
||||
return dataView.getBigUint64(0, true);
|
||||
}
|
||||
|
||||
public async nextInt(max?: number): Promise<number> {
|
||||
if (max === undefined || max === maxInt) {
|
||||
return Number(await this._nextInt(maxRngSupportBit));
|
||||
}
|
||||
if (max > maxInt) {
|
||||
throw new Error('Over max int');
|
||||
}
|
||||
if (max === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (max < 0) {
|
||||
return -this.nextInt(-max);
|
||||
}
|
||||
|
||||
const mask = calcBitMask(BigInt(max));
|
||||
const bits = intBitMapMask.get(mask) as number;
|
||||
|
||||
let n = Number(this._nextInt(bits));
|
||||
while (n > max) {
|
||||
n = Number(this._nextInt(bits));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
public async nextFloat1(): Promise<number> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const nInt = await this._nextInt(maxRngSupportBit + 1);
|
||||
if (nInt < maxIntMore1) {
|
||||
return Number(nInt) / maxIntMore1f;
|
||||
}
|
||||
if (nInt === maxIntMore1) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static build(seed: BytesLike, stateIdx = 0): LiteHashDRBG {
|
||||
return new LiteHashDRBG(seed, stateIdx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export type ValidEncAlg = 'AES256-GCM' | 'AES128-GCM' | 'AES256-CBC' | 'AES128-CBC';
|
||||
|
||||
const encAlgMap: Record<ValidEncAlg, AesDerivedKeyParams> = {
|
||||
'AES256-GCM': {
|
||||
name: 'AES-GCM',
|
||||
length: 256,
|
||||
},
|
||||
'AES128-GCM' : {
|
||||
name: 'AES-GCM',
|
||||
length: 128,
|
||||
},
|
||||
'AES256-CBC' : {
|
||||
name: 'AES-CBC',
|
||||
length: 256,
|
||||
},
|
||||
'AES128-CBC' : {
|
||||
name: 'AES-CBC',
|
||||
length: 128,
|
||||
},
|
||||
}
|
||||
|
||||
export async function PBKDF2_SHA512(password: string, salt: BufferSource, alg: ValidEncAlg = 'AES256-GCM', iterations = 600000): Promise<CryptoKey> {
|
||||
const param: Pbkdf2Params = {
|
||||
name: 'PBKDF2',
|
||||
hash: 'SHA-512',
|
||||
salt: salt,
|
||||
iterations,
|
||||
};
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const baseKey = await subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(password),
|
||||
{ name: "PBKDF2" },
|
||||
false,
|
||||
["deriveBits", "deriveKey"]
|
||||
);
|
||||
|
||||
return await subtle.deriveKey(
|
||||
param,
|
||||
baseKey,
|
||||
encAlgMap[alg],
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface RNG {
|
||||
|
||||
/**
|
||||
* nextInt()가 반환 가능한 최댓값
|
||||
*/
|
||||
getMaxInt(): number;
|
||||
|
||||
nextBytes(bytes: number): Promise<Uint8Array<ArrayBuffer>>;
|
||||
nextBits(bits: number): Promise<Uint8Array<ArrayBuffer>>;
|
||||
|
||||
nextInt(max?: number): Promise<number>;
|
||||
nextFloat1(): Promise<number>;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { RNG } from './RNG.js';
|
||||
|
||||
export class RandUtil {
|
||||
constructor(protected rng: RNG) {
|
||||
|
||||
}
|
||||
|
||||
public nextFloat1(): Promise<number> {
|
||||
return this.rng.nextFloat1();
|
||||
}
|
||||
|
||||
public async nextRange(min: number, max: number): Promise<number> {
|
||||
const range = max - min;
|
||||
return await this.nextFloat1() * (range) + min;
|
||||
}
|
||||
|
||||
public async nextRangeInt(min: number, max: number): Promise<number> {
|
||||
const range = max - min;
|
||||
return await this.rng.nextInt(range) + min;
|
||||
}
|
||||
|
||||
public nextInt(max?: number): Promise<number> {
|
||||
return this.rng.nextInt(max);
|
||||
}
|
||||
|
||||
public async nextBit(): Promise<boolean> {
|
||||
const view = await this.rng.nextBits(1);
|
||||
return view[0] != 0;
|
||||
}
|
||||
|
||||
public async nextBool(prob = 0.5): Promise<boolean> {
|
||||
if (prob >= 1) {
|
||||
return true;
|
||||
}
|
||||
if (prob === 0.5){
|
||||
return this.nextBit();
|
||||
}
|
||||
if (prob <= 0){
|
||||
return false;
|
||||
}
|
||||
return await this.nextFloat1() < prob;
|
||||
}
|
||||
|
||||
public async shuffle<T>(srcArray: T[]): Promise<T[]> {
|
||||
const cnt = srcArray.length;
|
||||
if(cnt === 0){
|
||||
return [];
|
||||
}
|
||||
if (cnt > this.rng.getMaxInt()) {
|
||||
throw 'Invalid random int range';
|
||||
}
|
||||
|
||||
const result: T[] = Array.from(srcArray);
|
||||
for (let srcIdx = 0; srcIdx < cnt; srcIdx += 1) {
|
||||
const destIdx = await this.rng.nextInt(cnt - srcIdx - 1) + srcIdx;
|
||||
if(srcIdx === destIdx){
|
||||
continue;
|
||||
}
|
||||
[result[srcIdx], result[destIdx]] = [result[destIdx], result[srcIdx]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//Object는 integer key에 예외가 있어 shuffleAssoc은 없음
|
||||
|
||||
public async choice<T>(items: T[] | Record<string | number, T> | Set<T>): Promise<T> {
|
||||
if (items instanceof Array) {
|
||||
if(items.length === 0){
|
||||
throw new Error('Empty items');
|
||||
}
|
||||
const idx = await this.rng.nextInt(items.length - 1);
|
||||
return items[idx];
|
||||
}
|
||||
|
||||
if (items instanceof Set) {
|
||||
return this.choice(Array.from(items.values()));
|
||||
}
|
||||
|
||||
return items[await this.choice(Array.from(Object.keys(items)))];
|
||||
}
|
||||
|
||||
public async choiceUsingWeight(items: Record<string | number, number>): Promise<string | number> {
|
||||
if(Object.keys(items).length === 0){
|
||||
throw new Error('Empty items');
|
||||
}
|
||||
let sum = 0;
|
||||
for (const value of Object.values(items)) {
|
||||
if (value <= 0) {
|
||||
continue;
|
||||
}
|
||||
sum += value;
|
||||
}
|
||||
|
||||
let rd = await this.nextFloat1() * sum;
|
||||
|
||||
for (const [item, value] of Object.entries(items)) {
|
||||
if (value <= 0) {
|
||||
if (rd <= 0) {
|
||||
return item;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rd <= value) {
|
||||
return item;
|
||||
}
|
||||
rd -= value;
|
||||
}
|
||||
|
||||
throw new Error('Unreacheable');
|
||||
}
|
||||
|
||||
public async choiceUsingWeightPair<T>(items: [T, number][]): Promise<T> {
|
||||
if(items.length === 0){
|
||||
throw new Error('Empty items');
|
||||
}
|
||||
let sum = 0;
|
||||
for (const [, value] of items) {
|
||||
if (value <= 0) {
|
||||
continue;
|
||||
}
|
||||
sum += value;
|
||||
}
|
||||
|
||||
let rd = await this.nextFloat1() * sum;
|
||||
|
||||
for (const [item, value] of items) {
|
||||
if (value <= 0) {
|
||||
if (rd <= 0) {
|
||||
return item;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rd <= value) {
|
||||
return item;
|
||||
}
|
||||
rd -= value;
|
||||
}
|
||||
|
||||
throw new Error('Unreacheable');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { WrappedBuffer } from "@sammo/util";
|
||||
|
||||
export interface ECDSA_PKCS8_P384_SignKey extends WrappedBuffer{
|
||||
_w_type?: "ECDSA_PKCS8_P384_SignKey";
|
||||
}
|
||||
|
||||
export interface ECDSA_P384_VerifyKey extends WrappedBuffer{
|
||||
_w_type?: "ECDSA_P384_VerifyKey";
|
||||
}
|
||||
|
||||
export interface ECDSASignatureRaw extends WrappedBuffer{
|
||||
_w_type?: "ECDSASignature";
|
||||
}
|
||||
|
||||
export interface ECDSASignatureBSON extends WrappedBuffer{
|
||||
_w_type?: "ECDSASignatureBSON";
|
||||
}
|
||||
|
||||
export interface ECDSASignatureJSON extends WrappedBuffer{
|
||||
_w_type?: "ECDSASignatureJSON";
|
||||
}
|
||||
|
||||
export type ECDSASignature = ECDSASignatureRaw | ECDSASignatureBSON | ECDSASignatureJSON;
|
||||
|
||||
export interface ECDHe_P384_PublicKey extends WrappedBuffer{
|
||||
_w_type?: "ECDHe_P384_PublicKey";
|
||||
}
|
||||
|
||||
export interface ECDHe_P384_PrivateKey extends WrappedBuffer{
|
||||
_w_type?: "ECDHe_P384_PrivateKey";
|
||||
}
|
||||
|
||||
export interface ECDHe_P384_LiteKeyPair {
|
||||
publicKey: ECDHe_P384_PublicKey;
|
||||
privateKey: ECDHe_P384_PrivateKey;
|
||||
}
|
||||
|
||||
export interface ECDHe_P384_PublicKeyInfo {
|
||||
publicKey: ECDHe_P384_PublicKey;
|
||||
verifyKey: ECDSA_P384_VerifyKey;
|
||||
sign: ECDSASignatureRaw; //sign of publicKey
|
||||
}
|
||||
export interface ECDHe_P384_KeyPair{
|
||||
publicInfo: ECDHe_P384_PublicKeyInfo;
|
||||
privateKey: ECDHe_P384_PrivateKey;
|
||||
}
|
||||
|
||||
export {
|
||||
TypeLength
|
||||
} from "./TypeLength.js";
|
||||
@@ -0,0 +1,11 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { sha256, sha512 } from "./SHA2.js";
|
||||
|
||||
test('sha2', async () => {
|
||||
const msg = Buffer.from('hello');
|
||||
const answer256 = Buffer.from('2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824', 'hex');
|
||||
const answer512 = Buffer.from('9B71D224BD62F3785D96D46AD3EA3D73319BFBC2890CAADAE2DFF72519673CA72323C3D99BA5C11D7C7ACC6E14B8C5DA0C4663475C2E5C3ADEF46F73BCDEC043', 'hex');
|
||||
|
||||
expect(await sha256(msg)).toEqual(answer256.buffer);
|
||||
expect(await sha512(msg)).toEqual(answer512.buffer);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { isBufferSource } from "./types.js";
|
||||
import { BSON } from "bson";
|
||||
import { type Bsonifiable, bsonify } from "@sammo/util";
|
||||
const subtle = globalThis.crypto.subtle;
|
||||
|
||||
export async function sha256(msg: Bsonifiable | BufferSource | Uint8Array): Promise<ArrayBuffer> {
|
||||
if (msg instanceof Uint8Array){
|
||||
if(!(msg.buffer instanceof ArrayBuffer)){
|
||||
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
|
||||
nmsg.set(msg);
|
||||
return await subtle.digest('SHA-256', nmsg);
|
||||
}
|
||||
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
|
||||
const nmsg = msg as Uint8Array<ArrayBuffer>;
|
||||
return await subtle.digest('SHA-256', nmsg);
|
||||
}
|
||||
if (!isBufferSource(msg)) {
|
||||
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
|
||||
const nmsg = BSON.serialize(bsonify(msg)) as Uint8Array<ArrayBuffer>;
|
||||
if (!(nmsg.buffer instanceof ArrayBuffer)){ throw new Error("Internal error: BSON.serialize did not return ArrayBuffer"); }
|
||||
return await subtle.digest('SHA-256', nmsg);
|
||||
}
|
||||
return await subtle.digest('SHA-256', msg);
|
||||
}
|
||||
|
||||
export async function sha512(msg: Bsonifiable | BufferSource | Uint8Array): Promise<ArrayBuffer> {
|
||||
if (msg instanceof Uint8Array){
|
||||
if(!(msg.buffer instanceof ArrayBuffer)){
|
||||
const nmsg = new Uint8Array(new ArrayBuffer(msg.length));
|
||||
nmsg.set(msg);
|
||||
return await subtle.digest('SHA-512', nmsg);
|
||||
}
|
||||
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
|
||||
const nmsg = msg as Uint8Array<ArrayBuffer>;
|
||||
return await subtle.digest('SHA-512', nmsg);
|
||||
}
|
||||
if (!isBufferSource(msg)) {
|
||||
//XXX: 멍청한 TS 5.9 버그. 신 버전 나오면 Uint8Array 해결할 것
|
||||
const nmsg = BSON.serialize(bsonify(msg)) as Uint8Array<ArrayBuffer>;
|
||||
if (!(nmsg.buffer instanceof ArrayBuffer)){ throw new Error("Internal error: BSON.serialize did not return ArrayBuffer"); }
|
||||
return await subtle.digest('SHA-512', nmsg);
|
||||
}
|
||||
return await subtle.digest('SHA-512', msg);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum TypeLength {
|
||||
//ECDSA P384 서명
|
||||
ECDSA_PKCS8_P384_SignKey = 185,
|
||||
ECDSA_SPKI_P384_VerifyKey = 120,
|
||||
ECDSASignature = 96,
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { InvalidArgument } from '@sammo/util';
|
||||
import type { RandUtil } from './RandUtil.js';
|
||||
|
||||
export class InvalidVType extends InvalidArgument {
|
||||
public override name = 'InvalidVType';
|
||||
}
|
||||
|
||||
export class InvalidArgumentBufferSize extends InvalidArgument {
|
||||
public override name = 'InvalidArgumentBufferSize';
|
||||
}
|
||||
|
||||
export class RuntimeError extends Error {
|
||||
public override name = 'RuntimeError';
|
||||
constructor(public override message: string = '') {
|
||||
super(message);
|
||||
}
|
||||
override toString(): string {
|
||||
if (this.message) {
|
||||
return this.name + ': ' + this.message;
|
||||
}
|
||||
else {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotNullExpected extends RuntimeError {
|
||||
public override name = 'NotNullExpected';
|
||||
}
|
||||
|
||||
export class PrivilegedGenViolation extends RuntimeError {
|
||||
public override name = 'PrivilegedGenViolation';
|
||||
}
|
||||
|
||||
export type MergeableBuffer = MergeableBuffer[] | Buffer;
|
||||
|
||||
export function calcMergeableBuffer(item: MergeableBuffer): number {
|
||||
if (item instanceof Uint8Array) {
|
||||
return item.byteLength;
|
||||
}
|
||||
let bufferSize = 0;
|
||||
for (const subItem of item) {
|
||||
bufferSize += calcMergeableBuffer(subItem);
|
||||
}
|
||||
return bufferSize;
|
||||
}
|
||||
|
||||
export function mergeBuffer(...buffers: MergeableBuffer[]): Buffer {
|
||||
if (buffers.length == 0) {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
if (buffers.length == 1) {
|
||||
const item = buffers[0];
|
||||
if (item instanceof Buffer) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
const fillBuffer = function (item: MergeableBuffer, bufferIdx: number): number {
|
||||
if (item instanceof Uint8Array) {
|
||||
result.set(item, bufferIdx);
|
||||
bufferIdx += item.byteLength;
|
||||
return bufferIdx;
|
||||
}
|
||||
for (const subItem of item) {
|
||||
bufferIdx = fillBuffer(subItem, bufferIdx);
|
||||
}
|
||||
return bufferIdx;
|
||||
}
|
||||
|
||||
const bufferSize = calcMergeableBuffer(buffers);
|
||||
const result = Buffer.alloc(bufferSize);
|
||||
|
||||
fillBuffer(buffers, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
type ErrType<T> = { new(msg?: string): T }
|
||||
type Nullable<T> = T | null | undefined
|
||||
|
||||
export function unwrap<T>(result: Nullable<T>): T {
|
||||
if (result === null || result === undefined) {
|
||||
throw new NotNullExpected();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function unwrap_err<T, ErrT extends Error>(result: Nullable<T>, errType: ErrType<ErrT>, errMsg?: string): T {
|
||||
if (result === null || result === undefined) {
|
||||
throw new errType(errMsg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export * as AES from './AES.js';
|
||||
export * as ECDSA from './ECDSA.js';
|
||||
export * as ECKey from './ECKey.js';
|
||||
export * as PBKDF2 from './PBKDF2.js';
|
||||
export * as SHA2 from './SHA2.js';
|
||||
export * as ECDHe from './ECDHe.js';
|
||||
export * as ECDHe_AES from './ECDHe_AES.js';
|
||||
|
||||
export { RandUtil } from './RandUtil.js';
|
||||
export { LiteHashDRBG } from './LiteHashDRBG.js';
|
||||
export type { RNG } from './RNG.js';
|
||||
|
||||
export * as RawTypes from './RawTypes.js';
|
||||
|
||||
export * from './utils.js';
|
||||
export * from './types.js';
|
||||
export { TypeLength } from './TypeLength.js';
|
||||
|
||||
export * from "./LiteHashDRBG.js"
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
export function isBufferSource(obj: unknown): obj is BufferSource {
|
||||
if (obj instanceof ArrayBuffer){
|
||||
return true;
|
||||
}
|
||||
if (ArrayBuffer.isView(obj)){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { WrappedBuffer } from "@sammo/util";
|
||||
|
||||
const crypto = globalThis.crypto;
|
||||
|
||||
export function randomBytes(length: number): Buffer {
|
||||
const buffer = Buffer.alloc(length);
|
||||
crypto.getRandomValues(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
//TODO: 필요할때마다 확장
|
||||
export type ValidPEMType = 'PUBLIC KEY' | 'EC PRIVATE KEY' | 'CERTIFICATE';
|
||||
|
||||
/**
|
||||
* PEM string에 내부 타입으로 WrappedBuffer를 보관한 형태
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type PEMString<T extends Buffer, S extends ValidPEMType> = string & {
|
||||
/** 타입구분자. 항상 undefined일 것이다 */
|
||||
_pem_b_type?: T;
|
||||
_pem_type?: S;
|
||||
}
|
||||
|
||||
export function encodePEM<T extends WrappedBuffer, S extends ValidPEMType>(data: T, pemType: S): PEMString<T, S>;
|
||||
export function encodePEM(data: Buffer, pemType: ValidPEMType): string;
|
||||
export function encodePEM(data: Buffer, pemType: ValidPEMType): string {
|
||||
const base64text = data.toString('base64');
|
||||
const splitText = base64text.match(/.{1,64}/g)?.join('\n') ?? '';
|
||||
|
||||
return `-----BEGIN ${pemType}-----
|
||||
${splitText}
|
||||
-----END ${pemType}-----
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type InferPEMType<T extends PEMString<any, any>> = Exclude<undefined, T['_pem_type']>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type InferPEMBuffer<T extends PEMString<any, any>> = Exclude<undefined, T['_pem_b_type']>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function decodePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>[];
|
||||
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer>[];
|
||||
export function decodePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer>[] {
|
||||
const tag = pemType ?? "[A-Z0-9 ]+";
|
||||
const pattern = new RegExp(`-{5}BEGIN ${tag}-{5}([a-zA-Z0-9=+\\/\\n\\r]+)-{5}END ${tag}-{5}`, "g");
|
||||
|
||||
const res: Buffer<ArrayBuffer>[] = [];
|
||||
let matches: RegExpExecArray | null = null;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (matches = pattern.exec(pem)) {
|
||||
const base64 = matches[1]
|
||||
.replace(/\r/g, "")
|
||||
.replace(/\n/g, "");
|
||||
res.push(Buffer.from(base64, 'base64'));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function decodeSinglePEM<T extends PEMString<any, any>>(pem: T, pemType: InferPEMType<T>): InferPEMBuffer<T>;
|
||||
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer>;
|
||||
export function decodeSinglePEM(pem: string, pemType?: ValidPEMType): Buffer<ArrayBuffer> {
|
||||
const res = decodePEM(pem, pemType);
|
||||
if (res.length != 1) {
|
||||
throw new Error("invalid pem");
|
||||
}
|
||||
return res[0];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../util"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@sammo/game_logic",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"author": "Hide_D <hided62@gmail.com>",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sammo/api_def": "workspace:^",
|
||||
"@sammo/crypto": "workspace:^",
|
||||
"@sammo/server_util": "workspace:^",
|
||||
"@sammo/util": "workspace:^",
|
||||
"@strpc/express": "workspace:^",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"mongoose": "^8.19.1",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { IResourceController } from "@/IResourceController.js";
|
||||
import type { CityID, GeneralID, NationID, SquadID } from "@/defs.js";
|
||||
import { NotYetImplemented } from "@sammo/util";
|
||||
|
||||
|
||||
export type QueueType = 'server' | 'api';
|
||||
//type QueueType = 'server' | 'npc' | 'api';
|
||||
|
||||
export function setDefenceTrained(invoker: QueueType, arg: { generalID: GeneralID, defenceTrained: number }, rc: IResourceController): void{
|
||||
throw new NotYetImplemented();
|
||||
}
|
||||
|
||||
export function kickSquadMember(invoker: QueueType, arg: { nationID: NationID, squadID: SquadID, memberID: GeneralID }, rc: IResourceController): void {
|
||||
throw new NotYetImplemented();
|
||||
}
|
||||
|
||||
|
||||
export function blockAttackCommand(invoker: QueueType, arg: { nationID: NationID, block: boolean }, rc: IResourceController): void {
|
||||
throw new NotYetImplemented();
|
||||
}
|
||||
|
||||
export function blockJoinNation(invoker: QueueType, arg: { nationID: NationID, block: boolean }, rc: IResourceController): void {
|
||||
throw new NotYetImplemented();
|
||||
}
|
||||
|
||||
export function createGeneral(invoker: QueueType, arg: {
|
||||
//유산 값등은 외부에서 처리하고 내부에는 순수 랜덤류만 입력
|
||||
name: string,
|
||||
ownerID: number,
|
||||
ownerName: string,
|
||||
leadership: number,
|
||||
strength: number,
|
||||
intel: number,
|
||||
pic: string
|
||||
character: string
|
||||
|
||||
turnTimeZone?: number,
|
||||
warSpecial?: string,
|
||||
city?: CityID,
|
||||
bonusStat?: [number, number, number],
|
||||
|
||||
//penalty: Record<GeneralPenalty, number>,
|
||||
}, rc: IResourceController):void{
|
||||
throw new NotYetImplemented();
|
||||
}
|
||||
|
||||
type ActionFunc<Arg extends Record<string, any>, Res> = (invoker: QueueType, arg: Arg, rc: IResourceController)=>Res;
|
||||
export type ActionPackDef = {
|
||||
[key: string]: ActionFunc<any, any>
|
||||
}
|
||||
|
||||
export const ActionRequestList = {
|
||||
setDefenceTrained,
|
||||
kickSquadMember,
|
||||
blockAttackCommand,
|
||||
blockJoinNation,
|
||||
createGeneral,
|
||||
} as const satisfies ActionPackDef;
|
||||
|
||||
export type ActionRequestKey = keyof typeof ActionRequestList;
|
||||
export type ActionMainArg<T extends ActionRequestKey> = Parameters<typeof ActionRequestList[T]>[1];
|
||||
export type ActionRes<T extends ActionRequestKey> = ReturnType<typeof ActionRequestList[T]>;
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ICityEntity } from "./Entity/CityEntity.js";
|
||||
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
|
||||
import type { IResourceController } from "./IResourceController.js";
|
||||
import type { CityID, GeneralID, NationID } from "./defs.js";
|
||||
import type { General } from "./General.js";
|
||||
import { must } from "@sammo/util";
|
||||
|
||||
export class City extends LazyEntityUpdater<ICityEntity>{
|
||||
protected readonly _indirectNames: (keyof ICityEntity)[] = ["id", "nationID"];
|
||||
protected readonly _entityType = "City";
|
||||
protected readonly _raw: ICityEntity;
|
||||
|
||||
protected _members = new Map<GeneralID, WeakRef<General>>;
|
||||
protected _otherMembers = new Map<NationID, Map<GeneralID, WeakRef<General>>>;
|
||||
|
||||
constructor(
|
||||
raw: ICityEntity,
|
||||
protected readonly rc: IResourceController,
|
||||
bootstrapInfo: {
|
||||
general: GeneralID[];
|
||||
}
|
||||
){
|
||||
super();
|
||||
this._raw = raw;
|
||||
this._bootstrapInfo = bootstrapInfo;
|
||||
}
|
||||
|
||||
private _bootstrapInfo?: ConstructorParameters<typeof City>[2];
|
||||
public bootstrap(){
|
||||
const rc = this.rc;
|
||||
|
||||
const bootstrapInfo = must(this._bootstrapInfo);
|
||||
delete this._bootstrapInfo;
|
||||
|
||||
const nationID = this._raw.nationID;
|
||||
|
||||
for(const id of bootstrapInfo.general){
|
||||
const general = must(rc.general(id));
|
||||
this.notifyJoinGeneral(general);
|
||||
}
|
||||
}
|
||||
|
||||
get id(): CityID {
|
||||
return this._raw.id;
|
||||
}
|
||||
|
||||
notifyJoinGeneral(general: General){
|
||||
const nationID = general.raw.nationID;
|
||||
if(nationID === this._raw.nationID){
|
||||
this._members.set(general.id, new WeakRef(general));
|
||||
return;
|
||||
}
|
||||
|
||||
let otherMembers = this._otherMembers.get(nationID);
|
||||
if(!otherMembers){
|
||||
otherMembers = new Map();
|
||||
this._otherMembers.set(nationID, otherMembers);
|
||||
}
|
||||
otherMembers.set(general.id, new WeakRef(general));
|
||||
}
|
||||
|
||||
notifyQuitGeneral(general: General){
|
||||
const nationID = general.raw.nationID;
|
||||
if(nationID === this._raw.nationID){
|
||||
this._members.delete(general.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const otherMembers = must(this._otherMembers.get(nationID));
|
||||
otherMembers.delete(general.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { GeneralID } from "@/defs.js"
|
||||
|
||||
//TODO: 작성!!
|
||||
|
||||
export type DestGeneralArgType = {
|
||||
destGeneralID: GeneralID;
|
||||
}
|
||||
|
||||
export type DestCityArgType = {
|
||||
destCityID: string;
|
||||
}
|
||||
|
||||
export type DestNationArgType = {
|
||||
destNationID: string;
|
||||
}
|
||||
|
||||
export type ColorArgType = {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export type TextArgType = {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type ArmyTypeArgType = {
|
||||
armyType: number;
|
||||
}
|
||||
|
||||
export type ArgType =
|
||||
| DestGeneralArgType
|
||||
| DestCityArgType
|
||||
| DestNationArgType
|
||||
;
|
||||
@@ -0,0 +1,290 @@
|
||||
import type { City } from "@/City.js";
|
||||
import { testContraints, type IConstraint } from "@/Constraint/index.js";
|
||||
import type { GameEnv } from "@/GameEnv.js";
|
||||
import type { General } from "@/General.js";
|
||||
import type { Nation } from "@/Nation.js";
|
||||
import type { IntYearMonth } from "@/defs.js";
|
||||
import type { RandUtil } from "@sammo/crypto";
|
||||
import { InvalidArgument, must } from "@sammo/util";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const enum CommandType {
|
||||
General = 0,
|
||||
Nation = 1,
|
||||
};
|
||||
|
||||
export const enum CompensateType {
|
||||
Neutral = 0,
|
||||
Positive = 1,
|
||||
Negative = -1,
|
||||
}
|
||||
|
||||
export abstract class BaseCommand<ArgType extends Record<string, any>>{
|
||||
abstract readonly actionName: string;
|
||||
abstract readonly className: string;
|
||||
abstract readonly type: CommandType;
|
||||
|
||||
public abstract readonly compensateType: CompensateType;
|
||||
|
||||
readonly isLazyCalcReqTurn: boolean = false;
|
||||
|
||||
protected cachedPermissionToReserve = false;
|
||||
protected cachedMinConditionMet = false;
|
||||
protected cachedFullConditionMet = false;
|
||||
|
||||
protected reasonNotFullConditionMet: string | undefined = undefined;
|
||||
protected reasonNotMinConditionMet: string | undefined = undefined;
|
||||
protected reasonNotPermissionToReserve: string | undefined = undefined;
|
||||
|
||||
protected _destNation?: Nation;
|
||||
protected _destCity?: City;
|
||||
protected _destGeneral?: General;
|
||||
|
||||
public get destNation(): Readonly<Nation> | undefined {
|
||||
return this._destNation;
|
||||
}
|
||||
public get destCity(): Readonly<City> | undefined {
|
||||
return this._destCity;
|
||||
}
|
||||
public get destGeneral(): Readonly<General> | undefined {
|
||||
return this._destGeneral;
|
||||
}
|
||||
|
||||
protected abstract alternative: BaseCommand<any> | undefined;
|
||||
|
||||
protected abstract permissionConstraints: IConstraint<any>[] | undefined;
|
||||
protected abstract minConditionConstraints: IConstraint<any>[] | undefined;
|
||||
protected abstract fullConditionConstraints: IConstraint<any>[] | undefined;
|
||||
protected abstract errorInvoker: string | undefined;
|
||||
|
||||
protected abstract readonly argSchema: {} extends ArgType ? undefined : z.ZodObject<ArgType>;
|
||||
protected _arg: ArgType | undefined;
|
||||
protected readonly reasonArgTestFailed?: string;
|
||||
public get arg(): Readonly<ArgType> {
|
||||
return must(this._arg);
|
||||
}
|
||||
|
||||
|
||||
public constructor(
|
||||
public readonly general: General,
|
||||
public readonly env: GameEnv,
|
||||
arg: ArgType
|
||||
) {
|
||||
this.init();
|
||||
|
||||
this.reasonArgTestFailed = this.testArg(arg);
|
||||
if(this.reasonArgTestFailed === undefined){
|
||||
this.initWithArg();
|
||||
}
|
||||
}
|
||||
|
||||
private testArg(rawArg: ArgType): string | undefined {
|
||||
if(this.argSchema === undefined){
|
||||
if(Object.keys(rawArg).length === 0){
|
||||
return undefined;
|
||||
}
|
||||
return "이 커맨드는 인자를 받지 않습니다";
|
||||
}
|
||||
const result = must(this.argSchema).safeParse(this.arg);
|
||||
if(!result.success){
|
||||
return result.error.errors[0].message;
|
||||
}
|
||||
this._arg = result.data as unknown as ArgType;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected abstract init(): void;
|
||||
protected initWithArg(): void {
|
||||
if (Object.keys(this.arg).length == 0) {
|
||||
return;
|
||||
}
|
||||
throw new InvalidArgument("initWithArg must be overridden if arg is not undefined");
|
||||
}
|
||||
|
||||
public get commandBrief(): string {
|
||||
return this.actionName;
|
||||
}
|
||||
|
||||
|
||||
//TODO: ExecuteKey의 타입
|
||||
abstract getNextExecuteKey(): string;
|
||||
abstract getNextAvailableTurn(): IntYearMonth | undefined;
|
||||
abstract setNextAvailableTurn(turn?: IntYearMonth): void;
|
||||
|
||||
abstract getCost(): [gold: number, rice: number];
|
||||
abstract getPreReqTurn(): number;
|
||||
abstract getPostReqTurn(): number;
|
||||
|
||||
protected testPostReqTurn(): ["testPostReqTurn", string] | undefined {
|
||||
if (this.getPostReqTurn() === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nextAvailableTurn = this.getNextAvailableTurn();
|
||||
if (nextAvailableTurn === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const yearMonth = this.env.raw.yearMonth;
|
||||
const remainTurn = nextAvailableTurn - yearMonth;
|
||||
if (remainTurn <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return ["testPostReqTurn", `${remainTurn}턴 더 기다려야 합니다`];
|
||||
}
|
||||
|
||||
testPermissionToReserve(): string | undefined {
|
||||
if (this.cachedPermissionToReserve) {
|
||||
return this.reasonNotPermissionToReserve;
|
||||
}
|
||||
|
||||
if (this.reasonNotPermissionToReserve) {
|
||||
return this.reasonNotPermissionToReserve;
|
||||
}
|
||||
|
||||
const testResult = testContraints(this.permissionConstraints ?? [], this);
|
||||
if (testResult !== undefined) {
|
||||
[this.errorInvoker, this.reasonNotPermissionToReserve] = testResult;
|
||||
}
|
||||
this.cachedPermissionToReserve = true;
|
||||
return this.reasonNotPermissionToReserve;
|
||||
}
|
||||
|
||||
public canDisplay(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public testMinConditionMet(): string | undefined {
|
||||
if (this.argSchema === undefined && this.minConditionConstraints === undefined) {
|
||||
return this.testFullConditionMet();
|
||||
}
|
||||
|
||||
if(this.minConditionConstraints === undefined){
|
||||
throw new InvalidArgument("minConditionConstraints가 제대로 설정되지 않았습니다");
|
||||
}
|
||||
|
||||
if (this.cachedMinConditionMet) {
|
||||
return this.reasonNotMinConditionMet;
|
||||
}
|
||||
|
||||
const testResult = testContraints(this.minConditionConstraints, this);
|
||||
if (testResult !== undefined) {
|
||||
[this.errorInvoker, this.reasonNotMinConditionMet] = testResult;
|
||||
}
|
||||
|
||||
if (this.reasonNotMinConditionMet === undefined && !this.isLazyCalcReqTurn) {
|
||||
const result = this.testPostReqTurn();
|
||||
if (result !== undefined) {
|
||||
[this.errorInvoker, this.reasonNotMinConditionMet] = result;
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedMinConditionMet = true;
|
||||
return this.reasonNotMinConditionMet;
|
||||
}
|
||||
|
||||
public testFullConditionMet(): string | undefined {
|
||||
if(this.reasonArgTestFailed !== undefined){
|
||||
this.reasonNotFullConditionMet = this.reasonArgTestFailed;
|
||||
this.cachedFullConditionMet = true;
|
||||
return this.reasonNotFullConditionMet;
|
||||
}
|
||||
|
||||
if (this.fullConditionConstraints === undefined) {
|
||||
throw new InvalidArgument('fullConditionConstraints가 제대로 설정되지 않았습니다');
|
||||
}
|
||||
|
||||
if (this.cachedFullConditionMet) {
|
||||
return this.reasonNotFullConditionMet;
|
||||
}
|
||||
|
||||
const testResult = testContraints(this.fullConditionConstraints, this);
|
||||
if (testResult !== undefined) {
|
||||
[this.errorInvoker, this.reasonNotFullConditionMet] = testResult;
|
||||
}
|
||||
|
||||
if (this.reasonNotFullConditionMet === undefined) {
|
||||
const result = this.testPostReqTurn();
|
||||
if (result !== undefined) {
|
||||
[this.errorInvoker, this.reasonNotFullConditionMet] = result;
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedFullConditionMet = true;
|
||||
return this.reasonNotFullConditionMet;
|
||||
}
|
||||
|
||||
public get termString(): string {
|
||||
const commandName = this.commandBrief;
|
||||
const term = this.getNextAvailableTurn();
|
||||
const termMax = this.getPreReqTurn() + 1;
|
||||
return `${commandName} 수행중... (${term}/${termMax})`;
|
||||
}
|
||||
|
||||
public addTermStack(): boolean {
|
||||
if (this.getPreReqTurn() === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastTurn = this.general.raw.aux.lastTurn;
|
||||
|
||||
const nextTurn: Exclude<typeof lastTurn, undefined> = {
|
||||
action: this.className,
|
||||
arg: this.arg,
|
||||
brief: this.commandBrief,
|
||||
term: 1
|
||||
};
|
||||
|
||||
if (lastTurn === undefined) {
|
||||
this.general.update((raw) => {
|
||||
raw.aux.lastTurn = nextTurn;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastTurn.action !== this.className || lastTurn.arg !== this.arg) {
|
||||
this.general.update((raw) => {
|
||||
raw.aux.lastTurn = nextTurn;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastTurn.term < this.getPreReqTurn()) {
|
||||
nextTurn.term = lastTurn.term + 1;
|
||||
this.general.update((raw) => {
|
||||
raw.aux.lastTurn = nextTurn;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public get hasPermissionToReserve(): boolean {
|
||||
return this.testPermissionToReserve() === undefined;
|
||||
}
|
||||
|
||||
public get hasMinConditionMet(): boolean {
|
||||
return this.testMinConditionMet() === undefined;
|
||||
}
|
||||
|
||||
public get hasFullConditionMet(): boolean {
|
||||
return this.testFullConditionMet() === undefined;
|
||||
}
|
||||
|
||||
public get failString(): string {
|
||||
const commandName = this.commandBrief;
|
||||
const failReason = this.testFullConditionMet();
|
||||
if (failReason === undefined) {
|
||||
throw new Error('실행 가능한 커맨드에 대해 실패 이유를 수집');
|
||||
}
|
||||
return `${failReason} ${commandName} 실패.`;
|
||||
}
|
||||
|
||||
public get alternativeCommand(): BaseCommand<any> {
|
||||
return must(this.alternative);
|
||||
}
|
||||
|
||||
abstract run(rng: RandUtil): boolean;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { City } from "@/City.js"
|
||||
import type { GameEnv } from "@/GameEnv.js"
|
||||
import type { General } from "@/General.js"
|
||||
import type { Nation } from "@/Nation.js"
|
||||
|
||||
export const enum RequiredDestLevel {
|
||||
None = 0,
|
||||
Nation = 1,
|
||||
City = 2,
|
||||
General = 3,
|
||||
}
|
||||
|
||||
export interface IConstraint<ArgType extends Record<string, unknown> = {}> {
|
||||
readonly reason?: string;
|
||||
readonly requiredDestLevel: RequiredDestLevel;
|
||||
readonly arg: ArgType;
|
||||
test(input: TestInput): undefined | [testName: string, err: string];
|
||||
}
|
||||
|
||||
export const constraints: {
|
||||
[key: string]: IConstraint
|
||||
} = {
|
||||
}
|
||||
|
||||
export interface TestInput {
|
||||
general: Readonly<General>,
|
||||
destNation?: Readonly<Nation>,
|
||||
destCity?: Readonly<City>,
|
||||
destGeneral?: Readonly<General>,
|
||||
env: Readonly<GameEnv>,
|
||||
}
|
||||
|
||||
export function testContraints(testList: IConstraint[], input: TestInput): undefined | [testName: string, err: string]{
|
||||
for(const test of testList){
|
||||
if(test.requiredDestLevel >= RequiredDestLevel.Nation && input.destNation === undefined){
|
||||
return [test.constructor.name, "require destNation"];
|
||||
}
|
||||
else if(test.requiredDestLevel >= RequiredDestLevel.City && input.destCity === undefined){
|
||||
return [test.constructor.name, "require destCity"];
|
||||
}
|
||||
else if(test.requiredDestLevel >= RequiredDestLevel.General && input.destGeneral === undefined){
|
||||
return [test.constructor.name, "require destGeneral"];
|
||||
}
|
||||
const result = test.test(input);
|
||||
if(result !== undefined){
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { CityID, CityLevel, CityName, CityOfficerLevel, CityState, FrontStatus, NationID, FractionalRange, RegionID, IntMonth } from "../defs.js";
|
||||
|
||||
|
||||
export interface ICityEntity{
|
||||
id: CityID;
|
||||
name: CityName;
|
||||
nationID: NationID;
|
||||
|
||||
level: CityLevel;
|
||||
|
||||
//보급품 있음
|
||||
isSupplied: boolean;
|
||||
|
||||
//도시 접경 상태
|
||||
frontStatus: FrontStatus;
|
||||
|
||||
//인구
|
||||
population: {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
//농업
|
||||
agriculture: {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
//상업
|
||||
commerce: {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
//치안
|
||||
security: {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
//민도
|
||||
loyalty: {
|
||||
current: number;
|
||||
max: 100;
|
||||
}
|
||||
|
||||
//수비병
|
||||
defence: {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
//성벽
|
||||
wall: {
|
||||
current: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
//전투 사망자 수(혹은 패잔병)
|
||||
battleDeath: number;
|
||||
|
||||
//상인 존재 및 거래율, base 1
|
||||
traderStatus: {
|
||||
exists: false;
|
||||
} | {
|
||||
exists: true;
|
||||
rate: FractionalRange;
|
||||
}
|
||||
|
||||
officerAssignability: Record<CityOfficerLevel, boolean>;
|
||||
|
||||
regionID: RegionID;
|
||||
|
||||
state: CityState;
|
||||
//전투 잔여 개월
|
||||
remainWarMonth: IntMonth;
|
||||
|
||||
//국가별 수비병 사살 수
|
||||
cityDamageRatio: Record<NationID, number>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { DiplomacyState, NationID } from "@/defs.js";
|
||||
|
||||
export interface Diplomacy {
|
||||
_id: string;
|
||||
|
||||
lowerNationID: NationID;
|
||||
higherNationID: NationID;
|
||||
|
||||
state: DiplomacyState;
|
||||
|
||||
lowerNationDeadCrew?: number;
|
||||
higherNationDeadCrew?: number;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { GeneralID, GeneralName, NationID } from "@/defs.js";
|
||||
|
||||
export interface IDiplomaticLetter {
|
||||
_id: string;
|
||||
|
||||
srcNationID: NationID;
|
||||
destNationID: NationID;
|
||||
|
||||
prevID?: string;
|
||||
|
||||
state: 'proposed' | 'accepted' | 'rejected' | 'canceled' | 'replaced';
|
||||
|
||||
srcSigner: {
|
||||
generalID: GeneralID;
|
||||
generalName: GeneralName;
|
||||
icon: string;
|
||||
};
|
||||
destSigner: {
|
||||
generalID: GeneralID;
|
||||
generalName: GeneralName;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
publicText: string;
|
||||
privateText: string;
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { GeneralID } from "@/defs.js";
|
||||
|
||||
//게임 외부적인 General Rank 정보
|
||||
export interface IGeneralExternalRankEntity {
|
||||
generalID: GeneralID;
|
||||
|
||||
//토너먼트 결과가 앞으로도 게임 내부 데이터로 반영되지 않을것인가?
|
||||
|
||||
//토너먼트 결과
|
||||
tournamentResult: Record<'all'|'leadership'|'strength'|'intel', {
|
||||
win: number,
|
||||
lose: number,
|
||||
draw: number,
|
||||
|
||||
//토너먼트 참가 횟수
|
||||
cnt: number,
|
||||
//포인트
|
||||
point: number,
|
||||
}>;
|
||||
|
||||
tournamentBet: {
|
||||
//베팅 성공 횟수
|
||||
betWin: number;
|
||||
//베팅 금액
|
||||
betGold: number;
|
||||
//베팅 수익금
|
||||
betProfit: number;
|
||||
}
|
||||
|
||||
//유상 포인트 획득(지연)
|
||||
inheritPointEarned: number;
|
||||
//유상 포인트 소모(지연)
|
||||
inheritPointSpent: number;
|
||||
|
||||
//유산 포인트 획득량(동적)
|
||||
inheritPointEarnedDyn: number;
|
||||
//유산 포인트 획득량(베팅 등 별도 명령)
|
||||
inheritPointEarnedAction: number;
|
||||
//유산 포인트 소모량(동적)
|
||||
inheritPointSpentDyn: number;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { LogType } from "@/defs.js";
|
||||
|
||||
export interface IGeneralRecord {
|
||||
_id: string;
|
||||
|
||||
generalID: string;
|
||||
|
||||
logType: LogType;
|
||||
|
||||
year: number;
|
||||
month: number;
|
||||
|
||||
text: string;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { GeneralID, ReservedTurn } from "@/defs.js";
|
||||
|
||||
export interface IGeneralReservedTurn {
|
||||
generalID: GeneralID;
|
||||
turn: ReservedTurn[];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IGlobalRecord {
|
||||
_id: string;
|
||||
type: 'history' | 'action';
|
||||
year: number;
|
||||
month: number;
|
||||
text: string;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { GeneralID, GeneralName, MessageType, NationID, NationName } from "@/defs.js";
|
||||
|
||||
export interface MessageTarget {
|
||||
mailbox: number;
|
||||
generalID: GeneralID;
|
||||
generalName: GeneralName;
|
||||
nationID: NationID;
|
||||
nationName: NationName;
|
||||
icon: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface IMessage{
|
||||
_id: string;
|
||||
|
||||
mailbox: number;
|
||||
|
||||
state: 'unread' | 'read' | 'deleted';
|
||||
|
||||
type: MessageType;
|
||||
|
||||
src: MessageTarget;
|
||||
dest: MessageTarget;
|
||||
|
||||
createdAt: Date;
|
||||
|
||||
text: string;
|
||||
|
||||
action?: {
|
||||
validUntil?: Date;
|
||||
used: boolean;
|
||||
action: string;
|
||||
arg: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { NationID } from "@/defs.js";
|
||||
|
||||
export interface INationRecord {
|
||||
_id: string;
|
||||
nationID: NationID;
|
||||
year: number;
|
||||
month: number;
|
||||
text: string;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ReservedTurn, StaffLevel } from "@/defs.js";
|
||||
|
||||
export interface INationReservedTurn {
|
||||
_id: string;
|
||||
|
||||
nationID: string;
|
||||
officerLevel: StaffLevel,
|
||||
|
||||
turn: ReservedTurn[];
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export interface ISurvey {
|
||||
_id: string;
|
||||
|
||||
title: string;
|
||||
description: string;
|
||||
|
||||
//투표 시작 시간
|
||||
startTime: Date;
|
||||
//투표 종료 시간
|
||||
endTime?: Date;
|
||||
|
||||
multipleOptions: number;
|
||||
|
||||
options: string[];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { GeneralID, GeneralName, NationID, NationName } from "@/defs.js";
|
||||
|
||||
export interface ISurveyComment {
|
||||
_id: string;
|
||||
surveyID: string;
|
||||
|
||||
generalID: GeneralID;
|
||||
nationID: NationID;
|
||||
|
||||
generalName: GeneralName;
|
||||
nationName: NationName;
|
||||
|
||||
comment: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { GeneralID, NationID } from "@/defs.js";
|
||||
|
||||
export interface ISurveyResponse {
|
||||
_id: string;
|
||||
surveyID: string;
|
||||
|
||||
//투표자
|
||||
generalID: GeneralID;
|
||||
|
||||
nationID: NationID;
|
||||
|
||||
//투표한 선택지
|
||||
selectedOption: number[];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { NationID } from "@/defs.js";
|
||||
|
||||
//국제 정세
|
||||
export interface IWorldHistory {
|
||||
//MongoDB ID
|
||||
_id: string;
|
||||
|
||||
//nationID -> year -> month
|
||||
year: number;
|
||||
month: number;
|
||||
nationID?: NationID;
|
||||
|
||||
text: string;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { IntYear, IntYearMonth } from "@/defs.js";
|
||||
|
||||
export type UnitedGameState = 'onGame' | 'united' | 'onEvent' | 'endEvent';
|
||||
|
||||
export interface IGameEnvEntity {
|
||||
startTime: Date;
|
||||
lastExecuted: Date;
|
||||
|
||||
initYear: number;
|
||||
initMonth: number;
|
||||
|
||||
gameStartYear: number;
|
||||
gameStartMonth: number;
|
||||
|
||||
year: IntYear;
|
||||
month: IntYearMonth;
|
||||
yearMonth: IntYearMonth;
|
||||
|
||||
secondsPerTurn: number;
|
||||
|
||||
numberOfIdleTurnTillDeath: number;
|
||||
|
||||
isFictionMode: boolean;
|
||||
unitedGameState: UnitedGameState;
|
||||
blockGeneralCreate: boolean;
|
||||
|
||||
mapTheme: string;
|
||||
prevWinnerName?: string;
|
||||
|
||||
scenarioID: number;
|
||||
scenarioName: string;
|
||||
gameSeason: number;
|
||||
|
||||
maxGeneralCnt: number;
|
||||
maxNationCnt: number;
|
||||
|
||||
remainGeniusCnt: number;
|
||||
autoRunUserPolicy: Record<string, unknown>;
|
||||
|
||||
//게임 외부는 다른 엔티티로 분리
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, IntYearMonth, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, ReservedTurn, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js";
|
||||
|
||||
export interface IGeneralEntity {
|
||||
id: GeneralID;
|
||||
ownerID?: UserID;
|
||||
npcType: NpcType;
|
||||
affinity?: number;
|
||||
icon?: URILike;
|
||||
name: GeneralName;
|
||||
ownerName?: UserName;
|
||||
nationID: NationID;
|
||||
cityID: number;
|
||||
squadID?: SquadID;
|
||||
|
||||
leadership: number;
|
||||
leadershipExp: number;
|
||||
strength: number;
|
||||
strengthExp: number;
|
||||
intel: number;
|
||||
intelExp: number;
|
||||
|
||||
injury: FractionalRange;
|
||||
|
||||
experience: number;
|
||||
dedication: number;
|
||||
|
||||
experienceLevel: number;
|
||||
dedicationLevel: number;
|
||||
|
||||
armyClassExperience: number[];
|
||||
|
||||
officerLevel: OfficerLevel;
|
||||
officerCityID?: CityID;
|
||||
|
||||
diplomaticPermission: DiplomaticPermission;
|
||||
|
||||
gold: number;
|
||||
rice: number;
|
||||
|
||||
item: Record<ItemKeyType, {
|
||||
id: ItemID,
|
||||
option: Record<string, unknown>,
|
||||
}>;
|
||||
|
||||
turnTime: Date;
|
||||
|
||||
idleTurn: IntMonth;
|
||||
|
||||
age: IntYear;
|
||||
startAge: IntYear;
|
||||
|
||||
nationJoinedYearMonth: IntYearMonth;
|
||||
|
||||
betray: number;
|
||||
|
||||
personality: PersonalityType;
|
||||
specialityDomestic: SpecialityDomesticType;
|
||||
specialityWar: SpecialityWarType;
|
||||
|
||||
defenceTrained: number;
|
||||
|
||||
armyType: number;
|
||||
armyCount: number;
|
||||
trained: number;
|
||||
morale: number;
|
||||
|
||||
npcSpec?: {
|
||||
msg?: string;
|
||||
bornyear: number;
|
||||
deadyear: number;
|
||||
}
|
||||
|
||||
aux: {
|
||||
lastBattleDate?: Date;
|
||||
lastBattleYearMonth?: IntYearMonth;
|
||||
lastTurn?: ReservedTurn & { term: number };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { GeneralID } from "@/defs.js";
|
||||
|
||||
export interface IGeneralRankEntity {
|
||||
generalID: GeneralID;
|
||||
|
||||
//계략 성공 횟수
|
||||
sabotageCnt: number;
|
||||
|
||||
//전투 횟수
|
||||
battleCnt: number;
|
||||
//전투 승리 횟수
|
||||
battleWinCnt: number;
|
||||
//전투 패배 횟수
|
||||
battleLoseCnt: number;
|
||||
|
||||
//누적 병력 사망 수
|
||||
allDeathCnt: number;
|
||||
//누적 병력 사살 수
|
||||
allKillCnt: number;
|
||||
|
||||
//누적 대인 병력 사망 수
|
||||
armyDeathCnt: number;
|
||||
//누적 대인 병력 사살 수
|
||||
armyKillCnt: number;
|
||||
|
||||
//점령 수
|
||||
occupyCnt: number;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { CityID, ColorType, IntYearMonth, NationCmdType, NationID, NationLevel, NationName, NationType, FractionalRange, IntMonth, ReservedTurn, StaffLevel, StrategicCmdType } from "../defs.js";
|
||||
|
||||
export interface INationEntity{
|
||||
id: NationID;
|
||||
name: NationName;
|
||||
type: NationType;
|
||||
|
||||
capitalID: CityID;
|
||||
color: ColorType;
|
||||
|
||||
level: NationLevel;
|
||||
|
||||
lastExecutedYearMonth: Record<
|
||||
'capitalSet'
|
||||
,number>;
|
||||
|
||||
//TODO: 이게 뭐더라
|
||||
capset: IntYearMonth;
|
||||
|
||||
strategicCmdLimit: Record<StrategicCmdType, IntYearMonth>;
|
||||
|
||||
//세율
|
||||
taxRate: FractionalRange;
|
||||
//지급율
|
||||
payRate: FractionalRange;
|
||||
|
||||
gold: number;
|
||||
rice: number;
|
||||
|
||||
blockJoin: boolean;
|
||||
blockWar: boolean;
|
||||
|
||||
//특수외교커맨드 제한
|
||||
specialDiplomacyCmdLimit: IntYearMonth;
|
||||
|
||||
//기술력
|
||||
technology: number;
|
||||
|
||||
//환산 국력
|
||||
power: number;
|
||||
|
||||
//첩보 제한
|
||||
spyLimit: Map<CityID, IntMonth>;
|
||||
|
||||
//커맨드별 옵션
|
||||
commandOption: Map<NationCmdType, Map<string, unknown>>;
|
||||
|
||||
lastExecutedTurn: Map<StaffLevel, ReservedTurn>;
|
||||
|
||||
aux: {
|
||||
availableWarSettingCnt?: number;
|
||||
prevIncomeGold: number;
|
||||
prevIncomeRice: number;
|
||||
|
||||
lastBattleDate?: Date;
|
||||
lastBattleYearMonth?: IntYearMonth;
|
||||
}
|
||||
|
||||
//TODO: 타입 정의
|
||||
nationPolicy: Record<string, unknown>;
|
||||
//TODO: 타입 정의
|
||||
npcPolicy: Record<string, unknown>;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { GeneralID, GeneralName, NationName, ScenarioID, ServerID, UserID, UserName } from "@/defs.js"
|
||||
import type { IGeneralRankEntity } from "../GeneralRankEntity.js";
|
||||
import type { IGeneralExternalRankEntity } from "../External/GeneralExternalRankEntity.js";
|
||||
|
||||
|
||||
|
||||
export interface IHallOfFame {
|
||||
_id: string
|
||||
|
||||
serverID: ServerID;
|
||||
serverIdx: number;
|
||||
season: number;
|
||||
|
||||
scenarioID: ScenarioID;
|
||||
scenarioName: string;
|
||||
|
||||
generalID: GeneralID;
|
||||
generalName: GeneralName;
|
||||
ownerID: UserID;
|
||||
ownerName: UserName;
|
||||
icon: string;
|
||||
|
||||
nationName: NationName;
|
||||
nationColor: string;
|
||||
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
|
||||
ingame: IGeneralRankEntity;
|
||||
external: IGeneralExternalRankEntity;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { CityID, CityLevel, CityState, NationID, NpcType, OfficerLevel, RegionID, ServerID } from "@/defs.js";
|
||||
|
||||
export interface IHistory {
|
||||
_id: string;
|
||||
|
||||
serverID: ServerID;
|
||||
year: number;
|
||||
month: number;
|
||||
mapBase: string;
|
||||
nations: Map<NationID, {
|
||||
name: string;
|
||||
color: string;
|
||||
cities: Set<CityID>;
|
||||
generals: {
|
||||
name: string;
|
||||
npcType: NpcType;
|
||||
}[];
|
||||
}>;
|
||||
cities: Map<CityID, {
|
||||
nationID: NationID;
|
||||
name: string;
|
||||
isSupplied: boolean;
|
||||
|
||||
level: CityLevel;
|
||||
state: CityState;
|
||||
regionID: RegionID;
|
||||
|
||||
remainWarMonth: number;
|
||||
}>
|
||||
globalHistory: string[];
|
||||
globalAction: string[];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { CityLevel, GeneralID, ServerID, UserID } from "@/defs.js";
|
||||
import type { IGeneralEntity } from "../GeneralEntity.js";
|
||||
import type { IGeneralRankEntity } from "../GeneralRankEntity.js";
|
||||
|
||||
export interface OldGeneral {
|
||||
_id: string;
|
||||
|
||||
serverID: ServerID;
|
||||
generalID: GeneralID;
|
||||
ownerID?: UserID;
|
||||
|
||||
recordVersion: 1 | 2;
|
||||
|
||||
lastYearMonth: number;
|
||||
|
||||
general: IGeneralEntity;
|
||||
rank: IGeneralRankEntity;
|
||||
|
||||
squadName?: string;
|
||||
cityBasic: {
|
||||
name: string;
|
||||
level: CityLevel;
|
||||
}
|
||||
nationBasic: {
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { GeneralID, GeneralName, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, ServerID, SpecialityDomesticType, SpecialityWarType, URILike, UserID } from "@/defs.js";
|
||||
import type { INationEntity } from "../NationEntity.js";
|
||||
|
||||
export interface IOldNation {
|
||||
_id: string;
|
||||
|
||||
serverID: ServerID;
|
||||
nationID: NationID;
|
||||
|
||||
recordVersion: 1 | 2;
|
||||
|
||||
nation: INationEntity;
|
||||
|
||||
lastYearMonth: number;
|
||||
|
||||
members: Record<number, {
|
||||
generalID: GeneralID;
|
||||
generalName: GeneralName;
|
||||
ownerID?: UserID;
|
||||
ownerName?: string;
|
||||
|
||||
npcType?: NpcType;
|
||||
icon?: URILike;
|
||||
|
||||
officerLevel: OfficerLevel;
|
||||
officerCityID?: number;
|
||||
}>
|
||||
|
||||
history: {
|
||||
year: number;
|
||||
month: number;
|
||||
text: string[];
|
||||
};
|
||||
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { ServerID, UserID } from "@/defs.js";
|
||||
|
||||
export interface IUserRecord {
|
||||
_id: string;
|
||||
userID: UserID;
|
||||
serverID: ServerID;
|
||||
|
||||
logType: 'inheritPoint';
|
||||
|
||||
//게임 내 연도
|
||||
year: number;
|
||||
month: number;
|
||||
|
||||
text: string;
|
||||
|
||||
createdAt: Date;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NationID, SquadID } from "../defs.js";
|
||||
|
||||
export interface ISquadEntity {
|
||||
id: SquadID;
|
||||
nationID: NationID;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ValuesOf } from '@sammo/util';
|
||||
import type { ICityEntity } from './CityEntity.js';
|
||||
import type { IGeneralEntity } from './GeneralEntity.js';
|
||||
import type { INationEntity } from './NationEntity.js';
|
||||
import type { ISquadEntity } from './SquadEntity.js';
|
||||
import type { IGameEnvEntity } from './GameEnvEntity.js';
|
||||
|
||||
export type { IGeneralEntity } from './GeneralEntity.js';
|
||||
export type { ICityEntity } from './CityEntity.js';
|
||||
export type { ISquadEntity } from './SquadEntity.js';
|
||||
export type { INationEntity } from './NationEntity.js';
|
||||
|
||||
|
||||
export type ValidEntityList = {
|
||||
'Nation': INationEntity;
|
||||
'City': ICityEntity;
|
||||
'Squad': ISquadEntity;
|
||||
'General': IGeneralEntity;
|
||||
|
||||
'GameEnv': IGameEnvEntity;
|
||||
};
|
||||
|
||||
export type ValidEntity = ValuesOf<ValidEntityList>;
|
||||
export type ValidEntityType = keyof ValidEntityList;
|
||||
export type EntityType<Entity> = Entity extends ValidEntityList[infer T extends keyof ValidEntityList] ? T : never;
|
||||
@@ -0,0 +1,6 @@
|
||||
export class LogicError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = 'LogicError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { IGameEnvEntity } from "./Entity/GameEnvEntity.js";
|
||||
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
|
||||
import type { IResourceController } from "./IResourceController.js";
|
||||
|
||||
export class GameEnv extends LazyEntityUpdater<IGameEnvEntity>{
|
||||
protected readonly _indirectNames: readonly (keyof IGameEnvEntity)[] = [
|
||||
//사실상 모두.
|
||||
];
|
||||
protected readonly _entityType = "GameEnv";
|
||||
protected readonly _raw: IGameEnvEntity;
|
||||
|
||||
constructor(
|
||||
raw: IGameEnvEntity,
|
||||
protected readonly rc: IResourceController
|
||||
){
|
||||
super();
|
||||
this._raw = raw;
|
||||
}
|
||||
|
||||
public bootstrap(){
|
||||
//do nothing;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { IResourceController } from "./IResourceController.js";
|
||||
import type { GeneralID, IntMonth, IntYear, LogType, NationID } from "./defs.js";
|
||||
|
||||
type LoggerStack = {
|
||||
symbol: Symbol;
|
||||
year: IntYear;
|
||||
month: IntMonth;
|
||||
|
||||
global: {
|
||||
action: string[];
|
||||
history: string[];
|
||||
}
|
||||
nation: {
|
||||
nationID: NationID;
|
||||
history: string[];
|
||||
}
|
||||
general: {
|
||||
[key in LogType]: string[];
|
||||
} & {
|
||||
generalID: GeneralID;
|
||||
}
|
||||
}
|
||||
|
||||
export class GameLoggerEngine {
|
||||
private static instance: GameLoggerEngine;
|
||||
|
||||
private constructor(
|
||||
private rc: IResourceController
|
||||
) {
|
||||
}
|
||||
|
||||
public static initInstance(rc: IResourceController): GameLoggerEngine{
|
||||
if(GameLoggerEngine.instance){
|
||||
throw new Error("GameLogger is already initialized");
|
||||
}
|
||||
GameLoggerEngine.instance = new GameLoggerEngine(rc);
|
||||
return GameLoggerEngine.instance;
|
||||
}
|
||||
|
||||
public static getInstance(): GameLoggerEngine {
|
||||
if (!GameLoggerEngine.instance) {
|
||||
throw new Error("GameLogger is not initialized");
|
||||
}
|
||||
|
||||
return GameLoggerEngine.instance;
|
||||
}
|
||||
|
||||
private logStack = new Map<Symbol, LoggerStack>();
|
||||
|
||||
public addStack(stack: LoggerStack) {
|
||||
this.logStack.set(stack.symbol, stack);
|
||||
return stack;
|
||||
}
|
||||
|
||||
public getGameLogger(year: IntYear, month: IntMonth): GameLogger {
|
||||
return new GameLogger(year, month, this);
|
||||
}
|
||||
|
||||
public getNationLogger(year: IntYear, month: IntMonth, nationID: NationID): NationLogger {
|
||||
return new NationLogger(year, month, nationID, this);
|
||||
}
|
||||
|
||||
public getGeneralLogger(year: IntYear, month: IntMonth, nationID: NationID, generalID: GeneralID): GeneralLogger {
|
||||
return new GeneralLogger(year, month, nationID, generalID, this);
|
||||
}
|
||||
|
||||
flushAll() {
|
||||
if(this.logStack.size === 0){
|
||||
return;
|
||||
}
|
||||
//rc?
|
||||
throw new Error("Not yet implemented");
|
||||
this.logStack.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class GameLogger {
|
||||
protected stack: LoggerStack;
|
||||
|
||||
public constructor(
|
||||
year: IntYear,
|
||||
month: IntMonth,
|
||||
protected readonly engine: GameLoggerEngine,
|
||||
) {
|
||||
this.stack = {
|
||||
symbol: Symbol(),
|
||||
year,
|
||||
month,
|
||||
global: {
|
||||
action: [],
|
||||
history: []
|
||||
},
|
||||
nation: {
|
||||
nationID: 0,
|
||||
history: []
|
||||
},
|
||||
general: {
|
||||
action: [],
|
||||
battle: [],
|
||||
battleBrief: [],
|
||||
history: [],
|
||||
generalID: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pushGlobalActionLog(text: string) {
|
||||
this.stack.global.action.push(text);
|
||||
}
|
||||
|
||||
pushGlobalHistoryLog(text: string) {
|
||||
this.stack.global.history.push(text);
|
||||
}
|
||||
|
||||
reset() {
|
||||
const { year, month } = this.stack;
|
||||
const nationID = this.stack.nation.nationID;
|
||||
const generalID = this.stack.general.generalID;
|
||||
this.stack = {
|
||||
symbol: Symbol(),
|
||||
year,
|
||||
month,
|
||||
global: {
|
||||
action: [],
|
||||
history: []
|
||||
},
|
||||
nation: {
|
||||
nationID,
|
||||
history: []
|
||||
},
|
||||
general: {
|
||||
action: [],
|
||||
battle: [],
|
||||
battleBrief: [],
|
||||
history: [],
|
||||
generalID
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
commit() {
|
||||
this.engine.addStack(this.stack);
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export class NationLogger extends GameLogger {
|
||||
public constructor(
|
||||
year: IntYear,
|
||||
month: IntMonth,
|
||||
private readonly nationID: NationID,
|
||||
engine: GameLoggerEngine
|
||||
) {
|
||||
super(year, month, engine);
|
||||
this.stack.nation.nationID = nationID;
|
||||
}
|
||||
|
||||
pushNationLog(text: string) {
|
||||
this.stack.nation.history.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
export class GeneralLogger extends NationLogger {
|
||||
public constructor(
|
||||
year: IntYear,
|
||||
month: IntMonth,
|
||||
nationID: NationID,
|
||||
private readonly generalID: GeneralID,
|
||||
engine: GameLoggerEngine
|
||||
) {
|
||||
super(year, month, nationID, engine);
|
||||
this.stack.general.generalID = generalID;
|
||||
}
|
||||
|
||||
pushGeneralActionLog(text: string) {
|
||||
this.stack.general.action.push(text);
|
||||
}
|
||||
|
||||
pushGeneralBattleLog(text: string) {
|
||||
this.stack.general.battle.push(text);
|
||||
}
|
||||
|
||||
pushGeneralBattleBriefLog(text: string) {
|
||||
this.stack.general.battleBrief.push(text);
|
||||
}
|
||||
|
||||
pushGeneralHistoryLog(text: string) {
|
||||
this.stack.general.history.push(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { InvalidArgument, NotYetImplemented } from "@sammo/util";
|
||||
import { City } from "./City.js";
|
||||
import type { IGeneralEntity } from "./Entity/GeneralEntity.js";
|
||||
import type { IResourceController } from "./IResourceController.js";
|
||||
import { DiplomaticPermission, type CityID, type GeneralID, type GeneralName, type NationID, OfficerLevel, type SquadID, NpcType, StaffLevel, CityOfficerLevel } from "./defs.js";
|
||||
import { must } from "@sammo/util";
|
||||
import { Nation } from "./Nation.js";
|
||||
import { Squad } from "./Squad.js";
|
||||
import { LazyEntityUpdater } from "./LazyEntityUpdater.js";
|
||||
import type { IAction, IActionKey } from "./IAction.js";
|
||||
import { addSeconds } from "date-fns";
|
||||
|
||||
export class General extends LazyEntityUpdater<IGeneralEntity> {
|
||||
protected readonly _indirectNames: (keyof IGeneralEntity)[] = [
|
||||
"id", "nationID", "squadID", "nationID", "turnTime"
|
||||
];
|
||||
protected readonly _entityType = "General";
|
||||
protected readonly _raw: IGeneralEntity;
|
||||
|
||||
protected _city!: WeakRef<City>;
|
||||
protected _squad!: WeakRef<Squad> | undefined;
|
||||
protected _nation!: WeakRef<Nation>;
|
||||
|
||||
protected readonly iActionHandlers = new Map<IActionKey, IAction>;
|
||||
|
||||
constructor(
|
||||
raw: IGeneralEntity,
|
||||
protected readonly rc: IResourceController
|
||||
){
|
||||
super();
|
||||
this._raw = raw;
|
||||
}
|
||||
|
||||
public bootstrap(){
|
||||
const rc = this.rc;
|
||||
const raw = this._raw;
|
||||
this._city = new WeakRef(must(rc.city(raw.cityID)));
|
||||
this._nation = new WeakRef(must(rc.nation(raw.nationID)));
|
||||
|
||||
if(raw.squadID){
|
||||
this._squad = new WeakRef(must(rc.squad(raw.squadID, raw.nationID)));
|
||||
}
|
||||
|
||||
this.reconfigureIActionHandlers();
|
||||
}
|
||||
|
||||
public get id(): GeneralID {
|
||||
return this._raw.id;
|
||||
}
|
||||
|
||||
public get name(): GeneralName {
|
||||
return this._raw.name;
|
||||
}
|
||||
|
||||
public get city(): City {
|
||||
return must(this._city.deref());
|
||||
}
|
||||
|
||||
public get squad(): Squad | undefined {
|
||||
return this._squad?.deref();
|
||||
}
|
||||
|
||||
public get nation(): Nation {
|
||||
return must(this._nation.deref());
|
||||
}
|
||||
|
||||
public get isUser(): boolean {
|
||||
return this._raw.npcType <= NpcType.user_borrowed_npc;
|
||||
}
|
||||
|
||||
public get staffLevel(): StaffLevel | undefined {
|
||||
if(this._raw.officerLevel < OfficerLevel.staffIntel3){
|
||||
return undefined;
|
||||
}
|
||||
return this._raw.officerLevel as number as StaffLevel;
|
||||
}
|
||||
|
||||
public get cityOfficerLevel(): CityOfficerLevel | undefined {
|
||||
if(this._raw.officerLevel >= OfficerLevel.staffIntel3){
|
||||
return undefined;
|
||||
}
|
||||
if(this._raw.officerLevel < OfficerLevel.cityLeadership){
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._raw.officerLevel as number as CityOfficerLevel;
|
||||
}
|
||||
|
||||
public changeCity(city: City){
|
||||
this.city.notifyQuitGeneral(this);
|
||||
|
||||
this._city = new WeakRef(city);
|
||||
this.forceUpdate((raw) => {
|
||||
raw.cityID = city.id;
|
||||
});
|
||||
city.notifyJoinGeneral(this);
|
||||
|
||||
this.reconfigureIActionHandlers();
|
||||
}
|
||||
|
||||
public quitSquad(){
|
||||
if(!this._squad){
|
||||
return;
|
||||
}
|
||||
|
||||
const squad = must(this._squad.deref());
|
||||
if(squad.leader.id === this._raw.id){
|
||||
squad.destroySquad(false);
|
||||
}
|
||||
else{
|
||||
squad.notifyQuitGeneral(this);
|
||||
}
|
||||
|
||||
this._squad = undefined;
|
||||
this.forceUpdate((raw) => {
|
||||
raw.squadID = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public changeSquad(squad: Squad){
|
||||
this.quitSquad();
|
||||
|
||||
this._squad = new WeakRef(squad);
|
||||
this.forceUpdate((raw) => {
|
||||
raw.squadID = squad.id;
|
||||
});
|
||||
squad.notifyJoinGeneral(this);
|
||||
}
|
||||
|
||||
public changeNation(nation: Nation){
|
||||
this.quitSquad();
|
||||
this.nation.notifyQuitGeneral(this);
|
||||
|
||||
this._nation = new WeakRef(nation);
|
||||
this.forceUpdate((raw) => {
|
||||
raw.nationID = nation.id;
|
||||
raw.diplomaticPermission = DiplomaticPermission.none;
|
||||
if(nation.id == 0){
|
||||
raw.officerLevel = OfficerLevel.none;
|
||||
}
|
||||
else{
|
||||
raw.officerLevel = OfficerLevel.normal;
|
||||
}
|
||||
raw.nationJoinedYearMonth = this.rc.gameEnv.raw.yearMonth;
|
||||
});
|
||||
nation.notifyJoinGeneral(this);
|
||||
|
||||
this.reconfigureIActionHandlers();
|
||||
}
|
||||
|
||||
private reconfigureIActionHandlers(){
|
||||
throw new NotYetImplemented();
|
||||
}
|
||||
|
||||
public updateTurnTime(){
|
||||
const secondsPerTurn = this.rc.gameEnv.raw.secondsPerTurn;
|
||||
|
||||
this.forceUpdate((raw) => {
|
||||
raw.turnTime = addSeconds(raw.turnTime, secondsPerTurn);
|
||||
});
|
||||
}
|
||||
|
||||
public compareTurnTime(other: General): number {
|
||||
if(this._raw.turnTime < other._raw.turnTime){
|
||||
return -1;
|
||||
}
|
||||
if(this._raw.turnTime > other._raw.turnTime){
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(this._raw.id < other._raw.id){
|
||||
return -1;
|
||||
}
|
||||
if(this._raw.id > other._raw.id){
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { IAction } from "../IAction.js";
|
||||
import type { SpecialReqType, WeightType } from "./defs.js";
|
||||
|
||||
export abstract class BaseSpecial implements IAction {
|
||||
public abstract readonly name: string;
|
||||
|
||||
static readonly selectWeight: WeightType = {
|
||||
type: "normal",
|
||||
weight: 1,
|
||||
}
|
||||
|
||||
static readonly type: SpecialReqType[] = [];
|
||||
|
||||
getName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
abstract getInfo(): string[]|string;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { General } from "../../General.js";
|
||||
import type { BattlePhaseTrigger, CalcStat, CalcStatRange } from "../../IActionMethod.js";
|
||||
import { WarUnitTriggerCaller } from "../../TriggerCaller/WarUnitTriggerCaller.js";
|
||||
import type { WarUnit } from "../../WarUnit.js";
|
||||
import { che_필살강화_회피불가 } from "../../WarUnitTrigger/che_필살강화_회피불가.js";
|
||||
import type { CalcStatName, CalcStatRangeName } from "@/defs.js";
|
||||
import { BaseSpecial } from "../BaseSpecial.js";
|
||||
import type { SpecialReqType, WeightNormal } from "../defs.js";
|
||||
|
||||
export class che_필살 extends BaseSpecial implements CalcStat, CalcStatRange, BattlePhaseTrigger {
|
||||
public readonly name = "필살";
|
||||
|
||||
static override readonly selectWeight: WeightNormal = {
|
||||
type: "normal",
|
||||
weight: 1,
|
||||
}
|
||||
static override type: SpecialReqType[] = [
|
||||
'statLeadership',
|
||||
'statStrength',
|
||||
'statIntel',
|
||||
];
|
||||
|
||||
getInfo() {
|
||||
return "[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상";
|
||||
}
|
||||
|
||||
onCalcStat(general: General, statName: CalcStatName, value: number, aux?: unknown[] | undefined): number {
|
||||
if (statName == 'warCriticalRatio') {
|
||||
return value + 0.3;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
onCalcStatRange(general: General, statName: CalcStatRangeName, value: [number, number], aux?: unknown[] | undefined): [number, number] {
|
||||
if (statName === 'criticalDamageRange'){
|
||||
const [rangeMin, rangeMax] = value;
|
||||
return [(rangeMin + rangeMax) / 2, rangeMax];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
getBattlePhaseSkillTriggerList(unit: WarUnit): WarUnitTriggerCaller | undefined {
|
||||
return new WarUnitTriggerCaller(
|
||||
new che_필살강화_회피불가(unit),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
class SpecialityHelper {
|
||||
}
|
||||
|
||||
export const specialityHelper = new SpecialityHelper();
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
export type SpecialReqType =
|
||||
"statLeadership" |
|
||||
"statStrength" |
|
||||
"statIntel" |
|
||||
|
||||
"armyFootMan" |
|
||||
"armyArcher" |
|
||||
"armyCavalry" |
|
||||
"armyWizard" |
|
||||
"armySiege" |
|
||||
|
||||
"reqDexterity" |
|
||||
|
||||
"statNotLeadership" |
|
||||
"statNotStrength" |
|
||||
"statNotIntel"
|
||||
;
|
||||
|
||||
export type WeightNormal = {
|
||||
type: "normal";
|
||||
weight: number; // 1을 기준으로 한 가중치 곱
|
||||
};
|
||||
|
||||
export type WeightPercent = {
|
||||
type: "percent";
|
||||
weight: number; // 1이면 100%
|
||||
};
|
||||
|
||||
export type WeightType = WeightNormal | WeightPercent;
|
||||
@@ -0,0 +1,8 @@
|
||||
export { BaseSpecial } from "./BaseSpecial.js";
|
||||
|
||||
import { che_필살 } from "./SpecialWar/che_필살.js";
|
||||
|
||||
|
||||
export const SpecialWar = {
|
||||
che_필살
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { ValuesOf } from "@sammo/util";
|
||||
import type * as IMethod from "./IActionMethod.js";
|
||||
import type { ItemKeyType } from "./defs.js";
|
||||
export type * as IMethod from "./IActionMethod.js";
|
||||
|
||||
export interface IActionMethod extends
|
||||
IMethod.TurnExecuteTriggerList,
|
||||
IMethod.CalcDomestic,
|
||||
IMethod.CalcStat,
|
||||
IMethod.CalcStatRange,
|
||||
IMethod.CalcOpposeStat,
|
||||
IMethod.CalcStrategic,
|
||||
IMethod.CalcIncome,
|
||||
IMethod.WarPowerMultiplier,
|
||||
IMethod.BattleInitTrigger,
|
||||
IMethod.BattlePhaseTrigger,
|
||||
IMethod.ArbitraryAction {
|
||||
}
|
||||
|
||||
export interface IAction extends Partial<IActionMethod> {
|
||||
readonly name: string;
|
||||
|
||||
getName(): string;
|
||||
getInfo(): string[] | string;
|
||||
}
|
||||
|
||||
export const IActionOrder = [
|
||||
'nationType',
|
||||
'officerLevel',
|
||||
'specialityDomestic',
|
||||
'specialityWar',
|
||||
'personlity',
|
||||
'armyType',
|
||||
'inheritBuff',
|
||||
'scenarioEffect',
|
||||
'horse',
|
||||
'weapon',
|
||||
'book',
|
||||
'item',
|
||||
];
|
||||
|
||||
export type IActionKey = ValuesOf<IAction>;
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { RandUtil } from "@sammo/crypto";
|
||||
import type { WarUnitTriggerCaller } from "./TriggerCaller/WarUnitTriggerCaller.js";
|
||||
import type { GeneralTriggerCaller } from "./TriggerCaller/GeneralTriggerCaller.js";
|
||||
import type { General } from "./General.js";
|
||||
import type { WarUnit } from "./WarUnit.js";
|
||||
import type { ArbitraryActionType, CalcIncomeType, CalcStatName, CalcStatRangeName, CalcStrategicVarType, StrategicCmdType } from "./defs.js";
|
||||
|
||||
//TODO: turnType, varType이 string이면 안됨. enum에 가까워야함!
|
||||
type onCalcDomesticFunc = (turnType: string, varType: string, value: number, aux?: unknown[]) => number;
|
||||
|
||||
type onCalcStatFunc = (general: General, statName: CalcStatName, value: number, aux?: unknown[]) => number;
|
||||
type onCalcStatRangeFunc = (general: General, statName: CalcStatRangeName, value: [number, number], aux?: unknown[]) => [number, number];
|
||||
type onCalcStrategicFunc = (turnType: StrategicCmdType, varType: CalcStrategicVarType, value: number) => number;
|
||||
type onCalcIncomeFunc = (type: CalcIncomeType, value: number) => number;
|
||||
|
||||
type getWarPowerMultiplierFunc = (unit: WarUnit) => [number, number];
|
||||
type getWarUnitTriggerListFunc = (unit: WarUnit) => WarUnitTriggerCaller | undefined;
|
||||
|
||||
type onArbitraryActionFunc = (general: General, rng: RandUtil, actionType: ArbitraryActionType, phase?: string, aux?: unknown[]) => null | string[];
|
||||
|
||||
export interface TurnExecuteTriggerList {
|
||||
getPreTurnExecuteTriggerList(general: General): GeneralTriggerCaller | undefined;
|
||||
}
|
||||
|
||||
export interface CalcDomestic {
|
||||
onCalcDomestic: onCalcDomesticFunc;
|
||||
}
|
||||
|
||||
export interface CalcStat {
|
||||
onCalcStat: onCalcStatFunc;
|
||||
}
|
||||
|
||||
export interface CalcStatRange {
|
||||
onCalcStatRange: onCalcStatRangeFunc;
|
||||
}
|
||||
|
||||
export interface CalcOpposeStat {
|
||||
onCalcOpposeStat: onCalcStatFunc;
|
||||
}
|
||||
|
||||
export interface CalcStrategic {
|
||||
onCalcStrategic: onCalcStrategicFunc;
|
||||
}
|
||||
|
||||
export interface CalcIncome {
|
||||
onCalcNationalIncome: onCalcIncomeFunc;
|
||||
}
|
||||
|
||||
export interface WarPowerMultiplier {
|
||||
getWarPowerMultiplier: getWarPowerMultiplierFunc;
|
||||
}
|
||||
|
||||
export interface BattleInitTrigger {
|
||||
getBatteInitSkillTriggerList: getWarUnitTriggerListFunc;
|
||||
}
|
||||
|
||||
export interface BattlePhaseTrigger {
|
||||
getBattlePhaseSkillTriggerList: getWarUnitTriggerListFunc;
|
||||
}
|
||||
|
||||
export interface ArbitraryAction {
|
||||
onArbitraryAction: onArbitraryActionFunc;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { General } from "./General.js";
|
||||
import type { City } from "./City.js";
|
||||
import type { Nation } from "./Nation.js";
|
||||
import type { CityID, GeneralID, NationID, ReservedTurn, SquadID } from "./defs.js";
|
||||
import type { GameEnv } from "./GameEnv.js";
|
||||
import type { IGeneralEntity } from "./Entity/GeneralEntity.js";
|
||||
import type { Squad } from "./Squad.js";
|
||||
import type { ISquadEntity } from "./Entity/SquadEntity.js";
|
||||
import type { LazyEntityUpdater } from "./LazyEntityUpdater.js";
|
||||
import type { ICityEntity } from "./Entity/CityEntity.js";
|
||||
import type { INationEntity } from "./Entity/NationEntity.js";
|
||||
import type { EntityType, ValidEntityType } from "./Entity/index.js";
|
||||
|
||||
export interface IResourceController {
|
||||
|
||||
nation(id: NationID): Nation | undefined;
|
||||
city(id: CityID): City | undefined;
|
||||
squad(id: SquadID, nation: NationID): Squad | undefined;
|
||||
general(id: GeneralID): General | undefined;
|
||||
|
||||
peekGeneralTurnTimeQueue(): General | undefined;
|
||||
popGeneralTurnTimeQueue(): General | undefined;
|
||||
insertGeneralTurnTimeQueue(general: General, maybeTail: boolean): void;
|
||||
|
||||
readonly gameEnv: GameEnv;
|
||||
|
||||
readonly nationList: Map<NationID, Nation>;
|
||||
readonly cityList: Map<CityID, City>;
|
||||
readonly squadList: Map<SquadID, Squad>;
|
||||
readonly generalList: Map<GeneralID, General>;
|
||||
|
||||
//----Insert, Update, Delete
|
||||
|
||||
reserveUpdate(entity: LazyEntityUpdater<any>): void;
|
||||
reserveDelete(entity: LazyEntityUpdater<any>): void;
|
||||
|
||||
//TODO: create* 방식과 구현 방식 비교
|
||||
reserveInsert(entity: LazyEntityUpdater<any>): void;
|
||||
|
||||
//TODO: reserveInsert와 구현 방식 비교
|
||||
createGeneral(raw: IGeneralEntity): General;
|
||||
createNation(raw: INationEntity): Nation;
|
||||
createSquad(raw: ISquadEntity): Squad;
|
||||
|
||||
|
||||
_getAllChanges(): Record<
|
||||
ValidEntityType,
|
||||
Map<number, [op: 'insert' | 'update' | 'delete', value: LazyEntityUpdater<any>]>
|
||||
>;
|
||||
|
||||
//---- DB 접근 ---
|
||||
|
||||
//예턴은 호출마다 DB에서 조회
|
||||
getReservedTurn(general: General): Promise<ReservedTurn>;
|
||||
nationReservedTurn(general: General): Promise<ReservedTurn | undefined>;
|
||||
|
||||
resetAndFill(): Promise<void>;
|
||||
saveAll(): Promise<void>;
|
||||
}
|
||||
|
||||
let _resourceController: IResourceController | undefined = undefined;
|
||||
|
||||
export const setResourceController = (rc: IResourceController) => {
|
||||
_resourceController = rc;
|
||||
}
|
||||
|
||||
export const getResourceController = () => {
|
||||
if (_resourceController === undefined) {
|
||||
throw new Error("ResourceController not set");
|
||||
}
|
||||
return _resourceController;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ValidEntity, EntityType } from "./Entity/index.js";
|
||||
import type { IResourceController } from "./IResourceController.js";
|
||||
|
||||
export abstract class LazyEntityUpdater<Entity extends ValidEntity>{
|
||||
protected abstract readonly _indirectNames: ReadonlyArray<keyof Entity>;
|
||||
protected abstract readonly _entityType: EntityType<Entity>;
|
||||
|
||||
protected abstract readonly _raw: Entity;
|
||||
|
||||
protected abstract readonly rc: IResourceController;
|
||||
|
||||
public abstract bootstrap(): void;
|
||||
|
||||
public get raw(): Readonly<Entity> {
|
||||
return this._raw;
|
||||
}
|
||||
|
||||
public get entityType(): EntityType<Entity> {
|
||||
return this._entityType;
|
||||
}
|
||||
|
||||
protected forceUpdate(callback: (raw: Entity) => void) {
|
||||
callback(this._raw);
|
||||
this.rc.reserveUpdate(this);
|
||||
}
|
||||
|
||||
public update(callback: (raw: Entity) => void) {
|
||||
const oldRaw = { ...this._raw };
|
||||
callback(this._raw);
|
||||
|
||||
//for구문이 빠른가, Proxy setter로 제어하는게 빠른가
|
||||
for (const indirectName of this._indirectNames) {
|
||||
if (oldRaw[indirectName] !== this._raw[indirectName]) {
|
||||
throw new Error(`${String(indirectName)} change by update() is not allowed`);
|
||||
}
|
||||
}
|
||||
this.rc.reserveUpdate(this);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user