initial commit
This commit is contained in:
commit
83f7154665
|
|
@ -0,0 +1 @@
|
||||||
|
/node_modules
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>School of Magic</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "react",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"human-names": "^1.0.12",
|
||||||
|
"mobx-react-lite": "^4.1.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.9",
|
||||||
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
|
"sass": "^1.97.3",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,174 @@
|
||||||
|
#root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
font-size: 16pt;
|
||||||
|
|
||||||
|
background: #404040;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 15rem;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-areas: "header header" "school upgrades" "debug debug";
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
grid-area: header;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resources {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.units {
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.students {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-flow: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.school {
|
||||||
|
grid-area: school;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #ffffff40;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.owner {
|
||||||
|
background: #ff800040;
|
||||||
|
border-color: #ff800080;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.student {
|
||||||
|
background: #0080ff40;
|
||||||
|
border-color: #0080ff80;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .cooldown {
|
||||||
|
background: #ffffff40;
|
||||||
|
min-height: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrades {
|
||||||
|
grid-area: upgrades;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
& > .upgrade {
|
||||||
|
font-size: 60%;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
background-color: #80ff8080;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1rem 8px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
opacity: 0.5;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.disabled) {
|
||||||
|
background-color: #80ff80c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .name {
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .cost {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .level {
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
background-color: #408040;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -5px;
|
||||||
|
right: -5px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug {
|
||||||
|
grid-area: debug;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import './App.scss'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { StatsView } from './views/StatsView'
|
||||||
|
import { SchoolView } from './views/SchoolView'
|
||||||
|
import { UpgradeListView } from './views/UpgradeListView'
|
||||||
|
import { DebugView } from './views/DebugView'
|
||||||
|
|
||||||
|
export const App = observer(function () {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<StatsView />
|
||||||
|
<SchoolView />
|
||||||
|
<UpgradeListView />
|
||||||
|
<DebugView />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { App } from './App'
|
||||||
|
import './index.css'
|
||||||
|
import { GameClock } from './utils/gameClock'
|
||||||
|
|
||||||
|
GameClock.start();
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
import { school } from './school';
|
||||||
|
import { Progress } from './progress';
|
||||||
|
|
||||||
|
export class Calendar {
|
||||||
|
|
||||||
|
public day: number = 1;
|
||||||
|
|
||||||
|
public progress = new Progress(24, 1, true, () => this.handleProgress());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleProgress(): boolean {
|
||||||
|
this.day++;
|
||||||
|
school.collectStudentGold();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get dayFraction(): number {
|
||||||
|
return Math.floor(this.progress.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Progress } from './progress';
|
||||||
|
import { school } from './school';
|
||||||
|
import { IUnit } from './unit';
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
|
||||||
|
export class Owner implements IUnit {
|
||||||
|
|
||||||
|
public readonly id = 'owner';
|
||||||
|
|
||||||
|
public progress = new Progress(2, 1, false, () => this.handleProgress());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleProgress(): boolean {
|
||||||
|
school.addReputation(1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
|
||||||
|
export class Progress {
|
||||||
|
|
||||||
|
public value: number = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public length: number,
|
||||||
|
public speed: number,
|
||||||
|
public readonly autoRun: boolean,
|
||||||
|
private callback: () => boolean,
|
||||||
|
) {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get normalValue(): number {
|
||||||
|
return this.value / this.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isFull() {
|
||||||
|
return this.value == this.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public tick(time: number): void {
|
||||||
|
if (this.isFull && this.autoRun) {
|
||||||
|
this.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = Math.min(this.length, this.value + time / this.speed);
|
||||||
|
if (this.autoRun)
|
||||||
|
this.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public run(): void {
|
||||||
|
if (this.isFull) {
|
||||||
|
if (this.callback())
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
import { Owner } from './owner';
|
||||||
|
import { Student } from './student';
|
||||||
|
import { IUnit } from './unit';
|
||||||
|
import { Upgrades } from './upgrades';
|
||||||
|
import { PartialResourceSet, ResourceSet } from '../types/resources';
|
||||||
|
import { addResourceSets, fullResourceSet, mapResourceSet, multiplyResourceSet, roundResourceSet, subtractResourceSets } from '../utils/resources';
|
||||||
|
import { v7 as uuid } from 'uuid';
|
||||||
|
import { GameClock } from '../utils/gameClock';
|
||||||
|
import { Calendar } from './calendar';
|
||||||
|
import { getRandomHumanName } from '../utils/humanNames';
|
||||||
|
|
||||||
|
const resourceScale = 100;
|
||||||
|
|
||||||
|
export class School {
|
||||||
|
|
||||||
|
public timeScale: number = 1;
|
||||||
|
public tps: number = 0;
|
||||||
|
|
||||||
|
public calendar = new Calendar();
|
||||||
|
|
||||||
|
// units
|
||||||
|
public owner: Owner = new Owner();
|
||||||
|
public students: Array<Student> = new Array<Student>();
|
||||||
|
|
||||||
|
public upgrades = new Upgrades();
|
||||||
|
|
||||||
|
public scaledResources: ResourceSet = multiplyResourceSet({ reputation: 19, gold: 1000 }, resourceScale);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.increaseStudentCount();
|
||||||
|
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get resources(): ResourceSet {
|
||||||
|
return roundResourceSet(multiplyResourceSet(this.scaledResources, 1 / resourceScale), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public tick(time: number) {
|
||||||
|
const scaledTime = time * this.timeScale;
|
||||||
|
|
||||||
|
this.calendar.progress.tick(scaledTime);
|
||||||
|
|
||||||
|
for (let unit of this.units)
|
||||||
|
unit.progress.tick(scaledTime);
|
||||||
|
|
||||||
|
this.tps = GameClock.tps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTimeScale(value: number) {
|
||||||
|
this.timeScale = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get units(): IUnit[] {
|
||||||
|
return [this.owner, ...this.students]
|
||||||
|
}
|
||||||
|
|
||||||
|
public addResources(value: PartialResourceSet): void {
|
||||||
|
this.scaledResources = roundResourceSet(
|
||||||
|
addResourceSets(
|
||||||
|
this.scaledResources,
|
||||||
|
multiplyResourceSet(fullResourceSet(value), resourceScale),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addReputation(reputation: number): void {
|
||||||
|
this.addResources({ reputation });
|
||||||
|
}
|
||||||
|
|
||||||
|
public addGold(gold: number): void {
|
||||||
|
this.addResources({ gold });
|
||||||
|
}
|
||||||
|
|
||||||
|
public increaseStudentCount(): void {
|
||||||
|
this.students.push(new Student(uuid(), getRandomHumanName().join(' ')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeResources(resources: ResourceSet) {
|
||||||
|
this.scaledResources = subtractResourceSets(this.scaledResources, multiplyResourceSet(resources, resourceScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
public collectStudentGold(): void {
|
||||||
|
this.addGold(this.students.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const school = new School();
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Progress } from './progress';
|
||||||
|
import { school } from './school';
|
||||||
|
import { IUnit } from './unit';
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
|
||||||
|
export class Student implements IUnit {
|
||||||
|
|
||||||
|
public progress = new Progress(10, 1, true, () => this.handleProgress());
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly id: string,
|
||||||
|
public readonly name: string,
|
||||||
|
) {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleProgress(): boolean {
|
||||||
|
school.addReputation(0.1);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
import { Progress } from './progress';
|
||||||
|
|
||||||
|
export interface IUnit {
|
||||||
|
get id(): string;
|
||||||
|
get progress(): Progress;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { UpgradePrefab } from "../prefabs/upgrades";
|
||||||
|
import { addResourceSets, fullResourceSet, isResourceSetGte } from "../utils/resources";
|
||||||
|
import { evalLevelValue } from "../utils/upgrades";
|
||||||
|
import { school } from "./school";
|
||||||
|
import { ResourceSet } from "../types/resources";
|
||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
|
export class Upgrade {
|
||||||
|
public readonly id: string;
|
||||||
|
|
||||||
|
public level: number;
|
||||||
|
|
||||||
|
constructor(public readonly prefab: UpgradePrefab) {
|
||||||
|
this.id = prefab.id;
|
||||||
|
this.level = 0;
|
||||||
|
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return evalLevelValue(this.prefab.name, this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
get description(): string {
|
||||||
|
return evalLevelValue(this.prefab.description, this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
get costToView(): ResourceSet {
|
||||||
|
return this.evalCostToView(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
get costToOpen(): ResourceSet {
|
||||||
|
return this.evalCostToOpen(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
get costToBuy(): ResourceSet {
|
||||||
|
return this.evalCostToBuy(this.level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public evalCostToStepView(level: number): ResourceSet {
|
||||||
|
return fullResourceSet(evalLevelValue(this.prefab.costToView, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
public evalCostToView(level: number): ResourceSet {
|
||||||
|
let sum = fullResourceSet();
|
||||||
|
for (let lvl = 0; lvl <= level; lvl++)
|
||||||
|
sum = addResourceSets(sum, this.evalCostToStepView(lvl));
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evalCostToStepOpen(level: number): ResourceSet {
|
||||||
|
return fullResourceSet(evalLevelValue(this.prefab.costToOpen, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
public evalCostToOpen(level: number): ResourceSet {
|
||||||
|
let sum = fullResourceSet();
|
||||||
|
for (let lvl = 0; lvl <= level; lvl++)
|
||||||
|
sum = addResourceSets(sum, this.evalCostToStepOpen(lvl));
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public evalCostToStepBuy(level: number): ResourceSet {
|
||||||
|
return fullResourceSet(evalLevelValue(this.prefab.costToBuy, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
public evalCostToBuy(level: number): ResourceSet {
|
||||||
|
return this.evalCostToStepBuy(level);
|
||||||
|
|
||||||
|
// let sum = fullResourceSet();
|
||||||
|
// for (let lvl = 0; lvl <= level; lvl++)
|
||||||
|
// sum = addResourceSets(sum, this.evalCostToStepBuy(lvl));
|
||||||
|
|
||||||
|
// return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public increaseLevel(): void {
|
||||||
|
this.level++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute(): void {
|
||||||
|
let cost = this.costToBuy;
|
||||||
|
|
||||||
|
this.prefab.execute(this.level);
|
||||||
|
school.removeResources(cost);
|
||||||
|
|
||||||
|
this.increaseLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isVisible(): boolean {
|
||||||
|
return isResourceSetGte(school.resources, this.costToView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isOpen(): boolean {
|
||||||
|
return isResourceSetGte(school.resources, this.costToOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAffordable(): boolean {
|
||||||
|
return isResourceSetGte(school.resources, this.costToBuy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
import { UpgradePrefab, upgrades } from '../prefabs/upgrades';
|
||||||
|
import { Upgrade } from './upgrade';
|
||||||
|
|
||||||
|
export class Upgrades {
|
||||||
|
|
||||||
|
public readonly allItems: Upgrade[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.allItems = upgrades.map((item) => new Upgrade(item));
|
||||||
|
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get visibleItems(): Upgrade[] {
|
||||||
|
return this.allItems.filter((item) => item.isVisible());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { school } from "../model/school";
|
||||||
|
import { PartialResourceSet, Resource } from "../types/resources";
|
||||||
|
import { LevelValue, MaybeLevelValue } from "../types/upgrades";
|
||||||
|
import { fullResourceSet } from "../utils/resources";
|
||||||
|
|
||||||
|
export type UpgradePrefab = {
|
||||||
|
id: string;
|
||||||
|
maxLevel?: number;
|
||||||
|
name: MaybeLevelValue<string>;
|
||||||
|
description: MaybeLevelValue<string>;
|
||||||
|
costToView: MaybeLevelValue<PartialResourceSet>;
|
||||||
|
costToOpen: MaybeLevelValue<PartialResourceSet>;
|
||||||
|
costToBuy: MaybeLevelValue<PartialResourceSet>;
|
||||||
|
execute: LevelValue<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const upgrades: UpgradePrefab[] = [
|
||||||
|
{
|
||||||
|
id: 'moreStudents',
|
||||||
|
name: "Больше учеников",
|
||||||
|
description: (level) => `Увеличить количество учеников с ${level} до ${level + 1}`,
|
||||||
|
costToView: (level) => fullResourceSet(),
|
||||||
|
costToOpen: (level) => ({ reputation: 20 * (level / 10 + 1) }),
|
||||||
|
costToBuy: (level) => ({ gold: 10 }),
|
||||||
|
execute: (level) => {
|
||||||
|
school.increaseStudentCount();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fixedCostTest',
|
||||||
|
name: "fixedCostTest",
|
||||||
|
description: "fixedCostTest",
|
||||||
|
costToView: (level) => fullResourceSet(),
|
||||||
|
costToOpen: fullResourceSet(),
|
||||||
|
costToBuy: { reputation: 3 },
|
||||||
|
execute: (level) => { console.log('fixed', level)},
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'runningCostTest',
|
||||||
|
name: "runningCostTest",
|
||||||
|
description: (level) => `runningCostTest ${level} => ${level + 1}`,
|
||||||
|
costToView: (level) => fullResourceSet(),
|
||||||
|
costToOpen: (level) => ({ gold: 1000 }),
|
||||||
|
costToBuy: (level) => ({ reputation: level * 2 + 1 }),
|
||||||
|
execute: (level) => { console.log('running', level)},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface ITick {
|
||||||
|
tick(deltaTime: number): void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const RESOURCES = ['reputation', 'gold'] as const;
|
||||||
|
export type Resource = (typeof RESOURCES)[number];
|
||||||
|
|
||||||
|
export type ResourceSet = Record<Resource, number>;
|
||||||
|
|
||||||
|
export type PartialResourceSet = Partial<ResourceSet>;
|
||||||
|
|
||||||
|
export const ResourceNames: Record<Resource, string> = {
|
||||||
|
reputation: 'Репутация',
|
||||||
|
gold: 'Золото',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export type LevelValue<T> = (level: number) => T;
|
||||||
|
export type MaybeLevelValue<T> = T | LevelValue<T>;
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { school } from "../model/school";
|
||||||
|
|
||||||
|
export class GameClock {
|
||||||
|
|
||||||
|
private static frames = new Array<number>();
|
||||||
|
|
||||||
|
// public static async start(): Promise<void> {
|
||||||
|
// let lastTime = performance.now();
|
||||||
|
|
||||||
|
// setInterval(() => {
|
||||||
|
// let now = performance.now();
|
||||||
|
// let delta = now - lastTime;
|
||||||
|
|
||||||
|
// school.tick(delta / 1000);
|
||||||
|
|
||||||
|
// lastTime = now;
|
||||||
|
// }, 33);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public static start(): void {
|
||||||
|
let lastTime = performance.now();
|
||||||
|
|
||||||
|
const loop = (now: number) => {
|
||||||
|
const delta = now - lastTime;
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
|
GameClock.frames.push(lastTime);
|
||||||
|
while (GameClock.frames[0] < lastTime - 1000)
|
||||||
|
GameClock.frames.shift();
|
||||||
|
|
||||||
|
school.tick(delta / 1000);
|
||||||
|
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get tps(): number {
|
||||||
|
if (GameClock.frames.length < 2)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
let timeSpan = GameClock.frames[GameClock.frames.length - 1] - GameClock.frames[0];
|
||||||
|
|
||||||
|
return Math.round(GameClock.frames.length / timeSpan * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
export const humans: [string, string, 0 | 1][] = [
|
||||||
|
["Альрик", "Блэкмор", 0],
|
||||||
|
["Эдгар", "Кроули", 0],
|
||||||
|
["Мириам", "Фоксгрейв", 1],
|
||||||
|
["Беатрис", "Уинтерборн", 1],
|
||||||
|
["Тобиас", "Гримвуд", 0],
|
||||||
|
["Селеста", "Морвен", 1],
|
||||||
|
["Персиваль", "Хоторн", 0],
|
||||||
|
["Люсинда", "Брайтвелл", 1],
|
||||||
|
["Гектор", "Дрейвен", 0],
|
||||||
|
["Виолетта", "Муркрофт", 1],
|
||||||
|
["Руфус", "Кинсбери", 0],
|
||||||
|
["Офелия", "Дарквуд", 1],
|
||||||
|
["Амброз", "Фарнли", 0],
|
||||||
|
["Корделия", "Найтли", 1],
|
||||||
|
["Игнатиус", "Брамвелл", 0],
|
||||||
|
["Розалин", "Греймарк", 1],
|
||||||
|
["Бэзил", "Крофтон", 0],
|
||||||
|
["Эвелин", "Шадоур", 1],
|
||||||
|
["Седрик", "Мелроуз", 0],
|
||||||
|
["Матильда", "Фэйрхолл", 1],
|
||||||
|
["Кассиус", "Рейвенхарт", 0],
|
||||||
|
["Дороти", "Вейл", 1],
|
||||||
|
["Октавиус", "Торнфилд", 0],
|
||||||
|
["Элеонора", "Блум", 1],
|
||||||
|
["Фабиан", "Голдкрест", 0],
|
||||||
|
["Гвендолин", "Брукшир", 1],
|
||||||
|
["Морис", "Найтвуд", 0],
|
||||||
|
["Агнес", "Холлоуэй", 1],
|
||||||
|
["Леонард", "Вестфорд", 0],
|
||||||
|
["Имоджен", "Квинс", 1],
|
||||||
|
["Бернард", "Кроуфорд", 0],
|
||||||
|
["Сибилла", "Хейзелтон", 1],
|
||||||
|
["Реджинальд", "Сторм", 0],
|
||||||
|
["Флора", "Эмберлин", 1],
|
||||||
|
["Годрик", "Айронвуд", 0],
|
||||||
|
["Кларисса", "Бельмонт", 1],
|
||||||
|
["Освальд", "Пенрит", 0],
|
||||||
|
["Лилиан", "Сноу", 1],
|
||||||
|
["Тристан", "Блэквилл", 0],
|
||||||
|
["Мейв", "Ферн", 1],
|
||||||
|
["Эммет", "Фроствейл", 0],
|
||||||
|
["Виннифред", "Кейл", 1],
|
||||||
|
["Птолемей", "Грейсон", 0],
|
||||||
|
["Серафина", "Ларк", 1],
|
||||||
|
["Роланд", "Киплинг", 0],
|
||||||
|
["Дафна", "Риверс", 1],
|
||||||
|
["Алдус", "Брайтвуд", 0],
|
||||||
|
["Жозефина", "Марш", 1],
|
||||||
|
["Виктор", "Фарроу", 0],
|
||||||
|
["Нора", "Хиллкрофт", 1],
|
||||||
|
["Клеменс", "Вейлкрофт", 0],
|
||||||
|
["Теодора", "Мист", 1],
|
||||||
|
["Августин", "Руквуд", 0],
|
||||||
|
["Белинда", "Грейвс", 1],
|
||||||
|
["Орион", "Мортлейк", 0],
|
||||||
|
["Кассандра", "Фэйрчайлд", 1],
|
||||||
|
["Лоуренс", "Шейд", 0],
|
||||||
|
["Элиза", "Мерроу", 1],
|
||||||
|
["Сильвестр", "Брайр", 0],
|
||||||
|
["Хелена", "Старк", 1],
|
||||||
|
["Теренс", "Кроссвелл", 0],
|
||||||
|
["Миранда", "Кроу", 1],
|
||||||
|
["Джаспер", "Вулф", 0],
|
||||||
|
["Аделаида", "Найтингейл", 1],
|
||||||
|
["Горацио", "Блэкворт", 0],
|
||||||
|
["Ребекка", "Винтер", 1],
|
||||||
|
["Финеас", "Голдберн", 0],
|
||||||
|
["Марианна", "Шор", 1],
|
||||||
|
["Эверетт", "Локвуд", 0],
|
||||||
|
["Кэтрин", "Фэйрвью", 1],
|
||||||
|
["Руперт", "Даск", 0],
|
||||||
|
["Селина", "Дейл", 1],
|
||||||
|
["Дориан", "Кин", 0],
|
||||||
|
["Фелисити", "Хартвелл", 1],
|
||||||
|
["Илайас", "Моркрест", 0],
|
||||||
|
["Беатриса", "Гринвей", 1],
|
||||||
|
["Малкольм", "Стрейд", 0],
|
||||||
|
["Оливия", "Брайтфорд", 1],
|
||||||
|
["Конрад", "Эшфилд", 0],
|
||||||
|
["Лавиния", "Вейлвуд", 1],
|
||||||
|
["Гарольд", "Треверс", 0],
|
||||||
|
["Пенелопа", "Кроуэлл", 1],
|
||||||
|
["Сайлас", "Рук", 0],
|
||||||
|
["Амелия", "Фоксвуд", 1],
|
||||||
|
["Юлиус", "Дрейк", 0],
|
||||||
|
["Эстер", "Брайткрофт", 1],
|
||||||
|
["Натан", "Кобб", 0],
|
||||||
|
["Розамунд", "Сильвер", 1],
|
||||||
|
["Арчибальд", "Мур", 0],
|
||||||
|
["Ирена", "Кастелл", 1],
|
||||||
|
["Валентин", "Фернвуд", 0],
|
||||||
|
["Глория", "Сейбл", 1],
|
||||||
|
["Кристофер", "Даркмор", 0],
|
||||||
|
["Люсия", "Эмбер", 1],
|
||||||
|
["Доминик", "Блэкридж", 0],
|
||||||
|
["София", "Рейн", 1],
|
||||||
|
["Тадеус", "Фолкнер", 0],
|
||||||
|
["Изабель", "Винчестер", 1]
|
||||||
|
];
|
||||||
|
|
||||||
|
const humanMales = humans.filter(([, , s]) => s === 0);
|
||||||
|
const humanFemales = humans.filter(([, , s]) => s === 1);
|
||||||
|
|
||||||
|
function randomInRange(min: number, max: number, digits: number = 0): number {
|
||||||
|
const v = (Math.random() * (max - min)) + min;
|
||||||
|
return Math.round(v * 10 ** digits) / (10 ** digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomHuman(): [string, string, 0 | 1] {
|
||||||
|
const idx = randomInRange(0, humans.length - 1);
|
||||||
|
return humans[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomLastName(gender: 0 | 1): string {
|
||||||
|
const hums = gender ? humanFemales : humanMales;
|
||||||
|
return hums[randomInRange(0, hums.length - 1)][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRandomHumanName(): [string, string] {
|
||||||
|
const human = getRandomHuman();
|
||||||
|
const gender = human[2];
|
||||||
|
return [human[0], getRandomLastName(gender)];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { PartialResourceSet, RESOURCES, ResourceSet } from "../types/resources";
|
||||||
|
|
||||||
|
// not exported to prevent non-cloned usage
|
||||||
|
// use fullResourceSet() instead
|
||||||
|
const zeroResourceSet = Object.fromEntries(RESOURCES.map((res) => [res, 0])) as ResourceSet;
|
||||||
|
|
||||||
|
export function fullResourceSet(partial: PartialResourceSet = {}): ResourceSet {
|
||||||
|
return {...zeroResourceSet, ...partial};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResourceSetEqual(a: ResourceSet, b: ResourceSet): boolean {
|
||||||
|
return RESOURCES.every((r) => a[r] == b[r]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResourceSetZero(a: ResourceSet): boolean {
|
||||||
|
return RESOURCES.every((r) => a[r] == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResourceSetGte(a: ResourceSet, b: ResourceSet): boolean {
|
||||||
|
return RESOURCES.every((r) => a[r] >= b[r]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addResourceSets(a: ResourceSet, b: ResourceSet): ResourceSet {
|
||||||
|
const res = fullResourceSet(a);
|
||||||
|
let k: keyof ResourceSet;
|
||||||
|
for (k in b)
|
||||||
|
res[k] = res[k] + b[k];
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subtractResourceSets(a: ResourceSet, b: ResourceSet): ResourceSet {
|
||||||
|
const res = fullResourceSet(a);
|
||||||
|
let k: keyof ResourceSet;
|
||||||
|
for (k in b)
|
||||||
|
res[k] = res[k] - b[k];
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapResourceSet(a: ResourceSet, predicate: (v: number) => number): ResourceSet {
|
||||||
|
const res = fullResourceSet(a);
|
||||||
|
let k: keyof ResourceSet;
|
||||||
|
for (k in a)
|
||||||
|
res[k] = predicate(res[k]);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function multiplyResourceSet(a: ResourceSet, factor: number): ResourceSet {
|
||||||
|
return mapResourceSet(a, (v) => v * factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roundResourceSet(a: ResourceSet, digits: number = 0): ResourceSet {
|
||||||
|
const factor = 10 ** digits;
|
||||||
|
return mapResourceSet(a, (v) => Math.round(v * factor) / factor);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { MaybeLevelValue } from "../types/upgrades";
|
||||||
|
|
||||||
|
export function evalLevelValue<T>(value: MaybeLevelValue<T>, level: number): T {
|
||||||
|
if (typeof(value) === 'function')
|
||||||
|
return (value as (level: number) => T)(level);
|
||||||
|
else
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { school } from "../model/school";
|
||||||
|
import { ChangeEvent } from "react";
|
||||||
|
|
||||||
|
export const DebugView = observer(function () {
|
||||||
|
|
||||||
|
function handleTimeScaleChange(event: ChangeEvent<HTMLInputElement>): void {
|
||||||
|
school.setTimeScale(Number(event.target.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="debug">
|
||||||
|
<div className="timeScale">
|
||||||
|
Масштаб времени
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
value={school.timeScale}
|
||||||
|
onChange={handleTimeScaleChange}
|
||||||
|
style={{width: '100%'}}
|
||||||
|
/>
|
||||||
|
x{school.timeScale}
|
||||||
|
</div>
|
||||||
|
<div className="tps">TPS: {school.tps}</div>
|
||||||
|
</div>
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Owner } from "../model/owner";
|
||||||
|
import { school } from "../model/school";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
unit: Owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OwnerUnitView = observer(function ({ unit }: Props) {
|
||||||
|
function handleButtonClick(): void {
|
||||||
|
if (unit.progress.isFull) {
|
||||||
|
unit.progress.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="unit button owner" onClick={handleButtonClick}>
|
||||||
|
<div className="name">Владелец школы</div>
|
||||||
|
<div className="cooldown" style={{width: `${Math.round(unit.progress.normalValue*100)}%`}}></div>
|
||||||
|
</div>
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { PartialResourceSet, ResourceNames } from "../types/resources";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
resources: PartialResourceSet;
|
||||||
|
partial?: boolean;
|
||||||
|
prefix?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResourceStringView = observer(function (props: Props) {
|
||||||
|
return <div className={`${props.className ?? ''} resources`}>{
|
||||||
|
props.prefix !== undefined
|
||||||
|
? <>{props.prefix}:{' '}</>
|
||||||
|
: <></>
|
||||||
|
}{
|
||||||
|
Object.entries(props.resources)
|
||||||
|
.filter((kvp) => !props.partial || kvp[1] != 0)
|
||||||
|
.map((kvp) => <div key={kvp[0]}>{(ResourceNames as Record<string, string>)[kvp[0]]} {kvp[1]}</div>)
|
||||||
|
}</div>
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { UnitListView } from './UnitListView'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
|
||||||
|
export const SchoolView = observer(function () {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="school">
|
||||||
|
<UnitListView />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { school } from "../model/school";
|
||||||
|
import { ResourceStringView } from "./ResourceStringView";
|
||||||
|
|
||||||
|
export const StatsView = observer(function () {
|
||||||
|
|
||||||
|
return <div className="stats">
|
||||||
|
<ResourceStringView resources={school.resources} partial={false} />
|
||||||
|
<div className="time">День {school.calendar.day} : { school.calendar.dayFraction }</div>
|
||||||
|
</div>
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Student } from "../model/student";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
unit: Student;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StudentUnitView = observer(function ({ unit }: Props) {
|
||||||
|
|
||||||
|
return <div className="unit button student">
|
||||||
|
<div className="name">{unit.name ?? 'Ученик'}</div>
|
||||||
|
<div className="cooldown" style={{width: `${Math.round(unit.progress.normalValue*100)}%`}}></div>
|
||||||
|
</div>
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { school } from "../model/school";
|
||||||
|
import { Student } from "../model/student";
|
||||||
|
import { Owner } from "../model/owner";
|
||||||
|
import { OwnerUnitView } from "./OwnerUnitView";
|
||||||
|
import { IUnit } from "../model/unit";
|
||||||
|
import { StudentUnitView } from "./StudentUnitView";
|
||||||
|
|
||||||
|
export const UnitListView = observer(function () {
|
||||||
|
|
||||||
|
function renderUnit(unit: IUnit) {
|
||||||
|
if (unit instanceof Owner)
|
||||||
|
return <OwnerUnitView unit={unit} />
|
||||||
|
else if (unit instanceof Student)
|
||||||
|
return <StudentUnitView unit={unit} />
|
||||||
|
else
|
||||||
|
return '???';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="units">
|
||||||
|
<div className="owner">
|
||||||
|
<OwnerUnitView unit={school.owner} />
|
||||||
|
</div>
|
||||||
|
<div className="students">
|
||||||
|
{school.students.map((unit) => <StudentUnitView key={unit.id} unit={unit} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { school } from "../model/school";
|
||||||
|
import { UpgradeView } from "./UpgradeView";
|
||||||
|
|
||||||
|
export const UpgradeListView = observer(function () {
|
||||||
|
|
||||||
|
return <div className="upgrades">{
|
||||||
|
school.upgrades.visibleItems.map((upgrade) => <UpgradeView key={upgrade.id} upgrade={upgrade} />)
|
||||||
|
}</div>;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { ResourceStringView } from "./ResourceStringView";
|
||||||
|
import { Upgrade } from "../model/upgrade";
|
||||||
|
import { isResourceSetZero } from "../utils/resources";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
upgrade: Upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpgradeView = observer(function ({ upgrade }: Props) {
|
||||||
|
|
||||||
|
function handleClick(): void {
|
||||||
|
if (upgrade.isOpen() && upgrade.isAffordable())
|
||||||
|
upgrade.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
let classNames = ['upgrade'];
|
||||||
|
if (!upgrade.isOpen())
|
||||||
|
classNames.push('closed')
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={classNames.join(' ')}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className="name">{upgrade.name}</div>
|
||||||
|
{upgrade.level > 0 ? <div className="level">{upgrade.level} ур</div> : <></>}
|
||||||
|
<div className="description">{upgrade.description}</div>
|
||||||
|
{
|
||||||
|
!upgrade.isOpen()
|
||||||
|
? (
|
||||||
|
!isResourceSetZero(upgrade.costToOpen)
|
||||||
|
? <ResourceStringView className="cost" prefix="Открывается на" resources={upgrade.costToOpen} partial={true} />
|
||||||
|
: <></>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
!isResourceSetZero(upgrade.costToBuy)
|
||||||
|
? <ResourceStringView className="cost" prefix="Покупка" resources={upgrade.costToBuy} partial={true} />
|
||||||
|
: <></>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{/* <div>Открывается на: <ResourceStringView className="cost" resources={upgrade.costToOpen} partial={true} /></div>
|
||||||
|
<div>Покупка: <ResourceStringView className="cost" resources={upgrade.costToBuy} partial={true} /></div> */}
|
||||||
|
{/* {
|
||||||
|
[...Array(10).keys()].map((lvl: number) => <div>
|
||||||
|
{lvl}
|
||||||
|
<ResourceStringView className="cost" resources={upgrade.evalCostToUpgrade(lvl)} partial={true} />
|
||||||
|
<ResourceStringView className="cost" resources={upgrade.evalCostToStepUpgrade(lvl)} partial={true} />
|
||||||
|
</div>)
|
||||||
|
} */}
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue