Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
60aaa69
use hotkey
nicfv Aug 7, 2026
fe19f91
pass on hotkey
nicfv Aug 7, 2026
e486cf2
no hotkeys
nicfv Aug 7, 2026
8cc49fb
space hotkey to advance thru tutorial
nicfv Aug 7, 2026
45fea89
no hotkeys
nicfv Aug 7, 2026
4f52d95
no hotkeys
nicfv Aug 7, 2026
68f45a7
actually, no hotkey for tutorial
nicfv Aug 7, 2026
a1e439b
actually make hotkeys plural
nicfv Aug 7, 2026
daed1af
hotkeys should be plural here too
nicfv Aug 7, 2026
3659989
add +/- hotkeys
nicfv Aug 7, 2026
58fc1d6
add various hotkeys
nicfv Aug 7, 2026
32373e4
check hotkeys
nicfv Aug 7, 2026
1a12653
mention hotkeys
nicfv Aug 7, 2026
ca44c9d
create stats class
nicfv Aug 7, 2026
4cf16b9
render shade and labels
nicfv Aug 7, 2026
fc1bdd8
create stats in state
nicfv Aug 7, 2026
e8ff680
add visibility and close button
nicfv Aug 7, 2026
ae34107
implement it in index
nicfv Aug 7, 2026
7565b3e
oops
nicfv Aug 7, 2026
388b8a5
start working on interface
nicfv Aug 7, 2026
b9f12a9
move to form controls subfolder
nicfv Aug 7, 2026
8898ce3
move menu options to ui
nicfv Aug 7, 2026
e78a1eb
move almost everything into subdirs
nicfv Aug 7, 2026
c8ffb2f
create and assign default stats
nicfv Aug 8, 2026
c971eb2
add money spent
nicfv Aug 8, 2026
3b4e62b
track money totals
nicfv Aug 8, 2026
162d707
get next ascensions exp
nicfv Aug 8, 2026
b64acf2
new ascension button to show progress
nicfv Aug 8, 2026
c1af0a3
track orbits completed and ascensions purchased
nicfv Aug 8, 2026
b9918b2
track speed levels purchased
nicfv Aug 8, 2026
9aa1078
show stats
nicfv Aug 8, 2026
e5822a3
hide is public
nicfv Aug 8, 2026
f260625
create stats button
nicfv Aug 8, 2026
dd10d75
move menu up a bit
nicfv Aug 8, 2026
797d3c8
create saveload abstract
nicfv Aug 8, 2026
9decb8a
extend that
nicfv Aug 8, 2026
b863613
statsdata should be saved/loaded
nicfv Aug 8, 2026
40fe950
readonly
nicfv Aug 8, 2026
985cfe9
make data public
nicfv Aug 8, 2026
c465a24
extend saveload
nicfv Aug 8, 2026
e3cd4f3
simplify constructor, remove defaults
nicfv Aug 8, 2026
418f440
extend saveload, simplify constructor, remove defaults
nicfv Aug 8, 2026
81fe9e2
oops, i guess i deleted too much
nicfv Aug 8, 2026
d193d88
extend saveload
nicfv Aug 8, 2026
4a785a9
load should be flexible
nicfv Aug 8, 2026
30d5c70
should be partial
nicfv Aug 8, 2026
d63d204
properly extend class
nicfv Aug 8, 2026
6e546b7
save and load stats
nicfv Aug 8, 2026
c25a0b8
super -> this
nicfv Aug 8, 2026
c51b2f1
constructor duplicates data
nicfv Aug 8, 2026
1ee5487
typecheck
nicfv Aug 8, 2026
8966478
positioning, remove "x" button
nicfv Aug 8, 2026
b5bebf4
shift a few items up
nicfv Aug 8, 2026
f79cfe5
fix tutorial positioning
nicfv Aug 8, 2026
f3ce606
add hotkey "A" for stats
nicfv Aug 8, 2026
d77b995
write hotkey
nicfv Aug 8, 2026
cf905c6
remove console log
nicfv Aug 8, 2026
8bb621e
remove |0 in player
nicfv Aug 8, 2026
f16c888
change risky |0 to math.floor
nicfv Aug 8, 2026
cfdf49a
fallback on unitless
nicfv Aug 8, 2026
f3686d5
typecheck (better)
nicfv Aug 8, 2026
8318a38
migrate all custom tsconfig rules to root
nicfv Aug 8, 2026
2501088
update deps
nicfv Aug 8, 2026
f2e2434
|0 to bar dimensions
nicfv Aug 8, 2026
cccd009
v1.1
nicfv Aug 8, 2026
da3f701
v1.3
nicfv Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/start
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"DOM",
"ESNext",
],
"module": "esnext",
},
}
4 changes: 2 additions & 2 deletions www/games/geomarin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"start": "../../../cmd/start"
},
"dependencies": {
"graphico": "1.3.0",
"smath": "2.2.0"
"graphico": "1.6.0",
"smath": "3.0.0"
}
}
2 changes: 1 addition & 1 deletion www/games/geomarin/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const global = {
/**
* Application version number
*/
version: '1.0',
version: '1.1',
/**
* Colors for SVG paths (regions)
*/
Expand Down
2 changes: 1 addition & 1 deletion www/games/geomarin/src/lib/bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 0 additions & 6 deletions www/games/geomarin/tsconfig.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,4 +9,5 @@ export interface GameData {
readonly timestamp: number;
readonly playerData: PlayerData;
readonly solarData: SolarData;
readonly statsData: StatsData;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends object> {
/**
* 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<T>): void;
}
33 changes: 33 additions & 0 deletions www/games/orbit-idle/src/data/state.ts
Original file line number Diff line number Diff line change
@@ -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.');
87 changes: 87 additions & 0 deletions www/games/orbit-idle/src/data/stats.ts
Original file line number Diff line number Diff line change
@@ -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<StatsData> 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<StatsData> = 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,
};
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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));
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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());
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
];
}
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading