diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e54ace4..f48d7c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup Bun uses: oven-sh/setup-bun@v2 - name: Build Project run: bun start && bun clean prod - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@v5 with: path: 'www' deploy: @@ -36,4 +36,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/www/games/orbit-idle/index.html b/www/games/orbit-idle/index.html index c740adf..8f7d124 100644 --- a/www/games/orbit-idle/index.html +++ b/www/games/orbit-idle/index.html @@ -17,7 +17,7 @@

Orbit Idle

-

This is the 4th game in the Revolution Idle series.

+

This game takes a new spin as the 4th game in the Revolution Idle series.

Controls: Mouse/click for everything. Instructions are shown in-game.

The original game: Revolution Idle (2017)

Fan collab/remake: Revolution Idle 2 (2017)

diff --git a/www/games/orbit-idle/src/button-full.ts b/www/games/orbit-idle/src/button-full.ts index 6fc272b..82c3b66 100644 --- a/www/games/orbit-idle/src/button-full.ts +++ b/www/games/orbit-idle/src/button-full.ts @@ -29,8 +29,8 @@ export class FullAscendButton extends Button { * Update the text property of this button */ private setText(): void { - super.text = `+1 Planet\n${N(player.score())}/${N(player.scoreRequired())}\nNew system`; - super.enabled = player.canAscend(); + super.text = `+1 Planet\n$${N(player.incomeRequired())}/s\nNew system`; + super.enabled = income.getIncomePerSecond() > player.incomeRequired(); } public draw(graphics: CanvasRenderingContext2D): void { this.setText(); diff --git a/www/games/orbit-idle/src/cursor.ts b/www/games/orbit-idle/src/cursor.ts new file mode 100644 index 0000000..d8122f8 --- /dev/null +++ b/www/games/orbit-idle/src/cursor.ts @@ -0,0 +1,65 @@ +import { Drawable } from 'graphico'; +import { Vec3 } from 'smath'; + +/** + * Represents the user's cursor + */ +export class Cursor implements Drawable { + /** + * 2pi + */ + private static readonly TAU: number = 2 * Math.PI; + /** + * Star radius + */ + private static readonly lgR = 4; + /** + * Planet radius + */ + private static readonly smR = 2; + /** + * The rotational speed of the planet + */ + private static readonly planetHz = 1; + /** + * Mouse coordinates + */ + private pos: Vec3; + /** + * The angle of the orbiting planet + */ + private planetAngle: number; + /** + * Create a new instance of a cursor + */ + constructor() { + this.pos = new Vec3(0, 0); + this.planetAngle = 0; + } + /** + * Move the cursor when the user moves the mouse + */ + public move(x: number, y: number): void { + this.pos = new Vec3(x, y); + } + /** + * Rotate the small planet + */ + public tick(dt: number): void { + const dtheta: number = Cursor.TAU * Cursor.planetHz * dt / 1e3; + this.planetAngle = (this.planetAngle + dtheta) % Cursor.TAU; + } + public draw(graphics: CanvasRenderingContext2D): void { + // Draw the main sequence star + graphics.fillStyle = 'white'; + graphics.beginPath(); + graphics.arc(this.pos.x, this.pos.y, Cursor.lgR, 0, Cursor.TAU); + graphics.fill(); + // Draw the small orbiting planet + const planetPos: Vec3 = this.pos.plus(Vec3.fromPolar(Cursor.lgR + Cursor.smR, this.planetAngle)); + graphics.fillStyle = 'red'; + graphics.beginPath(); + graphics.arc(planetPos.x, planetPos.y, Cursor.smR, 0, Cursor.TAU); + graphics.fill(); + } +} diff --git a/www/games/orbit-idle/src/help.ts b/www/games/orbit-idle/src/help.ts index 460760d..1159048 100644 --- a/www/games/orbit-idle/src/help.ts +++ b/www/games/orbit-idle/src/help.ts @@ -1,11 +1,15 @@ -import { Drawable } from 'graphico'; import { Color } from 'viridis'; import { Label } from './label'; +import { Button } from './button'; /** * Represents a help message */ -export class Help implements Drawable { +export class Help extends Button { + /** + * Transparent color + */ + private static readonly transparent: Color = new Color(0, 0, 0, 0); /** * Shade color for the background */ @@ -33,7 +37,8 @@ export class Help implements Drawable { /** * Create a new help message with optional highlighted region. */ - constructor(text: string, textOffsetX: number, textOffsetY: number, private readonly x: number, private readonly y: number, private readonly w: number, private readonly h: number) { + 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); this.tip = new Label(text, Help.highlightStroke, 1, false, textOffsetX < 0 ? 'right' : 'left', 'top', x + textOffsetX, y + textOffsetY); } /** @@ -45,18 +50,14 @@ export class Help implements Drawable { public draw(graphics: CanvasRenderingContext2D): void { // Shade in the background graphics.fillStyle = Help.shade.toString(); - graphics.fillRect(0, 0, graphics.canvas.width, this.y); - graphics.fillRect(0, this.y + this.h, graphics.canvas.width, graphics.canvas.height); - graphics.fillRect(0, this.y, this.x, this.h); - graphics.fillRect(this.x + this.w, this.y, graphics.canvas.width, this.h); + graphics.fillRect(0, 0, graphics.canvas.width, graphics.canvas.height); + graphics.clearRect(this.x, this.y, this.w, this.h); // Render label and highlighted region this.tip.draw(graphics); graphics.lineWidth = 2; - graphics.fillStyle = Help.highlightFill.toString(); graphics.strokeStyle = Help.highlightStroke.toString(); graphics.strokeRect(this.x, this.y, this.w, this.h); - if (this.time > Help.highlightBlinkMS) { - graphics.fillRect(this.x, this.y, this.w, this.h); - } + super.color = (this.time > Help.highlightBlinkMS) ? Help.highlightFill : Help.transparent; + super.draw(graphics); } } diff --git a/www/games/orbit-idle/src/index.ts b/www/games/orbit-idle/src/index.ts index c52bfd8..a72e3f3 100644 --- a/www/games/orbit-idle/src/index.ts +++ b/www/games/orbit-idle/src/index.ts @@ -1,6 +1,6 @@ import './close-info-handler'; import { Canvas } from 'graphico'; -import { ascBtn, buyType, income, menu, paused, player, shop, system, tutorial, zoom } from './state'; +import { ascBtn, buyType, cursor, income, menu, paused, player, shop, system, tutorial, zoom } from './state'; import { GameData } from './gamedata'; /** @@ -16,10 +16,12 @@ const canv: Canvas = new Canvas({ borderBlur: 'gray', width: 800, height: 600, - numLayers: 2, + numLayers: 3, + showMouse: false, parent: document.getElementById('game') as HTMLElement, loop(dt) { canv.clear(1); + canv.clear(2); system.step(dt); menu.autosave(dt); canv.draw(system, 0); @@ -31,9 +33,12 @@ const canv: Canvas = new Canvas({ canv.draw(ascBtn, 1); canv.draw(menu, 1); tutorial.tick(dt); - canv.draw(tutorial, 1); + canv.draw(tutorial, 2); + cursor.tick(dt); + canv.draw(cursor, 2); }, mousemove(x, y) { + cursor.move(x, y); tutorial.checkHover(x, y); zoom.checkHover(x, y); buyType.checkHover(x, y); @@ -57,7 +62,7 @@ const canv: Canvas = new Canvas({ system.step(dt); }, blur() { - canv.draw(paused, 1); + canv.draw(paused, 2); }, }); diff --git a/www/games/orbit-idle/src/menu.ts b/www/games/orbit-idle/src/menu.ts index 417ed7b..60953a0 100644 --- a/www/games/orbit-idle/src/menu.ts +++ b/www/games/orbit-idle/src/menu.ts @@ -89,7 +89,7 @@ export class Menu implements Drawable { this.title = new Label('Menu', new Color(255, 255, 255), 1, false, 'center', 'bottom', x, y - Menu.btnPadding); this.autosaveLabel = new Label(`Created by Nicolas Ventura [${version}]`, new Color(255, 255, 255), 1, false, 'right', 'bottom', 0, 0); this.save = new ToggleButton([ - ['Save', Menu.btnColor, () => { this.saveCallback(); }], + ['Save', Menu.btnColor, () => this.saveCallback()], ['Saved!', new Color(150, 250, 150), () => { return }], ], x - (width / 2), y, width, Menu.btnHeight); this.help = new ToggleButton([ @@ -97,13 +97,13 @@ export class Menu implements Drawable { ['End Tutorial', Menu.btnColor, () => tutorial.hide()], ], x - (width / 2), y + Menu.btnHeight + Menu.btnPadding, width, Menu.btnHeight); this.mute = new ToggleButton([ - ['Mute', Menu.btnColor, () => { this.muteCallback(); }], - ['Unmute', Menu.btnColor, () => { this.unmuteCallback(); }], + ['Mute', Menu.btnColor, () => this.muteCallback()], + ['Unmute', Menu.btnColor, () => this.unmuteCallback()], ], x - (width / 2), y + (Menu.btnHeight + Menu.btnPadding) * 2, width, Menu.btnHeight); this.clear = new ToggleButton([ ['Wipe Data', Menu.btnColor, () => { return }], - ['Confirm', new Color(255, 0, 0), () => { this.clearCallback(); }], - ['Reload Page', Menu.btnColor, () => { window.location.reload(); }], + ['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); this.back = new ToggleButton([ ['Menu', Menu.btnColor, () => this.toggle()], @@ -122,7 +122,7 @@ export class Menu implements Drawable { /** * Open or close the menu depending on its current state */ - public toggle(): void { + private toggle(): void { this.isOpen = !this.isOpen; this.save.reset(); this.clear.reset(); diff --git a/www/games/orbit-idle/src/orbit.ts b/www/games/orbit-idle/src/orbit.ts index b4fde60..05d6879 100644 --- a/www/games/orbit-idle/src/orbit.ts +++ b/www/games/orbit-idle/src/orbit.ts @@ -54,7 +54,7 @@ export class Orbit implements Drawable { this.maxSpeedLevel = 100; this.baseCost = 10 ** index; this.costIncrease = 1.02 + 0.02 * index; - this.ascCostIncrease = 1.25 + 0.05 * index; + this.ascCostIncrease = 2.25 + 0.05 * index; } /** * Copy orbit data for saving this orbit. @@ -93,9 +93,10 @@ export class Orbit implements Drawable { * Get the cost for the next `N` levels. */ public getNextNCost(n: number): number { + const ascensionMult: number = this.ascCostIncrease ** this.data.ascensions; let cost = 0; for (let level = this.data.speedLevel; level < SMath.clamp(this.data.speedLevel + n, 0, this.maxSpeedLevel); level++) { - cost += this.baseCost * (this.costIncrease ** level) * (this.ascCostIncrease ** this.data.ascensions); + cost += this.baseCost * (this.costIncrease ** level) * ascensionMult; } return cost; } @@ -135,7 +136,6 @@ export class Orbit implements Drawable { if (this.isMaxed()) { this.data.ascensions += n | 0; this.data.angle = 0; - // this.data.rotations = 0; this.data.speedLevel = 0; } } diff --git a/www/games/orbit-idle/src/player.ts b/www/games/orbit-idle/src/player.ts index 45eaf97..2a9b20a 100644 --- a/www/games/orbit-idle/src/player.ts +++ b/www/games/orbit-idle/src/player.ts @@ -23,7 +23,6 @@ export class Player implements Drawable { */ public earn(amount: number): void { this.data.money += amount; - this.data.accumulation += amount; } /** * Spend a certain amount of money from the player's bank @@ -38,22 +37,10 @@ export class Player implements Drawable { return this.data.money >= amount; } /** - * Current player score `log10(money)` + * Calculate the income required to ascend (gain another planet/orbit) */ - public score(): number { - return this.data.accumulation > 1 ? Math.log10(this.data.accumulation) : 0; - } - /** - * Calculate the score required to gain another planet/orbit - */ - public scoreRequired(): number { - return (1.25 * (this.data.orbits ** 1.75) + 1) | 0; - } - /** - * Determine if this player can ascend - */ - public canAscend(): boolean { - return this.score() >= this.scoreRequired(); + public incomeRequired(): number { + return 10 ** ((1.25 * (this.data.orbits ** 1.75) + 1) | 0); } /** * Get the total number of orbits @@ -66,7 +53,6 @@ export class Player implements Drawable { */ public ascend(): void { this.data.money = 0; - this.data.accumulation = 0; this.data.orbits++; } /** @@ -80,7 +66,6 @@ export class Player implements Drawable { */ public load(data: PlayerData = defaultPlayerData): void { this.data.money = SMath.clamp(data.money, 0, Infinity); - this.data.accumulation = SMath.clamp(data.accumulation, this.data.money, Infinity); this.data.orbits = SMath.clamp(data.orbits, 2, Infinity) | 0; } public draw(graphics: CanvasRenderingContext2D): void { @@ -100,12 +85,10 @@ export class Player implements Drawable { */ export interface PlayerData { money: number; - accumulation: number; orbits: number; } const defaultPlayerData: PlayerData = { money: 0, - accumulation: 0, orbits: 2, }; diff --git a/www/games/orbit-idle/src/state.ts b/www/games/orbit-idle/src/state.ts index d4c5190..9e91472 100644 --- a/www/games/orbit-idle/src/state.ts +++ b/www/games/orbit-idle/src/state.ts @@ -1,5 +1,6 @@ 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'; @@ -10,9 +11,10 @@ import { Tutorial } from './tutorial'; import { Zoom } from './zoom'; // Global constants -export const version = 'v1.1.1'; +export const version = 'v1.2.0'; // 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); @@ -24,6 +26,6 @@ export const income: Income = new Income(); export const shop: Shop = new Shop(10, 120); // Additional popups -export const tutorial: Tutorial = new Tutorial(400, 400); +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/tutorial.ts b/www/games/orbit-idle/src/tutorial.ts index 9bdb052..dcab659 100644 --- a/www/games/orbit-idle/src/tutorial.ts +++ b/www/games/orbit-idle/src/tutorial.ts @@ -1,6 +1,5 @@ import { Drawable } from 'graphico'; import { Color } from 'viridis'; -import { Button } from './button'; import { Label } from './label'; import { Help } from './help'; @@ -8,30 +7,6 @@ import { Help } from './help'; * Represents the in-game tutorial. */ export class Tutorial implements Drawable { - /** - * Button sizing - */ - private static readonly btnSize: number = Label.fontSize * 2; - /** - * Button padding - */ - private static readonly btnPad: number = Label.fontSize / 2; - /** - * Button color - */ - private static readonly btnColor: Color = new Color(200, 200, 200); - /** - * Navigation title - */ - private readonly title: Label; - /** - * Next button - */ - private readonly next: Button; - /** - * Back button - */ - private readonly back: Button; /** * Determines whether or not the tutorial is visible */ @@ -55,27 +30,23 @@ export class Tutorial implements Drawable { /** * Create a new tutorial instance. */ - constructor(x: number, y: number) { - this.title = new Label('', new Color(255, 255, 255), 1, false, 'center', 'bottom', x, y - Tutorial.btnPad); - this.back = new Button('<', Tutorial.btnColor, true, x - Tutorial.btnSize - Tutorial.btnPad / 2, y, Tutorial.btnSize, Tutorial.btnSize, () => this.backPage()); - this.next = new Button('>', Tutorial.btnColor, true, x + Tutorial.btnPad / 2, y, Tutorial.btnSize, Tutorial.btnSize, () => this.nextPage()); + constructor() { this.first = new Label('Welcome to Orbit Idle!\n\nNeed help?\n\nOpen Menu and click Tutorial\nfor a quick guided tour.', new Color(255, 255, 255), 1.5, true, 'center', 'top', 0, Label.fontSize * 5); this.visible = false; this.firstDuration = 0; this.helpPage = 0; this.pages = [ - new Help('Welcome!\nOrbit Idle is all about building a strong\nsolar system. Each orbit spins to generate\ncash, and more planets compound your income\nfor faster growth. Click ">" to continue.', 0, -75, 325, 375, 150, 75), - new Help('This is your personal bank.\nThis indicates your spending power.', 0, -30, 5, 550, 790, 45), - new Help('Click the buy buttons to spend cash\nto speed up individual orbits.\nTry upgrading your red planet now!', 140, 0, 5, 100, 130, 400), - 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), - 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), - 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), - 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), - new Help('For convenience, you can buy\nspeed upgrades in bulk. It\ndoes not affect ascending.', -10, 0, 660, 220, 130, 60), - new Help('When you have accumulated enough income,\nyou will be given the option to perform\na full ascension. This resets the entire\nsolar system, but adds an additional planet.\nThe required "score" is the log10 of your\naccumulated cash. For example, 9 = $1B.', -10, 0, 660, 290, 130, 70), - 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), + new Help('Welcome!\nOrbit Idle is all about building a strong\nsolar system. Each orbit spins to generate\ncash, and more planets compound your income\nfor faster growth. Click to continue.', 0, -75, 325, 225, 150, 150, () => this.next()), + new Help('This is your personal bank.\nThis indicates your spending power.', 0, -30, 5, 550, 790, 45, () => this.next()), + new Help('Click the buy buttons to spend cash\nto speed up individual orbits.\nTry upgrading your red planet now!', 140, 0, 5, 100, 130, 400, () => this.next()), + 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()), ]; - this.setButtonAbility(); } /** * Show the tutorial @@ -83,7 +54,6 @@ export class Tutorial implements Drawable { public show(): void { this.visible = true; this.helpPage = 0; - this.setButtonAbility(); } /** * Close the tutorial @@ -92,55 +62,24 @@ export class Tutorial implements Drawable { this.visible = false; } /** - * Go forward one page + * Navigate to the next help page */ - private nextPage(): void { + private next(): void { if (this.helpPage < this.pages.length - 1) { this.helpPage++; - this.setButtonAbility(); - } - } - /** - * Go back one page - */ - private backPage(): void { - if (this.helpPage > 0) { - this.helpPage--; - this.setButtonAbility(); - } - } - /** - * Set the enabled/disabled property for the pagination buttons. - */ - private setButtonAbility(): void { - if (this.helpPage > 0) { - this.back.enable(); - } else { - this.back.disable(); - } - if (this.helpPage < this.pages.length - 1) { - this.next.enable(); - } else { - this.next.disable(); } } /** * Check if the mouse is currently hovering over any of the controls. */ public checkHover(mx: number, my: number): void { - if (this.visible) { - this.back.checkHover(mx, my); - this.next.checkHover(mx, my); - } + this.pages[this.helpPage].checkHover(mx, my); } /** * Check if the mouse has clicked on any of the controls. */ public click(button: number): void { - if (this.visible) { - this.back.click(button); - this.next.click(button); - } + this.pages[this.helpPage].click(button); } /** * Run the tick cycle for the active help page @@ -162,10 +101,6 @@ export class Tutorial implements Drawable { public draw(graphics: CanvasRenderingContext2D): void { if (this.visible) { this.firstDuration = 0; - this.title.value = `Help ${this.helpPage + 1}/${this.pages.length}`; - this.title.draw(graphics); - this.back.draw(graphics); - this.next.draw(graphics); this.pages[this.helpPage].draw(graphics); } if (this.firstDuration > 0) {