Compare commits
15 Commits
3975672453
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
231d346695 | ||
|
|
8d1d939d94 | ||
|
|
efa32c13b5 | ||
|
|
b094595b94 | ||
|
|
1c180baf89 | ||
|
|
8c95abdc50 | ||
|
|
f3ff668cb9 | ||
|
|
79e4593db3 | ||
|
|
4cdafa8d2b | ||
|
|
f5fd6fb36d | ||
|
|
4746ff594f | ||
|
|
39273e757c | ||
|
|
a71126ea29 | ||
|
|
3f205c2a94 | ||
|
|
ca42519222 |
71
.gitea/workflows/deploy.yaml
Normal file
71
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Deploy website to VPS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: site-builder
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Hugo Version
|
||||
run: |
|
||||
if [ -f "hugoblox.yaml" ]; then
|
||||
VERSION=$(grep "hugo_version" hugoblox.yaml | awk '{print $2}' | tr -d "'\"")
|
||||
echo "HUGO_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
else
|
||||
echo "HUGO_VERSION=0.125.0" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Install Node Dependencies
|
||||
run: |
|
||||
if [ -f "package.json" ]; then
|
||||
# Force pnpm to use the version baked into our Nix image
|
||||
sed -i '/"packageManager":/d' package.json
|
||||
pnpm config set manage-package-manager false
|
||||
pnpm install --no-frozen-lockfile
|
||||
fi
|
||||
|
||||
- name: Build with Hugo
|
||||
env:
|
||||
HUGO_ENVIRONMENT: production
|
||||
run: |
|
||||
git config --global http.sslCAInfo /etc/ssl/certs/ca-certificates.crt
|
||||
hugo --minify --baseURL "https://ejs.cam/"
|
||||
|
||||
- name: Generate Pagefind search index
|
||||
run: |
|
||||
if [ -f "package.json" ] && grep -q "pagefind" package.json; then
|
||||
pnpm dlx pagefind --source "public" || npx pagefind --source "public"
|
||||
fi
|
||||
|
||||
- name: Deploy via Rsync
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.REMOTE_USER }}
|
||||
REMOTE_PORT: ${{ secrets.REMOTE_PORT }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
|
||||
ssh-keyscan -p "$REMOTE_PORT" "$REMOTE_HOST" >> ~/.ssh/known_hosts
|
||||
|
||||
rsync -avz --delete \
|
||||
--exclude 'cv.pdf' \
|
||||
--omit-dir-times \
|
||||
--no-perms \
|
||||
--no-owner \
|
||||
--no-group \
|
||||
-e "ssh -p $REMOTE_PORT" \
|
||||
./public/ \
|
||||
$REMOTE_USER@$REMOTE_HOST:/var/www/
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
github: gcushen
|
||||
custom: https://hugoblox.com/sponsor/
|
||||
BIN
.github/preview.webp
vendored
BIN
.github/preview.webp
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
124
.github/workflows/deploy.yml
vendored
124
.github/workflows/deploy.yml
vendored
@@ -1,124 +0,0 @@
|
||||
name: Deploy website to GitHub Pages
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
|
||||
on:
|
||||
# Trigger the workflow every time you push to the `main` branch
|
||||
push:
|
||||
branches: ['main']
|
||||
# Allows you to run this workflow manually from the Actions tab on GitHub
|
||||
workflow_dispatch:
|
||||
|
||||
# Provide permission to clone the repo and deploy it to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build website
|
||||
build:
|
||||
if: github.repository_owner != 'HugoBlox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Fetch history for Hugo's .GitInfo and .Lastmod
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Setup pnpm
|
||||
if: hashFiles('package.json') != ''
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Get Hugo Version
|
||||
id: hugo-version
|
||||
run: |
|
||||
VERSION=$(grep "hugo_version" hugoblox.yaml | awk '{print $2}' | tr -d "'\"")
|
||||
echo "HUGO_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
# Install Tailwind CLI if package.json exists
|
||||
if [ -f "package.json" ]; then
|
||||
echo "Installing Tailwind dependencies..."
|
||||
pnpm install --no-frozen-lockfile || npm install
|
||||
fi
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
with:
|
||||
hugo-version: ${{ env.HUGO_VERSION }}
|
||||
extended: true
|
||||
|
||||
# Cache dependencies (Go modules, node_modules) - stable, rarely changes
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/hugo_cache_runner/
|
||||
node_modules/
|
||||
modules/*/node_modules/
|
||||
key: ${{ runner.os }}-hugo-deps-${{ hashFiles('**/go.mod', '**/package-lock.json',
|
||||
'**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-hugo-deps-
|
||||
|
||||
# Cache Hugo resources (processed images, CSS) - invalidates only when assets/config change
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: resources/
|
||||
key: ${{ runner.os }}-hugo-resources-${{ hashFiles('assets/**/*', 'config/**/*',
|
||||
'hugo.yaml', 'package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-hugo-resources-
|
||||
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Build with Hugo
|
||||
env:
|
||||
HUGO_ENVIRONMENT: production
|
||||
run: |
|
||||
echo "Hugo Cache Dir: $(hugo config | grep cachedir)"
|
||||
hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
|
||||
|
||||
- name: Generate Pagefind search index (if applicable)
|
||||
run: |
|
||||
# Check if site uses Pagefind search
|
||||
if [ -f "package.json" ] && grep -q "pagefind" package.json; then
|
||||
pnpm dlx pagefind --source "public" || npx pagefind --source "public"
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: ./public
|
||||
|
||||
# Deploy website to GitHub Pages hosting
|
||||
deploy:
|
||||
if: github.repository_owner != 'HugoBlox'
|
||||
needs: build
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
122
.github/workflows/import-publications.yml
vendored
122
.github/workflows/import-publications.yml
vendored
@@ -1,122 +0,0 @@
|
||||
# Hugo Blox GitHub Action to convert Bibtex publications to Markdown-based webpages
|
||||
name: Import Publications From Bibtex
|
||||
|
||||
# Require permission to create a PR (least privilege principle)
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
# Run workflow when a `.bib` file is added or updated in the `data/` folder
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
paths: ['publications.bib']
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Prevent concurrent runs of this workflow
|
||||
concurrency:
|
||||
group: import-publications-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
hugoblox:
|
||||
if: github.repository_owner != 'HugoBlox'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
env:
|
||||
ACADEMIC_VERSION: '>=0.10.0'
|
||||
PYTHON_VERSION: '3.12'
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Only need recent history for publication import
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Setup pip cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-academic-${{ env.ACADEMIC_VERSION }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-academic-
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install "academic${{ env.ACADEMIC_VERSION }}"
|
||||
- name: Validate publications.bib file
|
||||
if: ${{ hashFiles('publications.bib') != '' }}
|
||||
run: |
|
||||
if [ ! -f "publications.bib" ]; then
|
||||
echo "❌ publications.bib file not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ publications.bib file found"
|
||||
|
||||
- name: Run Academic (Bibtex To Markdown Converter)
|
||||
# Check `.bib` file exists for case when action runs on `.bib` deletion
|
||||
# Note GH only provides hashFiles func in `steps.if` context, not `jobs.if` context
|
||||
if: ${{ hashFiles('publications.bib') != '' }}
|
||||
run: |
|
||||
echo "🚀 Starting publication import..."
|
||||
academic import publications.bib content/publication/ --compact --verbose
|
||||
echo "✅ Publication import completed successfully"
|
||||
|
||||
# Verify that files were created
|
||||
if [ -d "content/publication" ] && [ "$(ls -A content/publication/)" ]; then
|
||||
echo "📚 Publications imported: $(ls content/publication/ | wc -l) items"
|
||||
else
|
||||
echo "⚠️ No publications were imported"
|
||||
fi
|
||||
continue-on-error: false
|
||||
- name: Create Pull Request
|
||||
# Set ID for `Check outputs` stage
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit-message: 'feat(publications): import latest publications from bibtex'
|
||||
title: 'Hugo Blox Builder - Import latest publications from Bibtex'
|
||||
body: |
|
||||
🔄 **Automated Publication Import**
|
||||
|
||||
This PR automatically imports the latest publications from `publications.bib` to `content/publication/`.
|
||||
|
||||
**Changes:**
|
||||
- 📚 Updated publication entries
|
||||
- 🏷️ Processed bibliographic data
|
||||
|
||||
---
|
||||
将最新的出版物从`publications.bib`导入到`content/publication/`。
|
||||
|
||||
📖 [View Documentation](https://github.com/GetRD/academic-file-converter)
|
||||
base: main
|
||||
labels: automated-pr, content, publications
|
||||
branch: hugoblox-import-publications
|
||||
delete-branch: true
|
||||
draft: false
|
||||
- name: Check outputs
|
||||
if: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
run: |
|
||||
echo "✅ Successfully created Pull Request!"
|
||||
echo "📝 Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
|
||||
echo "🔗 Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
|
||||
echo "🎯 Operation completed at $(date)"
|
||||
|
||||
- name: Report workflow status
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ job.status }}" == "success" ]; then
|
||||
echo "🎉 Workflow completed successfully"
|
||||
else
|
||||
echo "❌ Workflow failed - check logs for details"
|
||||
exit 1
|
||||
fi
|
||||
24
.github/workflows/updater-wip.yml
vendored
24
.github/workflows/updater-wip.yml
vendored
@@ -1,24 +0,0 @@
|
||||
# This workflow is only for the Hugo Blox repository. It is safe to delete from your site.
|
||||
name: Updater (WIP)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 0 * * 0
|
||||
# Allows you to run this workflow manually from the Actions tab on GitHub.
|
||||
workflow_dispatch:
|
||||
|
||||
# Provide permission to clone the repo and deploy it to GitHub Pages
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
if: github.repository_owner == 'HugoBlox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: HugoBlox/gh-action-updater@v2
|
||||
with:
|
||||
feed-url: https://hugoblox.com/rss.xml
|
||||
readme-section: news
|
||||
branch: main
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
# Leave the homepage title empty to use the site title
|
||||
title: ''
|
||||
date: 2022-10-24
|
||||
date: 2026-01-08
|
||||
type: landing
|
||||
|
||||
design:
|
||||
@@ -17,7 +17,7 @@ sections:
|
||||
# Show a call-to-action button under your biography? (optional)
|
||||
button:
|
||||
text: Download CV
|
||||
url: https://cv.ejs.cam
|
||||
url: https://ejs.cam/cv.pdf
|
||||
headings:
|
||||
about: ''
|
||||
education: ''
|
||||
@@ -48,8 +48,9 @@ sections:
|
||||
- projects
|
||||
# featured_only: true
|
||||
design:
|
||||
view: article-grid
|
||||
columns: 3
|
||||
view: card
|
||||
# view: article-grid
|
||||
# columns: 3
|
||||
- block: collection
|
||||
id: blogs
|
||||
content:
|
||||
|
||||
74
content/blog/self-hosted-pipeline/index.md
Normal file
74
content/blog/self-hosted-pipeline/index.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Building a Deterministic, Self-Hosted Build Pipeline on NixOS
|
||||
summary: Taking control of reliability and privacy by implementing a private, fully controlled CI/CD pipeline that eliminates external dependencies.
|
||||
date: 2026-01-08
|
||||
|
||||
authors:
|
||||
- me
|
||||
|
||||
tags:
|
||||
- CI/CD
|
||||
- OCI
|
||||
- Infrastructure as code
|
||||
- Reproducability
|
||||
- Envrionment Parity
|
||||
- NixOS
|
||||
|
||||
content_meta:
|
||||
trending: true
|
||||
---
|
||||
|
||||
## **Building a Deterministic, Self-Hosted Build Pipeline on NixOS**
|
||||
|
||||
In modern DevOps, the "path of least resistance" usually involves a mix of GitHub Actions, Docker Hub, and managed hosting. While efficient, this approach often treats the build environment as a "black box," introducing external dependencies and privacy trade-offs. To deepen my understanding of infrastructure and gain total control over my deployment lifecycle, I migrated my infrastructure to a fully self-hosted, declarative stack on NixOS.
|
||||
|
||||
The journey began with a migration from a standard Debian VPS on Digital Ocean. Using the `nixos-consume` script, I bootstrapped NixOS directly onto the live instance, replacing a mutable, manually-managed OS with a deterministic one. My entire server is now defined via Nix Flakes, allowing me to manage Nginx, Gitea, and my CI/CD runners from a single, version-controlled source of truth.
|
||||
|
||||
##### **The Architecture: Gitea, Podman, and Nginx**
|
||||
|
||||
At the center of this ecosystem is Gitea, which handles both source control and CI/CD via Gitea Actions. For the container runtime, I opted for Podman, integrated directly into my NixOS configuration. Nginx acts as the frontend, utilizing the ACME module for automated SSL certificate renewals.
|
||||
|
||||
One of the most significant technical challenges was the creation of a custom OCI image for my runners. Rather than pulling generic images from the internet, I used Nix’s `dockerTools.buildLayeredImage` to "bake" a specialized environment containing only the tools I need: Hugo, Go, Node.js, and Python’s `uv`.
|
||||
|
||||
```nix
|
||||
# Snippet from runner-image.nix
|
||||
runnerImage = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "localhost/site-builder";
|
||||
tag = "latest";
|
||||
contents = with pkgs; [ bashInteractive nodejs_20 hugo go uv rsync openssh cacert ];
|
||||
fakeRootCommands = ''
|
||||
mkdir -p etc/ssl/certs
|
||||
ln -s ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt etc/ssl/certs/ca-certificates.crt
|
||||
echo "root:x:0:0:root:/root:/bin/bash" > etc/passwd
|
||||
'';
|
||||
};
|
||||
```
|
||||
|
||||
##### **Solving the "Minimal Image" Hurdles**
|
||||
|
||||
Building a container image from scratch in Nix results in an extremely minimal environment. This led to several interesting DevOps puzzles. For instance, `rsync` and `ssh` initially failed because the naked image lacked a `/etc/passwd` file, leaving the system unable to identify the current user. Similarly, the Go compiler and Git struggled with SSL verification because they expected certificates at hardcoded Linux paths.
|
||||
|
||||
I solved these by using `fakeRootCommands` to inject a standard user structure and symlink the Nix-provided certificates into `/etc/ssl/certs`. This ensured that the polyglot toolchain—Node.js for search indexing, Go for Hugo modules, and Python for resume rendering—all shared a consistent, secure environment.
|
||||
|
||||
##### **The Deployment Pipeline**
|
||||
|
||||
With the custom runner image loaded into Podman, the deployment pipeline is remarkably lean. Because the image is pre-baked with all dependencies, the Gitea Action doesn't spend time downloading packages or setting up environments. It simply clones the repo, builds the site, and uses `rsync` to move the static files to `/var/www`.
|
||||
|
||||
```yaml
|
||||
# deploy.yaml snippet
|
||||
- name: Build with Hugo
|
||||
run: hugo --minify --baseURL "https://ejs.cam/"
|
||||
|
||||
- name: Deploy via Rsync
|
||||
run: |
|
||||
ssh-keyscan -p "$REMOTE_PORT" "$REMOTE_HOST" >> ~/.ssh/known_hosts
|
||||
rsync -avz --delete --exclude 'cv.pdf' ./public/ user@host:/var/www/
|
||||
```
|
||||
|
||||
This setup protects specific files, like my CV (generated by a separate `rendercv` pipeline), while ensuring the rest of the site is updated atomically.
|
||||
|
||||
##### **Conclusion and Future Outlook**
|
||||
|
||||
By self-hosting this entire stack, I’ve achieved a build-and-deploy cycle that is both private and incredibly fast, with zero reliance on external service uptimes. The next step in this project is migrating my plaintext secrets into an encrypted management system like `sops-nix`. This will allow me to publish my entire NixOS configuration publicly, serving as a fully transparent and reproducible blueprint for declarative DevOps.
|
||||
|
||||
This project was more than just a hosting exercise; it was a deep dive into the "plumbing" of the modern web, proving that with the right tools, it’s possible to build professional-grade infrastructure that is both autonomous and easy to maintain.
|
||||
BIN
content/projects/transit-sim/featured.png
Normal file
BIN
content/projects/transit-sim/featured.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 722 KiB |
59
content/projects/transit-sim/index.md
Normal file
59
content/projects/transit-sim/index.md
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: 3D City Transit Simulator
|
||||
summary: Interactively plan, construct, manage, and view simulated ridership of your own transit network for cities with my transit simulator!
|
||||
date: 2025-12-15
|
||||
links:
|
||||
- type: code
|
||||
url: https://git.ejs.cam/evan/transit-sim
|
||||
- type: site
|
||||
url: https://evan203.github.io/city-sim
|
||||
tags:
|
||||
- THREE.js
|
||||
- Python
|
||||
---
|
||||
|
||||
Interactively plan, construct, manage, and view simulated ridership of your own
|
||||
transit network for cities with my transit simulator!
|
||||
|
||||
### 1. Route Planning & Construction
|
||||
|
||||
* **Drafting Mode:** The game separates "Planning" from "Building." You must click "Create New Route" to enter a drafting mode where you can experiment without spending money.
|
||||
* **Point-and-Click Plotting:** You build routes by clicking on the ground. The system automatically finds the shortest path along the road network between the points you select.
|
||||
* **Smart Snapping (Ghost Marker):** When hovering the mouse over the map in drafting mode, a transparent "ghost" sphere appears, indicating exactly which road intersection or node the route will snap to if you click.
|
||||
* **Draft Interaction:**
|
||||
* **Add Point:** Left-click to extend the route.
|
||||
* **Move Point:** Click and drag existing yellow markers to adjust the path dynamically.
|
||||
* **Draft Statistics:** While planning, you see real-time estimates for:
|
||||
* **Length:** Total distance of the route.
|
||||
* **Cost:** Construction cost (track/road upgrades) + Fleet cost (buses required).
|
||||
* **Est. Riders:** Projected daily passengers based on the population density near your stops.
|
||||
* **Commit or Discard:** You can finalize the route (spending the money) or discard the draft to cancel.
|
||||
|
||||
### 2. Route Management
|
||||
|
||||
* **Active Route List:** A sidebar lists all currently operating transit lines.
|
||||
* **Color Customization:** You can click the colored box next to any route number to pick a custom color for that line and its vehicles.
|
||||
* **Editing:** You can click the "Pencil" icon to edit a route. *Note: This deletes the existing route and puts its nodes back into "Draft Mode" for you to redraw.*
|
||||
* **Deleting:** You can permanently remove a route to clear clutter (though construction costs are sunk).
|
||||
|
||||
### 3. Economy & Simulation
|
||||
|
||||
* **Budget System:** You start with a fixed amount of capital ($1,000,000). You must manage construction costs against your remaining funds.
|
||||
* **Daily Income:** Every in-game "Day," you earn cash based on the total ridership across all your lines (Ticket Sales).
|
||||
* **Floating Feedback:** When a day passes, floating green text appears over the UI showing exactly how much cash you just earned.
|
||||
* **Ridership Logic:** Ridership is calculated based on "Synergy"—connecting Residential areas (Population) to Commercial/Industrial areas (Jobs).
|
||||
* **Public Approval:** A percentage score (0-100%) that tracks how happy the city is. This is calculated based on how many buildings are within walking distance (approx. 600m) of your transit stops.
|
||||
|
||||
### 4. Visuals & Map Modes
|
||||
|
||||
* **3D City Rendering:** The map features extruded 3D buildings, water bodies, parks, and a road network.
|
||||
* **Vehicle Simulation:** Small blocky buses travel along your constructed routes in real-time.
|
||||
* **Data Views:** You can toggle the map visualization to help plan better routes:
|
||||
* **Standard:** Default visual look.
|
||||
* **Zoning Density:** Colors buildings by type (Purple for Residential, Blue for Commercial) and intensity (darker colors = higher density/more potential riders).
|
||||
* **Transit Coverage (Approval):** A heat map showing service coverage. Buildings turn Green if they are close to a stop, Yellow if they are borderline, and Red if they have no transit access.
|
||||
|
||||
### 5. System Features
|
||||
|
||||
* **Save/Load System:** You can save your current city state (routes, budget, day, approval) to a local JSON file and load it back later to continue playing.
|
||||
* **UI Toggling:** The entire interface can be hidden/shown via a hamburger menu button for cinematic screenshots.
|
||||
Reference in New Issue
Block a user