diff --git a/cmd/start b/cmd/start index b307561..1929da7 100755 --- a/cmd/start +++ b/cmd/start @@ -11,4 +11,4 @@ else fi # Lint and build project -bun x eslint src && bun build "${args[@]}" --outfile=dist/main.js src/index.ts +bun x tsc --noEmit && bun x eslint src && bun build "${args[@]}" --outfile=dist/main.js src/index.ts diff --git a/tsconfig.json b/tsconfig.json index 876b35d..ab75db4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,5 +5,6 @@ "DOM", "ESNext", ], + "module": "esnext", }, } \ No newline at end of file diff --git a/www/games/geomarin/package.json b/www/games/geomarin/package.json index 3250766..6176a95 100644 --- a/www/games/geomarin/package.json +++ b/www/games/geomarin/package.json @@ -4,7 +4,7 @@ "start": "../../../cmd/start" }, "dependencies": { - "graphico": "1.3.0", - "smath": "2.2.0" + "graphico": "1.6.0", + "smath": "3.0.0" } } \ No newline at end of file diff --git a/www/games/geomarin/src/globals.ts b/www/games/geomarin/src/globals.ts index 3ac135e..0e66978 100644 --- a/www/games/geomarin/src/globals.ts +++ b/www/games/geomarin/src/globals.ts @@ -5,7 +5,7 @@ export const global = { /** * Application version number */ - version: '1.0', + version: '1.1', /** * Colors for SVG paths (regions) */ diff --git a/www/games/geomarin/src/lib/bar.ts b/www/games/geomarin/src/lib/bar.ts index 0e08efe..7bbbe75 100644 --- a/www/games/geomarin/src/lib/bar.ts +++ b/www/games/geomarin/src/lib/bar.ts @@ -15,7 +15,7 @@ export class Bar implements Drawable { const barWidth: number = this.value / this.maxValue * maxWidth; // Draw the bar background ctx.fillStyle = this.color; - ctx.fillRect(this.thickness * 1.5, yOffset - this.thickness / 2, barWidth + this.thickness * 0.5, this.thickness); + ctx.fillRect((this.thickness * 1.5) | 0, (yOffset - this.thickness / 2) | 0, (barWidth + this.thickness * 0.5) | 0, this.thickness | 0); // Draw the bar name and value ctx.font = `${this.thickness}px sans-serif`; ctx.fillStyle = 'white'; diff --git a/www/games/geomarin/tsconfig.json b/www/games/geomarin/tsconfig.json deleted file mode 100644 index 9ec768c..0000000 --- a/www/games/geomarin/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "module": "esnext", - }, -} \ No newline at end of file diff --git a/www/games/orbit-idle/src/gamedata.ts b/www/games/orbit-idle/src/data/gamedata.ts similarity index 55% rename from www/games/orbit-idle/src/gamedata.ts rename to www/games/orbit-idle/src/data/gamedata.ts index ff5e5a9..c2a1606 100644 --- a/www/games/orbit-idle/src/gamedata.ts +++ b/www/games/orbit-idle/src/data/gamedata.ts @@ -1,5 +1,6 @@ -import { PlayerData } from "./player"; -import { SolarData } from "./solar-system"; +import { PlayerData } from '../game/player'; +import { SolarData } from '../game/solar-system'; +import { StatsData } from './stats'; /** * Represents and interface for saving and loading game data @@ -8,4 +9,5 @@ export interface GameData { readonly timestamp: number; readonly playerData: PlayerData; readonly solarData: SolarData; + readonly statsData: StatsData; }; diff --git a/www/games/orbit-idle/src/lib.ts b/www/games/orbit-idle/src/data/lib.ts similarity index 66% rename from www/games/orbit-idle/src/lib.ts rename to www/games/orbit-idle/src/data/lib.ts index d50b0cb..964fb69 100644 --- a/www/games/orbit-idle/src/lib.ts +++ b/www/games/orbit-idle/src/data/lib.ts @@ -31,3 +31,25 @@ export function N(num: number): string { return num.toFixed(2); } }; + +/** + * Represents a data type which can be saved and loaded from memory + */ +export abstract class SaveLoad { + /** + * Create a new instance of this class + */ + constructor(protected readonly data: T) { + this.data = JSON.parse(JSON.stringify(data)); + } + /** + * Make a copy of this data object for saving + */ + public save(): T { + return JSON.parse(JSON.stringify(this.data)); + } + /** + * Load the saved data into this object + */ + public abstract load(data?: Partial): void; +} diff --git a/www/games/orbit-idle/src/data/state.ts b/www/games/orbit-idle/src/data/state.ts new file mode 100644 index 0000000..be68c6b --- /dev/null +++ b/www/games/orbit-idle/src/data/state.ts @@ -0,0 +1,33 @@ +import { FullAscendButton } from '../form-controls/button-full'; +import { BuyType } from '../ui/buy-type'; +import { Cursor } from '../ui/cursor'; +import { Income } from '../game/income'; +import { Menu } from '../ui/menu'; +import { Pause } from '../ui/pause'; +import { Player } from '../game/player'; +import { Shop } from '../game/shop'; +import { SolarSystem } from '../game/solar-system'; +import { Statistics } from './stats'; +import { Tutorial } from '../form-controls/tutorial'; +import { Zoom } from '../ui/zoom'; + +// Global constants +export const version = 'v1.3.0-alpha'; + +// UI elements +export const cursor: Cursor = new Cursor(); +export const player: Player = new Player(); +export const zoom: Zoom = new Zoom(725, 150); +export const buyType: BuyType = new BuyType(725, 225); +export const ascBtn: FullAscendButton = new FullAscendButton(675, 275, 100, 50); +export const stats: Statistics = new Statistics(); + +// Game elements +export const system: SolarSystem = new SolarSystem(); +export const income: Income = new Income(); +export const shop: Shop = new Shop(10, 120); + +// Additional popups +export const tutorial: Tutorial = new Tutorial(); +export const menu: Menu = new Menu(725, 365, 100); +export const paused: Pause = new Pause('Orbit Idle has lost focus!\nThe game is NOT paused, but has\nstopped rendering to save resources.\nThe game is still running in the background.'); diff --git a/www/games/orbit-idle/src/data/stats.ts b/www/games/orbit-idle/src/data/stats.ts new file mode 100644 index 0000000..2d83f55 --- /dev/null +++ b/www/games/orbit-idle/src/data/stats.ts @@ -0,0 +1,87 @@ +import { Drawable } from 'graphico'; +import { Label } from '../form-controls/label'; +import { Color } from 'viridis'; +import { player } from './state'; +import { N, SaveLoad } from './lib'; +import { SMath } from 'smath'; + +/** + * Represents an in-game statistics panel + */ +export class Statistics extends SaveLoad implements Drawable { + private static readonly shade: Color = new Color(25, 25, 25, 75); + private visible: boolean; + private readonly title: Label; + private readonly headers: Label; + private readonly content: Label; + /** + * Create a new statistics window + */ + constructor(public readonly data: StatsData = defaultStats) { + super(data); + this.visible = false; + this.title = new Label('Orbit Idle: Statistics', new Color(255, 255, 255), 2, true, 'center', 'bottom', 0, Label.fontSize * 16); + this.headers = new Label('Started on\nSolar system size\nTotal earned\nTotal spent\nTotal planetary years\nSpeed levels purchased\nAscensions purchased\n\nCredits\n\n\nDisclaimer', new Color(255, 255, 255), 1, false, 'right', 'top', 0, Label.fontSize * 18); + this.content = new Label('', new Color(255, 255, 255), 1, true, 'left', 'top', 0, Label.fontSize * 18); + } + public load(data: Partial = defaultStats): void { + this.data.startTime = Math.floor(SMath.clamp(data.startTime ?? defaultStats.startTime, 0, Infinity)); + this.data.moneyEarned = SMath.clamp(data.moneyEarned ?? defaultStats.moneyEarned, 0, Infinity); + this.data.moneySpent = SMath.clamp(data.moneySpent ?? defaultStats.moneySpent, 0, Infinity); + this.data.orbitsCompleted = Math.floor(SMath.clamp(data.orbitsCompleted ?? defaultStats.orbitsCompleted, 0, Infinity)); + this.data.speedLevelsPurchased = Math.floor(SMath.clamp(data.speedLevelsPurchased ?? defaultStats.speedLevelsPurchased, 0, Infinity)); + this.data.ascensionsPurchased = Math.floor(SMath.clamp(data.ascensionsPurchased ?? defaultStats.ascensionsPurchased, 0, Infinity)); + } + /** + * Show the statistics panel + */ + public show(): void { + this.visible = true; + } + /** + * Hide the statistics panel + */ + public hide(): void { + this.visible = false; + } + public draw(graphics: CanvasRenderingContext2D): void { + // Skip if not visible + if (!this.visible) { + return; + } + // Draw background shading + graphics.fillStyle = Statistics.shade.toString(); + graphics.fillRect(Label.fontSize * 15, Label.fontSize * 10, graphics.canvas.width - Label.fontSize * 30, graphics.canvas.height - Label.fontSize * 20); + // Set content + this.content.value = `${new Date(this.data.startTime).toLocaleString()}\n${N(player.getNumOrbits())} planets\n$${N(this.data.moneyEarned)}\n$${N(this.data.moneySpent)}\n${N(this.data.orbitsCompleted)} orbits\n${N(this.data.speedLevelsPurchased)}\n${N(this.data.ascensionsPurchased)}\n\nNicolas Ventura\nnicfv.com\n\nGenerative AI was used for\n30-second looping audio but\nnot for game design or code`; + // X-center all labels + this.title.x = graphics.canvas.width / 2; + this.headers.x = (graphics.canvas.width - Label.fontSize) / 2; + this.content.x = (graphics.canvas.width + Label.fontSize) / 2; + // Draw all labels and close button + this.title.draw(graphics); + this.headers.draw(graphics); + this.content.draw(graphics); + } +} + +/** + * Interface containing various in-game statistics + */ +export interface StatsData { + startTime: number; + moneyEarned: number; + moneySpent: number; + orbitsCompleted: number; + speedLevelsPurchased: number; + ascensionsPurchased: number; +} + +const defaultStats: StatsData = { + startTime: Date.now(), + moneyEarned: 0, + moneySpent: 0, + orbitsCompleted: 0, + speedLevelsPurchased: 0, + ascensionsPurchased: 0, +}; diff --git a/www/games/orbit-idle/src/button-asc.ts b/www/games/orbit-idle/src/form-controls/button-asc.ts similarity index 86% rename from www/games/orbit-idle/src/button-asc.ts rename to www/games/orbit-idle/src/form-controls/button-asc.ts index a436b63..6d218cc 100644 --- a/www/games/orbit-idle/src/button-asc.ts +++ b/www/games/orbit-idle/src/form-controls/button-asc.ts @@ -1,8 +1,8 @@ import { Button } from './button'; import { Label } from './label'; -import { N } from './lib'; -import { player, zoom } from './state'; -import { Orbit } from './orbit'; +import { N } from '../data/lib'; +import { player, zoom } from '../data/state'; +import { Orbit } from '../game/orbit'; /** * Represents a button to ascend a specific orbit @@ -20,7 +20,7 @@ export class AscendButton extends Button { * Initialize a new ascension button */ constructor(x: number, width: number, private readonly orbit: Orbit) { - super('', orbit.color, false, x, 0, width, Label.fontSize * 3, () => { + super('', orbit.color, false, x, 0, width, Label.fontSize * 3, null, () => { this.orbit.ascend(this.ascensions); player.spend(this.ascAmount); player.earn(this.orbit.getNextNCost(1)); @@ -39,7 +39,7 @@ export class AscendButton extends Button { } // Calculate and display cost this.ascAmount = this.orbit.getNextNAscensionCost(this.ascensions); - super.text = `Ascend +${this.ascensions}\n$${N(this.ascAmount)}`; + super.text = `^${N(this.orbit.getExp())} > ^${N(this.orbit.getNextNAscensionExp(this.ascensions))}\n$${N(this.ascAmount)}`; // Check if player has sufficient funds to ascend if (player.hasFunds(this.ascAmount)) { super.enable(); diff --git a/www/games/orbit-idle/src/button-buy.ts b/www/games/orbit-idle/src/form-controls/button-buy.ts similarity index 91% rename from www/games/orbit-idle/src/button-buy.ts rename to www/games/orbit-idle/src/form-controls/button-buy.ts index c6224f3..1d8a88a 100644 --- a/www/games/orbit-idle/src/button-buy.ts +++ b/www/games/orbit-idle/src/form-controls/button-buy.ts @@ -1,8 +1,8 @@ import { Button } from './button'; import { Label } from './label'; -import { N } from './lib'; -import { buyType, player, zoom } from './state'; -import { Orbit } from './orbit'; +import { N } from '../data/lib'; +import { buyType, player, zoom } from '../data/state'; +import { Orbit } from '../game/orbit'; /** * Represents a button to increase a orbit's speed. @@ -16,7 +16,7 @@ export class BuyButton extends Button { * Initialize a new buy button. */ constructor(x: number, width: number, private readonly orbit: Orbit) { - super('', orbit.color, true, x, 0, width, Label.fontSize * 3, () => { + super('', orbit.color, true, x, 0, width, Label.fontSize * 3, null, () => { player.spend(this.cost); orbit.increaseSpeed(buyType.getQuantity()); }); diff --git a/www/games/orbit-idle/src/button-full.ts b/www/games/orbit-idle/src/form-controls/button-full.ts similarity index 83% rename from www/games/orbit-idle/src/button-full.ts rename to www/games/orbit-idle/src/form-controls/button-full.ts index 82c3b66..26addd0 100644 --- a/www/games/orbit-idle/src/button-full.ts +++ b/www/games/orbit-idle/src/form-controls/button-full.ts @@ -1,7 +1,7 @@ import { Color } from 'viridis'; import { Button } from './button'; -import { income, player, shop, system, zoom } from './state'; -import { N } from './lib'; +import { income, player, shop, system, zoom } from '../data/state'; +import { N } from '../data/lib'; /** * Represents a button to perform a full ascension @@ -11,7 +11,7 @@ export class FullAscendButton extends Button { * Create a new full ascend button */ constructor(x: number, y: number, w: number, h: number) { - super('', new Color(200, 200, 200), false, x, y, w, h, () => this.ascend()); + super('', new Color(200, 200, 200), false, x, y, w, h, null, () => this.ascend()); } /** * Ascend and regenerate the game elements diff --git a/www/games/orbit-idle/src/button-toggle.ts b/www/games/orbit-idle/src/form-controls/button-toggle.ts similarity index 92% rename from www/games/orbit-idle/src/button-toggle.ts rename to www/games/orbit-idle/src/form-controls/button-toggle.ts index 7841a85..afabfe7 100644 --- a/www/games/orbit-idle/src/button-toggle.ts +++ b/www/games/orbit-idle/src/form-controls/button-toggle.ts @@ -12,8 +12,8 @@ export class ToggleButton extends Button { /** * Initialize a new toggle button, where the first state is the default state. */ - constructor(private readonly states: [string, Color, () => void][], x: number, y: number, width: number, height: number) { - super(states[0][0], states[0][1], true, x, y, width, height, () => { + constructor(private readonly states: [string, Color, () => void][], x: number, y: number, width: number, height: number, hotkeys: string[] | null) { + super(states[0][0], states[0][1], true, x, y, width, height, hotkeys, () => { // Execute the current state's callback function states[0][2](); // Change the current state of the toggle button diff --git a/www/games/orbit-idle/src/button.ts b/www/games/orbit-idle/src/form-controls/button.ts similarity index 88% rename from www/games/orbit-idle/src/button.ts rename to www/games/orbit-idle/src/form-controls/button.ts index 8b8ea27..2150e78 100644 --- a/www/games/orbit-idle/src/button.ts +++ b/www/games/orbit-idle/src/form-controls/button.ts @@ -21,7 +21,7 @@ export class Button implements Drawable { /** * Initialize a new button. */ - constructor(protected text: string, protected color: Color, protected enabled: boolean, protected x: number, protected y: number, protected w: number, protected h: number, protected callback: () => void) { + constructor(protected text: string, protected color: Color, protected enabled: boolean, protected x: number, protected y: number, protected w: number, protected h: number, protected hotkeys: string[] | null, protected callback: () => void) { this.isHover = false; this.label = new Label(text, color.getContrastingColor(), 1, false, 'center', 'top', this.x + this.w / 2, this.y + Label.fontSize / 2); this.disabledColor = new Color(color.red, color.green, color.blue, color.alpha * 0.5); @@ -61,6 +61,14 @@ export class Button implements Drawable { this.callback(); } } + /** + * Attempt to use the hotkey for this button. + */ + public hotkey(key: string): void { + if (this.hotkeys?.includes(key) && this.enabled) { + this.callback(); + } + } public draw(graphics: CanvasRenderingContext2D): void { // Render the button and outline, if needed graphics.fillStyle = this.enabled ? this.color.toString() : this.disabledColor.toString(); diff --git a/www/games/orbit-idle/src/help.ts b/www/games/orbit-idle/src/form-controls/help.ts similarity index 96% rename from www/games/orbit-idle/src/help.ts rename to www/games/orbit-idle/src/form-controls/help.ts index 1159048..1d8a429 100644 --- a/www/games/orbit-idle/src/help.ts +++ b/www/games/orbit-idle/src/form-controls/help.ts @@ -38,7 +38,7 @@ export class Help extends Button { * Create a new help message with optional highlighted region. */ constructor(text: string, textOffsetX: number, textOffsetY: number, protected readonly x: number, protected readonly y: number, protected readonly w: number, protected readonly h: number, callback: () => void) { - super('', Help.transparent, true, x, y, w, h, callback); + super('', Help.transparent, true, x, y, w, h, null, callback); this.tip = new Label(text, Help.highlightStroke, 1, false, textOffsetX < 0 ? 'right' : 'left', 'top', x + textOffsetX, y + textOffsetY); } /** diff --git a/www/games/orbit-idle/src/label.ts b/www/games/orbit-idle/src/form-controls/label.ts similarity index 100% rename from www/games/orbit-idle/src/label.ts rename to www/games/orbit-idle/src/form-controls/label.ts diff --git a/www/games/orbit-idle/src/tutorial.ts b/www/games/orbit-idle/src/form-controls/tutorial.ts similarity index 92% rename from www/games/orbit-idle/src/tutorial.ts rename to www/games/orbit-idle/src/form-controls/tutorial.ts index dcab659..73177fc 100644 --- a/www/games/orbit-idle/src/tutorial.ts +++ b/www/games/orbit-idle/src/form-controls/tutorial.ts @@ -42,10 +42,10 @@ export class Tutorial implements Drawable { new Help('When an orbit reaches its max speed, you\ncan Ascend it. Ascensions are expensive\nand reset speed for that orbit, but\nincrease your long-term income.', 140, 0, 135, 100, 130, 400, () => this.next()), new Help('The breakdown of your income is shown here.\nEach planet gains +0.01 bonus each planetary year\nby completing a full revolution around the sun.\nBonuses from individual orbits are multiplied\ntogether and raised to their ascension level.', 0, 50, 5, 5, 790, 45, () => this.next()), new Help('This is your total income and\ntotal revolutions per second.\nEach time any planet completes\none full revolution, you earn\nthe income under $/rev.', -10, 0, 600, 50, 200, 80, () => this.next()), - new Help('Use the Zoom controls on the top right to focus\non different planets. Zooming changes the store\nbuttons and income labels to match the focused orbit.', -10, 0, 675, 150, 100, 60, () => this.next()), - new Help('For convenience, you can buy\nspeed upgrades in bulk. It\ndoes not affect ascending.', -10, 0, 660, 220, 130, 60, () => this.next()), - new Help('When you have make the required income\nper second, you will be given the option\nto perform a full ascension. This resets\nthe entire solar system, but adds\nan additional planet.', -10, 0, 660, 290, 130, 70, () => this.next()), - new Help('Open Menu > Tutorial anytime to review these slides.\nYour progress saves automatically, and the game\ncontinues earning even when the tab is not active.\nClick "End Tutorial" to close this message.\nGood luck!', -10, 75, 660, 375, 130, 175, () => this.next()), + new Help('Use the Zoom controls on the top right to focus\non different planets. Zooming changes the store\nbuttons and income labels to match the focused\norbit. Hotkeys: + and -', -10, 0, 675, 125, 100, 60, () => this.next()), + new Help('For convenience, you can buy\nspeed upgrades in bulk. It\ndoes not affect ascending.', -10, 0, 660, 195, 130, 60, () => this.next()), + new Help('When you have make the required income\nper second, you will be given the option\nto perform a full ascension. This resets\nthe entire solar system, but adds\nan additional planet.', -10, 0, 660, 265, 130, 70, () => this.next()), + new Help('Open Menu > Tutorial anytime to review these slides.\nYour progress saves automatically, and the game\ncontinues earning even when the tab is not active.\nClick "End Tutorial" to close this message.\nGood luck!\n\nHotkeys: (S)ave, (H)elp, (M)ute, St(A)ts, (ESC) open/close menu', -10, 75, 660, 335, 130, 215, () => this.next()), ]; } /** diff --git a/www/games/orbit-idle/src/income.ts b/www/games/orbit-idle/src/game/income.ts similarity index 96% rename from www/games/orbit-idle/src/income.ts rename to www/games/orbit-idle/src/game/income.ts index 8ebe1ab..1fc26fb 100644 --- a/www/games/orbit-idle/src/income.ts +++ b/www/games/orbit-idle/src/game/income.ts @@ -1,10 +1,10 @@ import { SMath } from 'smath'; import { Drawable } from 'graphico'; import { OrbitScore } from './orbit-score'; -import { N } from './lib'; -import { Label } from './label'; +import { N } from '../data/lib'; +import { Label } from '../form-controls/label'; import { Color } from 'viridis'; -import { player, system, zoom } from './state'; +import { player, system, zoom } from '../data/state'; /** * Handles and renders game income. diff --git a/www/games/orbit-idle/src/orbit-score.ts b/www/games/orbit-idle/src/game/orbit-score.ts similarity index 96% rename from www/games/orbit-idle/src/orbit-score.ts rename to www/games/orbit-idle/src/game/orbit-score.ts index 74436d7..f60bce6 100644 --- a/www/games/orbit-idle/src/orbit-score.ts +++ b/www/games/orbit-idle/src/game/orbit-score.ts @@ -1,8 +1,8 @@ import { Drawable } from 'graphico'; import { Color } from 'viridis'; import { Orbit } from './orbit'; -import { N } from './lib'; -import { Label } from './label'; +import { N } from '../data/lib'; +import { Label } from '../form-controls/label'; /** * Renders the orbit score to the user interface. diff --git a/www/games/orbit-idle/src/orbit.ts b/www/games/orbit-idle/src/game/orbit.ts similarity index 80% rename from www/games/orbit-idle/src/orbit.ts rename to www/games/orbit-idle/src/game/orbit.ts index 05d6879..d01788d 100644 --- a/www/games/orbit-idle/src/orbit.ts +++ b/www/games/orbit-idle/src/game/orbit.ts @@ -1,12 +1,13 @@ import { Drawable } from 'graphico'; import { SMath, Vec3 } from 'smath'; import { Color } from 'viridis'; -import { player, zoom } from './state'; +import { player, stats, zoom } from '../data/state'; +import { SaveLoad } from '../data/lib'; /** * Represents a single planetary orbit */ -export class Orbit implements Drawable { +export class Orbit extends SaveLoad implements Drawable { /** * 2pi */ @@ -46,7 +47,8 @@ export class Orbit implements Drawable { /** * Create a new planet. */ - constructor(public readonly index: number, private readonly data: OrbitData = JSON.parse(JSON.stringify(defaultData))) { + constructor(public readonly index: number, data?: OrbitData) { + super(data ?? defaultOrbitData); this.color = Color.hsl(SMath.translate(index, 0, player.getNumOrbits(), 0, 360), 100, 55); this.radius = 100 * (1.3 ** index); this.thickness = this.radius * 0.2; @@ -56,20 +58,11 @@ export class Orbit implements Drawable { this.costIncrease = 1.02 + 0.02 * index; this.ascCostIncrease = 2.25 + 0.05 * index; } - /** - * Copy orbit data for saving this orbit. - */ - public save(): OrbitData { - return JSON.parse(JSON.stringify(this.data)); - } - /** - * Load orbit data into this orbit. - */ - public load(data: OrbitData): void { - this.data.angle = SMath.clamp(data.angle, 0, Orbit.TAU); - this.data.ascensions = SMath.clamp(data.ascensions, 0, Infinity) | 0; - this.data.rotations = SMath.clamp(data.rotations, 0, Infinity) | 0; - this.data.speedLevel = SMath.clamp(data.speedLevel, 0, Infinity) | 0; + public load(data: Partial = defaultOrbitData): void { + this.data.angle = SMath.clamp(data.angle ?? defaultOrbitData.angle, 0, Orbit.TAU); + this.data.ascensions = Math.floor(SMath.clamp(data.ascensions ?? defaultOrbitData.ascensions, 0, Infinity)); + this.data.rotations = Math.floor(SMath.clamp(data.rotations ?? defaultOrbitData.rotations, 0, Infinity)); + this.data.speedLevel = Math.floor(SMath.clamp(data.speedLevel ?? defaultOrbitData.speedLevel, 0, Infinity)); } /** * Determine if the orbit has been activated. @@ -117,11 +110,19 @@ export class Orbit implements Drawable { public getNextNSpeed(n: number): number { return SMath.translate(SMath.clamp(this.data.speedLevel + n, 0, this.maxSpeedLevel), 0, this.maxSpeedLevel, 0, this.maxSpeed); } + /** + * Get the next ascension exponent after `N` ascensions. + */ + public getNextNAscensionExp(n: number): number { + return (this.data.ascensions + n) / 100 + 1; + } /** * Increase the speed by `N` levels. */ public increaseSpeed(n: number): void { - this.data.speedLevel = SMath.clamp(this.data.speedLevel + n, 0, this.maxSpeedLevel) | 0; + const upgradeAmount: number = SMath.clamp(n, 0, this.maxSpeedLevel - this.data.speedLevel) | 0; + this.data.speedLevel += upgradeAmount; + stats.data.speedLevelsPurchased += upgradeAmount; } /** * Calculates the current speed in rotations per second. (Hz) @@ -137,6 +138,7 @@ export class Orbit implements Drawable { this.data.ascensions += n | 0; this.data.angle = 0; this.data.speedLevel = 0; + stats.data.ascensionsPurchased += n | 0; } } /** @@ -156,9 +158,10 @@ export class Orbit implements Drawable { */ public rotate(dt: number): number { this.data.angle += this.currentSpeedHz() * dt / 1e3 * Orbit.TAU; - const rotations: number = (this.data.angle / Orbit.TAU) | 0; + const rotations: number = Math.floor(this.data.angle / Orbit.TAU); this.data.rotations += rotations; this.data.angle %= Orbit.TAU; + stats.data.orbitsCompleted += rotations; return rotations; } public draw(graphics: CanvasRenderingContext2D): void { @@ -201,9 +204,9 @@ export interface OrbitData { speedLevel: number; } -const defaultData: OrbitData = { +const defaultOrbitData: OrbitData = { angle: 0, - rotations: 0, ascensions: 0, + rotations: 0, speedLevel: 0, }; diff --git a/www/games/orbit-idle/src/player.ts b/www/games/orbit-idle/src/game/player.ts similarity index 75% rename from www/games/orbit-idle/src/player.ts rename to www/games/orbit-idle/src/game/player.ts index 2a9b20a..3cd2b4e 100644 --- a/www/games/orbit-idle/src/player.ts +++ b/www/games/orbit-idle/src/game/player.ts @@ -1,13 +1,14 @@ import { Drawable } from 'graphico'; -import { N } from './lib'; -import { Label } from './label'; +import { N, SaveLoad } from '../data/lib'; +import { Label } from '../form-controls/label'; import { Color } from 'viridis'; import { SMath } from 'smath'; +import { stats } from '../data/state'; /** * Represents the main player of the game */ -export class Player implements Drawable { +export class Player extends SaveLoad implements Drawable { /** * The label showing the amount of money in the player's bank */ @@ -15,7 +16,8 @@ export class Player implements Drawable { /** * Create a new instance of the game player */ - constructor(private readonly data: PlayerData = defaultPlayerData) { + constructor() { + super(defaultPlayerData); this.moneyLabel = new Label('', new Color(255, 255, 255), 3, true, 'center', 'bottom', 0, 0); } /** @@ -23,12 +25,14 @@ export class Player implements Drawable { */ public earn(amount: number): void { this.data.money += amount; + stats.data.moneyEarned += amount; } /** * Spend a certain amount of money from the player's bank */ public spend(amount: number): void { this.data.money -= amount; + stats.data.moneySpent += amount; } /** * Determine if the player has sufficient funds to purchase something of a certain amount @@ -40,7 +44,7 @@ export class Player implements Drawable { * Calculate the income required to ascend (gain another planet/orbit) */ public incomeRequired(): number { - return 10 ** ((1.25 * (this.data.orbits ** 1.75) + 1) | 0); + return 10 ** Math.floor(1.25 * (this.data.orbits ** 1.75) + 1); } /** * Get the total number of orbits @@ -55,18 +59,9 @@ export class Player implements Drawable { this.data.money = 0; this.data.orbits++; } - /** - * Copy player data for saving - */ - public save(): PlayerData { - return JSON.parse(JSON.stringify(this.data)); - } - /** - * Load data for the player - */ - public load(data: PlayerData = defaultPlayerData): void { - this.data.money = SMath.clamp(data.money, 0, Infinity); - this.data.orbits = SMath.clamp(data.orbits, 2, Infinity) | 0; + public load(data: Partial = defaultPlayerData): void { + this.data.money = SMath.clamp(data.money ?? defaultPlayerData.money, 0, Infinity); + this.data.orbits = Math.floor(SMath.clamp(data.orbits ?? defaultPlayerData.orbits, 2, Infinity)); } public draw(graphics: CanvasRenderingContext2D): void { // Render black background diff --git a/www/games/orbit-idle/src/shop.ts b/www/games/orbit-idle/src/game/shop.ts similarity index 90% rename from www/games/orbit-idle/src/shop.ts rename to www/games/orbit-idle/src/game/shop.ts index 829fb60..8f997e2 100644 --- a/www/games/orbit-idle/src/shop.ts +++ b/www/games/orbit-idle/src/game/shop.ts @@ -1,8 +1,8 @@ import { Drawable } from 'graphico'; -import { Label } from './label'; -import { BuyButton } from './button-buy'; -import { AscendButton } from './button-asc'; -import { system } from './state'; +import { Label } from '../form-controls/label'; +import { BuyButton } from '../form-controls/button-buy'; +import { AscendButton } from '../form-controls/button-asc'; +import { system } from '../data/state'; /** * Represents a collection of buy buttons and ascension buttons (if applicable.) diff --git a/www/games/orbit-idle/src/solar-system.ts b/www/games/orbit-idle/src/game/solar-system.ts similarity index 91% rename from www/games/orbit-idle/src/solar-system.ts rename to www/games/orbit-idle/src/game/solar-system.ts index 336263e..48d3a04 100644 --- a/www/games/orbit-idle/src/solar-system.ts +++ b/www/games/orbit-idle/src/game/solar-system.ts @@ -1,13 +1,14 @@ import { Drawable } from 'graphico'; import { Orbit, OrbitData } from './orbit'; -import { income, player, zoom } from './state'; +import { income, player, zoom } from '../data/state'; import { Color, Gradient } from 'viridis'; import { Vec3 } from 'smath'; +import { SaveLoad } from '../data/lib'; /** * Represents the solar system in the game */ -export class SolarSystem implements Drawable { +export class SolarSystem extends SaveLoad implements Drawable { /** * The gradient for the main central star */ @@ -23,12 +24,13 @@ export class SolarSystem implements Drawable { * Create a new solar system, passing in optional orbit data */ constructor() { + super([]); this.orbits = []; } /** * Generate data for all orbits in the solar system for saving data */ - public save(): SolarData { + public override save(): SolarData { return this.orbits.map(o => o.save()); } /** diff --git a/www/games/orbit-idle/src/index.ts b/www/games/orbit-idle/src/index.ts index 58e4e1b..f841c98 100644 --- a/www/games/orbit-idle/src/index.ts +++ b/www/games/orbit-idle/src/index.ts @@ -1,7 +1,7 @@ import './close-info-handler'; import { Canvas } from 'graphico'; -import { ascBtn, buyType, cursor, income, menu, paused, player, shop, system, tutorial, zoom } from './state'; -import { GameData } from './gamedata'; +import { ascBtn, buyType, cursor, income, menu, paused, player, shop, stats, system, tutorial, zoom } from './data/state'; +import { GameData } from './data/gamedata'; /** * Whether the canvas started playing audio yet @@ -32,6 +32,7 @@ const canv: Canvas = new Canvas({ canv.draw(buyType, 1); canv.draw(ascBtn, 1); canv.draw(menu, 1); + canv.draw(stats, 1); tutorial.tick(dt); canv.draw(tutorial, 2); cursor.tick(dt); @@ -58,6 +59,10 @@ const canv: Canvas = new Canvas({ canv.playAudio('./Between_The_Sleepless_Stars.mp3', true, 0.25); } }, + keydown(key) { + menu.checkHotkeys(key); + zoom.checkHotkeys(key); + }, focus(dt) { system.step(dt); }, @@ -71,6 +76,7 @@ menu.setCallbacks(() => { timestamp: Date.now(), playerData: player.save(), solarData: system.save(), + statsData: stats.save(), }); }, () => canv.mute(), () => canv.unmute(), () => canv.clearData()); @@ -79,6 +85,7 @@ if (data) { // Load and parse saved data player.load(data.playerData); system.load(data.solarData); + stats.load(data.statsData); const dt: number = Date.now() - (data.timestamp ?? Date.now()); system.step(dt); // Reset zoom and regenerate UI diff --git a/www/games/orbit-idle/src/state.ts b/www/games/orbit-idle/src/state.ts deleted file mode 100644 index 41ea334..0000000 --- a/www/games/orbit-idle/src/state.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { FullAscendButton } from './button-full'; -import { BuyType } from './buy-type'; -import { Cursor } from './cursor'; -import { Income } from './income'; -import { Menu } from './menu'; -import { Pause } from './pause'; -import { Player } from './player'; -import { Shop } from './shop'; -import { SolarSystem } from './solar-system'; -import { Tutorial } from './tutorial'; -import { Zoom } from './zoom'; - -// Global constants -export const version = 'v1.2.1-alpha'; - -// UI elements -export const cursor: Cursor = new Cursor(); -export const player: Player = new Player(); -export const zoom: Zoom = new Zoom(725, 175); -export const buyType: BuyType = new BuyType(725, 250); -export const ascBtn: FullAscendButton = new FullAscendButton(675, 300, 100, 50); - -// Game elements -export const system: SolarSystem = new SolarSystem(); -export const income: Income = new Income(); -export const shop: Shop = new Shop(10, 120); - -// Additional popups -export const tutorial: Tutorial = new Tutorial(); -export const menu: Menu = new Menu(725, 400, 100); -export const paused: Pause = new Pause('Orbit Idle has lost focus!\nThe game is NOT paused, but has\nstopped rendering to save resources.\nThe game is still running in the background.'); diff --git a/www/games/orbit-idle/src/buy-type.ts b/www/games/orbit-idle/src/ui/buy-type.ts similarity index 88% rename from www/games/orbit-idle/src/buy-type.ts rename to www/games/orbit-idle/src/ui/buy-type.ts index e16ca5b..6ed8675 100644 --- a/www/games/orbit-idle/src/buy-type.ts +++ b/www/games/orbit-idle/src/ui/buy-type.ts @@ -1,7 +1,7 @@ import { Drawable } from 'graphico'; -import { Button } from './button'; +import { Button } from '../form-controls/button'; import { Color } from 'viridis'; -import { Label } from './label'; +import { Label } from '../form-controls/label'; /** * Represents a control to change how many items are being purchased at once. @@ -29,9 +29,9 @@ export class BuyType implements Drawable { */ constructor(x: number, y: number) { this.quantity = 1; - this.buy1 = new Button('1', BuyType.btnColor, true, x - BuyType.btnSize * 3 / 2 - BuyType.btnPad, y, BuyType.btnSize, BuyType.btnSize, () => this.quantity = 1); - this.buy10 = new Button('10', BuyType.btnColor, true, x - BuyType.btnSize / 2, y, BuyType.btnSize, BuyType.btnSize, () => this.quantity = 10); - this.buy100 = new Button('100', BuyType.btnColor, true, x + BuyType.btnSize / 2 + BuyType.btnPad, y, BuyType.btnSize, BuyType.btnSize, () => this.quantity = 100); + this.buy1 = new Button('1', BuyType.btnColor, true, x - BuyType.btnSize * 3 / 2 - BuyType.btnPad, y, BuyType.btnSize, BuyType.btnSize, null, () => this.quantity = 1); + this.buy10 = new Button('10', BuyType.btnColor, true, x - BuyType.btnSize / 2, y, BuyType.btnSize, BuyType.btnSize, null, () => this.quantity = 10); + this.buy100 = new Button('100', BuyType.btnColor, true, x + BuyType.btnSize / 2 + BuyType.btnPad, y, BuyType.btnSize, BuyType.btnSize, null, () => this.quantity = 100); this.label = new Label('', new Color(255, 255, 255), 1, false, 'center', 'bottom', x, y - Label.fontSize); } /** diff --git a/www/games/orbit-idle/src/cursor.ts b/www/games/orbit-idle/src/ui/cursor.ts similarity index 100% rename from www/games/orbit-idle/src/cursor.ts rename to www/games/orbit-idle/src/ui/cursor.ts diff --git a/www/games/orbit-idle/src/menu.ts b/www/games/orbit-idle/src/ui/menu.ts similarity index 85% rename from www/games/orbit-idle/src/menu.ts rename to www/games/orbit-idle/src/ui/menu.ts index d02ccf4..c9f548d 100644 --- a/www/games/orbit-idle/src/menu.ts +++ b/www/games/orbit-idle/src/ui/menu.ts @@ -1,8 +1,8 @@ import { Drawable } from 'graphico'; import { Color } from 'viridis'; -import { Label } from './label'; -import { ToggleButton } from './button-toggle'; -import { tutorial, version } from './state'; +import { Label } from '../form-controls/label'; +import { ToggleButton } from '../form-controls/button-toggle'; +import { stats, tutorial, version } from '../data/state'; /** * Represents the in-game menu @@ -48,6 +48,10 @@ export class Menu implements Drawable { * Mute button */ private readonly mute: ToggleButton; + /** + * Stats button + */ + private readonly stats: ToggleButton; /** * Clear data button */ @@ -105,24 +109,28 @@ export class Menu implements Drawable { this.save = new ToggleButton([ ['Save', Menu.btnColor, () => { this.saveReset = Menu.buttonResetDuration; this.saveCallback(); }], ['Saved!', new Color(150, 250, 150), () => { return }], - ], x - (width / 2), y, width, Menu.btnHeight); + ], x - (width / 2), y, width, Menu.btnHeight, ['s']); this.help = new ToggleButton([ ['Tutorial', Menu.btnColor, () => tutorial.show()], ['End Tutorial', Menu.btnColor, () => tutorial.hide()], - ], x - (width / 2), y + Menu.btnHeight + Menu.btnPadding, width, Menu.btnHeight); + ], x - (width / 2), y + Menu.btnHeight + Menu.btnPadding, width, Menu.btnHeight, ['h']); this.mute = new ToggleButton([ ['Mute', Menu.btnColor, () => this.muteCallback()], ['Unmute', Menu.btnColor, () => this.unmuteCallback()], - ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 2, width, Menu.btnHeight); + ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 2, width, Menu.btnHeight, ['m']); + this.stats = new ToggleButton([ + ['Show Stats', Menu.btnColor, () => stats.show()], + ['Close Stats', Menu.btnColor, () => stats.hide()], + ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 3, width, Menu.btnHeight, ['a']); this.clear = new ToggleButton([ ['Wipe Data', Menu.btnColor, () => this.clearReset = Menu.buttonResetDuration], ['Confirm', new Color(255, 0, 0), () => this.clearCallback()], ['Reload Page', Menu.btnColor, () => window.location.reload()], - ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 3, width, Menu.btnHeight); + ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 4, width, Menu.btnHeight, null); this.back = new ToggleButton([ ['Menu', Menu.btnColor, () => this.toggle()], ['Back', Menu.btnColor, () => this.toggle()], - ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 4, width, Menu.btnHeight); + ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 5, width, Menu.btnHeight, ['escape']); } /** * Set all callbacks, needed from the main canvas @@ -141,6 +149,16 @@ export class Menu implements Drawable { this.save.reset(); this.clear.reset(); } + /** + * Check to see if the user inputted any hotkeys. + */ + public checkHotkeys(key: string): void { + this.save.hotkey(key); + this.help.hotkey(key); + this.mute.hotkey(key); + this.back.hotkey(key); + this.stats.hotkey(key); + } /** * Menu tick for resetting buttons and autosaving */ @@ -178,6 +196,7 @@ export class Menu implements Drawable { this.save.checkHover(x, y); this.help.checkHover(x, y); this.mute.checkHover(x, y); + this.stats.checkHover(x, y); this.clear.checkHover(x, y); } this.back.checkHover(x, y); @@ -190,6 +209,7 @@ export class Menu implements Drawable { this.save.click(button); this.help.click(button); this.mute.click(button); + this.stats.click(button); this.clear.click(button); } this.back.click(button); @@ -200,6 +220,7 @@ export class Menu implements Drawable { this.save.draw(graphics); this.help.draw(graphics); this.mute.draw(graphics); + this.stats.draw(graphics); this.clear.draw(graphics); } this.back.draw(graphics); diff --git a/www/games/orbit-idle/src/pause.ts b/www/games/orbit-idle/src/ui/pause.ts similarity index 95% rename from www/games/orbit-idle/src/pause.ts rename to www/games/orbit-idle/src/ui/pause.ts index 4482f43..31d8ed1 100644 --- a/www/games/orbit-idle/src/pause.ts +++ b/www/games/orbit-idle/src/ui/pause.ts @@ -1,5 +1,5 @@ import { Drawable } from 'graphico'; -import { Label } from './label'; +import { Label } from '../form-controls/label'; import { Color } from 'viridis'; /** diff --git a/www/games/orbit-idle/src/zoom.ts b/www/games/orbit-idle/src/ui/zoom.ts similarity index 86% rename from www/games/orbit-idle/src/zoom.ts rename to www/games/orbit-idle/src/ui/zoom.ts index 3746493..0bdf7e0 100644 --- a/www/games/orbit-idle/src/zoom.ts +++ b/www/games/orbit-idle/src/ui/zoom.ts @@ -1,8 +1,8 @@ import { Drawable } from 'graphico'; -import { Button } from './button'; +import { Button } from '../form-controls/button'; import { Color } from 'viridis'; -import { Label } from './label'; -import { player } from './state'; +import { Label } from '../form-controls/label'; +import { player } from '../data/state'; /** * Represents a zoom control. @@ -36,8 +36,8 @@ export class Zoom implements Drawable { * Create a new zoom control. */ constructor(x: number, y: number, private zoom: number = player.getNumOrbits()) { - this.zoomOutBtn = new Button('-', Zoom.btnColor, true, x - Zoom.btnSize - Zoom.btnPad / 2, y, Zoom.btnSize, Zoom.btnSize, () => this.zoomOut()); - this.zoomInBtn = new Button('+', Zoom.btnColor, true, x + Zoom.btnPad / 2, y, Zoom.btnSize, Zoom.btnSize, () => this.zoomIn()); + this.zoomOutBtn = new Button('-', Zoom.btnColor, true, x - Zoom.btnSize - Zoom.btnPad / 2, y, Zoom.btnSize, Zoom.btnSize, ['-', '_'], () => this.zoomOut()); + this.zoomInBtn = new Button('+', Zoom.btnColor, true, x + Zoom.btnPad / 2, y, Zoom.btnSize, Zoom.btnSize, ['=', '+'], () => this.zoomIn()); this.label = new Label('', new Color(255, 255, 255), 1, false, 'center', 'bottom', x, y - Zoom.btnPad); this.setButtonAbility(); } @@ -101,6 +101,13 @@ export class Zoom implements Drawable { this.zoomInBtn.click(button); this.zoomOutBtn.click(button); } + /** + * Check to see if the user inputted any hotkeys. + */ + public checkHotkeys(key: string): void { + this.zoomInBtn.hotkey(key); + this.zoomOutBtn.hotkey(key); + } public draw(graphics: CanvasRenderingContext2D): void { this.zoomInBtn.draw(graphics); this.zoomOutBtn.draw(graphics); diff --git a/www/tools/dimensional/src/program.ts b/www/tools/dimensional/src/program.ts index dbbc722..a0721e5 100644 --- a/www/tools/dimensional/src/program.ts +++ b/www/tools/dimensional/src/program.ts @@ -36,7 +36,7 @@ export function getUnitNames(): string[] { * Find the unit matching the selected name */ export function getUnitByName(name: string): Unit { - return allUnits.find(nu => nu[0] === name)?.[1]; + return allUnits.find(nu => nu[0] === name)?.[1] ?? units.Unitless; } /** * Swap the input and output of a pair