Compare commits
208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc58cc5a12 | ||
|
|
a274fb174f | ||
|
|
3622a5ec58 | ||
|
|
8a5f8a4d74 | ||
|
|
c1d341eecd | ||
|
|
3176e6775d | ||
|
|
34dcd0a235 | ||
|
|
cbda7dfbc9 | ||
|
|
d039e2cfe6 | ||
|
|
c02bd06ec0 | ||
|
|
efa63771e6 | ||
|
|
9f6d27fb3c | ||
|
|
1a61ae6f77 | ||
|
|
83c816fd0e | ||
|
|
adbaa92dd5 | ||
|
|
580df9f233 | ||
|
|
d5d17560f9 | ||
|
|
cd05ab97b5 | ||
|
|
4eecbd692b | ||
|
|
72beed7177 | ||
|
|
e0595de71a | ||
|
|
6687008d1a | ||
|
|
5b9f1b8f51 | ||
|
|
c570de7f41 | ||
|
|
d0138a6c23 | ||
|
|
29aa25e866 | ||
|
|
ef28be93db | ||
|
|
0d7be6a94e | ||
|
|
4fe78c4a63 | ||
|
|
b52edb2746 | ||
|
|
79d5412fe6 | ||
|
|
fcec2cd1dc | ||
|
|
2038ce15a7 | ||
|
|
08557011cb | ||
|
|
3e87bfd6cc | ||
|
|
ef86dd3ecf | ||
|
|
c887bcf7b9 | ||
|
|
709f2459e4 | ||
|
|
cdf8686c64 | ||
|
|
2fdf74f6ee | ||
|
|
e689679aac | ||
|
|
f70f65d1c3 | ||
|
|
d9b316270d | ||
|
|
e2668b330e | ||
|
|
90b5d1430f | ||
|
|
7c47c43655 | ||
|
|
9e45219706 | ||
|
|
d098800c88 | ||
|
|
3a40076958 | ||
|
|
06108df3d4 | ||
|
|
a442cf5a4d | ||
|
|
6dee29d213 | ||
|
|
7711c644a0 | ||
|
|
aab0a56349 | ||
|
|
13245bbc98 | ||
|
|
c25166d35a | ||
|
|
fc09693c93 | ||
|
|
b71c72db8b | ||
|
|
66591e32b5 | ||
|
|
fba05fa0fb | ||
|
|
11357d4fb5 | ||
|
|
674eb237e0 | ||
|
|
939269b060 | ||
|
|
f54200a7dd | ||
|
|
9ae2357493 | ||
|
|
da525cd111 | ||
|
|
c3f07c0ef5 | ||
|
|
2e7643aa2a | ||
|
|
aca9baf585 | ||
|
|
b4371ba3e0 | ||
|
|
4e118dd8e9 | ||
|
|
9279e21b84 | ||
|
|
8d9bb4a2c9 | ||
|
|
1040c61863 | ||
|
|
e86bdf52fe | ||
|
|
53b3f0af9c | ||
|
|
09f48d08b9 | ||
|
|
4eb592b740 | ||
|
|
c603e8f006 | ||
|
|
f334a2ad56 | ||
|
|
a39f287a88 | ||
|
|
758b3e4704 | ||
|
|
aa70dcbdc2 | ||
|
|
3667d53eae | ||
|
|
01df337ccc | ||
|
|
ad182d68ec | ||
|
|
f7dcc8f57c | ||
|
|
f73f738459 | ||
|
|
bf74a3c7d4 | ||
|
|
e8fb50659d | ||
|
|
00df0899fa | ||
|
|
ae5ba67fc8 | ||
|
|
bc929988b2 | ||
|
|
2346040d46 | ||
|
|
2eb6b3e0b4 | ||
|
|
2edcd89780 | ||
|
|
a63e5c5b55 | ||
|
|
af21e10e97 | ||
|
|
1b97527120 | ||
|
|
8074e2a82e | ||
|
|
db1afb6477 | ||
|
|
45311408d6 | ||
|
|
1141fca63a | ||
|
|
7b70def11f | ||
|
|
aac0c3813b | ||
|
|
49786842f0 | ||
|
|
9f9dfe03a6 | ||
|
|
792da2ce4b | ||
|
|
0c9d78a3d3 | ||
|
|
e929f43a96 | ||
|
|
01eff40690 | ||
|
|
23813a4c31 | ||
|
|
1248b94244 | ||
|
|
f754d91e14 | ||
|
|
7246016b8b | ||
|
|
b42eec96f6 | ||
|
|
efd98460c5 | ||
|
|
698dbd81ae | ||
|
|
13c2a0ba0c | ||
|
|
d0fdb469dd | ||
|
|
32366483dc | ||
|
|
707b2845b1 | ||
|
|
693087afae | ||
|
|
51940080a8 | ||
|
|
a204fce4b5 | ||
|
|
7bab2f1b7a | ||
|
|
f5ee3aada6 | ||
|
|
449e25e0f3 | ||
|
|
3cbb95831c | ||
|
|
146baf1d23 | ||
|
|
e7cc716590 | ||
|
|
3aa2d549d1 | ||
|
|
901012064a | ||
|
|
bf2336a172 | ||
|
|
708a112449 | ||
|
|
85ee724754 | ||
|
|
ff2ee3d6db | ||
|
|
6bc04830d3 | ||
|
|
589bb365bd | ||
|
|
0b8a43eb91 | ||
|
|
bb3087dc37 | ||
|
|
92f56570d9 | ||
|
|
938da0d4dc | ||
|
|
3d94859151 | ||
|
|
a85b1873dd | ||
|
|
446ad080e1 | ||
|
|
ead61e648a | ||
|
|
600fbfd3b7 | ||
|
|
f3031d6cd0 | ||
|
|
7152ae093e | ||
|
|
2bd93ff9e0 | ||
|
|
f68e45f898 | ||
|
|
7eca07c7d1 | ||
|
|
ee4ec2fc39 | ||
|
|
b93a5a3ac0 | ||
|
|
de63e0e52d | ||
|
|
daef2fd2f2 | ||
|
|
6705ce8980 | ||
|
|
f443816355 | ||
|
|
c8c08d5fbe | ||
|
|
b8328657df | ||
|
|
8d235ddf12 | ||
|
|
05f284e3fa | ||
|
|
566baa250c | ||
|
|
19a8bd41a9 | ||
|
|
58cad839b6 | ||
|
|
34035ae6ac | ||
|
|
3a4547fb80 | ||
|
|
86b21bb6dd | ||
|
|
8cf114cbb4 | ||
|
|
f9100da8a2 | ||
|
|
f9c1a3e71a | ||
|
|
73594c8599 | ||
|
|
239f35389e | ||
|
|
95d3296dd9 | ||
|
|
c566f90d16 | ||
|
|
9410af3a69 | ||
|
|
8627fc52ef | ||
|
|
813cc8dbbc | ||
|
|
d90d81d7ff | ||
|
|
b1f62cc58c | ||
|
|
38da997069 | ||
|
|
88d5f6455b | ||
|
|
eb3a41be69 | ||
|
|
1332af93ab | ||
|
|
93adf50498 | ||
|
|
291fd9ead0 | ||
|
|
e86138ec00 | ||
|
|
c431f117e9 | ||
|
|
847a3ef314 | ||
|
|
bab09fed6d | ||
|
|
d56c983e01 | ||
|
|
69df7302d5 | ||
|
|
01f7e715a4 | ||
|
|
e71a823848 | ||
|
|
a8865594ca | ||
|
|
23d764c534 | ||
|
|
c7aee73dcb | ||
|
|
925d1fc437 | ||
|
|
a6dbedb3cd | ||
|
|
74a6b9bfe6 | ||
|
|
40126060fb | ||
|
|
6032c034bc | ||
|
|
6b4062eee6 | ||
|
|
0ea21e86eb | ||
|
|
30c5da879b | ||
|
|
1a76081fec | ||
|
|
045c4b49ef |
17
.cell/cell.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
sdl_video = "main"
|
||||
[dependencies]
|
||||
extramath = "https://gitea.pockle.world/john/extramath@master"
|
||||
[system]
|
||||
ar_timer = 60
|
||||
actor_memory = 0
|
||||
net_service = 0.1
|
||||
reply_timeout = 60
|
||||
actor_max = "10_000"
|
||||
stack_max = 0
|
||||
[actors]
|
||||
[actors.prosperon/sdl_video]
|
||||
main = true
|
||||
[actors.prosperon/prosperon]
|
||||
main = true
|
||||
[actors.prosperon]
|
||||
main = true
|
||||
6
.cell/lock.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[modules]
|
||||
[modules.extramath]
|
||||
hash = "MCLZT3JABTAENS4WVXKGWJ7JPBLZER4YQ5VN2PE7ZD2Z4WYGTIMA===="
|
||||
url = "https://gitea.pockle.world/john/extramath@master"
|
||||
downloaded = "Monday June 2 12:07:20.42 PM -5 2025 AD"
|
||||
commit = "84d81a19a8455bcf8dc494739e9e6d545df6ff2c"
|
||||
190
.github/workflows/build.yml
vendored
@@ -3,10 +3,13 @@ name: Build and Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [ "*" ]
|
||||
tags: [ "v*" ]
|
||||
tags: [ "v*" ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# LINUX BUILD
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
@@ -15,20 +18,17 @@ jobs:
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: Build Prosperon (Linux)
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
|
||||
meson compile -C build
|
||||
|
||||
- name: Test Prosperon (Linux)
|
||||
env:
|
||||
TRACY_NO_INVARIANT_CHECK: 1
|
||||
env: { TRACY_NO_INVARIANT_CHECK: 1 }
|
||||
run: |
|
||||
meson test --print-errorlogs -C build
|
||||
strip build/prosperon
|
||||
|
||||
- name: Upload Test Log (Linux)
|
||||
if: ${{ always() }}
|
||||
@@ -44,11 +44,42 @@ jobs:
|
||||
name: prosperon-artifacts-linux
|
||||
path: build/prosperon
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Gitea Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: gitea.pockle.world
|
||||
username: ${{ secrets.USER_GITEA }}
|
||||
password: ${{ secrets.TOKEN_GITEA }}
|
||||
|
||||
- name: Determine Docker Tag
|
||||
id: docker_tag
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/v.* ]]; then
|
||||
TAG=$(echo "${{ github.ref }}" | sed 's#refs/tags/##')
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=latest" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: gitea.pockle.world/john/prosperon:${{ steps.docker_tag.outputs.tag }}
|
||||
platforms: linux/amd64
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# WINDOWS BUILD (MSYS2 / CLANG64)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
build-windows:
|
||||
runs-on: win-native
|
||||
strategy:
|
||||
matrix:
|
||||
msystem: [CLANG64]
|
||||
matrix: { msystem: [ CLANG64 ] }
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
@@ -61,32 +92,26 @@ jobs:
|
||||
update: true
|
||||
cache: true
|
||||
install: |
|
||||
git
|
||||
zip
|
||||
gzip
|
||||
tar
|
||||
base-devel
|
||||
git zip gzip tar base-devel
|
||||
pacboy: |
|
||||
meson
|
||||
cmake
|
||||
toolchain
|
||||
|
||||
- name: Build Prosperon
|
||||
- name: Build Prosperon (Windows)
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_ndebug=true -Dtracy:only_localhost=true -Dtracy:no_broadcast=true
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true -Dtracy:only_localhost=true -Dtracy:no_broadcast=true
|
||||
meson compile -C build
|
||||
|
||||
- name: Test Prosperon
|
||||
- name: Test Prosperon (Windows)
|
||||
shell: msys2 {0}
|
||||
continue-on-error: true
|
||||
env:
|
||||
TRACY_NO_INVARIANT_CHECK: 1
|
||||
run: |
|
||||
meson test --print-errorlogs -C build
|
||||
strip build/prosperon.exe
|
||||
|
||||
- name: Upload Test Log
|
||||
- name: Upload Test Log (Windows)
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -100,15 +125,52 @@ jobs:
|
||||
name: prosperon-artifacts-windows
|
||||
path: build/prosperon.exe
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# MACOS BUILD
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: Build Prosperon (macOS)
|
||||
run: |
|
||||
meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
|
||||
meson compile -C build
|
||||
|
||||
- name: Test Prosperon (macOS)
|
||||
run: |
|
||||
meson test --print-errorlogs -C build
|
||||
|
||||
- name: Upload Test Log (macOS)
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: testlog-macos
|
||||
path: build/meson-logs/testlog.txt
|
||||
|
||||
- name: Upload Artifact (macOS)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: prosperon-artifacts-macos
|
||||
path: build/prosperon
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# PACKAGE CROSS-PLATFORM DIST
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
package-dist:
|
||||
needs: [build-linux, build-windows]
|
||||
needs: [ build-linux, build-windows, build-macos ]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: get_tag
|
||||
@@ -128,16 +190,21 @@ jobs:
|
||||
name: prosperon-artifacts-windows
|
||||
path: windows_artifacts
|
||||
|
||||
- name: Create the Dist Folder
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: prosperon-artifacts-macos
|
||||
path: mac_artifacts
|
||||
|
||||
- name: Create Dist Folder
|
||||
run: |
|
||||
mkdir dist
|
||||
cp README.md dist/
|
||||
cp license.txt dist/
|
||||
cp -r examples dist/
|
||||
mkdir dist/linux
|
||||
mkdir dist/win
|
||||
cp linux_artifacts/* dist/linux/
|
||||
mkdir -p dist/linux dist/win dist/mac
|
||||
cp README.md dist/
|
||||
cp license.txt dist/
|
||||
cp -r examples dist/
|
||||
cp linux_artifacts/* dist/linux/
|
||||
cp windows_artifacts/* dist/win/
|
||||
cp mac_artifacts/* dist/mac/
|
||||
|
||||
- name: Package Final Dist
|
||||
run: |
|
||||
@@ -151,14 +218,17 @@ jobs:
|
||||
name: "prosperon-${{ steps.get_tag.outputs.tag }}"
|
||||
path: "prosperon-${{ steps.get_tag.outputs.tag }}.zip"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# DEPLOY TO ITCH.IO (single ZIP containing all OSes)
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
deploy-itch:
|
||||
needs: [package-dist]
|
||||
needs: [ package-dist ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: get_tag
|
||||
@@ -177,25 +247,30 @@ jobs:
|
||||
|
||||
- name: Push to itch.io
|
||||
run: |
|
||||
butler push "dist/prosperon-${{ steps.get_tag.outputs.tag }}.zip" ${{ secrets.ITCHIO_USERNAME }}/prosperon:win-linux --userversion ${{ steps.get_tag.outputs.tag }}
|
||||
butler push "dist/prosperon-${{ steps.get_tag.outputs.tag }}.zip" \
|
||||
${{ secrets.ITCHIO_USERNAME }}/prosperon:universal \
|
||||
--userversion ${{ steps.get_tag.outputs.tag }}
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.ITCHIO_API_KEY }}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# DEPLOY TO SELF-HOSTED GITEA
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
deploy-gitea:
|
||||
needs: [package-dist]
|
||||
needs: [ package-dist ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: Get Latest Tag and Commit Message
|
||||
- name: Get Latest Tag & Commit Message
|
||||
id: get_tag
|
||||
run: |
|
||||
TAG=$(git describe --tags --abbrev=0)
|
||||
COMMIT_MSG=$(git log -1 --pretty=%B "${TAG}")
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
COMMIT_MSG=$(git log -1 --pretty=%B "$TAG")
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "commit_msg=$COMMIT_MSG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download Final Distribution
|
||||
@@ -204,39 +279,26 @@ jobs:
|
||||
name: "prosperon-${{ steps.get_tag.outputs.tag }}"
|
||||
path: dist
|
||||
|
||||
- name: Create or Update Gitea Release
|
||||
- name: Create / Update Gitea Release
|
||||
run: |
|
||||
TAG=${{ steps.get_tag.outputs.tag }}
|
||||
ZIP_FILE="dist/prosperon-${TAG}.zip"
|
||||
COMMIT_MSG=$(echo "${{ steps.get_tag.outputs.commit_msg }}" | jq -R -s '.')
|
||||
ZIP=dist/prosperon-${TAG}.zip
|
||||
BODY=$(echo "${{ steps.get_tag.outputs.commit_msg }}" | jq -R -s '.')
|
||||
RELEASE=$(curl -s -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/tags/$TAG" | jq -r '.id')
|
||||
|
||||
# Check if release exists
|
||||
RELEASE_ID=$(curl -s -H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/tags/${TAG}" | jq -r '.id')
|
||||
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" == "null" ]; then
|
||||
# Create a new release if it doesn't exist
|
||||
echo "Creating new release for tag ${TAG}"
|
||||
RELEASE_ID=$(curl -X POST \
|
||||
if [ "$RELEASE" = "null" ] || [ -z "$RELEASE" ]; then
|
||||
RELEASE=$(curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"target_commitish\":\"${{ github.sha }}\",\"name\":\"${TAG}\",\"body\":${COMMIT_MSG},\"draft\":false,\"prerelease\":false}" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"${{ github.sha }}\",\"name\":\"$TAG\",\"body\":$BODY,\"draft\":false,\"prerelease\":false}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases" | jq -r '.id')
|
||||
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" == "null" ]; then
|
||||
echo "Failed to create release for tag ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created release with ID: ${RELEASE_ID}"
|
||||
else
|
||||
echo "Release already exists with ID: ${RELEASE_ID}"
|
||||
fi
|
||||
|
||||
# Upload the zip file as an asset
|
||||
curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.TOKEN_GITEA }}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${ZIP_FILE}" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/${RELEASE_ID}/assets?name=prosperon-${TAG}.zip"
|
||||
--data-binary @"$ZIP" \
|
||||
"https://gitea.pockle.world/api/v1/repos/john/prosperon/releases/$RELEASE/assets?name=prosperon-${TAG}.zip"
|
||||
env:
|
||||
TOKEN_GITEA: ${{ secrets.TOKEN_GITEA }}
|
||||
64
.github/workflows/macbuild.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: Build
|
||||
|
||||
jobs:
|
||||
# ===============================================================
|
||||
# MACOS BUILD (Using Homebrew SDL3)
|
||||
# ===============================================================
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
# 1) Check out code
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# 2) Install dependencies (SDL3 via Homebrew) + ccache
|
||||
- name: Install Dependencies (macOS)
|
||||
run: |
|
||||
brew update
|
||||
brew install sdl3 meson ninja cmake ccache
|
||||
|
||||
# 3) Configure ccache
|
||||
- name: Configure ccache
|
||||
run: |
|
||||
echo "CMAKE_C_COMPILER_LAUNCHER=ccache" >> $GITHUB_ENV
|
||||
echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache" >> $GITHUB_ENV
|
||||
|
||||
# 4) Cache ccache
|
||||
- name: Cache ccache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/Library/Caches/ccache
|
||||
key: ccache-macos-${{ hashFiles('**/*.c', '**/*.cpp', '**/*.h', '**/CMakeLists.txt', '**/meson.build') }}
|
||||
restore-keys: |
|
||||
ccache-macos-
|
||||
|
||||
# 5) Build Prosperon (macOS) linking against Homebrew's SDL3
|
||||
- name: Build Prosperon (macOS)
|
||||
run: |
|
||||
# Ensure pkg-config can find Homebrew's SDL3 .pc files
|
||||
export PKG_CONFIG_PATH="$(brew --prefix sdl3)/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
meson setup build_macos -Dbuildtype=release -Db_lto=true -Db_ndebug=true
|
||||
meson compile -C build_macos
|
||||
|
||||
# 6) Copy SDL3 .dylib from Homebrew for packaging
|
||||
- name: Copy SDL3 library for packaging
|
||||
run: |
|
||||
SDL3_PREFIX=$(brew --prefix sdl3)
|
||||
mkdir -p sdl3-macos
|
||||
# Copy all versions of the SDL3 dynamic library
|
||||
cp -a "${SDL3_PREFIX}/lib/libSDL3*.dylib" sdl3-macos/ || echo "No .dylib found, ignoring"
|
||||
|
||||
# 7) Create minimal artifact folder (macOS)
|
||||
- name: Create artifact folder (macOS)
|
||||
run: |
|
||||
mkdir _pack
|
||||
cp build_macos/prosperon _pack/
|
||||
cp sdl3-macos/libSDL3*.dylib _pack/ || echo "No .dylib found, ignoring"
|
||||
|
||||
# 8) Upload artifact (macOS)
|
||||
- name: Upload Artifact (macOS)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: prosperon-artifacts-macos
|
||||
path: _pack
|
||||
12
.gitignore
vendored
@@ -6,26 +6,18 @@ build/
|
||||
*.o
|
||||
*.a
|
||||
*.d
|
||||
tags
|
||||
Jenkinsfile
|
||||
*~
|
||||
*.log
|
||||
*.gz
|
||||
*.tar
|
||||
.nova/
|
||||
packer*
|
||||
primum
|
||||
sokol-shdc*
|
||||
source/shaders/*.h
|
||||
core.cdb
|
||||
primum.exe
|
||||
core.cdb.h
|
||||
jsc
|
||||
.DS_Store
|
||||
*.html
|
||||
.vscode
|
||||
*.icns
|
||||
game.zip
|
||||
icon.ico
|
||||
steam/
|
||||
subprojects/*/
|
||||
build_dbg/
|
||||
modules/
|
||||
|
||||
26
AGENTS.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project Overview
|
||||
This is a game engine developed using a QuickJS fork as its scripting language. It is an actor based system, based on Douglas Crockford's Misty. It is a Meson compiled project with a number of dependencies.
|
||||
|
||||
## File Structure
|
||||
- `source/`: Contains the C source code
|
||||
- `scripts/`: Contains script code that is loaded on executable start, and modules
|
||||
- `shaders/`: Contains shaders that ship with the engine (for shader based backends)
|
||||
- `benchmarks/`: Benchmark programs for testing speed
|
||||
- `tests/`: Unit tests
|
||||
- `examples/`: Contains full game examples
|
||||
|
||||
## Coding Practices
|
||||
- Use K&R style C
|
||||
- Javascript style prefers objects and prototypical inheritence over ES6 classes, liberal use of closures, and var everywhere
|
||||
|
||||
## Instructions
|
||||
- When generating code, adhere to the coding practices outlined above.
|
||||
- When adding new features, ensure they align with the project's goals.
|
||||
- When fixing bugs, review the code carefully before making changes.
|
||||
- When writing unit tests, cover all important scenarios.
|
||||
|
||||
## Compiling, running, and testing
|
||||
- To compile the code, run "make", which generates a prosperon executable in build_dbg/, and copy it into the root folder
|
||||
- Run a test by giving it as its command: so ./prosperon tests/overling.js would run the test overling.js, ./prosperon tests/nota.js runs the nota benchmark
|
||||
405
CLAUDE.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Build variants
|
||||
- `make` - Make and install debug version. Usually all that's needed.
|
||||
- `make fast` - Build optimized version
|
||||
- `make release` - Build release version with LTO and optimizations
|
||||
- `make small` - Build minimal size version
|
||||
- `make web` - Build for web/emscripten platform
|
||||
- `make crosswin` - Cross-compile for Windows using mingw32
|
||||
|
||||
### Testing
|
||||
After install with 'make', just run 'cell' and point it at the actor you want to launch. "cell tests/toml" runs the actor "tests/toml.js"
|
||||
|
||||
## Scripting language
|
||||
This is called "cell", but it is is a variant of javascript and extremely similar.
|
||||
|
||||
### Common development commands
|
||||
- `meson setup build_<variant>` - Configure build directory
|
||||
- `meson compile -C build_<variant>` - Compile in build directory
|
||||
- `./build_dbg/prosperon examples/<example>` - Run example from build directory
|
||||
- Copy prosperon to game directory and run: `cp build_dbg/prosperon <game-dir>/ && cd <game-dir> && ./prosperon`
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Prosperon is an actor-based game engine inspired by Douglas Crockford's Misty system. Key architectural principles:
|
||||
|
||||
### Actor Model
|
||||
- Each actor runs on its own thread
|
||||
- Communication only through message passing (no shared JavaScript objects)
|
||||
- Hierarchical actor system with spawning/killing
|
||||
- Actor lifecycle: awake, update, draw, garbage collection
|
||||
|
||||
### JavaScript Style Guide
|
||||
- Use `use()` function for imports (Misty-style, not ES6 import/export)
|
||||
- Prefer closures and javascript objects and prototypes over ES6 style classes
|
||||
- Follow existing JavaScript patterns in the codebase
|
||||
- Functions as first-class citizens
|
||||
- Do not use const or let; only var
|
||||
|
||||
### Core Systems
|
||||
1. **Actor System** (scripts/core/engine.js)
|
||||
- Message passing via `send()`, `$_.receive()`
|
||||
- Actor spawning/management
|
||||
- Register-based component system (update, draw, gui, etc.)
|
||||
|
||||
2. **Module System**
|
||||
- `use()` function for loading modules
|
||||
- Module paths: `scripts/modules/`, `scripts/modules/ext/`
|
||||
- Custom QuickJS build with embedded C modules
|
||||
|
||||
3. **Build System**
|
||||
- Meson build configuration (Makefile is convenience wrapper)
|
||||
- Multiple platform targets (Windows, macOS, Linux, Web)
|
||||
- Custom QuickJS build in `subprojects/`
|
||||
- Uses SDL3 for cross-platform support
|
||||
|
||||
### Engine Entry Points
|
||||
- `source/prosperon.c` - Main C entry point
|
||||
- `scripts/core/engine.js` - JavaScript engine initialization for system
|
||||
- `scripts/core/base.js` has modifications to this Javascript runtime (for example, additions to the base Array, String, etc)
|
||||
|
||||
### Subprojects
|
||||
- C code has many subprojects, who's source and sometimes documentation can be found in subprojects. subprojects/quickjs/doc has documentation for quickjs
|
||||
|
||||
### Resource System
|
||||
- Scripts are bundled into `core.zip` during build
|
||||
- Runtime module loading via PhysFS
|
||||
- Resource paths checked in order: `/`, `scripts/modules/`, `scripts/modules/ext/`
|
||||
|
||||
### Notable Dependencies
|
||||
- QuickJS (custom build) - JavaScript runtime
|
||||
- SDL3 - Platform abstraction
|
||||
- Chipmunk2D - Physics
|
||||
- ENet - Networking
|
||||
- Soloud - Audio
|
||||
- Tracy - Profiling (when enabled)
|
||||
|
||||
## Development Tips
|
||||
|
||||
### Running Games
|
||||
```bash
|
||||
# Build first
|
||||
make debug
|
||||
|
||||
# Run example from build directory
|
||||
./build_dbg/prosperon examples/chess
|
||||
|
||||
# Or copy to game directory
|
||||
cp build_dbg/prosperon examples/chess/
|
||||
cd examples/chess
|
||||
./prosperon
|
||||
```
|
||||
|
||||
### Documentation
|
||||
- Documentation is found in docs
|
||||
- Documentation for the JS modules loaded with 'use' is docs/api/modules
|
||||
- .md files directly in docs gives a high level overview
|
||||
- docs/dull is what this specific Javascript system is (including alterations from quickjs/es6)
|
||||
|
||||
### Shader Development
|
||||
- Shaders are in `shaders/` directory as HLSL
|
||||
- Compile script: `shaders/compile.sh`
|
||||
- Outputs to platform-specific formats: `dxil/`, `msl/`, `spv/`
|
||||
|
||||
### Example Games
|
||||
Located in `examples/` directory:
|
||||
- `chess` - Chess implementation (has its own Makefile)
|
||||
- `pong` - Classic pong game
|
||||
- `snake` - Snake game
|
||||
- `tetris` - Tetris clone
|
||||
- `bunnymark` - Performance test
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Run all tests
|
||||
meson test -C build_dbg
|
||||
|
||||
# Run specific test
|
||||
./build_dbg/prosperon tests/spawn_actor.js
|
||||
```
|
||||
|
||||
### Debugging
|
||||
- Use debug build: `make debug`
|
||||
- Tracy profiler support when enabled
|
||||
- Console logging available via `log.console()`, `log.error()`, etc.
|
||||
- Log files written to `.prosperon/log.txt`
|
||||
|
||||
# Project Structure Notes
|
||||
|
||||
## Core JavaScript Modules
|
||||
|
||||
- JavaScript modules are defined using the MISTUSE macro in jsffi.c
|
||||
- The `js_os_funcs`, `js_io_funcs`, etc. arrays define the available functions for each module
|
||||
- New functions are added with MIST_FUNC_DEF(module, function, args_count)
|
||||
|
||||
## File I/O
|
||||
|
||||
- `io.slurp(path)` - Reads a file as text
|
||||
- `io.slurpbytes(path)` - Reads a file as an ArrayBuffer
|
||||
- `io.slurpwrite(path, data)` - Writes data (string or ArrayBuffer) to a file
|
||||
- `io.exists(path)` - Checks if a file exists
|
||||
|
||||
## Script Loading
|
||||
|
||||
- The `use(path)` function in engine.js loads JavaScript modules
|
||||
- Script loading happens in prosperon.c and the engine.js script
|
||||
- jsffi.c contains the C hooks for the QuickJS JavaScript engine
|
||||
- Added functionality for bytecode compilation and loading:
|
||||
- `os.compile_bytecode(source, filename)` - Compiles JS to bytecode, returns ArrayBuffer
|
||||
- `os.eval_bytecode(bytecode)` - Evaluates bytecode from an ArrayBuffer
|
||||
- `compile(scriptPath)` - Compiles a JS file to a .jso bytecode file
|
||||
- Modified `use()` to check for .jso files before loading .js files
|
||||
|
||||
## QuickJS Bytecode API
|
||||
|
||||
- `JS_Eval` with JS_EVAL_FLAG_COMPILE_ONLY - Compiles without executing
|
||||
- `JS_WriteObject` with JS_WRITE_OBJ_BYTECODE - Serializes to bytecode
|
||||
- `JS_ReadObject` with JS_READ_OBJ_BYTECODE - Deserializes and loads bytecode
|
||||
- Bytecode files use .jso extension alongside .js files
|
||||
|
||||
## Available JavaScript APIs
|
||||
|
||||
### Core APIs
|
||||
- `actor` - Base prototype for all actor objects
|
||||
- `$_` - Special global for actor messaging
|
||||
- `prosperon` - Global engine interface
|
||||
- `console` - Logging and debugging interface
|
||||
|
||||
### Framework APIs
|
||||
- `moth` - Higher-level game framework that simplifies Prosperon usage
|
||||
- Handles window creation, game loop, and event dispatching
|
||||
- Provides simple configuration via config.js
|
||||
- Auto-initializes systems like rendering and input
|
||||
- Manages camera, resolution, and FPS automatically
|
||||
|
||||
### Rendering
|
||||
- `draw2d` - 2D drawing primitives
|
||||
- `render` - Low-level rendering operations
|
||||
- `graphics` - Higher-level graphics utilities
|
||||
- `camera` - Camera controls and transformations
|
||||
- `sprite` - Sprite rendering and management
|
||||
|
||||
### Physics and Math
|
||||
- `math` - Mathematical utilities
|
||||
- `geometry` - Geometric calculations and shapes
|
||||
- `transform` - Object transformations
|
||||
|
||||
### Input and Events
|
||||
- `input` - Mouse, keyboard, and touch handling
|
||||
- `event` - Event management system
|
||||
|
||||
### Networking
|
||||
- `enet` - Networking through ENet library
|
||||
- `http` - HTTP client capabilities
|
||||
|
||||
### Audio
|
||||
- `sound` - Audio playback using SoLoud
|
||||
|
||||
### Utility Modules
|
||||
- `time` - Time management and delays
|
||||
- **Must be imported with `use('time')`**
|
||||
- No `time.now()` function - use:
|
||||
- `time.number()` - Number representation of current time
|
||||
- `time.record()` - Struct representation of current time
|
||||
- `time.text()` - Text representation of current time
|
||||
- `io` - File I/O operations
|
||||
- `json` - JSON parsing and serialization
|
||||
- `util` - General utilities
|
||||
- `color` - Color manipulation
|
||||
- `miniz` - Compression utilities
|
||||
- `nota` - Structured data format
|
||||
- `wota` - Serialization format
|
||||
- `qr` - QR code generation/reading
|
||||
- `tween` - Animation tweening
|
||||
- `spline` - Spline calculations
|
||||
- `imgui` - Immediate mode GUI
|
||||
|
||||
## Game Development Patterns
|
||||
|
||||
### Project Structure
|
||||
- Game config is typically in `config.js`
|
||||
- Main entry point is `main.js`
|
||||
- Resource loading through `resources.js`
|
||||
|
||||
### Actor Pattern Usage
|
||||
- Create actors with `actor.spawn(script, config)`
|
||||
- Start actors with `$_.start(callback, script)` - the system automatically sends a greeting, callback receives {type: 'greet', actor: actor_ref}
|
||||
- No need to manually send greetings - `$_.start` handles this automatically
|
||||
- Manage actor hierarchy with overlings and underlings
|
||||
- Schedule actor tasks with `$_.delay()` method
|
||||
- Clean up with `kill()` and `garbage()`
|
||||
|
||||
### Actor Messaging with Callbacks
|
||||
When sending a message with a callback, respond by sending to the message itself:
|
||||
```javascript
|
||||
// Sender side:
|
||||
send(actor, {type: 'status'}, response => {
|
||||
log.console(response); // Handle the response
|
||||
});
|
||||
|
||||
// Receiver side:
|
||||
$_.receiver(msg => {
|
||||
if (msg.type === 'status') {
|
||||
send(msg, {status: 'ok'}); // Send response to the message itself
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Critical Rules for Message Callbacks**:
|
||||
- **A message can only be used ONCE as a send target** - after sending a response to a message, it cannot be used again
|
||||
- If you need to send multiple updates (like progress), only the download request message should be used for the final response
|
||||
- Status requests should each get their own individual response
|
||||
- Actor objects and message headers are completely opaque - never try to access internal properties
|
||||
- Never access `msg.__HEADER__` or similar - the actor system handles routing internally
|
||||
- Use `$_.delay()` to schedule work and avoid blocking the message receiver
|
||||
|
||||
### Game Loop Registration
|
||||
- Register functions like `update`, `draw`, `gui`, etc.
|
||||
- Set function.layer property to control execution order
|
||||
- Use `Register` system to manage callbacks
|
||||
|
||||
### Program vs Module Pattern
|
||||
- Programs are actor scripts that don't return values, they execute top-to-bottom
|
||||
- Modules are files that return single values (usually objects) that get frozen
|
||||
- Programs can spawn other programs as underlings
|
||||
- Programs have lifecycle hooks: awake, update, draw, garbage, etc.
|
||||
|
||||
## Technical Capabilities
|
||||
|
||||
### Graphics Pipeline
|
||||
- Supports multiple render backends (Direct3D, Metal, Vulkan via SDL3)
|
||||
- Custom shader system with cross-platform compilation
|
||||
- Sprite batching for efficient 2D rendering
|
||||
- Camera systems for both 2D and 3D
|
||||
|
||||
### Asset Support
|
||||
- Images: PNG, JPG, QOI, etc.
|
||||
- Audio: Various formats through SoLoud
|
||||
- Models: Basic 3D model support
|
||||
- Custom formats: Aseprite animations, etc.
|
||||
|
||||
### Developer Tools
|
||||
- Built-in documentation system with `cell.DOC`
|
||||
- Tracy profiler integration for performance monitoring
|
||||
- Imgui debugging tools
|
||||
- Console logging with various severity levels
|
||||
|
||||
## Misty Networking Patterns
|
||||
|
||||
Prosperon implements the Misty actor networking model. Understanding these patterns is critical for building distributed applications.
|
||||
|
||||
### Portal Reply Pattern
|
||||
Portals must reply with an actor object, not application data:
|
||||
```javascript
|
||||
// CORRECT: Portal replies with actor
|
||||
$_.portal(e => {
|
||||
send(e, $_); // Reply with server actor
|
||||
}, 5678);
|
||||
|
||||
// WRONG: Portal sends application data
|
||||
$_.portal(e => {
|
||||
send(e, {type: 'game_start'}); // This breaks the pattern
|
||||
}, 5678);
|
||||
```
|
||||
|
||||
### Two-Phase Connection Protocol
|
||||
Proper Misty networking follows a two-phase pattern:
|
||||
|
||||
**Phase 1: Actor Connection**
|
||||
- Client contacts portal using `$_.contact()`
|
||||
- Portal replies with an actor object
|
||||
- This establishes the communication channel
|
||||
|
||||
**Phase 2: Application Communication**
|
||||
- Client sends application messages to the received actor
|
||||
- Normal bidirectional messaging begins
|
||||
- Application logic handles game/service initialization
|
||||
|
||||
### Message Handling Best Practices
|
||||
Messages should be treated as opaque objects with your application data:
|
||||
|
||||
```javascript
|
||||
// CORRECT: Store actor references separately
|
||||
var players = {};
|
||||
$_.receiver(msg => {
|
||||
if (msg.type === 'join_game' && msg.player_id) {
|
||||
// Store the message for later response
|
||||
players[msg.player_id] = msg;
|
||||
// Later, respond to the stored message
|
||||
send(players[msg.player_id], {type: 'game_start'});
|
||||
}
|
||||
});
|
||||
|
||||
// WRONG: Trying to access internal message properties
|
||||
$_.receiver(msg => {
|
||||
var sender = msg.__HEADER__.replycc; // Never do this!
|
||||
});
|
||||
```
|
||||
|
||||
### Return ID Lifecycle
|
||||
- Each reply callback gets a unique return ID
|
||||
- Return IDs are consumed once and then deleted
|
||||
- Reusing message objects with return headers causes "Could not find return function" errors
|
||||
- Always create clean actor references for ongoing communication
|
||||
|
||||
### Actor Object Transparency
|
||||
Actor objects must be completely opaque black boxes that work identically regardless of transport:
|
||||
|
||||
```javascript
|
||||
// Actor objects work transparently for:
|
||||
// - Same-process communication (fastest - uses mailbox)
|
||||
// - Inter-process communication (uses mailbox)
|
||||
// - Network communication (uses ENet)
|
||||
|
||||
// The actor shouldn't know or care about the transport mechanism
|
||||
send(opponent, {type: 'move', from: [0,0], to: [1,1]});
|
||||
```
|
||||
|
||||
**Key Implementation Details:**
|
||||
- `actor_send()` in `scripts/core/engine.js` handles routing based on available actor data
|
||||
- Actor objects sent in message data automatically get address/port populated when received over network
|
||||
- Three communication pathways: `os.mailbox_exist()` check → mailbox send → network send
|
||||
- Actor objects must contain all necessary routing information for transparent messaging
|
||||
|
||||
### Common Networking Bugs
|
||||
1. **Portal sending application data**: Portal should only establish actor connections
|
||||
2. **Return ID collision**: Reusing messages with return headers for multiple sends
|
||||
3. **Mixed phases**: Trying to do application logic during connection establishment
|
||||
4. **Header pollution**: Using received message objects as actor references
|
||||
5. **Missing actor address info**: Actor objects in message data need network address population (fixed in engine.js:746-766)
|
||||
|
||||
### Example: Correct Chess Networking
|
||||
```javascript
|
||||
// Server: Portal setup
|
||||
$_.portal(e => {
|
||||
send(e, $_); // Just reply with actor
|
||||
}, 5678);
|
||||
|
||||
// Client: Two-phase connection
|
||||
$_.contact((actor, reason) => {
|
||||
if (actor) {
|
||||
opponent = actor;
|
||||
send(opponent, {type: 'join_game'}); // Phase 2: app messaging
|
||||
}
|
||||
}, {address: "localhost", port: 5678});
|
||||
|
||||
// Server: Handle application messages
|
||||
$_.receiver(e => {
|
||||
if (e.type === 'join_game') {
|
||||
opponent = e.__HEADER__.replycc;
|
||||
send(opponent, {type: 'game_start', your_color: 'black'});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Memory Management
|
||||
|
||||
- When working with a conversational AI system like Claude, it's important to maintain a clean and focused memory
|
||||
- Regularly review and update memories to ensure they remain relevant and helpful
|
||||
- Delete or modify memories that are no longer accurate or useful
|
||||
- Prioritize information that can genuinely assist in future interactions
|
||||
54
Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
||||
# Builder stage
|
||||
FROM ubuntu:plucky AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip \
|
||||
libasound2-dev \
|
||||
libpulse-dev \
|
||||
libudev-dev \
|
||||
libwayland-dev \
|
||||
wayland-protocols \
|
||||
libxkbcommon-dev \
|
||||
libx11-dev \
|
||||
libxext-dev \
|
||||
libxrandr-dev \
|
||||
libxcursor-dev \
|
||||
libxi-dev \
|
||||
libxinerama-dev \
|
||||
libxss-dev \
|
||||
libegl1-mesa-dev \
|
||||
libgl1-mesa-dev \
|
||||
cmake \
|
||||
ninja-build \
|
||||
git \
|
||||
build-essential \
|
||||
binutils \
|
||||
pkg-config \
|
||||
meson \
|
||||
zip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
RUN git clone https://gitea.pockle.world/john/prosperon.git
|
||||
WORKDIR /app/prosperon
|
||||
RUN git checkout jsffi_refactor
|
||||
RUN meson setup build -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true
|
||||
RUN meson compile -C build
|
||||
|
||||
# Runtime stage
|
||||
FROM ubuntu:latest
|
||||
|
||||
# Install minimal runtime dependencies (e.g., for dynamically linked libraries)
|
||||
RUN apt-get update && apt-get install -y libstdc++6 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy the compiled prosperon binary from the build stage
|
||||
COPY --from=builder /app/prosperon/build/prosperon /usr/local/bin/prosperon
|
||||
|
||||
# Create an entrypoint script
|
||||
RUN echo '#!/bin/bash' > /entrypoint.sh && \
|
||||
echo '/usr/local/bin/prosperon "$@" &' >> /entrypoint.sh && \
|
||||
echo 'tail -f /dev/null' >> /entrypoint.sh && \
|
||||
chmod +x /entrypoint.sh
|
||||
|
||||
WORKDIR /workdir
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
14
Makefile
@@ -1,22 +1,22 @@
|
||||
debug: FORCE
|
||||
meson setup build_dbg -Dbuildtype=debugoptimized
|
||||
meson compile -C build_dbg
|
||||
meson setup build_dbg -Dbuildtype=debug
|
||||
meson install --only-changed -C build_dbg
|
||||
|
||||
fast: FORCE
|
||||
meson setup build_fast
|
||||
meson compile -C build_fast
|
||||
meson install -C build_fast
|
||||
|
||||
release: FORCE
|
||||
meson setup -Dbuildtype=release -Db_lto=true -Db_ndebug=true build_release
|
||||
meson compile -C build_release
|
||||
meson setup -Dbuildtype=release -Db_lto=true -Db_lto_mode=thin -Db_ndebug=true build_release
|
||||
meson install -C build_release
|
||||
|
||||
sanitize: FORCE
|
||||
meson setup -Db_sanitize=address -Db_sanitize=memory -Db_sanitize=leak -Db_sanitize=undefined build_sani
|
||||
meson compile -C build_sani
|
||||
meson install -C build_sani
|
||||
|
||||
small: FORCE
|
||||
meson setup -Dbuildtype=minsize -Db_lto=true -Db_ndebug=true build_small
|
||||
meson compile -C build_small
|
||||
meson install -C build_small
|
||||
|
||||
web: FORCE
|
||||
meson setup -Deditor=false -Dbuildtype=minsize -Db_lto=true -Db_ndebug=true --cross-file emscripten.cross build_web
|
||||
|
||||
@@ -2,6 +2,6 @@ Thank you for using Prosperon!
|
||||
|
||||
Provided are prosperon builds for all available platforms. Simply run prosperon for your platform in a game folder to play!
|
||||
|
||||
To get started, take a dive into the provided example games in the examples folder. Just copy the prosperon executable for your platform, into any provided example folder, then run it!
|
||||
To get started, take a dive into the provided example games in the examples folder. You can either copy the prosperon executable into an example directory and run it there, or run `prosperon path/to/example` from the project root.
|
||||
|
||||
You can take a look through the docs folder for the prosperon manual to learn all about it. The manual is available on the web at [docs.prosperon.dev](https://docs.prosperon.dev).
|
||||
|
||||
76
benchmarks/nota.ce
Normal file
@@ -0,0 +1,76 @@
|
||||
var nota = use('nota')
|
||||
var os = use('os')
|
||||
var io = use('io')
|
||||
|
||||
var ll = io.slurp('benchmarks/nota.json')
|
||||
|
||||
var newarr = []
|
||||
var accstr = ""
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
accstr += i;
|
||||
newarr.push(i.toString())
|
||||
}
|
||||
// Arrays to store timing results
|
||||
var jsonDecodeTimes = [];
|
||||
var jsonEncodeTimes = [];
|
||||
var notaEncodeTimes = [];
|
||||
var notaDecodeTimes = [];
|
||||
var notaSizes = [];
|
||||
|
||||
// Run 100 tests
|
||||
for (let i = 0; i < 100; i++) {
|
||||
// JSON Decode test
|
||||
let start = os.now();
|
||||
var jll = json.decode(ll);
|
||||
jsonDecodeTimes.push((os.now() - start) * 1000);
|
||||
|
||||
// JSON Encode test
|
||||
start = os.now();
|
||||
let jsonStr = JSON.stringify(jll);
|
||||
jsonEncodeTimes.push((os.now() - start) * 1000);
|
||||
|
||||
// NOTA Encode test
|
||||
start = os.now();
|
||||
var nll = nota.encode(jll);
|
||||
notaEncodeTimes.push((os.now() - start) * 1000);
|
||||
|
||||
// NOTA Decode test
|
||||
start = os.now();
|
||||
var oll = nota.decode(nll);
|
||||
notaDecodeTimes.push((os.now() - start) * 1000);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
function getStats(arr) {
|
||||
const avg = arr.reduce((a, b) => a + b) / arr.length;
|
||||
const min = Math.min(...arr);
|
||||
const max = Math.max(...arr);
|
||||
return { avg, min, max };
|
||||
}
|
||||
|
||||
// Pretty print results
|
||||
log.console("\n=== Performance Test Results (100 iterations) ===");
|
||||
log.console("\nJSON Decoding (ms):");
|
||||
const jsonDecStats = getStats(jsonDecodeTimes);
|
||||
log.console(`Average: ${jsonDecStats.avg.toFixed(2)} ms`);
|
||||
log.console(`Min: ${jsonDecStats.min.toFixed(2)} ms`);
|
||||
log.console(`Max: ${jsonDecStats.max.toFixed(2)} ms`);
|
||||
|
||||
log.console("\nJSON Encoding (ms):");
|
||||
const jsonEncStats = getStats(jsonEncodeTimes);
|
||||
log.console(`Average: ${jsonEncStats.avg.toFixed(2)} ms`);
|
||||
log.console(`Min: ${jsonEncStats.min.toFixed(2)} ms`);
|
||||
log.console(`Max: ${jsonEncStats.max.toFixed(2)} ms`);
|
||||
|
||||
log.console("\nNOTA Encoding (ms):");
|
||||
const notaEncStats = getStats(notaEncodeTimes);
|
||||
log.console(`Average: ${notaEncStats.avg.toFixed(2)} ms`);
|
||||
log.console(`Min: ${notaEncStats.min.toFixed(2)} ms`);
|
||||
log.console(`Max: ${notaEncStats.max.toFixed(2)} ms`);
|
||||
|
||||
log.console("\nNOTA Decoding (ms):");
|
||||
const notaDecStats = getStats(notaDecodeTimes);
|
||||
log.console(`Average: ${notaDecStats.avg.toFixed(2)} ms`);
|
||||
log.console(`Min: ${notaDecStats.min.toFixed(2)} ms`);
|
||||
log.console(`Max: ${notaDecStats.max.toFixed(2)} ms`);
|
||||
|
||||
2132
benchmarks/nota.json
Normal file
106
benchmarks/wota.ce
Normal file
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// wota_benchmark.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs wota_benchmark.js
|
||||
//
|
||||
// Prerequisite:
|
||||
var wota = use('wota');
|
||||
var os = use('os');
|
||||
// or otherwise ensure `wota` and `os` are available.
|
||||
// Make sure wota_benchmark.js is loaded after wota.js or combined with it.
|
||||
//
|
||||
|
||||
// Helper to run a function repeatedly and measure total time in seconds.
|
||||
// Returns elapsed time in seconds.
|
||||
function measureTime(fn, iterations) {
|
||||
let t1 = os.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
fn();
|
||||
}
|
||||
let t2 = os.now();
|
||||
return t2 - t1;
|
||||
}
|
||||
|
||||
// We'll define a function that does `encode -> decode` for a given value:
|
||||
function roundTripWota(value) {
|
||||
let encoded = wota.encode(value);
|
||||
let decoded = wota.decode(encoded);
|
||||
// Not doing a deep compare here, just measuring performance.
|
||||
// (We trust the test suite to verify correctness.)
|
||||
}
|
||||
|
||||
// A small suite of data we want to benchmark. Each entry includes:
|
||||
// name: label for printing
|
||||
// data: the test value(s) to encode/decode
|
||||
// iterations: how many times to loop
|
||||
//
|
||||
// You can tweak these as you like for heavier or lighter tests.
|
||||
const benchmarks = [
|
||||
{
|
||||
name: "Small Integers",
|
||||
data: [0, 42, -1, 2023],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Strings (short, emoji)",
|
||||
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Small Objects",
|
||||
data: [
|
||||
{ a:1, b:2.2, c:"3", d:false },
|
||||
{ x:42, y:null, z:"test" }
|
||||
],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Nested Arrays",
|
||||
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Large Array (1k numbers)",
|
||||
// A thousand random numbers
|
||||
data: [ Array.from({length:1000}, (_, i) => i * 0.5) ],
|
||||
iterations: 1000
|
||||
},
|
||||
{
|
||||
name: "Large Binary Blob (256KB)",
|
||||
// A 256KB ArrayBuffer
|
||||
data: [ new Uint8Array(256 * 1024).buffer ],
|
||||
iterations: 200
|
||||
}
|
||||
];
|
||||
|
||||
// Print a header
|
||||
log.console("Wota Encode/Decode Benchmark");
|
||||
log.console("============================\n");
|
||||
|
||||
// We'll run each benchmark scenario in turn.
|
||||
for (let bench of benchmarks) {
|
||||
// We'll measure how long it takes to do 'iterations' *for each test value*
|
||||
// in bench.data. The total loop count is `bench.iterations * bench.data.length`.
|
||||
// Then we compute an overall encode+decode throughput (ops/s).
|
||||
let totalIterations = bench.iterations * bench.data.length;
|
||||
|
||||
// We'll define a function that does a roundTrip for *each* data item in bench.data
|
||||
// to measure in one loop iteration. Then we multiply by bench.iterations.
|
||||
function runAllData() {
|
||||
for (let val of bench.data) {
|
||||
roundTripWota(val);
|
||||
}
|
||||
}
|
||||
|
||||
let elapsedSec = measureTime(runAllData, bench.iterations);
|
||||
let opsPerSec = (totalIterations / elapsedSec).toFixed(1);
|
||||
|
||||
log.console(`${bench.name}:`);
|
||||
log.console(` Iterations: ${bench.iterations} × ${bench.data.length} data items = ${totalIterations}`);
|
||||
log.console(` Elapsed: ${elapsedSec.toFixed(3)} s`);
|
||||
log.console(` Throughput: ${opsPerSec} encode+decode ops/sec\n`);
|
||||
}
|
||||
|
||||
// All done
|
||||
log.console("Benchmark completed.\n");
|
||||
185
benchmarks/wota_nota_json.ce
Normal file
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// benchmark_wota_nota_json.js
|
||||
//
|
||||
// Usage in QuickJS:
|
||||
// qjs benchmark_wota_nota_json.js
|
||||
//
|
||||
// Ensure wota, nota, json, and os are all available, e.g.:
|
||||
var wota = use('wota');
|
||||
var nota = use('nota');
|
||||
var json = use('json');
|
||||
var jswota = use('jswota')
|
||||
var os = use('os');
|
||||
//
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 1. Setup "libraries" array to easily switch among Wota, Nota, and JSON
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const libraries = [
|
||||
{
|
||||
name: "Wota",
|
||||
encode: wota.encode,
|
||||
decode: wota.decode,
|
||||
// Wota produces an ArrayBuffer. We'll count `buffer.byteLength` as size.
|
||||
getSize(encoded) {
|
||||
return encoded.length;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Nota",
|
||||
encode: nota.encode,
|
||||
decode: nota.decode,
|
||||
// Nota also produces an ArrayBuffer:
|
||||
getSize(encoded) {
|
||||
return encoded.length;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
encode: json.encode,
|
||||
decode: json.decode,
|
||||
// JSON produces a JS string. We'll measure its UTF-16 code unit length
|
||||
// as a rough "size". Alternatively, you could convert to UTF-8 for
|
||||
// a more accurate byte size. Here we just use `string.length`.
|
||||
getSize(encodedStr) {
|
||||
return encodedStr.length;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 2. Test data sets (similar to wota benchmarks).
|
||||
// Each scenario has { name, data, iterations }
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const benchmarks = [
|
||||
{
|
||||
name: "Empty object",
|
||||
data: [{}, {}, {}, {}],
|
||||
iterations: 10000
|
||||
},
|
||||
{
|
||||
name: "Small Integers",
|
||||
data: [0, 42, -1, 2023],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Floating point",
|
||||
data: [0.1, 1e-50, 3.14159265359],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Strings (short, emoji)",
|
||||
data: ["Hello, Wota!", "short", "Emoji: \u{1f600}\u{1f64f}"],
|
||||
iterations: 100000
|
||||
},
|
||||
{
|
||||
name: "Small Objects",
|
||||
data: [
|
||||
{ a:1, b:2.2, c:"3", d:false },
|
||||
{ x:42, y:null, z:"test" }
|
||||
],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Nested Arrays",
|
||||
data: [ [ [ [1,2], [3,4] ] ], [[[]]], [1, [2, [3, [4]]]] ],
|
||||
iterations: 50000
|
||||
},
|
||||
{
|
||||
name: "Large Array (1k integers)",
|
||||
data: [ Array.from({length:1000}, (_, i) => i) ],
|
||||
iterations: 1000
|
||||
},
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 3. Utility: measureTime(fn) => how long fn() takes in seconds.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function measureTime(fn) {
|
||||
let start = os.now();
|
||||
fn();
|
||||
let end = os.now();
|
||||
return (end - start); // in seconds
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 4. For each library, we run each benchmark scenario and measure:
|
||||
// - Encoding time (seconds)
|
||||
// - Decoding time (seconds)
|
||||
// - Total encoded size (bytes or code units for JSON)
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function runBenchmarkForLibrary(lib, bench) {
|
||||
// We'll encode and decode each item in `bench.data`.
|
||||
// We do 'bench.iterations' times. Then sum up total time.
|
||||
|
||||
// Pre-store the encoded results for all items so we can measure decode time
|
||||
// in a separate pass. Also measure total size once.
|
||||
let encodedList = [];
|
||||
let totalSize = 0;
|
||||
|
||||
// 1) Measure ENCODING
|
||||
let encodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
// For each data item, encode it
|
||||
for (let j = 0; j < bench.data.length; j++) {
|
||||
let e = lib.encode(bench.data[j]);
|
||||
// store only in the very first iteration, so we can decode them later
|
||||
// but do not store them every iteration or we blow up memory.
|
||||
if (i === 0) {
|
||||
encodedList.push(e);
|
||||
totalSize += lib.getSize(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Measure DECODING
|
||||
let decodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
// decode everything we stored during the first iteration
|
||||
for (let e of encodedList) {
|
||||
let decoded = lib.decode(e);
|
||||
// not verifying correctness here, just measuring speed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { encodeTime, decodeTime, totalSize };
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 5. Main driver: run across all benchmarks, for each library.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
log.console("Benchmark: Wota vs Nota vs JSON");
|
||||
log.console("================================\n");
|
||||
|
||||
for (let bench of benchmarks) {
|
||||
log.console(`SCENARIO: ${bench.name}`);
|
||||
log.console(` Data length: ${bench.data.length} | Iterations: ${bench.iterations}\n`);
|
||||
|
||||
for (let lib of libraries) {
|
||||
let { encodeTime, decodeTime, totalSize } = runBenchmarkForLibrary(lib, bench);
|
||||
|
||||
// We'll compute total operations = bench.iterations * bench.data.length
|
||||
let totalOps = bench.iterations * bench.data.length;
|
||||
let encOpsPerSec = (totalOps / encodeTime).toFixed(1);
|
||||
let decOpsPerSec = (totalOps / decodeTime).toFixed(1);
|
||||
|
||||
log.console(` ${lib.name}:`);
|
||||
log.console(` Encode time: ${encodeTime.toFixed(3)}s => ${encOpsPerSec} encodes/sec [${(encodeTime/bench.iterations)*1000000000} ns/try]`);
|
||||
log.console(` Decode time: ${decodeTime.toFixed(3)}s => ${decOpsPerSec} decodes/sec [${(decodeTime/bench.iterations)*1000000000}/try]`);
|
||||
log.console(` Total size: ${totalSize} bytes (or code units for JSON)`);
|
||||
log.console("");
|
||||
}
|
||||
log.console("---------------------------------------------------------\n");
|
||||
}
|
||||
|
||||
log.console("Benchmark complete.\n");
|
||||
|
||||
os.exit()
|
||||
@@ -47,7 +47,7 @@ Certain functions are intrinsic to the program and cannot be overridden. They’
|
||||
- **Example**:
|
||||
```js
|
||||
this.delay(_ => {
|
||||
console.log("3 seconds later!")
|
||||
log.console("3 seconds later!")
|
||||
}, 3)
|
||||
```
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
Provides a consistent way to create documentation for prosperon elements. Objects are documented by adding docstrings directly to object-like things (functions, objects, ...), or to an object's own "doc object".
|
||||
|
||||
Docstrings are set to the symbol `prosperon.DOC`
|
||||
Docstrings are set to the symbol `cell.DOC`
|
||||
|
||||
```js
|
||||
// Suppose we have a module that returns a function
|
||||
function greet(name) { console.log("Hello, " + name) }
|
||||
function greet(name) { log.console("Hello, " + name) }
|
||||
|
||||
// We can attach a docstring
|
||||
greet.doc = `
|
||||
@@ -21,12 +21,12 @@ return greet
|
||||
```js
|
||||
// Another way is to add a docstring object to an object
|
||||
var greet = {
|
||||
hello() { console.log('hello!') }
|
||||
hello() { log.console('hello!') }
|
||||
}
|
||||
|
||||
greet[prosperon.DOC] = {}
|
||||
greet[prosperon.DOC][prosperon.DOC] = 'An object full of different greeter functions'
|
||||
greet[prosperon.DOC].hello = 'A greeter that says, "hello!"'
|
||||
greet[cell.DOC] = {}
|
||||
greet[cell.DOC][cell.DOC] = 'An object full of different greeter functions'
|
||||
greet[cell.DOC].hello = 'A greeter that says, "hello!"'
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ return {
|
||||
This will cause prosperon to launch a 500x500 window with the title 'Hello World'. In your ```main.js```, write the following:
|
||||
|
||||
```
|
||||
console.log("Hello world")
|
||||
log.console("Hello world")
|
||||
|
||||
this.delay(_ => {
|
||||
this.kill();
|
||||
@@ -62,6 +62,6 @@ this.delay(_ => {
|
||||
The global object called `prosperon` has a variety of engine specific settings on it that can be set to influence how the engine behaves. For example, `prosperon.argv` contains a list of the command line arguments given to prosperon; `prosperon.PATH` is an array of paths to resolve resources such as modules and images. `prosperon` is fully documented in the API section.
|
||||
|
||||
## Getting help
|
||||
The `prosperon` global has a 'doc' function, which can be invoked on any engine object to see a description of it and its members. For example, to learn about `prosperon`, try printing out `prosperon.doc(prosperon)` in your `main.js`.
|
||||
The `prosperon` global has a 'doc' function, which can be invoked on any engine object to see a description of it and its members. For example, to learn about `prosperon`, try printing out `cell.DOC(prosperon)` in your `main.js`.
|
||||
|
||||
Writing documentation for your own modules and game components will be explored in the chapter on actors & modules.
|
||||
@@ -3,9 +3,20 @@ c = 'emcc'
|
||||
cpp = 'em++'
|
||||
ar = 'emar'
|
||||
strip = 'emstrip'
|
||||
pkg-config = 'pkg-config'
|
||||
exe_wrapper = 'node'
|
||||
|
||||
[host_machine]
|
||||
system = 'emscripten'
|
||||
cpu_family = 'wasm64'
|
||||
cpu = 'wasm64'
|
||||
cpu_family = 'wasm32'
|
||||
cpu = 'wasm32'
|
||||
endian = 'little'
|
||||
|
||||
[built-in options]
|
||||
pkg_config_path = '$EMSDK/upstream/emscripten/cache/sysroot/lib/pkgconfig'
|
||||
cmake_prefix_path = '$EMSDK/upstream/emscripten/cache/sysroot'
|
||||
|
||||
[properties]
|
||||
needs_exe_wrapper = true
|
||||
cmake_system_name = 'Emscripten'
|
||||
sys_root = '@env:EMSDK@/upstream/emscripten/cache/sysroot'
|
||||
|
||||
234
examples/http_download_actor.ce
Normal file
@@ -0,0 +1,234 @@
|
||||
// HTTP Download Actor
|
||||
// Handles download requests and progress queries
|
||||
var http = use('http');
|
||||
var os = use('os');
|
||||
|
||||
// Actor state
|
||||
var state = {
|
||||
downloading: false,
|
||||
current_url: null,
|
||||
total_bytes: 0,
|
||||
downloaded_bytes: 0,
|
||||
start_time: 0,
|
||||
error: null,
|
||||
connection: null,
|
||||
download_msg: null,
|
||||
chunks: []
|
||||
};
|
||||
|
||||
// Helper to calculate progress percentage
|
||||
function get_progress() {
|
||||
if (state.total_bytes === 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.round((state.downloaded_bytes / state.total_bytes) * 100);
|
||||
}
|
||||
|
||||
// Helper to format status response
|
||||
function get_status() {
|
||||
if (!state.downloading) {
|
||||
return {
|
||||
status: 'idle',
|
||||
error: state.error
|
||||
};
|
||||
}
|
||||
|
||||
var elapsed = os.now() - state.start_time;
|
||||
var bytes_per_sec = elapsed > 0 ? state.downloaded_bytes / elapsed : 0;
|
||||
|
||||
return {
|
||||
status: 'downloading',
|
||||
url: state.current_url,
|
||||
progress: get_progress(),
|
||||
downloaded_bytes: state.downloaded_bytes,
|
||||
total_bytes: state.total_bytes,
|
||||
elapsed_seconds: elapsed,
|
||||
bytes_per_second: Math.round(bytes_per_sec)
|
||||
};
|
||||
}
|
||||
|
||||
// Main message receiver
|
||||
$_.receiver(function(msg) {
|
||||
switch (msg.type) {
|
||||
case 'download':
|
||||
if (state.downloading) {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'Already downloading',
|
||||
current_url: state.current_url
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msg.url) {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'No URL provided'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start download
|
||||
state.downloading = true;
|
||||
state.current_url = msg.url;
|
||||
state.total_bytes = 0;
|
||||
state.downloaded_bytes = 0;
|
||||
state.start_time = os.now();
|
||||
state.error = null;
|
||||
state.download_msg = msg;
|
||||
state.chunks = [];
|
||||
|
||||
try {
|
||||
// Start the connection
|
||||
state.connection = http.fetch_start(msg.url, msg.options || {});
|
||||
if (!state.connection) {
|
||||
throw new Error('Failed to start download');
|
||||
}
|
||||
|
||||
// Schedule the first chunk read
|
||||
$_.delay(read_next_chunk, 0);
|
||||
|
||||
} catch (e) {
|
||||
state.error = e.toString();
|
||||
state.downloading = false;
|
||||
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: state.error,
|
||||
url: msg.url
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
log.console(`got status request. current is ${json.encode(get_status())}`)
|
||||
send(msg, {
|
||||
type: 'status_response',
|
||||
...get_status()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
if (state.downloading) {
|
||||
// Cancel the download
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
state.connection = null;
|
||||
}
|
||||
state.downloading = false;
|
||||
state.current_url = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
|
||||
send(msg, {
|
||||
type: 'cancelled',
|
||||
message: 'Download cancelled',
|
||||
url: state.current_url
|
||||
});
|
||||
} else {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'No download in progress'
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'Unknown message type: ' + msg.type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Non-blocking chunk reader
|
||||
function read_next_chunk() {
|
||||
if (!state.downloading || !state.connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var chunk = http.fetch_read_chunk(state.connection);
|
||||
|
||||
if (chunk === null) {
|
||||
// Download complete
|
||||
finish_download();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store chunk
|
||||
state.chunks.push(chunk);
|
||||
|
||||
// Update progress
|
||||
var info = http.fetch_info(state.connection);
|
||||
state.downloaded_bytes = info.bytes_read;
|
||||
if (info.headers_complete && info.content_length > 0) {
|
||||
state.total_bytes = info.content_length;
|
||||
}
|
||||
|
||||
// Schedule next chunk read
|
||||
$_.delay(read_next_chunk, 0);
|
||||
|
||||
} catch (e) {
|
||||
// Error during download
|
||||
state.error = e.toString();
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
}
|
||||
|
||||
if (state.download_msg) {
|
||||
send(state.download_msg, {
|
||||
type: 'error',
|
||||
error: state.error,
|
||||
url: state.current_url
|
||||
});
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state.downloading = false;
|
||||
state.connection = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the download and send result
|
||||
function finish_download() {
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
}
|
||||
|
||||
// Combine all chunks into single ArrayBuffer
|
||||
var total_size = 0;
|
||||
for (var i = 0; i < state.chunks.length; i++) {
|
||||
total_size += state.chunks[i].byteLength;
|
||||
}
|
||||
|
||||
var result = new ArrayBuffer(total_size);
|
||||
var view = new Uint8Array(result);
|
||||
var offset = 0;
|
||||
|
||||
for (var i = 0; i < state.chunks.length; i++) {
|
||||
var chunk_view = new Uint8Array(state.chunks[i]);
|
||||
view.set(chunk_view, offset);
|
||||
offset += state.chunks[i].byteLength;
|
||||
}
|
||||
|
||||
// Send complete message
|
||||
if (state.download_msg) {
|
||||
send(state.download_msg, {
|
||||
type: 'complete',
|
||||
url: state.current_url,
|
||||
data: result,
|
||||
size: result.byteLength,
|
||||
duration: os.now() - state.start_time
|
||||
});
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state.downloading = false;
|
||||
state.connection = null;
|
||||
state.current_url = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
}
|
||||
29
examples/nat.ce
Normal file
@@ -0,0 +1,29 @@
|
||||
// NAT Punchthrough Server
|
||||
// This server helps two chess clients find each other through NAT
|
||||
// The server coordinates the punchthrough by having both clients
|
||||
// connect to each other simultaneously
|
||||
|
||||
var json = use('json');
|
||||
var waiting_client = null;
|
||||
var match_id = 0;
|
||||
|
||||
$_.portal(e => {
|
||||
log.console("NAT server: received connection request");
|
||||
|
||||
if (!is_actor(e.actor))
|
||||
send(e, {reason: "Must provide the actor you want to connect."});
|
||||
|
||||
if (waiting_client) {
|
||||
log.console(`sending out messages! to ${json.encode(e.actor)} and ${json.encode(waiting_client.actor)}`)
|
||||
send(waiting_client, e.actor)
|
||||
send(e, waiting_client.actor)
|
||||
|
||||
waiting_client = undefined
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
waiting_client = e
|
||||
|
||||
log.console(`actor ${json.encode(e.actor)} is waiting ...`)
|
||||
}, 4000);
|
||||
22
examples/nat_client.ce
Normal file
@@ -0,0 +1,22 @@
|
||||
log.console(`nat client starting`)
|
||||
|
||||
$_.contact((actor, reason) => {
|
||||
if (actor) {
|
||||
log.console(`trying to message ${json.encode(actor)}`)
|
||||
send(actor, {type:"greet"})
|
||||
} else {
|
||||
log.console(json.encode(reason))
|
||||
}
|
||||
}, {
|
||||
address: "108.210.60.32", // NAT server's public IP
|
||||
port: 4000,
|
||||
actor: $_
|
||||
})
|
||||
|
||||
$_.receiver(e => {
|
||||
switch(e.type) {
|
||||
case 'greet':
|
||||
log.console(`hello!`)
|
||||
break
|
||||
}
|
||||
})
|
||||
328
meson.build
@@ -1,4 +1,4 @@
|
||||
project('prosperon', ['c', 'cpp'],
|
||||
project('cell', ['c', 'cpp'],
|
||||
version: '0.9.3',
|
||||
meson_version: '>=1.4',
|
||||
default_options : [ 'cpp_std=c++11'])
|
||||
@@ -8,32 +8,35 @@ libtype = get_option('default_library')
|
||||
link = []
|
||||
src = []
|
||||
|
||||
fs = import('fs')
|
||||
|
||||
add_project_arguments('-pedantic', language: ['c'])
|
||||
|
||||
git_tag_cmd = run_command('git', 'describe', '--tags', '--abbrev=0', check: false)
|
||||
prosperon_version = 'unknown'
|
||||
cell_version = 'unknown'
|
||||
if git_tag_cmd.returncode() == 0
|
||||
prosperon_version = git_tag_cmd.stdout().strip()
|
||||
cell_version = git_tag_cmd.stdout().strip()
|
||||
endif
|
||||
|
||||
git_commit_cmd = run_command('git', 'rev-parse', '--short', 'HEAD', check: false)
|
||||
prosperon_commit = 'unknown'
|
||||
cell_commit = 'unknown'
|
||||
if git_commit_cmd.returncode() == 0
|
||||
prosperon_commit = git_commit_cmd.stdout().strip()
|
||||
cell_commit = git_commit_cmd.stdout().strip()
|
||||
endif
|
||||
|
||||
# Important: pass the definitions without double-escaping quotes
|
||||
add_project_arguments(
|
||||
'-DPROSPERON_VERSION="' + prosperon_version + '"',
|
||||
'-DPROSPERON_COMMIT="' + prosperon_commit + '"',
|
||||
'-DCELL_VERSION="' + cell_version + '"',
|
||||
'-DCELL_COMMIT="' + cell_commit + '"',
|
||||
language : 'c'
|
||||
)
|
||||
|
||||
add_project_arguments('-Wno-incompatible-pointer-types', language: 'c')
|
||||
add_project_arguments('-Wno-narrowing', language: 'cpp')
|
||||
add_project_arguments('-Wno-missing-braces', language:'c')
|
||||
add_project_arguments('-Wl,--disable-new-dtags', language:'cpp')
|
||||
add_project_arguments('-Wl,--disable-new-dtags', language:'c')
|
||||
add_project_arguments('-Wno-strict-prototypes', language:'c')
|
||||
add_project_arguments('-Wno-unused-command-line-argument', language: 'c')
|
||||
add_project_arguments('-Wno-unused-command-line-argument', language: 'cpp')
|
||||
|
||||
deps = []
|
||||
|
||||
@@ -64,12 +67,41 @@ endif
|
||||
|
||||
cmake = import('cmake')
|
||||
|
||||
# Try to find system-installed mbedtls first
|
||||
mbedtls_dep = dependency('mbedtls', static: true, required: false)
|
||||
mbedx509_dep = dependency('mbedx509', static: true, required: false)
|
||||
mbedcrypto_dep = dependency('mbedcrypto', static: true, required: false)
|
||||
|
||||
if not mbedtls_dep.found() or not mbedx509_dep.found() or not mbedcrypto_dep.found()
|
||||
message('⚙ System mbedtls not found, building subproject...')
|
||||
mbedtls_opts = cmake.subproject_options()
|
||||
mbedtls_opts.add_cmake_defines({
|
||||
'ENABLE_PROGRAMS': 'OFF', # Disable Mbed TLS programs
|
||||
'ENABLE_TESTING': 'OFF', # Disable Mbed TLS tests
|
||||
'CMAKE_BUILD_TYPE': 'Release', # Optimize for release
|
||||
'MBEDTLS_FATAL_WARNINGS': 'ON', # Treat warnings as errors
|
||||
'USE_STATIC_MBEDTLS_LIBRARY': 'ON',# Build static libraries
|
||||
'USE_SHARED_MBEDTLS_LIBRARY': 'OFF'# Disable shared libraries
|
||||
})
|
||||
mbedtls_proj = cmake.subproject('mbedtls', options: mbedtls_opts)
|
||||
deps += [
|
||||
mbedtls_proj.dependency('mbedtls'),
|
||||
mbedtls_proj.dependency('mbedx509'),
|
||||
mbedtls_proj.dependency('mbedcrypto')
|
||||
]
|
||||
else
|
||||
deps += [mbedtls_dep, mbedx509_dep, mbedcrypto_dep]
|
||||
endif
|
||||
|
||||
sdl3_opts = cmake.subproject_options()
|
||||
sdl3_opts.add_cmake_defines({
|
||||
'SDL_STATIC': 'ON',
|
||||
'SDL_SHARED': 'OFF',
|
||||
'SDL_TEST': 'OFF',
|
||||
'CMAKE_BUILD_TYPE': 'Release'
|
||||
'CMAKE_BUILD_TYPE': 'Release',
|
||||
'SDL_THREADS': 'ON',
|
||||
'SDL_PIPEWIRE': 'ON',
|
||||
'SDL_PULSEAUDIO': 'ON',
|
||||
})
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
@@ -94,147 +126,229 @@ if host_machine.system() == 'windows'
|
||||
deps += cc.find_library('imm32')
|
||||
deps += cc.find_library('version')
|
||||
deps += cc.find_library('cfgmgr32')
|
||||
sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # TODO: A hack to get this to compile on MSYS2; otherwise it doesn't link correctly
|
||||
deps += cc.find_library('bcrypt')
|
||||
sdl3_opts.add_cmake_defines({'HAVE_ISINF': '1'}) # Hack for MSYS2
|
||||
sdl3_opts.add_cmake_defines({'HAVE_ISNAN': '1'})
|
||||
link += '-static'
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'emscripten'
|
||||
link += '-sUSE_WEBGPU'
|
||||
# Use the pre-installed copy
|
||||
deps += dependency('sdl3',
|
||||
static : true,
|
||||
method : 'pkg-config', # or 'cmake' if you prefer
|
||||
required : true)
|
||||
else
|
||||
# Try to find system-installed SDL3 first
|
||||
sdl3_dep = dependency('sdl3', static: true, required: false)
|
||||
|
||||
if not sdl3_dep.found()
|
||||
message('⚙ System SDL3 not found, building subproject...')
|
||||
sdl3_proj = cmake.subproject('sdl3', options : sdl3_opts)
|
||||
deps += sdl3_proj.dependency('SDL3-static')
|
||||
else
|
||||
deps += sdl3_dep
|
||||
endif
|
||||
endif
|
||||
|
||||
sdl3_proj = cmake.subproject('sdl3', options: sdl3_opts)
|
||||
|
||||
deps += sdl3_proj.dependency('SDL3-static')
|
||||
|
||||
tracy_opts = ['fibers=true', 'on_demand=true']
|
||||
quickjs_opts = []
|
||||
|
||||
src += 'qjs_tracy.c'
|
||||
add_project_arguments('-DTRACY_ENABLE', language:['c','cpp'])
|
||||
deps += dependency('tracy', static:true, default_options:tracy_opts)
|
||||
|
||||
quickjs_opts += 'default_library=static'
|
||||
|
||||
deps += dependency('quickjs', static:true, default_options:quickjs_opts)
|
||||
|
||||
storefront = get_option('storefront')
|
||||
if storefront == 'steam'
|
||||
deps += dependency('qjs-steam',static:false)
|
||||
# Try to find system-installed qjs-layout first
|
||||
qjs_layout_dep = dependency('qjs-layout', static: true, required: false)
|
||||
if not qjs_layout_dep.found()
|
||||
message('⚙ System qjs-layout not found, building subproject...')
|
||||
deps += dependency('qjs-layout', static:true)
|
||||
else
|
||||
deps += qjs_layout_dep
|
||||
endif
|
||||
|
||||
deps += dependency('qjs-layout',static:true)
|
||||
deps += dependency('qjs-miniz',static:true)
|
||||
miniz_dep = dependency('miniz', static: true, required: false)
|
||||
if not miniz_dep.found()
|
||||
message('⚙ System miniz not found, building subproject...')
|
||||
deps += dependency('miniz', static:true)
|
||||
else
|
||||
deps += miniz_dep
|
||||
endif
|
||||
|
||||
deps += dependency('physfs', static:true)
|
||||
libuv_dep = dependency('libuv', static: true, required: false)
|
||||
if not libuv_dep.found()
|
||||
message('⚙ System libuv not found, building subproject...')
|
||||
deps += dependency('libuv', static:true, fallback: ['libuv', 'libuv_dep'])
|
||||
else
|
||||
deps += libuv_dep
|
||||
endif
|
||||
|
||||
#deps += dependency('opencv4')
|
||||
#deps += cc.find_library('opencv')
|
||||
# Try to find system-installed physfs first
|
||||
physfs_dep = dependency('physfs', static: true, required: false)
|
||||
if not physfs_dep.found()
|
||||
message('⚙ System physfs not found, building subproject...')
|
||||
deps += dependency('physfs', static:true)
|
||||
else
|
||||
deps += physfs_dep
|
||||
endif
|
||||
|
||||
deps += dependency('threads')
|
||||
deps += dependency('chipmunk', static:true)
|
||||
deps += dependency('enet', static:true)
|
||||
deps += dependency('soloud', static:true)
|
||||
|
||||
#deps += dependency('qjs-chipmunk', static:false)
|
||||
# Try to find system-installed chipmunk first
|
||||
chipmunk_dep = dependency('chipmunk', static: true, required: false)
|
||||
if not chipmunk_dep.found()
|
||||
message('⚙ System chipmunk not found, building subproject...')
|
||||
deps += dependency('chipmunk', static:true)
|
||||
else
|
||||
deps += chipmunk_dep
|
||||
endif
|
||||
|
||||
if host_machine.system() != 'emscripten'
|
||||
# Try to find system-installed enet first
|
||||
enet_dep = dependency('enet', static: true, required: false)
|
||||
if not enet_dep.found()
|
||||
message('⚙ System enet not found, building subproject...')
|
||||
deps += dependency('enet', static:true)
|
||||
else
|
||||
deps += enet_dep
|
||||
endif
|
||||
src += 'qjs_enet.c'
|
||||
|
||||
tracy_opts = ['fibers=true', 'no_exit=true', 'libunwind_backtrace=true']
|
||||
add_project_arguments('-DTRACY_ENABLE', language:['c','cpp'])
|
||||
|
||||
# Try to find system-installed tracy first
|
||||
tracy_dep = dependency('tracy', static: true, required: false)
|
||||
if not tracy_dep.found()
|
||||
message('⚙ System tracy not found, building subproject...')
|
||||
deps += dependency('tracy', static:true, default_options:tracy_opts)
|
||||
else
|
||||
deps += tracy_dep
|
||||
endif
|
||||
|
||||
src += 'qjs_dmon.c'
|
||||
endif
|
||||
|
||||
# Try to find system-installed soloud first
|
||||
soloud_dep = dependency('soloud', static: true, required: false)
|
||||
if not soloud_dep.found()
|
||||
message('⚙ System soloud not found, building subproject...')
|
||||
deps += dependency('soloud', static:true)
|
||||
else
|
||||
deps += soloud_dep
|
||||
endif
|
||||
|
||||
# Try to find system-installed qrencode first
|
||||
qr_dep = dependency('qrencode', static: true, required: false)
|
||||
if not qr_dep.found()
|
||||
message('⚙ System qrencode not found, building subproject...')
|
||||
deps += dependency('libqrencode', static:true)
|
||||
else
|
||||
deps += qr_dep
|
||||
endif
|
||||
|
||||
# Storefront SDK support
|
||||
storefront = get_option('storefront')
|
||||
if storefront == 'steam'
|
||||
steam_sdk_path = meson.current_source_dir() / 'sdk'
|
||||
|
||||
if host_machine.system() == 'darwin'
|
||||
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'osx' / 'libsteam_api.dylib'
|
||||
elif host_machine.system() == 'linux'
|
||||
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'linux64' / 'libsteam_api.so'
|
||||
elif host_machine.system() == 'windows'
|
||||
steam_lib_path = steam_sdk_path / 'redistributable_bin' / 'win64' / 'steam_api64.lib'
|
||||
else
|
||||
steam_lib_path = ''
|
||||
endif
|
||||
|
||||
if fs.exists(steam_lib_path)
|
||||
steam_dep = declare_dependency(
|
||||
include_directories: include_directories('sdk/public'),
|
||||
link_args: [steam_lib_path]
|
||||
)
|
||||
deps += steam_dep
|
||||
src += 'qjs_steam.cpp'
|
||||
message('Steam SDK enabled')
|
||||
else
|
||||
error('Steam SDK required but not found at: ' + steam_lib_path)
|
||||
endif
|
||||
else
|
||||
add_project_arguments('-DNSTEAM', language: ['c', 'cpp'])
|
||||
message('Storefront: ' + storefront)
|
||||
endif
|
||||
|
||||
link_args = link
|
||||
sources = []
|
||||
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','script.c','simplex.c','spline.c', 'timer.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c']
|
||||
src += [
|
||||
'anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c',
|
||||
'render.c','simplex.c','spline.c', 'transform.c','cell.c', 'wildmatch.c',
|
||||
'sprite.c', 'rtree.c', 'qjs_nota.c', 'qjs_soloud.c', 'qjs_sdl.c', 'qjs_sdl_input.c', 'qjs_sdl_video.c', 'qjs_sdl_surface.c', 'qjs_math.c', 'qjs_geometry.c', 'qjs_transform.c', 'qjs_sprite.c', 'qjs_io.c', 'qjs_fd.c', 'qjs_os.c', 'qjs_actor.c',
|
||||
'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c', 'qjs_rtree.c', 'qjs_spline.c', 'qjs_js.c', 'qjs_debug.c', 'picohttpparser.c', 'qjs_miniz.c', 'timer.c', 'qjs_socket.c', 'qjs_kim.c', 'qjs_utf8.c', 'qjs_fit.c', 'qjs_text.c'
|
||||
]
|
||||
|
||||
imsrc = ['GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp','imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp','implot_items.cpp','implot.cpp', 'imgui_impl_sdlrenderer3.cpp', 'imgui_impl_sdl3.cpp', 'imgui_impl_sdlgpu3.cpp']
|
||||
# js src
|
||||
src += [ 'libregexp.c', 'libunicode.c', 'cutils.c', 'dtoa.c', 'quickjs.c' ]
|
||||
|
||||
# quirc src
|
||||
src += [
|
||||
'thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c',
|
||||
'thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c'
|
||||
]
|
||||
|
||||
imsrc = [
|
||||
'GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp',
|
||||
'imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp',
|
||||
'implot_items.cpp','implot.cpp','imgui_impl_sdlrenderer3.cpp','imgui_impl_sdl3.cpp',
|
||||
'imgui_impl_sdlgpu3.cpp'
|
||||
]
|
||||
|
||||
srceng = 'source'
|
||||
tp = srceng / 'thirdparty'
|
||||
|
||||
includes = [srceng,tp / 'cgltf',tp / 'imgui',tp / 'par',tp / 'stb',tp,tp / 'pl_mpeg/include']
|
||||
includes = [
|
||||
srceng, tp / 'cgltf', tp / 'imgui', tp / 'par', tp / 'stb',
|
||||
tp, tp / 'pl_mpeg/include', tp / 'quirc'
|
||||
]
|
||||
|
||||
foreach file : src
|
||||
full_path = join_paths('source', file)
|
||||
sources += files(full_path)
|
||||
full_path = join_paths('source', file)
|
||||
sources += files(full_path)
|
||||
endforeach
|
||||
|
||||
if get_option('editor')
|
||||
sources += 'source/qjs_imgui.cpp'
|
||||
foreach imgui : imsrc
|
||||
sources += tp / 'imgui' / imgui
|
||||
endforeach
|
||||
endif
|
||||
|
||||
includers = []
|
||||
foreach inc : includes
|
||||
includers += include_directories(inc)
|
||||
endforeach
|
||||
|
||||
zip_folders = ['scripts', 'fonts', 'icons', 'shaders']
|
||||
zip_paths = []
|
||||
foreach folder: zip_folders
|
||||
zip_paths += meson.project_source_root() / folder
|
||||
endforeach
|
||||
|
||||
# Now use the hash file as a dependency so that any change in the files causes a rebuild.
|
||||
core = custom_target('core.zip',
|
||||
output : 'core.zip',
|
||||
command : ['sh', '-c',
|
||||
'cd ' + meson.project_source_root() +
|
||||
' && echo "Rebuilding core.zip" && rm -f ' + meson.current_build_dir() + '/core.zip && ' +
|
||||
'zip -r ' + meson.current_build_dir() + '/core.zip scripts fonts icons shaders'
|
||||
],
|
||||
build_always_stale: true,
|
||||
build_by_default: true
|
||||
)
|
||||
|
||||
prosperon_raw = executable('prosperon_raw', sources,
|
||||
dependencies: deps,
|
||||
include_directories: includers,
|
||||
link_args: link,
|
||||
build_rpath: '$ORIGIN',
|
||||
install:false
|
||||
)
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
exe_ext = '.exe'
|
||||
else
|
||||
exe_ext = ''
|
||||
endif
|
||||
|
||||
prosperon = custom_target('prosperon',
|
||||
output: 'prosperon' + exe_ext,
|
||||
input: [prosperon_raw, core],
|
||||
command: [
|
||||
'sh', '-c',
|
||||
'cat "$1" "$2" > "$3" && chmod +x "$3" >/dev/null 2>&1',
|
||||
'concat',
|
||||
'@INPUT0@',
|
||||
'@INPUT1@',
|
||||
'@OUTPUT@'
|
||||
],
|
||||
build_always_stale: true,
|
||||
build_by_default: true
|
||||
strip_enabled = ['release', 'minsize'].contains(get_option('buildtype'))
|
||||
|
||||
if strip_enabled
|
||||
add_project_link_arguments('-s', language: ['c', 'cpp'])
|
||||
endif
|
||||
|
||||
cell = executable('cell', sources,
|
||||
dependencies: deps,
|
||||
include_directories: includers,
|
||||
link_args: link,
|
||||
build_rpath: '$ORIGIN',
|
||||
install: true
|
||||
)
|
||||
|
||||
prosperon_dep = declare_dependency(
|
||||
link_with:prosperon
|
||||
)
|
||||
|
||||
copy_tests = custom_target(
|
||||
'copy_tests',
|
||||
output: 'tests',
|
||||
command: [
|
||||
'cp', '-rf',
|
||||
join_paths(meson.project_source_root(), 'tests'),
|
||||
meson.project_build_root()
|
||||
],
|
||||
build_always_stale: true,
|
||||
build_by_default: true
|
||||
cell_dep = declare_dependency(
|
||||
link_with: cell
|
||||
)
|
||||
|
||||
tests = [
|
||||
'spawn_actor',
|
||||
'empty',
|
||||
'nota',
|
||||
'enet'
|
||||
'wota',
|
||||
'portalspawner',
|
||||
'overling',
|
||||
'send',
|
||||
'delay'
|
||||
]
|
||||
|
||||
foreach file : tests
|
||||
test(file, prosperon_raw, args:['tests/' + file + '.js'], depends:copy_tests)
|
||||
test(file, cell, args:['tests/' + file])
|
||||
endforeach
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
option('editor', type:'boolean', value:true)
|
||||
option('chipmunk', type:'boolean', value:true)
|
||||
option('enet', type:'boolean', value:true)
|
||||
option('storefront', type:'combo', choices:['none','steam', 'gog', 'egs'], value:'none')
|
||||
|
||||
@@ -3,6 +3,7 @@ site_name: Prosperon Documentation
|
||||
plugins:
|
||||
- search
|
||||
- awesome-pages
|
||||
- mike
|
||||
|
||||
extra_css:
|
||||
- style.css
|
||||
@@ -31,6 +32,9 @@ extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-85ECSFGCBV
|
||||
version:
|
||||
default: latest
|
||||
provider: mike
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var cam = {}
|
||||
|
||||
var os = use('os')
|
||||
var transform = use('transform')
|
||||
|
||||
var basecam = {}
|
||||
basecam.draw_rect = function(size)
|
||||
@@ -88,7 +88,7 @@ function mode_rect(src,dst,mode = "stretch")
|
||||
cam.make = function()
|
||||
{
|
||||
var c = Object.create(basecam)
|
||||
c.transform = os.make_transform()
|
||||
c.transform = new transform
|
||||
c.transform.unit()
|
||||
c.zoom = 1
|
||||
c.size = [640,360]
|
||||
216
prosperon/color.cm
Normal file
@@ -0,0 +1,216 @@
|
||||
function tohex(n) {
|
||||
var s = Math.floor(n).toString(16);
|
||||
if (s.length === 1) s = "0" + s;
|
||||
return s.toUpperCase();
|
||||
};
|
||||
|
||||
var Color = {
|
||||
white: [1, 1, 1],
|
||||
black: [0, 0, 0],
|
||||
blue: [0, 0, 1],
|
||||
green: [0, 1, 0],
|
||||
yellow: [1, 1, 0],
|
||||
red: [1, 0, 0],
|
||||
gray: [0.71, 0.71, 0.71],
|
||||
cyan: [0, 1, 1],
|
||||
purple: [0.635, 0.365, 0.89],
|
||||
orange: [1, 0.565, 0.251],
|
||||
magenta: [1, 0, 1],
|
||||
};
|
||||
|
||||
Color.editor = {};
|
||||
Color.editor.ur = Color.green;
|
||||
|
||||
Color.tohtml = function (v) {
|
||||
var html = v.map(function (n) {
|
||||
return tohex(n * 255);
|
||||
});
|
||||
return "#" + html.join("");
|
||||
};
|
||||
|
||||
var esc = {};
|
||||
esc.reset = "\x1b[0";
|
||||
esc.color = function (v) {
|
||||
var c = v.map(function (n) {
|
||||
return Math.floor(n * 255);
|
||||
});
|
||||
var truecolor = "\x1b[38;2;" + c.join(";") + ";";
|
||||
return truecolor;
|
||||
};
|
||||
|
||||
esc.doc = "Functions and constants for ANSI escape sequences.";
|
||||
|
||||
Color.Arkanoid = {
|
||||
orange: [1, 0.561, 0],
|
||||
teal: [0, 1, 1],
|
||||
green: [0, 1, 0],
|
||||
red: [1, 0, 0],
|
||||
blue: [0, 0.439, 1],
|
||||
purple: [1, 0, 1],
|
||||
yellow: [1, 1, 0],
|
||||
silver: [0.616, 0.616, 0.616],
|
||||
gold: [0.737, 0.682, 0],
|
||||
};
|
||||
|
||||
Color.Arkanoid.Powerups = {
|
||||
red: [0.682, 0, 0] /* laser */,
|
||||
blue: [0, 0, 0.682] /* enlarge */,
|
||||
green: [0, 0.682, 0] /* catch */,
|
||||
orange: [0.878, 0.561, 0] /* slow */,
|
||||
purple: [0.824, 0, 0.824] /* break */,
|
||||
cyan: [0, 0.682, 1] /* disruption */,
|
||||
gray: [0.561, 0.561, 0.561] /* 1up */,
|
||||
};
|
||||
|
||||
Color.Gameboy = {
|
||||
darkest: [0.898, 0.42, 0.102],
|
||||
dark: [0.898, 0.741, 0.102],
|
||||
light: [0.741, 0.898, 0.102],
|
||||
lightest: [0.42, 0.898, 0.102],
|
||||
};
|
||||
|
||||
Color.Apple = {
|
||||
green: [0.369, 0.741, 0.243],
|
||||
yellow: [1, 0.725, 0],
|
||||
orange: [0.969, 0.51, 0],
|
||||
red: [0.886, 0.22, 0.22],
|
||||
purple: [0.592, 0.224, 0.6],
|
||||
blue: [0, 0.612, 0.875],
|
||||
};
|
||||
|
||||
Color.Debug = {
|
||||
boundingbox: Color.white,
|
||||
names: [0.329, 0.431, 1],
|
||||
};
|
||||
|
||||
Color.Editor = {
|
||||
grid: [0.388, 1, 0.502],
|
||||
select: [1, 1, 0.216],
|
||||
newgroup: [0.471, 1, 0.039],
|
||||
};
|
||||
|
||||
/* Detects the format of all colors and munges them into a floating point format */
|
||||
Color.normalize = function (c) {
|
||||
var add_a = function (a) {
|
||||
var n = this.slice();
|
||||
n[3] = a;
|
||||
return n;
|
||||
};
|
||||
|
||||
for (var p of Object.keys(c)) {
|
||||
if (typeof c[p] !== "object") continue;
|
||||
if (!Array.isArray(c[p])) {
|
||||
Color.normalize(c[p]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add alpha channel if not present
|
||||
if (c[p].length === 3) {
|
||||
c[p][3] = 1;
|
||||
}
|
||||
|
||||
// Check if any values are > 1 (meaning they're in 0-255 format)
|
||||
var needs_conversion = false;
|
||||
for (var color of c[p]) {
|
||||
if (color > 1) {
|
||||
needs_conversion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from 0-255 to 0-1 if needed
|
||||
if (needs_conversion) {
|
||||
c[p] = c[p].map(function (x) {
|
||||
return x / 255;
|
||||
});
|
||||
}
|
||||
|
||||
c[p].alpha = add_a;
|
||||
}
|
||||
};
|
||||
|
||||
Color.normalize(Color);
|
||||
|
||||
var ColorMap = {};
|
||||
ColorMap.makemap = function (map) {
|
||||
var newmap = Object.create(ColorMap);
|
||||
Object.assign(newmap, map);
|
||||
return newmap;
|
||||
};
|
||||
ColorMap.Jet = ColorMap.makemap({
|
||||
0: [0, 0, 0.514],
|
||||
0.125: [0, 0.235, 0.667],
|
||||
0.375: [0.02, 1, 1],
|
||||
0.625: [1, 1, 0],
|
||||
0.875: [0.98, 0, 0],
|
||||
1: [0.502, 0, 0],
|
||||
});
|
||||
|
||||
ColorMap.BlueRed = ColorMap.makemap({
|
||||
0: [0, 0, 1],
|
||||
1: [1, 0, 0],
|
||||
});
|
||||
|
||||
ColorMap.Inferno = ColorMap.makemap({
|
||||
0: [0, 0, 0.016],
|
||||
0.13: [0.122, 0.047, 0.282],
|
||||
0.25: [0.333, 0.059, 0.427],
|
||||
0.38: [0.533, 0.133, 0.416],
|
||||
0.5: [0.729, 0.212, 0.333],
|
||||
0.63: [0.89, 0.349, 0.2],
|
||||
0.75: [0.976, 0.549, 0.039],
|
||||
0.88: [0.976, 0.788, 0.196],
|
||||
1: [0.988, 1, 0.643],
|
||||
});
|
||||
|
||||
ColorMap.Bathymetry = ColorMap.makemap({
|
||||
0: [0.157, 0.102, 0.173],
|
||||
0.13: [0.233, 0.192, 0.353],
|
||||
0.25: [0.251, 0.298, 0.545],
|
||||
0.38: [0.247, 0.431, 0.592],
|
||||
0.5: [0.282, 0.557, 0.62],
|
||||
0.63: [0.333, 0.682, 0.639],
|
||||
0.75: [0.471, 0.808, 0.639],
|
||||
0.88: [0.733, 0.902, 0.675],
|
||||
1: [0.992, 0.996, 0.8],
|
||||
});
|
||||
|
||||
ColorMap.Viridis = ColorMap.makemap({
|
||||
0: [0.267, 0.004, 0.329],
|
||||
0.13: [0.278, 0.173, 0.478],
|
||||
0.25: [0.231, 0.318, 0.545],
|
||||
0.38: [0.173, 0.443, 0.557],
|
||||
0.5: [0.129, 0.565, 0.553],
|
||||
0.63: [0.153, 0.678, 0.506],
|
||||
0.75: [0.361, 0.784, 0.388],
|
||||
0.88: [0.667, 0.863, 0.196],
|
||||
1: [0.992, 0.906, 0.145],
|
||||
});
|
||||
|
||||
Color.normalize(ColorMap);
|
||||
|
||||
ColorMap.sample = function (t, map = this) {
|
||||
if (t < 0) return map[0];
|
||||
if (t > 1) return map[1];
|
||||
|
||||
var lastkey = 0;
|
||||
for (var key of Object.keys(map).sort()) {
|
||||
if (t < key) {
|
||||
var b = map[key];
|
||||
var a = map[lastkey];
|
||||
var tt = (key - lastkey) * t;
|
||||
return a.lerp(b, tt);
|
||||
}
|
||||
lastkey = key;
|
||||
}
|
||||
return map[1];
|
||||
};
|
||||
|
||||
ColorMap.doc = {
|
||||
sample: "Sample a given colormap at the given percentage (0 to 1).",
|
||||
};
|
||||
|
||||
Color.maps = ColorMap
|
||||
Color.utils = esc
|
||||
|
||||
return Color
|
||||
@@ -1,5 +1,4 @@
|
||||
var input = use('input')
|
||||
var util = use('util')
|
||||
|
||||
var downkeys = {};
|
||||
|
||||
@@ -295,7 +294,7 @@ var Player = {
|
||||
},
|
||||
|
||||
print_pawns() {
|
||||
[...this.pawns].reverse().forEach(x => console.log(x))
|
||||
[...this.pawns].reverse().forEach(x => log.console(x))
|
||||
},
|
||||
|
||||
create() {
|
||||
35
prosperon/device.cm
Normal file
@@ -0,0 +1,35 @@
|
||||
// helpful render devices. width and height in pixels; diagonal in inches.
|
||||
return {
|
||||
pc: { width: 1920, height: 1080 },
|
||||
macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 },
|
||||
ds_top: { width: 400, height: 240, diagonal: 3.53 },
|
||||
ds_bottom: { width: 320, height: 240, diagonal: 3.02 },
|
||||
playdate: { width: 400, height: 240, diagonal: 2.7 },
|
||||
switch: { width: 1280, height: 720, diagonal: 6.2 },
|
||||
switch_lite: { width: 1280, height: 720, diagonal: 5.5 },
|
||||
switch_oled: { width: 1280, height: 720, diagonal: 7 },
|
||||
dsi: { width: 256, height: 192, diagonal: 3.268 },
|
||||
ds: { width: 256, height: 192, diagonal: 3 },
|
||||
dsixl: { width: 256, height: 192, diagonal: 4.2 },
|
||||
ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 },
|
||||
iphone_se: { width: 1334, height: 750, diagonal: 4.7 },
|
||||
iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 },
|
||||
iphone_15: { width: 2556, height: 1179, diagonal: 6.1 },
|
||||
gba: { width: 240, height: 160, diagonal: 2.9 },
|
||||
gameboy: { width: 160, height: 144, diagonal: 2.48 },
|
||||
gbc: { width: 160, height: 144, diagonal: 2.28 },
|
||||
steamdeck: { width: 1280, height: 800, diagonal: 7 },
|
||||
vita: { width: 960, height: 544, diagonal: 5 },
|
||||
psp: { width: 480, height: 272, diagonal: 4.3 },
|
||||
imac_m3: { width: 4480, height: 2520, diagonal: 23.5 },
|
||||
macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 },
|
||||
ps1: { width: 320, height: 240, diagonal: 5 },
|
||||
ps2: { width: 640, height: 480 },
|
||||
snes: { width: 256, height: 224 },
|
||||
gamecube: { width: 640, height: 480 },
|
||||
n64: { width: 320, height: 240 },
|
||||
c64: { width: 320, height: 200 },
|
||||
macintosh: { width: 512, height: 342 },
|
||||
gamegear: { width: 160, height: 144, diagonal: 3.2 }
|
||||
};
|
||||
|
||||
221
prosperon/draw2d.cm
Normal file
@@ -0,0 +1,221 @@
|
||||
var math = use('math')
|
||||
var color = use('color')
|
||||
|
||||
var draw = {}
|
||||
draw[cell.DOC] = `
|
||||
A collection of 2D drawing functions that create drawing command lists.
|
||||
These are pure functions that return plain JavaScript objects representing
|
||||
drawing operations. No rendering or actor communication happens here.
|
||||
`
|
||||
|
||||
// Create a new command list
|
||||
draw.list = function() {
|
||||
var commands = []
|
||||
|
||||
return {
|
||||
// Add a command to this list
|
||||
push: function(cmd) {
|
||||
commands.push(cmd)
|
||||
},
|
||||
|
||||
// Get all commands
|
||||
get: function() {
|
||||
return commands
|
||||
},
|
||||
|
||||
// Clear all commands
|
||||
clear: function() {
|
||||
commands = []
|
||||
},
|
||||
|
||||
// Get command count
|
||||
length: function() {
|
||||
return commands.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default command list for convenience
|
||||
var current_list = draw.list()
|
||||
|
||||
// Set the current list
|
||||
draw.set_list = function(list) {
|
||||
current_list = list
|
||||
}
|
||||
|
||||
// Get current list
|
||||
draw.get_list = function() {
|
||||
return current_list
|
||||
}
|
||||
|
||||
// Clear current list
|
||||
draw.clear = function() {
|
||||
current_list.clear()
|
||||
}
|
||||
|
||||
// Get commands from current list
|
||||
draw.get_commands = function() {
|
||||
return current_list.get()
|
||||
}
|
||||
|
||||
// Helper to add a command
|
||||
function add_command(type, data) {
|
||||
var cmd = {cmd: type}
|
||||
Object.assign(cmd, data)
|
||||
current_list.push(cmd)
|
||||
}
|
||||
|
||||
// Default geometry definitions
|
||||
var ellipse_def = {
|
||||
start: 0,
|
||||
end: 1,
|
||||
mode: 'fill',
|
||||
thickness: 1,
|
||||
}
|
||||
|
||||
var line_def = {
|
||||
thickness: 1,
|
||||
cap:"butt",
|
||||
}
|
||||
|
||||
var rect_def = {
|
||||
thickness:1,
|
||||
radius: 0
|
||||
}
|
||||
|
||||
var slice9_info = {
|
||||
tile_top:true,
|
||||
tile_bottom:true,
|
||||
tile_left:true,
|
||||
tile_right:true,
|
||||
tile_center_x:true,
|
||||
tile_center_right:true,
|
||||
}
|
||||
|
||||
var image_info = {
|
||||
tile_x: false,
|
||||
tile_y: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
mode: 'linear'
|
||||
}
|
||||
|
||||
var circle_def = {
|
||||
inner_radius:1, // percentage: 1 means filled circle
|
||||
start:0,
|
||||
end: 1,
|
||||
}
|
||||
|
||||
// Drawing functions
|
||||
draw.point = function(pos, size, opt = {}, material) {
|
||||
add_command("draw_point", {
|
||||
pos: pos,
|
||||
size: size,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
draw.point[cell.DOC] = `
|
||||
:param pos: A 2D position ([x, y]) where the point should be drawn.
|
||||
:param size: The size of the point.
|
||||
:param opt: Optional geometry properties.
|
||||
:param material: Material/styling information (color, shaders, etc.)
|
||||
:return: None
|
||||
`
|
||||
|
||||
draw.ellipse = function(pos, radii, def, material) {
|
||||
var opt = def ? {...ellipse_def, ...def} : ellipse_def
|
||||
if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1])
|
||||
|
||||
add_command("draw_ellipse", {
|
||||
pos: pos,
|
||||
radii: radii,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.line = function(points, def, material)
|
||||
{
|
||||
var opt = def ? {...line_def, ...def} : line_def
|
||||
|
||||
add_command("draw_line", {
|
||||
points: points,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.cross = function render_cross(pos, size, def, material) {
|
||||
var a = [pos.add([0, size]), pos.add([0, -size])]
|
||||
var b = [pos.add([size, 0]), pos.add([-size, 0])]
|
||||
draw.line(a, def, material)
|
||||
draw.line(b, def, material)
|
||||
}
|
||||
|
||||
draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, def, material) {
|
||||
var dir = math.norm(end.sub(start))
|
||||
var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end]
|
||||
var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end]
|
||||
draw.line([start, end], def, material)
|
||||
draw.line(wing1, def, material)
|
||||
draw.line(wing2, def, material)
|
||||
}
|
||||
|
||||
draw.rectangle = function render_rectangle(rect, def, material) {
|
||||
var opt = def ? {...rect_def, ...def} : rect_def
|
||||
|
||||
add_command("draw_rect", {
|
||||
rect: rect,
|
||||
opt: opt,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, material) {
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
|
||||
add_command("draw_slice9", {
|
||||
image: image,
|
||||
rect: rect,
|
||||
slice: slice,
|
||||
info: info,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.image = function image(image, rect, rotation = 0, anchor = [0,0], shear = [0,0], info = {}, material) {
|
||||
if (!rect) throw Error('Need rectangle to render image.')
|
||||
if (!image) throw Error('Need an image to render.')
|
||||
|
||||
if (!('x' in rect && 'y' in rect)) throw Error('Must provide X and Y for image.')
|
||||
|
||||
info = Object.assign({}, image_info, info);
|
||||
|
||||
add_command("draw_image", {
|
||||
image: image,
|
||||
rect: rect,
|
||||
rotation: rotation,
|
||||
anchor: anchor,
|
||||
shear: shear,
|
||||
info: info,
|
||||
material: material
|
||||
})
|
||||
}
|
||||
|
||||
draw.circle = function render_circle(pos, radius, def, material) {
|
||||
draw.ellipse(pos, [radius,radius], def, material)
|
||||
}
|
||||
|
||||
draw.text = function text(text, pos, font = 'fonts/c64.ttf', size = 8, color = color.white, wrap = 0) {
|
||||
add_command("draw_text", {
|
||||
text,
|
||||
pos,
|
||||
font,
|
||||
size,
|
||||
wrap,
|
||||
material: {color}
|
||||
})
|
||||
}
|
||||
|
||||
return draw
|
||||
@@ -1,7 +1,6 @@
|
||||
var Color = use('color')
|
||||
var os = use('os')
|
||||
var color = use('color')
|
||||
var graphics = use('graphics')
|
||||
//var render = use('render')
|
||||
var transform = use('transform')
|
||||
|
||||
var ex = {}
|
||||
|
||||
@@ -15,7 +14,7 @@ ex.garbage = function()
|
||||
ex.update = function(dt)
|
||||
{
|
||||
for (var e of ex.emitters)
|
||||
try { e.step(dt) } catch(e) { console.error(e) }
|
||||
try { e.step(dt) } catch(e) { log.error(e) }
|
||||
}
|
||||
|
||||
ex.step_hook = function(p)
|
||||
@@ -77,7 +76,7 @@ ex.spawn = function(t)
|
||||
}
|
||||
|
||||
par = {
|
||||
transform: os.make_transform(),
|
||||
transform: new transform,
|
||||
life: this.life,
|
||||
time: 0,
|
||||
color: this.color,
|
||||
@@ -104,14 +103,14 @@ ex.scale = 1
|
||||
ex.grow_for = 0
|
||||
ex.spawn_timer = 0
|
||||
ex.pps = 0
|
||||
ex.color = Color.white
|
||||
ex.color = color.white
|
||||
|
||||
ex.draw = function()
|
||||
{
|
||||
/* var diff = graphics.texture(this.diffuse)
|
||||
if (!diff) throw new Error("emitter does not have a proper diffuse texture")
|
||||
|
||||
var mesh = render._main.make_sprite_mesh(this.particles)
|
||||
var mesh = graphics.make_sprite_mesh(this.particles)
|
||||
if (mesh.num_indices === 0) return
|
||||
render.queue({
|
||||
type:'geometry',
|
||||
|
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 449 B |
@@ -5,6 +5,7 @@ var sprite = use('sprite')
|
||||
var geom = use('geometry')
|
||||
var input = use('controller')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
var bunnyTex = graphics.texture("bunny")
|
||||
|
||||
@@ -65,5 +66,5 @@ this.hud = function() {
|
||||
draw.images(bunnyTex, bunnies)
|
||||
|
||||
var msg = 'FPS: ' + fpsAvg.toFixed(2) + ' Bunnies: ' + bunnies.length
|
||||
draw.text(msg, {x:0, y:0, width:config.width, height:40}, undefined, 0, Color.white, 0)
|
||||
draw.text(msg, {x:0, y:0, width:config.width, height:40}, undefined, 0, color.white, 0)
|
||||
}
|
||||
BIN
prosperon/examples/chess/black_bishop.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
prosperon/examples/chess/black_king.png
Normal file
|
After Width: | Height: | Size: 438 B |
BIN
prosperon/examples/chess/black_knight.png
Normal file
|
After Width: | Height: | Size: 398 B |
BIN
prosperon/examples/chess/black_pawn.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
prosperon/examples/chess/black_queen.png
Normal file
|
After Width: | Height: | Size: 390 B |
BIN
prosperon/examples/chess/black_rook.png
Normal file
|
After Width: | Height: | Size: 379 B |
395
prosperon/examples/chess/chess.ce
Normal file
@@ -0,0 +1,395 @@
|
||||
/* main.js – runs the demo with your prototype-based grid */
|
||||
|
||||
var json = use('json')
|
||||
var draw2d = use('prosperon/draw2d')
|
||||
|
||||
var blob = use('blob')
|
||||
|
||||
/*──── import our pieces + systems ───────────────────────────────────*/
|
||||
var Grid = use('grid'); // your new ctor
|
||||
var MovementSystem = use('movement').MovementSystem;
|
||||
var startingPos = use('pieces').startingPosition;
|
||||
var rules = use('rules');
|
||||
|
||||
/*──── build board ───────────────────────────────────────────────────*/
|
||||
var grid = new Grid(8, 8);
|
||||
grid.width = 8; // (the ctor didn't store them)
|
||||
grid.height = 8;
|
||||
|
||||
var mover = new MovementSystem(grid, rules);
|
||||
startingPos(grid);
|
||||
|
||||
/*──── networking and game state ─────────────────────────────────────*/
|
||||
var gameState = 'waiting'; // 'waiting', 'searching', 'server_waiting', 'connected'
|
||||
var isServer = false;
|
||||
var opponent = null;
|
||||
var myColor = null; // 'white' or 'black'
|
||||
var isMyTurn = false;
|
||||
|
||||
function updateTitle() {
|
||||
var title = "Misty Chess - ";
|
||||
|
||||
switch(gameState) {
|
||||
case 'waiting':
|
||||
title += "Press S to start server or J to join";
|
||||
break;
|
||||
case 'searching':
|
||||
title += "Searching for server...";
|
||||
break;
|
||||
case 'server_waiting':
|
||||
title += "Waiting for player to join...";
|
||||
break;
|
||||
case 'connected':
|
||||
if (myColor) {
|
||||
title += (mover.turn === myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")");
|
||||
} else {
|
||||
title += mover.turn + " turn";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
log.console(title)
|
||||
}
|
||||
|
||||
// Initialize title
|
||||
updateTitle();
|
||||
|
||||
/*──── mouse → click-to-move ─────────────────────────────────────────*/
|
||||
var selectPos = null;
|
||||
var hoverPos = null;
|
||||
var holdingPiece = false;
|
||||
|
||||
var opponentMousePos = null;
|
||||
var opponentHoldingPiece = false;
|
||||
var opponentSelectPos = null;
|
||||
|
||||
function handleMouseButtonDown(e) {
|
||||
if (e.which !== 0) return;
|
||||
|
||||
// Don't allow piece selection unless we have an opponent
|
||||
if (gameState !== 'connected' || !opponent) return;
|
||||
|
||||
var mx = e.mouse.x;
|
||||
var my = e.mouse.y;
|
||||
|
||||
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
|
||||
if (!grid.inBounds(c)) return;
|
||||
|
||||
var cell = grid.at(c);
|
||||
if (cell.length && cell[0].colour === mover.turn) {
|
||||
selectPos = c;
|
||||
holdingPiece = true;
|
||||
// Send pickup notification to opponent
|
||||
if (opponent) {
|
||||
send(opponent, {
|
||||
type: 'piece_pickup',
|
||||
pos: c
|
||||
});
|
||||
}
|
||||
} else {
|
||||
selectPos = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseButtonUp(e) {
|
||||
if (e.which !== 0 || !holdingPiece || !selectPos) return;
|
||||
|
||||
// Don't allow moves unless we have an opponent and it's our turn
|
||||
if (gameState !== 'connected' || !opponent || !isMyTurn) {
|
||||
holdingPiece = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var mx = e.mouse.x;
|
||||
var my = e.mouse.y;
|
||||
|
||||
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
|
||||
if (!grid.inBounds(c)) {
|
||||
holdingPiece = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mover.tryMove(grid.at(selectPos)[0], c)) {
|
||||
log.console("Made move from", selectPos, "to", c);
|
||||
// Send move to opponent
|
||||
log.console("Sending move to opponent:", opponent);
|
||||
send(opponent, {
|
||||
type: 'move',
|
||||
from: selectPos,
|
||||
to: c
|
||||
});
|
||||
isMyTurn = false; // It's now opponent's turn
|
||||
log.console("Move sent, now opponent's turn");
|
||||
selectPos = null;
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
holdingPiece = false;
|
||||
|
||||
// Send piece drop notification to opponent
|
||||
if (opponent) {
|
||||
send(opponent, {
|
||||
type: 'piece_drop'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMotion(e) {
|
||||
var mx = e.pos.x;
|
||||
var my = e.pos.y;
|
||||
|
||||
var c = [Math.floor(mx / 60), Math.floor(my / 60)];
|
||||
if (!grid.inBounds(c)) {
|
||||
hoverPos = null;
|
||||
return;
|
||||
}
|
||||
|
||||
hoverPos = c;
|
||||
|
||||
// Send mouse position to opponent in real-time
|
||||
if (opponent && gameState === 'connected') {
|
||||
send(opponent, {
|
||||
type: 'mouse_move',
|
||||
pos: c,
|
||||
holding: holdingPiece,
|
||||
selectPos: selectPos
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
// S key - start server
|
||||
if (e.scancode === 22 && gameState === 'waiting') { // S key
|
||||
startServer();
|
||||
}
|
||||
// J key - join server
|
||||
else if (e.scancode === 13 && gameState === 'waiting') { // J key
|
||||
joinServer();
|
||||
}
|
||||
}
|
||||
|
||||
/*──── drawing helpers ───────────────────────────────────────────────*/
|
||||
/* ── constants ─────────────────────────────────────────────────── */
|
||||
var S = 60; // square size in px
|
||||
var light = [0.93,0.93,0.93,1];
|
||||
var dark = [0.25,0.25,0.25,1];
|
||||
var allowedColor = [1.0, 0.84, 0.0, 1.0]; // Gold for allowed moves
|
||||
var myMouseColor = [0.0, 1.0, 0.0, 1.0]; // Green for my mouse
|
||||
var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse
|
||||
|
||||
/* ── draw one 8×8 chess board ──────────────────────────────────── */
|
||||
function drawBoard() {
|
||||
for (var y = 0; y < 8; ++y)
|
||||
for (var x = 0; x < 8; ++x) {
|
||||
var isMyHover = hoverPos && hoverPos[0] === x && hoverPos[1] === y;
|
||||
var isOpponentHover = opponentMousePos && opponentMousePos[0] === x && opponentMousePos[1] === y;
|
||||
var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]);
|
||||
|
||||
var color = ((x+y)&1) ? dark : light;
|
||||
|
||||
if (isValidMove) {
|
||||
color = allowedColor; // Gold for allowed moves
|
||||
} else if (isMyHover && !isOpponentHover) {
|
||||
color = myMouseColor; // Green for my mouse
|
||||
} else if (isOpponentHover) {
|
||||
color = opponentMouseColor; // Red for opponent mouse
|
||||
}
|
||||
|
||||
draw2d.rectangle(
|
||||
{ x: x*S, y: y*S, width: S, height: S },
|
||||
{ thickness: 0 },
|
||||
{ color: color }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isValidMoveForTurn(from, to) {
|
||||
if (!grid.inBounds(to)) return false;
|
||||
|
||||
var piece = grid.at(from)[0];
|
||||
if (!piece) return false;
|
||||
|
||||
// Check if the destination has a piece of the same color
|
||||
var destCell = grid.at(to);
|
||||
if (destCell.length && destCell[0].colour === piece.colour) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rules.canMove(piece, from, to, grid);
|
||||
}
|
||||
|
||||
/* ── draw every live piece ─────────────────────────────────────── */
|
||||
function drawPieces() {
|
||||
grid.each(function (piece) {
|
||||
if (piece.captured) return;
|
||||
|
||||
// Skip drawing the piece being held (by me or opponent)
|
||||
if (holdingPiece && selectPos &&
|
||||
piece.coord[0] === selectPos[0] &&
|
||||
piece.coord[1] === selectPos[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip drawing the piece being held by opponent
|
||||
if (opponentHoldingPiece && opponentSelectPos &&
|
||||
piece.coord[0] === opponentSelectPos[0] &&
|
||||
piece.coord[1] === opponentSelectPos[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var r = { x: piece.coord[0]*S, y: piece.coord[1]*S,
|
||||
width:S, height:S };
|
||||
|
||||
draw2d.image(piece.sprite, r);
|
||||
});
|
||||
|
||||
// Draw the held piece at the mouse position if we're holding one
|
||||
if (holdingPiece && selectPos && hoverPos) {
|
||||
var piece = grid.at(selectPos)[0];
|
||||
if (piece) {
|
||||
var r = { x: hoverPos[0]*S, y: hoverPos[1]*S,
|
||||
width:S, height:S };
|
||||
|
||||
draw2d.image(piece.sprite, r);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw opponent's held piece if they're dragging one
|
||||
if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) {
|
||||
var opponentPiece = grid.at(opponentSelectPos)[0];
|
||||
if (opponentPiece) {
|
||||
var r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S,
|
||||
width:S, height:S };
|
||||
|
||||
// Draw with slight transparency to show it's the opponent's piece
|
||||
draw2d.image(opponentPiece.sprite, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function update(dt)
|
||||
{
|
||||
return {}
|
||||
}
|
||||
|
||||
function draw()
|
||||
{
|
||||
draw2d.clear()
|
||||
drawBoard()
|
||||
drawPieces()
|
||||
return draw2d.get_commands()
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
gameState = 'server_waiting';
|
||||
isServer = true;
|
||||
myColor = 'white';
|
||||
isMyTurn = true;
|
||||
updateTitle();
|
||||
|
||||
$_.portal(e => {
|
||||
log.console("Portal received contact message");
|
||||
// Reply with this actor to establish connection
|
||||
log.console (json.encode($_))
|
||||
send(e, $_);
|
||||
log.console("Portal replied with server actor");
|
||||
}, 5678);
|
||||
}
|
||||
|
||||
function joinServer() {
|
||||
gameState = 'searching';
|
||||
updateTitle();
|
||||
|
||||
function contact_fn(actor, reason) {
|
||||
log.console("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason);
|
||||
if (actor) {
|
||||
opponent = actor;
|
||||
log.console("Connection established with server, sending join request");
|
||||
|
||||
// Send a greet message with our actor object
|
||||
send(opponent, {
|
||||
type: 'greet',
|
||||
client_actor: $_
|
||||
});
|
||||
} else {
|
||||
log.console(`Failed to connect: ${json.encode(reason)}`);
|
||||
gameState = 'waiting';
|
||||
updateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
$_.contact(contact_fn, {
|
||||
address: "192.168.0.149",
|
||||
port: 5678
|
||||
});
|
||||
}
|
||||
|
||||
$_.receiver(e => {
|
||||
if (e.kind == 'update')
|
||||
send(e, update(e.dt))
|
||||
else if (e.kind == 'draw')
|
||||
send(e, draw())
|
||||
else if (e.type === 'game_start' || e.type === 'move' || e.type === 'greet')
|
||||
log.console("Receiver got message:", e.type, e);
|
||||
|
||||
if (e.type === 'greet') {
|
||||
log.console("Server received greet from client");
|
||||
// Store the client's actor object for ongoing communication
|
||||
opponent = e.client_actor;
|
||||
log.console("Stored client actor:", json.encode(opponent));
|
||||
gameState = 'connected';
|
||||
updateTitle();
|
||||
|
||||
// Send game_start to the client
|
||||
log.console("Sending game_start to client");
|
||||
send(opponent, {
|
||||
type: 'game_start',
|
||||
your_color: 'black'
|
||||
});
|
||||
log.console("game_start message sent to client");
|
||||
}
|
||||
else if (e.type === 'game_start') {
|
||||
log.console("Game starting, I am:", e.your_color);
|
||||
myColor = e.your_color;
|
||||
isMyTurn = (myColor === 'white');
|
||||
gameState = 'connected';
|
||||
updateTitle();
|
||||
} else if (e.type === 'move') {
|
||||
log.console("Received move from opponent:", e.from, "to", e.to);
|
||||
// Apply opponent's move
|
||||
var fromCell = grid.at(e.from);
|
||||
if (fromCell.length) {
|
||||
var piece = fromCell[0];
|
||||
if (mover.tryMove(piece, e.to)) {
|
||||
isMyTurn = true; // It's now our turn
|
||||
updateTitle();
|
||||
log.console("Applied opponent move, now my turn");
|
||||
} else {
|
||||
log.console("Failed to apply opponent move");
|
||||
}
|
||||
} else {
|
||||
log.console("No piece found at from position");
|
||||
}
|
||||
} else if (e.type === 'mouse_move') {
|
||||
// Update opponent's mouse position
|
||||
opponentMousePos = e.pos;
|
||||
opponentHoldingPiece = e.holding;
|
||||
opponentSelectPos = e.selectPos;
|
||||
} else if (e.type === 'piece_pickup') {
|
||||
// Opponent picked up a piece
|
||||
opponentSelectPos = e.pos;
|
||||
opponentHoldingPiece = true;
|
||||
} else if (e.type === 'piece_drop') {
|
||||
// Opponent dropped their piece
|
||||
opponentHoldingPiece = false;
|
||||
opponentSelectPos = null;
|
||||
} else if (e.type === 'mouse_button_down') {
|
||||
handleMouseButtonDown(e)
|
||||
} else if (e.type === 'mouse_button_up') {
|
||||
handleMouseButtonUp(e)
|
||||
} else if (e.type === 'mouse_motion') {
|
||||
handleMouseMotion(e)
|
||||
} else if (e.type === 'key_down') {
|
||||
handleKeyDown(e)
|
||||
}
|
||||
})
|
||||
9
prosperon/examples/chess/config.cm
Normal file
@@ -0,0 +1,9 @@
|
||||
// Chess game configuration for Moth framework
|
||||
return {
|
||||
title: "Chess",
|
||||
resolution: { width: 480, height: 480 },
|
||||
internal_resolution: { width: 480, height: 480 },
|
||||
fps: 60,
|
||||
clearColor: [22/255, 120/255, 194/255, 1],
|
||||
mode: 'stretch' // No letterboxing for chess
|
||||
};
|
||||
57
prosperon/examples/chess/grid.cm
Normal file
@@ -0,0 +1,57 @@
|
||||
var CELLS = Symbol()
|
||||
|
||||
var key = function key(x,y) { return `${x},${y}` }
|
||||
|
||||
function grid(w, h)
|
||||
{
|
||||
this[CELLS] = new Map()
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
|
||||
grid.prototype = {
|
||||
cell(x,y) {
|
||||
var k = key(x,y)
|
||||
if (!this[CELLS].has(k)) this[CELLS].set(k,[])
|
||||
return this[CELLS].get(k)
|
||||
},
|
||||
|
||||
add(entity, pos) {
|
||||
this.cell(pos.x, pos.y).push(entity);
|
||||
entity.coord = pos.slice();
|
||||
},
|
||||
|
||||
remove(entity, pos) {
|
||||
var c = this.cell(pos.x, pos.y);
|
||||
c.splice(c.indexOf(entity), 1);
|
||||
},
|
||||
|
||||
at(pos) {
|
||||
return this.cell(pos.x, pos.y);
|
||||
},
|
||||
|
||||
inBounds(pos) {
|
||||
return pos.x >= 0 && pos.x < this.width && pos.y >= 0 && pos.y < this.height;
|
||||
},
|
||||
|
||||
each(fn) {
|
||||
for (var [k, list] of this[CELLS])
|
||||
for (var p of list) fn(p, p.coord);
|
||||
},
|
||||
|
||||
toString() {
|
||||
var out = `grid [${this.width}x${this.height}]
|
||||
`
|
||||
for (var y = 0; y < this.height; y++) {
|
||||
for (var x = 0; x < this.width; x++) {
|
||||
var cell = this.at([x,y]);
|
||||
out += cell.length
|
||||
}
|
||||
if (y !== this.height - 1) out += "\n"
|
||||
}
|
||||
|
||||
return out
|
||||
},
|
||||
}
|
||||
|
||||
return grid
|
||||
32
prosperon/examples/chess/movement.cm
Normal file
@@ -0,0 +1,32 @@
|
||||
var MovementSystem = function(grid, rules) {
|
||||
this.grid = grid;
|
||||
this.rules = rules || {}; // expects { canMove: fn }
|
||||
this.turn = 'white';
|
||||
}
|
||||
|
||||
MovementSystem.prototype.tryMove = function (piece, to) {
|
||||
if (piece.colour !== this.turn) return false;
|
||||
|
||||
// normalise ‘to’ into our hybrid coord
|
||||
var dest = [to.x !== undefined ? to.x : to[0],
|
||||
to.y !== undefined ? to.y : to[1]];
|
||||
|
||||
if (!this.grid.inBounds(dest)) return false;
|
||||
if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false;
|
||||
|
||||
var victims = this.grid.at(dest);
|
||||
if (victims.length && victims[0].colour === piece.colour) return false;
|
||||
if (victims.length) victims[0].captured = true;
|
||||
|
||||
this.grid.remove(piece, piece.coord);
|
||||
this.grid.add (piece, dest);
|
||||
|
||||
// grid.add() re-creates coord; re-add .x/.y fields:
|
||||
piece.coord.x = dest.x;
|
||||
piece.coord.y = dest.y;
|
||||
|
||||
this.turn = (this.turn === 'white') ? 'black' : 'white';
|
||||
return true;
|
||||
};
|
||||
|
||||
return { MovementSystem: MovementSystem };
|
||||
29
prosperon/examples/chess/pieces.cm
Normal file
@@ -0,0 +1,29 @@
|
||||
/* pieces.js – simple data holders + starting layout */
|
||||
function Piece(kind, colour) {
|
||||
this.kind = kind; // "pawn" etc.
|
||||
this.colour = colour; // "white"/"black"
|
||||
this.sprite = colour + '_' + kind; // for draw2d.image
|
||||
this.captured = false;
|
||||
this.coord = [0,0];
|
||||
}
|
||||
Piece.prototype.toString = function () {
|
||||
return this.colour.charAt(0) + this.kind.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
function startingPosition(grid) {
|
||||
var W = 'white', B = 'black', x;
|
||||
|
||||
// pawns
|
||||
for (x = 0; x < 8; x++) {
|
||||
grid.add(new Piece('pawn', W), [x, 6]);
|
||||
grid.add(new Piece('pawn', B), [x, 1]);
|
||||
}
|
||||
// major pieces
|
||||
var back = ['rook','knight','bishop','queen','king','bishop','knight','rook'];
|
||||
for (x = 0; x < 8; x++) {
|
||||
grid.add(new Piece(back[x], W), [x, 7]);
|
||||
grid.add(new Piece(back[x], B), [x, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
return { Piece, startingPosition };
|
||||
BIN
prosperon/examples/chess/prosperon
Executable file
45
prosperon/examples/chess/rules.cm
Normal file
@@ -0,0 +1,45 @@
|
||||
/* helper – robust coord access */
|
||||
function cx(c) { return (c.x !== undefined) ? c.x : c[0]; }
|
||||
function cy(c) { return (c.y !== undefined) ? c.y : c[1]; }
|
||||
|
||||
/* simple move-shape checks */
|
||||
var deltas = {
|
||||
pawn: function (pc, dx, dy, grid, to) {
|
||||
var dir = (pc.colour === 'white') ? -1 : 1;
|
||||
var base = (pc.colour === 'white') ? 6 : 1;
|
||||
var one = (dy === dir && dx === 0 && grid.at(to).length === 0);
|
||||
var two = (dy === 2 * dir && dx === 0 && cy(pc.coord) === base &&
|
||||
grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir }).length === 0 &&
|
||||
grid.at(to).length === 0);
|
||||
var cap = (dy === dir && Math.abs(dx) === 1 && grid.at(to).length);
|
||||
return one || two || cap;
|
||||
},
|
||||
rook : function (pc, dx, dy) { return (dx === 0 || dy === 0); },
|
||||
bishop: function (pc, dx, dy) { return Math.abs(dx) === Math.abs(dy); },
|
||||
queen : function (pc, dx, dy) { return (dx === 0 || dy === 0 || Math.abs(dx) === Math.abs(dy)); },
|
||||
knight: function (pc, dx, dy) { return (Math.abs(dx) === 1 && Math.abs(dy) === 2) ||
|
||||
(Math.abs(dx) === 2 && Math.abs(dy) === 1); },
|
||||
king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) === 1; }
|
||||
};
|
||||
|
||||
function clearLine(from, to, grid) {
|
||||
var dx = Math.sign(cx(to) - cx(from));
|
||||
var dy = Math.sign(cy(to) - cy(from));
|
||||
var x = cx(from) + dx, y = cy(from) + dy;
|
||||
while (x !== cx(to) || y !== cy(to)) {
|
||||
if (grid.at({ x: x, y: y }).length) return false;
|
||||
x += dx; y += dy;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function canMove(piece, from, to, grid) {
|
||||
var dx = cx(to) - cx(from);
|
||||
var dy = cy(to) - cy(from);
|
||||
var f = deltas[piece.kind];
|
||||
if (!f || !f(piece, dx, dy, grid, to)) return false;
|
||||
if (piece.kind === 'knight') return true;
|
||||
return clearLine(from, to, grid);
|
||||
}
|
||||
|
||||
return { canMove };
|
||||
BIN
prosperon/examples/chess/white_bishop.png
Normal file
|
After Width: | Height: | Size: 376 B |
BIN
prosperon/examples/chess/white_king.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
prosperon/examples/chess/white_knight.png
Normal file
|
After Width: | Height: | Size: 381 B |
BIN
prosperon/examples/chess/white_pawn.png
Normal file
|
After Width: | Height: | Size: 313 B |
BIN
prosperon/examples/chess/white_queen.png
Normal file
|
After Width: | Height: | Size: 378 B |
BIN
prosperon/examples/chess/white_rook.png
Normal file
|
After Width: | Height: | Size: 378 B |
@@ -2,6 +2,7 @@
|
||||
var draw = use('draw2d')
|
||||
var input = use('controller')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
prosperon.camera.transform.pos = [0,0]
|
||||
|
||||
@@ -73,13 +74,13 @@ this.hud = function() {
|
||||
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
|
||||
|
||||
// Draw paddles
|
||||
draw.rectangle({x:p1.x - paddleW*0.5, y:p1.y - paddleH*0.5, width:paddleW, height:paddleH}, Color.white)
|
||||
draw.rectangle({x:p2.x - paddleW*0.5, y:p2.y - paddleH*0.5, width:paddleW, height:paddleH}, Color.white)
|
||||
draw.rectangle({x:p1.x - paddleW*0.5, y:p1.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white)
|
||||
draw.rectangle({x:p2.x - paddleW*0.5, y:p2.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white)
|
||||
|
||||
// Draw ball
|
||||
draw.rectangle({x:ball.x - ball.size*0.5, y:ball.y - ball.size*0.5, width:ball.size, height:ball.size}, Color.white)
|
||||
draw.rectangle({x:ball.x - ball.size*0.5, y:ball.y - ball.size*0.5, width:ball.size, height:ball.size}, color.white)
|
||||
|
||||
// Simple score display
|
||||
var msg = score1 + " " + score2
|
||||
draw.text(msg, {x:0, y:10, width:config.width, height:40}, undefined, 0, Color.white, 0)
|
||||
draw.text(msg, {x:0, y:10, width:config.width, height:40}, undefined, 0, color.white, 0)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ var render = use('render')
|
||||
var graphics = use('graphics')
|
||||
var input = use('input')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
prosperon.camera.transform.pos = [0,0]
|
||||
|
||||
@@ -83,15 +84,15 @@ this.hud = function() {
|
||||
// Draw snake
|
||||
for (var i=0; i<snake.length; i++) {
|
||||
var s = snake[i]
|
||||
draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, Color.green)
|
||||
draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, color.green)
|
||||
}
|
||||
|
||||
// Draw apple
|
||||
draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, Color.red)
|
||||
draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, color.red)
|
||||
|
||||
if (gameState === "gameover") {
|
||||
var msg = "GAME OVER! Press SPACE to restart."
|
||||
draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, undefined, 0, Color.white)
|
||||
draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, undefined, 0, color.white)
|
||||
}
|
||||
}
|
||||
|
||||
187
prosperon/examples/steam_example.ce
Normal file
@@ -0,0 +1,187 @@
|
||||
// Steam Integration Example
|
||||
// This example shows how to use Steam achievements and stats
|
||||
|
||||
var steam = use("steam");
|
||||
|
||||
// Achievement names (these should match your Steam app configuration)
|
||||
var ACHIEVEMENTS = {
|
||||
FIRST_WIN: "ACH_FIRST_WIN",
|
||||
PLAY_10_GAMES: "ACH_PLAY_10_GAMES",
|
||||
HIGH_SCORE: "ACH_HIGH_SCORE_1000"
|
||||
};
|
||||
|
||||
// Stat names
|
||||
var STATS = {
|
||||
GAMES_PLAYED: "stat_games_played",
|
||||
TOTAL_SCORE: "stat_total_score",
|
||||
PLAY_TIME: "stat_play_time"
|
||||
};
|
||||
|
||||
var steam_available = false;
|
||||
var stats_loaded = false;
|
||||
|
||||
// Initialize Steam
|
||||
function init_steam() {
|
||||
if (!steam) {
|
||||
log.console("Steam module not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.console("Initializing Steam...");
|
||||
steam_available = steam.steam_init();
|
||||
|
||||
if (steam_available) {
|
||||
log.console("Steam initialized successfully");
|
||||
|
||||
// Request current stats/achievements
|
||||
if (steam.stats.stats_request()) {
|
||||
log.console("Stats requested");
|
||||
stats_loaded = true;
|
||||
}
|
||||
} else {
|
||||
log.console("Failed to initialize Steam");
|
||||
}
|
||||
|
||||
return steam_available;
|
||||
}
|
||||
|
||||
// Update Steam (call this regularly, e.g., once per frame)
|
||||
function update_steam() {
|
||||
if (steam_available) {
|
||||
steam.steam_run_callbacks();
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock an achievement
|
||||
function unlock_achievement(achievement_name) {
|
||||
if (!steam_available || !stats_loaded) return false;
|
||||
|
||||
// Check if already unlocked
|
||||
var unlocked = steam.achievement.achievement_get(achievement_name);
|
||||
if (unlocked) {
|
||||
log.console("Achievement already unlocked:", achievement_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unlock it
|
||||
if (steam.achievement.achievement_set(achievement_name)) {
|
||||
log.console("Achievement unlocked:", achievement_name);
|
||||
|
||||
// Store stats to make it permanent
|
||||
steam.stats.stats_store();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update a stat
|
||||
function update_stat(stat_name, value, is_float) {
|
||||
if (!steam_available || !stats_loaded) return false;
|
||||
|
||||
var success;
|
||||
if (is_float) {
|
||||
success = steam.stats.stats_set_float(stat_name, value);
|
||||
} else {
|
||||
success = steam.stats.stats_set_int(stat_name, value);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
log.console("Stat updated:", stat_name, "=", value);
|
||||
steam.stats.stats_store();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Get a stat value
|
||||
function get_stat(stat_name, is_float) {
|
||||
if (!steam_available || !stats_loaded) return 0;
|
||||
|
||||
if (is_float) {
|
||||
return steam.stats.stats_get_float(stat_name) || 0;
|
||||
} else {
|
||||
return steam.stats.stats_get_int(stat_name) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Example game logic
|
||||
var games_played = 0;
|
||||
var total_score = 0;
|
||||
var current_score = 0;
|
||||
|
||||
function start_game() {
|
||||
games_played = get_stat(STATS.GAMES_PLAYED, false);
|
||||
total_score = get_stat(STATS.TOTAL_SCORE, false);
|
||||
current_score = 0;
|
||||
|
||||
log.console("Starting game #" + (games_played + 1));
|
||||
}
|
||||
|
||||
function end_game(score) {
|
||||
current_score = score;
|
||||
games_played++;
|
||||
total_score += score;
|
||||
|
||||
// Update stats
|
||||
update_stat(STATS.GAMES_PLAYED, games_played, false);
|
||||
update_stat(STATS.TOTAL_SCORE, total_score, false);
|
||||
|
||||
// Check for achievements
|
||||
if (games_played === 1) {
|
||||
unlock_achievement(ACHIEVEMENTS.FIRST_WIN);
|
||||
}
|
||||
|
||||
if (games_played >= 10) {
|
||||
unlock_achievement(ACHIEVEMENTS.PLAY_10_GAMES);
|
||||
}
|
||||
|
||||
if (score >= 1000) {
|
||||
unlock_achievement(ACHIEVEMENTS.HIGH_SCORE);
|
||||
}
|
||||
}
|
||||
|
||||
// Cloud save example
|
||||
function save_to_cloud(save_data) {
|
||||
if (!steam_available) return false;
|
||||
|
||||
var json_data = JSON.stringify(save_data);
|
||||
return steam.cloud.cloud_write("savegame.json", json_data);
|
||||
}
|
||||
|
||||
function load_from_cloud() {
|
||||
if (!steam_available) return null;
|
||||
|
||||
var data = steam.cloud.cloud_read("savegame.json");
|
||||
if (data) {
|
||||
// Convert ArrayBuffer to string
|
||||
var decoder = new TextDecoder();
|
||||
var json_str = decoder.decode(data);
|
||||
return JSON.parse(json_str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
function cleanup_steam() {
|
||||
if (steam_available) {
|
||||
steam.steam_shutdown();
|
||||
log.console("Steam shut down");
|
||||
}
|
||||
}
|
||||
|
||||
// Export the API
|
||||
module.exports = {
|
||||
init: init_steam,
|
||||
update: update_steam,
|
||||
cleanup: cleanup_steam,
|
||||
unlock_achievement: unlock_achievement,
|
||||
update_stat: update_stat,
|
||||
get_stat: get_stat,
|
||||
start_game: start_game,
|
||||
end_game: end_game,
|
||||
save_to_cloud: save_to_cloud,
|
||||
load_from_cloud: load_from_cloud,
|
||||
is_available: function() { return steam_available; }
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
var draw = use('draw2d')
|
||||
var input = use('input')
|
||||
var config = use('config')
|
||||
var color = use('color')
|
||||
|
||||
prosperon.camera.transform.pos = [0,0]
|
||||
|
||||
@@ -248,7 +249,7 @@ this.hud = function() {
|
||||
}
|
||||
|
||||
// Next piece window
|
||||
draw.text("Next", {x:70, y:5, width:50, height:10}, undefined, 0, Color.white)
|
||||
draw.text("Next", {x:70, y:5, width:50, height:10}, undefined, 0, color.white)
|
||||
if (nextPiece) {
|
||||
for (var i=0; i<nextPiece.blocks.length; i++) {
|
||||
var nx = nextPiece.blocks[i][0]
|
||||
@@ -261,10 +262,10 @@ this.hud = function() {
|
||||
|
||||
// Score & Level
|
||||
var info = "Score: " + score + "\nLines: " + linesCleared + "\nLevel: " + level
|
||||
draw.text(info, {x:70, y:30, width:90, height:50}, undefined, 0, Color.white)
|
||||
draw.text(info, {x:70, y:30, width:90, height:50}, undefined, 0, color.white)
|
||||
|
||||
if (gameOver) {
|
||||
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, undefined, 0, Color.red)
|
||||
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, undefined, 0, color.red)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
var geometry = this
|
||||
geometry[prosperon.DOC] = `
|
||||
geometry[cell.DOC] = `
|
||||
A collection of geometry-related functions for circles, spheres, boxes, polygons,
|
||||
and rectangle utilities. Some functionality is implemented in C and exposed here.
|
||||
`
|
||||
@@ -7,7 +7,7 @@ and rectangle utilities. Some functionality is implemented in C and exposed here
|
||||
var math = use('math')
|
||||
|
||||
geometry.box = {}
|
||||
geometry.box[prosperon.DOC] = `
|
||||
geometry.box[cell.DOC] = `
|
||||
An object for box-related operations. Overridden later by a function definition, so
|
||||
its direct usage is overshadowed. Contains:
|
||||
- points(ll, ur): Return an array of four 2D points for a box from ll (lower-left) to ur (upper-right).
|
||||
@@ -16,7 +16,7 @@ its direct usage is overshadowed. Contains:
|
||||
geometry.box.points = function (ll, ur) {
|
||||
return [ll, ll.add([ur.x - ll.x, 0]), ur, ll.add([0, ur.y - ll.y])]
|
||||
}
|
||||
geometry.box.points[prosperon.DOC] = `
|
||||
geometry.box.points[cell.DOC] = `
|
||||
:param ll: Lower-left coordinate as a 2D vector (x,y).
|
||||
:param ur: Upper-right coordinate as a 2D vector (x,y).
|
||||
:return: An array of four points forming the corners of the box in order [ll, lower-right, ur, upper-left].
|
||||
@@ -24,14 +24,14 @@ Compute the four corners of a box given lower-left and upper-right corners.
|
||||
`
|
||||
|
||||
geometry.sphere = {}
|
||||
geometry.sphere[prosperon.DOC] = `
|
||||
geometry.sphere[cell.DOC] = `
|
||||
Sphere-related geometry functions:
|
||||
- volume(r): Return the volume of a sphere with radius r.
|
||||
- random(r, theta, phi): Return a random point on or inside a sphere.
|
||||
`
|
||||
|
||||
geometry.circle = {}
|
||||
geometry.circle[prosperon.DOC] = `
|
||||
geometry.circle[cell.DOC] = `
|
||||
Circle-related geometry functions:
|
||||
- area(r): Return the area of a circle with radius r.
|
||||
- random(r, theta): Return a random 2D point on a circle; uses sphere.random internally and extracts x,z.
|
||||
@@ -40,7 +40,7 @@ Circle-related geometry functions:
|
||||
geometry.sphere.volume = function (r) {
|
||||
return (Math.pi * r * r * r * 4) / 3
|
||||
}
|
||||
geometry.sphere.volume[prosperon.DOC] = `
|
||||
geometry.sphere.volume[cell.DOC] = `
|
||||
:param r: The sphere radius.
|
||||
:return: The volume of the sphere, calculated as (4/3) * pi * r^3.
|
||||
`
|
||||
@@ -55,7 +55,7 @@ geometry.sphere.random = function (r, theta = [0, 1], phi = [-0.5, 0.5]) {
|
||||
var pa = Math.turn2rad(Math.random_range(phi[0], phi[1]))
|
||||
return [ra * Math.sin(ta) * Math.cos(pa), ra * Math.sin(ta) * Math.sin(pa), ra * Math.cos(ta)]
|
||||
}
|
||||
geometry.sphere.random[prosperon.DOC] = `
|
||||
geometry.sphere.random[cell.DOC] = `
|
||||
:param r: A single number (radius) or a 2-element array [minRadius, maxRadius].
|
||||
:param theta: A single number or 2-element array defining the range in turns for the theta angle, default [0,1].
|
||||
:param phi: A single number or 2-element array defining the range in turns for the phi angle, default [-0.5,0.5].
|
||||
@@ -66,7 +66,7 @@ Generate a random point inside a sphere of variable radius, distributing angles
|
||||
geometry.circle.area = function (r) {
|
||||
return Math.pi * r * r
|
||||
}
|
||||
geometry.circle.area[prosperon.DOC] = `
|
||||
geometry.circle.area[cell.DOC] = `
|
||||
:param r: Radius of the circle.
|
||||
:return: The area, pi * r^2.
|
||||
`
|
||||
@@ -74,7 +74,7 @@ geometry.circle.area[prosperon.DOC] = `
|
||||
geometry.circle.random = function (r, theta) {
|
||||
return geometry.sphere.random(r, theta).xz
|
||||
}
|
||||
geometry.circle.random[prosperon.DOC] = `
|
||||
geometry.circle.random[cell.DOC] = `
|
||||
:param r: A radius or [minRadius, maxRadius].
|
||||
:param theta: Angle range in turns (single number or [min,max]).
|
||||
:return: A 2D point (x,z) in the circle, using the sphere random generator and ignoring y.
|
||||
@@ -91,7 +91,7 @@ geometry.box = function (w, h) {
|
||||
]
|
||||
return points
|
||||
}
|
||||
geometry.box[prosperon.DOC] = `
|
||||
geometry.box[cell.DOC] = `
|
||||
:param w: The width of the box.
|
||||
:param h: The height of the box.
|
||||
:return: An array of four 2D points representing the corners of a rectangle centered at [0,0].
|
||||
@@ -101,7 +101,7 @@ Construct a box centered at the origin with the given width and height. This ove
|
||||
geometry.ngon = function (radius, n) {
|
||||
return geometry.arc(radius, 360, n)
|
||||
}
|
||||
geometry.ngon[prosperon.DOC] = `
|
||||
geometry.ngon[cell.DOC] = `
|
||||
:param radius: The radius of the n-gon from center to each vertex.
|
||||
:param n: Number of sides/vertices.
|
||||
:return: An array of 2D points forming a regular n-gon.
|
||||
@@ -118,7 +118,7 @@ geometry.arc = function (radius, angle, n, start = 0) {
|
||||
for (var i = 0; i < n; i++) points.push(math.rotate([radius, 0], start + arclen * i))
|
||||
return points
|
||||
}
|
||||
geometry.arc[prosperon.DOC] = `
|
||||
geometry.arc[cell.DOC] = `
|
||||
:param radius: The distance from center to the arc points.
|
||||
:param angle: The total angle (in degrees) over which points are generated, capped at 360.
|
||||
:param n: Number of segments (if <=1, empty array is returned).
|
||||
@@ -131,7 +131,7 @@ geometry.circle.points = function (radius, n) {
|
||||
if (n <= 1) return []
|
||||
return geometry.arc(radius, 360, n)
|
||||
}
|
||||
geometry.circle.points[prosperon.DOC] = `
|
||||
geometry.circle.points[cell.DOC] = `
|
||||
:param radius: The circle's radius.
|
||||
:param n: Number of points around the circle.
|
||||
:return: An array of 2D points equally spaced around a full 360-degree circle.
|
||||
@@ -141,7 +141,7 @@ Shortcut for geometry.arc(radius, 360, n).
|
||||
geometry.corners2points = function (ll, ur) {
|
||||
return [ll, ll.add([ur.x, 0]), ur, ll.add([0, ur.y])]
|
||||
}
|
||||
geometry.corners2points[prosperon.DOC] = `
|
||||
geometry.corners2points[cell.DOC] = `
|
||||
:param ll: Lower-left 2D coordinate.
|
||||
:param ur: Upper-right 2D coordinate (relative offset in x,y).
|
||||
:return: A four-point array of corners [ll, lower-right, upper-right, upper-left].
|
||||
@@ -158,7 +158,7 @@ geometry.sortpointsccw = function (points) {
|
||||
})
|
||||
return ccw.map(function (x) { return x.add(cm) })
|
||||
}
|
||||
geometry.sortpointsccw[prosperon.DOC] = `
|
||||
geometry.sortpointsccw[cell.DOC] = `
|
||||
:param points: An array of 2D points.
|
||||
:return: A new array of the same points, sorted counterclockwise around their centroid.
|
||||
Sort an array of points in CCW order based on their angles from the centroid.
|
||||
@@ -185,61 +185,61 @@ geometry.points2cm = function(points) {
|
||||
})
|
||||
return [x / n, y / n]
|
||||
}
|
||||
geometry.points2cm[prosperon.DOC] = `
|
||||
geometry.points2cm[cell.DOC] = `
|
||||
:param points: An array of 2D points.
|
||||
:return: The centroid (average x,y) of the given points.
|
||||
`
|
||||
|
||||
geometry.rect_intersection[prosperon.DOC] = `
|
||||
geometry.rect_intersection[cell.DOC] = `
|
||||
:param a: The first rectangle as {x, y, w, h}.
|
||||
:param b: The second rectangle as {x, y, w, h}.
|
||||
:return: A rectangle that is the intersection of the two. May have zero width/height if no overlap.
|
||||
Return the intersection of two rectangles. The result may be empty if no intersection.
|
||||
`
|
||||
|
||||
geometry.rect_intersects[prosperon.DOC] = `
|
||||
geometry.rect_intersects[cell.DOC] = `
|
||||
:param a: Rectangle {x,y,w,h}.
|
||||
:param b: Rectangle {x,y,w,h}.
|
||||
:return: A boolean indicating whether the two rectangles overlap.
|
||||
`
|
||||
|
||||
geometry.rect_expand[prosperon.DOC] = `
|
||||
geometry.rect_expand[cell.DOC] = `
|
||||
:param a: Rectangle {x,y,w,h}.
|
||||
:param b: Rectangle {x,y,w,h}.
|
||||
:return: A new rectangle that covers the bounds of both input rectangles.
|
||||
Merge or combine two rectangles, returning their bounding rectangle.
|
||||
`
|
||||
|
||||
geometry.rect_inside[prosperon.DOC] = `
|
||||
geometry.rect_inside[cell.DOC] = `
|
||||
:param inner: A rectangle to test.
|
||||
:param outer: A rectangle that may contain 'inner'.
|
||||
:return: True if 'inner' is completely inside 'outer', otherwise false.
|
||||
`
|
||||
|
||||
geometry.rect_random[prosperon.DOC] = `
|
||||
geometry.rect_random[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:return: A random point within the rectangle (uniform distribution).
|
||||
`
|
||||
|
||||
geometry.cwh2rect[prosperon.DOC] = `
|
||||
geometry.cwh2rect[cell.DOC] = `
|
||||
:param center: A 2D point [cx, cy].
|
||||
:param wh: A 2D size [width, height].
|
||||
:return: A rectangle {x, y, w, h} with x,y set to center and w,h set to the given size.
|
||||
Helper: convert a center point and width/height vector to a rect object.
|
||||
`
|
||||
|
||||
geometry.rect_point_inside[prosperon.DOC] = `
|
||||
geometry.rect_point_inside[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:param point: A 2D point [px, py].
|
||||
:return: True if the point lies inside the rectangle, otherwise false.
|
||||
`
|
||||
|
||||
geometry.rect_pos[prosperon.DOC] = `
|
||||
geometry.rect_pos[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:return: A 2D vector [x,y] giving the rectangle's position.
|
||||
`
|
||||
|
||||
geometry.rect_move[prosperon.DOC] = `
|
||||
geometry.rect_move[cell.DOC] = `
|
||||
:param rect: A rectangle {x,y,w,h}.
|
||||
:param offset: A 2D vector to add to the rectangle's position.
|
||||
:return: A new rectangle with updated x,y offset.
|
||||
@@ -1,62 +1,201 @@
|
||||
var graphics = this
|
||||
graphics[prosperon.DOC] = `
|
||||
|
||||
graphics[cell.DOC] = `
|
||||
Provides functionality for loading and managing images, fonts, textures, and sprite meshes.
|
||||
Includes both JavaScript and C-implemented routines for creating geometry buffers, performing
|
||||
rectangle packing, etc.
|
||||
`
|
||||
|
||||
var renderer_actor = arg[0]
|
||||
|
||||
var io = use('io')
|
||||
var time = use('time')
|
||||
var res = use('resources')
|
||||
var json = use('json')
|
||||
|
||||
var GPU = Symbol()
|
||||
var CPU = Symbol()
|
||||
var LASTUSE = Symbol()
|
||||
var LOADING = Symbol()
|
||||
|
||||
var cache = new Map()
|
||||
|
||||
// Image constructor function
|
||||
graphics.Image = function(surfaceData) {
|
||||
// Initialize private properties
|
||||
this[CPU] = surfaceData || undefined;
|
||||
this[GPU] = undefined;
|
||||
this[LOADING] = false;
|
||||
this[LASTUSE] = time.number();
|
||||
this.rect = {x:0, y:0, width:1, height:1};
|
||||
}
|
||||
|
||||
// Define getters and methods on the prototype
|
||||
Object.defineProperties(graphics.Image.prototype, {
|
||||
gpu: {
|
||||
get: function() {
|
||||
this[LASTUSE] = time.number();
|
||||
if (!this[GPU] && !this[LOADING]) {
|
||||
this[LOADING] = true;
|
||||
var self = this;
|
||||
|
||||
// Send message to load texture
|
||||
send(renderer_actor, {
|
||||
kind: "renderer",
|
||||
op: "loadTexture",
|
||||
data: this[CPU]
|
||||
}, function(response) {
|
||||
if (response.error) {
|
||||
log.error("Failed to load texture:")
|
||||
log.error(response.error)
|
||||
self[LOADING] = false;
|
||||
} else {
|
||||
self[GPU] = response;
|
||||
decorate_rect_px(self);
|
||||
self[LOADING] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this[GPU]
|
||||
}
|
||||
},
|
||||
|
||||
texture: {
|
||||
get: function() { return this.gpu }
|
||||
},
|
||||
|
||||
cpu: {
|
||||
get: function() {
|
||||
this[LASTUSE] = time.number();
|
||||
// Note: Reading texture back from GPU requires async operation
|
||||
// For now, return the CPU data if available
|
||||
return this[CPU]
|
||||
}
|
||||
},
|
||||
|
||||
surface: {
|
||||
get: function() { return this.cpu }
|
||||
},
|
||||
|
||||
width: {
|
||||
get: function() {
|
||||
return this[CPU].width
|
||||
}
|
||||
},
|
||||
|
||||
height: {
|
||||
get: function() {
|
||||
return this[CPU].height
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add methods to prototype
|
||||
graphics.Image.prototype.unload_gpu = function() {
|
||||
this[GPU] = undefined
|
||||
}
|
||||
|
||||
graphics.Image.prototype.unload_cpu = function() {
|
||||
this[CPU] = undefined
|
||||
}
|
||||
|
||||
function calc_image_size(img) {
|
||||
if (!img.texture || !img.rect) return
|
||||
return [img.texture.width * img.rect.width, img.texture.height * img.rect.height]
|
||||
}
|
||||
|
||||
/**
|
||||
Internally loads image data from disk and prepares a GPU texture. Used by graphics.texture().
|
||||
Not intended for direct user calls.
|
||||
*/
|
||||
function create_image(path) {
|
||||
var data = io.slurpbytes(path)
|
||||
var newimg
|
||||
switch (path.ext()) {
|
||||
case 'gif':
|
||||
newimg = graphics.make_gif(data)
|
||||
if (newimg.surface)
|
||||
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
|
||||
else
|
||||
for (var frame of newimg.frames)
|
||||
frame.texture = prosperon.gpu.load_texture(frame.surface)
|
||||
break
|
||||
case 'ase':
|
||||
case 'aseprite':
|
||||
newimg = graphics.make_aseprite(data)
|
||||
if (newimg.surface)
|
||||
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
|
||||
else {
|
||||
for (var anim in newimg) {
|
||||
var a = newimg[anim]
|
||||
for (var frame of a.frames)
|
||||
frame.texture = prosperon.gpu.load_texture(frame.surface)
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
newimg = {
|
||||
surface: graphics.make_texture(data)
|
||||
}
|
||||
newimg.texture = prosperon.gpu.load_texture(newimg.surface)
|
||||
break
|
||||
function decorate_rect_px(img) {
|
||||
// needs a GPU texture to measure
|
||||
if (!img || !img.texture) return
|
||||
|
||||
// default UV rect is the whole image if none supplied
|
||||
img.rect ??= {x:0, y:0, width:1, height:1} // [u0,v0,uw,vh] in 0-1
|
||||
|
||||
// store pixel-space version: [x, y, w, h] in texels
|
||||
img.rect_px = {
|
||||
x:Math.round(img.rect.x * img.texture.width),
|
||||
y:Math.round(img.rect.y * img.texture.height),
|
||||
width:Math.round(img.rect.width * img.texture.width),
|
||||
height:Math.round(img.rect.height * img.texture.height)
|
||||
}
|
||||
}
|
||||
|
||||
function make_handle(obj)
|
||||
{
|
||||
return new graphics.Image(obj);
|
||||
}
|
||||
|
||||
function wrapSurface(surf, maybeRect){
|
||||
const h = make_handle(surf);
|
||||
if(maybeRect) h.rect = maybeRect; /* honour frame sub-rect */
|
||||
return h;
|
||||
}
|
||||
function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */
|
||||
return arr.map(f => ({
|
||||
image : wrapSurface(f.surface || f), /* accept bare surface too */
|
||||
time: f.time,
|
||||
rect: f.rect /* keep for reference */
|
||||
}));
|
||||
}
|
||||
function makeAnim(frames, loop=true){
|
||||
return { frames, loop }
|
||||
}
|
||||
|
||||
function decode_image(bytes, ext)
|
||||
{
|
||||
switch(ext) {
|
||||
case 'gif': return graphics.make_gif(bytes)
|
||||
case 'ase':
|
||||
case 'aseprite': return graphics.make_aseprite(bytes)
|
||||
default: return {surface:graphics.make_texture(bytes)}
|
||||
}
|
||||
}
|
||||
|
||||
function create_image(path){
|
||||
try{
|
||||
const bytes = io.slurpbytes(path);
|
||||
|
||||
let raw = decode_image(bytes, path.ext());
|
||||
|
||||
/* ── Case A: static image ─────────────────────────────────── */
|
||||
if(raw.surface) {
|
||||
var gg = new graphics.Image(raw.surface)
|
||||
return gg
|
||||
}
|
||||
|
||||
/* ── Case B: GIF helpers returned array [surf, …] ─────────── */
|
||||
if(Array.isArray(raw))
|
||||
return makeAnim( wrapFrames(raw), true );
|
||||
|
||||
/* ── Case C: GIF helpers returned {frames,loop} ───────────── */
|
||||
if(raw.frames && Array.isArray(raw.frames))
|
||||
return makeAnim( wrapFrames(raw.frames), !!raw.loop );
|
||||
|
||||
/* ── Case D: ASE helpers returned { animName:{frames,loop}, … } ── */
|
||||
const anims = {};
|
||||
for(const [name, anim] of Object.entries(raw)){
|
||||
if(anim && Array.isArray(anim.frames))
|
||||
anims[name] = makeAnim( wrapFrames(anim.frames), !!anim.loop );
|
||||
else if(anim && anim.surface) /* ase with flat surface */
|
||||
anims[name] = makeAnim(
|
||||
[{image:make_handle(anim.surface),time:0}], true );
|
||||
}
|
||||
if(Object.keys(anims).length) return anims;
|
||||
|
||||
throw new Error('Unsupported image structure from decoder');
|
||||
|
||||
}catch(e){
|
||||
log.error(`Error loading image ${path}: ${e.message}`);
|
||||
throw e;
|
||||
}
|
||||
return newimg
|
||||
}
|
||||
|
||||
var image = {}
|
||||
image.dimensions = function() {
|
||||
return [this.texture.width, this.texture.height].scale([this.rect[2], this.rect[3]])
|
||||
}
|
||||
image.dimensions[prosperon.DOC] = `
|
||||
image.dimensions[cell.DOC] = `
|
||||
:return: A 2D array [width, height] that is the scaled size of this image (texture size * rect size).
|
||||
`
|
||||
|
||||
@@ -77,22 +216,55 @@ function pack_into_sheet(images) {
|
||||
graphics.is_image = function(obj) {
|
||||
if (obj.texture && obj.rect) return true
|
||||
}
|
||||
graphics.is_image[prosperon.DOC] = `
|
||||
graphics.is_image[cell.DOC] = `
|
||||
:param obj: An object to check.
|
||||
:return: True if 'obj' has a .texture and a .rect property, indicating it's an image object.
|
||||
`
|
||||
|
||||
graphics.texture = function texture(path) {
|
||||
if (typeof path !== 'string') {
|
||||
return path // fallback if already an image object
|
||||
throw new Error('need a string for graphics.texture')
|
||||
}
|
||||
var parts = path.split(':')
|
||||
var ipath = res.find_image(parts[0])
|
||||
graphics.texture.cache[ipath] ??= create_image(ipath)
|
||||
return graphics.texture.cache[ipath]
|
||||
graphics.texture_from_data = function(data)
|
||||
{
|
||||
if (!(data instanceof ArrayBuffer)) return undefined
|
||||
|
||||
var image = graphics.make_texture(data);
|
||||
var img = make_handle(image)
|
||||
|
||||
img.gpu;
|
||||
|
||||
return img;
|
||||
}
|
||||
graphics.texture[prosperon.DOC] = `
|
||||
|
||||
graphics.from_surface = function(id, surf)
|
||||
{
|
||||
return make_handle(surf)
|
||||
}
|
||||
|
||||
graphics.from = function(id, data)
|
||||
{
|
||||
if (typeof id !== 'string')
|
||||
throw new Error('Expected a string ID')
|
||||
|
||||
if (data instanceof ArrayBuffer)
|
||||
return graphics.texture_from_data(data)
|
||||
}
|
||||
|
||||
graphics.texture = function texture(path) {
|
||||
if (path instanceof graphics.Image) return path
|
||||
|
||||
if (typeof path !== 'string')
|
||||
throw new Error('need a string for graphics.texture')
|
||||
|
||||
var id = path.split(':')[0]
|
||||
if (cache.has(id)) return cache.get(id)
|
||||
|
||||
var ipath = res.find_image(id)
|
||||
if (!ipath)
|
||||
throw new Error(`unknown image ${id}`)
|
||||
|
||||
var image = create_image(ipath)
|
||||
cache.set(id, image)
|
||||
return image
|
||||
}
|
||||
graphics.texture[cell.DOC] = `
|
||||
:param path: A string path to an image file or an already-loaded image object.
|
||||
:return: An image object with {surface, texture, frames?, etc.} depending on the format.
|
||||
Load or retrieve a cached image, converting it into a GPU texture. If 'path' is already an object, it’s returned directly.
|
||||
@@ -106,7 +278,7 @@ graphics.texture.total_size = function() {
|
||||
// Not yet implemented, presumably sum of (texture.width * texture.height * 4) for images in RAM
|
||||
return size
|
||||
}
|
||||
graphics.texture.total_size[prosperon.DOC] = `
|
||||
graphics.texture.total_size[cell.DOC] = `
|
||||
:return: The total estimated memory size of all cached textures in RAM, in bytes. (Not yet implemented.)
|
||||
`
|
||||
|
||||
@@ -115,23 +287,23 @@ graphics.texture.total_vram = function() {
|
||||
// Not yet implemented, presumably sum of GPU memory usage
|
||||
return vram
|
||||
}
|
||||
graphics.texture.total_vram[prosperon.DOC] = `
|
||||
graphics.texture.total_vram[cell.DOC] = `
|
||||
:return: The total estimated GPU memory usage of all cached textures, in bytes. (Not yet implemented.)
|
||||
`
|
||||
|
||||
graphics.tex_hotreload = function tex_hotreload(file) {
|
||||
console.log(`hot reloading ${file}`)
|
||||
log.console(`hot reloading ${file}`)
|
||||
if (!(file in graphics.texture.cache)) return
|
||||
console.log('really doing it')
|
||||
log.console('really doing it')
|
||||
|
||||
var img = create_image(file)
|
||||
var oldimg = graphics.texture.cache[file]
|
||||
console.log(`new image:${json.encode(img)}`)
|
||||
console.log(`old image: ${json.encode(oldimg)}`)
|
||||
log.console(`new image:${json.encode(img)}`)
|
||||
log.console(`old image: ${json.encode(oldimg)}`)
|
||||
|
||||
merge_objects(oldimg, img, ['surface', 'texture', 'loop', 'time'])
|
||||
}
|
||||
graphics.tex_hotreload[prosperon.DOC] = `
|
||||
graphics.tex_hotreload[cell.DOC] = `
|
||||
:param file: The file path that was changed on disk.
|
||||
:return: None
|
||||
Reload the image for the given file, updating the cached copy in memory and GPU.
|
||||
@@ -170,11 +342,26 @@ graphics.get_font = function get_font(path, size) {
|
||||
if (fontcache[fontstr]) return fontcache[fontstr]
|
||||
|
||||
var data = io.slurpbytes(fullpath)
|
||||
fontcache[fontstr] = graphics.make_font(data, size)
|
||||
fontcache[fontstr].texture = prosperon.gpu.load_texture(fontcache[fontstr].surface)
|
||||
return fontcache[fontstr]
|
||||
var font = graphics.make_font(data,size)
|
||||
|
||||
// Load font texture via renderer actor (async)
|
||||
send(renderer_actor, {
|
||||
kind: "renderer",
|
||||
op: "loadTexture",
|
||||
data: font.surface
|
||||
}, function(response) {
|
||||
if (response.error) {
|
||||
log.error("Failed to load font texture:", response.error);
|
||||
} else {
|
||||
font.texture = response;
|
||||
}
|
||||
});
|
||||
|
||||
fontcache[fontstr] = font
|
||||
|
||||
return font
|
||||
}
|
||||
graphics.get_font[prosperon.DOC] = `
|
||||
graphics.get_font[cell.DOC] = `
|
||||
:param path: A string path to a font file, optionally with ".size" appended.
|
||||
:param size: Pixel size of the font, if not included in 'path'.
|
||||
:return: A font object with .surface and .texture for rendering text.
|
||||
@@ -192,31 +379,14 @@ graphics.queue_sprite_mesh = function(queue) {
|
||||
}
|
||||
return [mesh.pos, mesh.uv, mesh.color, mesh.indices]
|
||||
}
|
||||
graphics.queue_sprite_mesh[prosperon.DOC] = `
|
||||
graphics.queue_sprite_mesh[cell.DOC] = `
|
||||
:param queue: An array of draw commands, some of which are {type:'sprite'} objects.
|
||||
:return: An array of references to GPU buffers [pos,uv,color,indices].
|
||||
Builds a single geometry mesh for all sprite-type commands in the queue, storing first_index/num_indices
|
||||
so they can be rendered in one draw call.
|
||||
`
|
||||
|
||||
graphics.make_sprite_mesh[prosperon.DOC] = `
|
||||
:param sprites: An array of sprite objects, each containing .rect (or transform), .src (UV region), .color, etc.
|
||||
:param oldMesh (optional): An existing mesh object to reuse/resize if possible.
|
||||
:return: A GPU mesh object with pos, uv, color, and indices buffers for all sprites.
|
||||
Given an array of sprites, build a single geometry mesh for rendering them.
|
||||
`
|
||||
|
||||
graphics.make_sprite_queue[prosperon.DOC] = `
|
||||
:param sprites: An array of sprite objects.
|
||||
:param camera: (unused in the C code example) Typically a camera or transform for sorting?
|
||||
:param pipeline: A pipeline object for rendering.
|
||||
:param sort: An integer or boolean for whether to sort sprites; if truthy, sorts by layer & texture.
|
||||
:return: An array of pipeline commands: geometry with mesh references, grouped by image.
|
||||
Given an array of sprites, optionally sort them, then build a queue of pipeline commands.
|
||||
Each group with a shared image becomes one command.
|
||||
`
|
||||
|
||||
graphics.make_text_buffer[prosperon.DOC] = `
|
||||
graphics.make_text_buffer[cell.DOC] = `
|
||||
:param text: The string to render.
|
||||
:param rect: A rectangle specifying position and possibly wrapping.
|
||||
:param angle: Rotation angle (unused or optional).
|
||||
@@ -227,7 +397,7 @@ graphics.make_text_buffer[prosperon.DOC] = `
|
||||
Generate a GPU buffer mesh of text quads for rendering with a font, etc.
|
||||
`
|
||||
|
||||
graphics.rectpack[prosperon.DOC] = `
|
||||
graphics.rectpack[cell.DOC] = `
|
||||
:param width: The width of the area to pack into.
|
||||
:param height: The height of the area to pack into.
|
||||
:param sizes: An array of [w,h] pairs for the rectangles to pack.
|
||||
@@ -235,67 +405,39 @@ graphics.rectpack[prosperon.DOC] = `
|
||||
Perform a rectangle packing using the stbrp library. Return positions for each rect.
|
||||
`
|
||||
|
||||
graphics.make_rtree[prosperon.DOC] = `
|
||||
:return: An R-Tree object for quickly querying many rectangles or sprite bounds.
|
||||
Create a new R-Tree for geometry queries.
|
||||
`
|
||||
|
||||
graphics.make_texture[prosperon.DOC] = `
|
||||
graphics.make_texture[cell.DOC] = `
|
||||
:param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer.
|
||||
:return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering.
|
||||
Convert raw image bytes into an SDL_Surface object.
|
||||
`
|
||||
|
||||
graphics.make_gif[prosperon.DOC] = `
|
||||
graphics.make_gif[cell.DOC] = `
|
||||
:param data: An ArrayBuffer containing GIF data.
|
||||
:return: An object with frames[], each frame having its own .surface. Some also have a .texture for GPU use.
|
||||
Load a GIF, returning its frames. If it's a single-frame GIF, the result may have .surface only.
|
||||
`
|
||||
|
||||
graphics.make_aseprite[prosperon.DOC] = `
|
||||
graphics.make_aseprite[cell.DOC] = `
|
||||
:param data: An ArrayBuffer containing Aseprite (ASE) file data.
|
||||
:return: An object containing frames or animations, each with .surface. May also have top-level .surface for a single-layer case.
|
||||
Load an Aseprite/ASE file from an array of bytes, returning frames or animations.
|
||||
`
|
||||
|
||||
graphics.cull_sprites[prosperon.DOC] = `
|
||||
graphics.cull_sprites[cell.DOC] = `
|
||||
:param sprites: An array of sprite objects (each has rect or transform).
|
||||
:param camera: A camera or bounding rectangle defining the view area.
|
||||
:return: A new array of sprites that are visible in the camera's view.
|
||||
Filter an array of sprites to only those visible in the provided camera’s view.
|
||||
`
|
||||
|
||||
graphics.rects_to_sprites[prosperon.DOC] = `
|
||||
:param rects: An array of rect coords or objects.
|
||||
:param image: An image object (with .texture).
|
||||
:return: An array of sprite objects referencing the 'image' and each rect for UV or position.
|
||||
Convert an array of rect coords into sprite objects referencing a single image.
|
||||
`
|
||||
|
||||
graphics.make_surface[prosperon.DOC] = `
|
||||
:param dimensions: The size object {width, height}, or an array [w,h].
|
||||
:return: A blank RGBA surface with the given dimensions, typically for software rendering or icons.
|
||||
Create a blank surface in RAM.
|
||||
`
|
||||
|
||||
graphics.make_cursor[prosperon.DOC] = `
|
||||
:param opts: An object with {surface, hotx, hoty} or similar.
|
||||
:return: An SDL_Cursor object referencing the given surface for a custom mouse cursor.
|
||||
`
|
||||
|
||||
graphics.make_font[prosperon.DOC] = `
|
||||
graphics.make_font[cell.DOC] = `
|
||||
:param data: TTF/OTF file data as an ArrayBuffer.
|
||||
:param size: Pixel size for rendering glyphs.
|
||||
:return: A font object with surface, texture, and glyph data, for text rendering with make_text_buffer.
|
||||
Load a font from TTF/OTF data at the given size.
|
||||
`
|
||||
|
||||
graphics.make_sprite[prosperon.DOC] = `
|
||||
:return: A new sprite object, which typically has .rect, .color, .layer, .image, etc.
|
||||
Create a new sprite object, storing default properties.
|
||||
`
|
||||
|
||||
graphics.make_line_prim[prosperon.DOC] = `
|
||||
graphics.make_line_prim[cell.DOC] = `
|
||||
:param points: An array of [x,y] points forming the line.
|
||||
:param thickness: The thickness (width) of the polyline.
|
||||
:param startCap: (Unused) Possibly the type of cap for the start.
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 867 B After Width: | Height: | Size: 867 B |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |