Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019f88c2bc | ||
|
|
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 | ||
|
|
af0996f6ab | ||
|
|
6c390aeae3 |
8
.cell/cell.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[dependencies]
|
||||
extramath = "https://gitea.pockle.world/john/extramath@master"
|
||||
|
||||
[system]
|
||||
ar_timer = 60 # seconds before idle actor reclamation
|
||||
actor_memory = 0 # MB of memory an actor can use; 0 for unbounded
|
||||
net_service = 0.1 # seconds per net service pull
|
||||
reply_timeout = 60 # seconds to hold callback for reply messages; 0 for unbounded
|
||||
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"
|
||||
193
.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() }}
|
||||
@@ -43,12 +43,43 @@ jobs:
|
||||
with:
|
||||
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,15 +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]
|
||||
if: ${{ false }}
|
||||
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
|
||||
@@ -178,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
|
||||
@@ -205,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 }}
|
||||
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/
|
||||
|
||||
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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
|
||||
- Use as little whitespace as possible
|
||||
- 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");
|
||||
189
benchmarks/wota_nota_json.ce
Normal file
@@ -0,0 +1,189 @@
|
||||
//
|
||||
// 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 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.byteLength;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Nota",
|
||||
encode: nota.encode,
|
||||
decode: nota.decode,
|
||||
// Nota also produces an ArrayBuffer:
|
||||
getSize(encoded) {
|
||||
return encoded.byteLength;
|
||||
}
|
||||
},
|
||||
{
|
||||
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
|
||||
},
|
||||
{
|
||||
name: "Large Binary Blob (256KB)",
|
||||
data: [ new Uint8Array(256 * 1024).buffer ],
|
||||
iterations: 200
|
||||
}
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 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)
|
||||
```
|
||||
|
||||
|
||||
39
docs/api/modules/dmon.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# dmon
|
||||
|
||||
### watch() <sub>function</sub>
|
||||
|
||||
Start watching the root directory, recursively.
|
||||
|
||||
This function begins monitoring the specified directory and its subdirectories recursively for events such as file creation, deletion, modification, or movement. Events are queued and can be retrieved by calling poll.
|
||||
|
||||
:throws: An error if dmon is already watching.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### unwatch() <sub>function</sub>
|
||||
|
||||
Stop watching the currently monitored directory.
|
||||
|
||||
This function halts filesystem monitoring for the directory previously set by watch. It clears the watch state, allowing a new watch to be started.
|
||||
|
||||
:throws: An error if no directory is currently being watched.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### poll(callback) <sub>function</sub>
|
||||
|
||||
Retrieve and process queued filesystem events.
|
||||
|
||||
This function dequeues all pending filesystem events and invokes the provided callback for each one. The callback receives an event object with properties: 'action' (string: "create", "delete", "modify", or "move"), 'root' (string: watched directory), 'file' (string: affected file path), and 'old' (string: previous file path for move events, empty if not applicable).
|
||||
|
||||
|
||||
|
||||
**callback**: A function to call for each event, receiving an event object as its argument.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
@@ -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!"'
|
||||
```
|
||||
|
||||
|
||||
|
||||
45
docs/api/modules/enet.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# enet
|
||||
|
||||
### initialize() <sub>function</sub>
|
||||
|
||||
|
||||
Initialize the ENet library. Must be called before using any ENet functionality.
|
||||
Throws an error if initialization fails.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### deinitialize() <sub>function</sub>
|
||||
|
||||
|
||||
Deinitialize the ENet library, cleaning up all resources. Call this when you no longer
|
||||
need any ENet functionality.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### create_host(address) <sub>function</sub>
|
||||
|
||||
|
||||
Create an ENet host for either a client-like unbound host or a server bound to a specific
|
||||
address and port:
|
||||
|
||||
- If no argument is provided, creates an unbound "client-like" host with default settings
|
||||
(maximum 32 peers, 2 channels, unlimited bandwidth).
|
||||
- If you pass an "ip:port" string (e.g. "127.0.0.1:7777"), it creates a server bound to
|
||||
that address. The server supports up to 32 peers, 2 channels, and unlimited bandwidth.
|
||||
|
||||
Throws an error if host creation fails for any reason.
|
||||
|
||||
omit to create an unbound client-like host.
|
||||
|
||||
|
||||
**address**: (optional) A string in 'ip:port' format to bind the host (server), or
|
||||
|
||||
|
||||
**Returns**: An ENetHost object.
|
||||
|
||||
30
docs/api/modules/nota.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# nota
|
||||
|
||||
### encode(value) <sub>function</sub>
|
||||
|
||||
Convert a JavaScript value into a NOTA-encoded ArrayBuffer.
|
||||
|
||||
This function serializes JavaScript values (such as numbers, strings, booleans, arrays, objects, or ArrayBuffers) into the NOTA binary format. The resulting ArrayBuffer can be stored or transmitted and later decoded back into a JavaScript value.
|
||||
|
||||
:throws: An error if no argument is provided.
|
||||
|
||||
|
||||
**value**: The JavaScript value to encode (e.g., number, string, boolean, array, object, or ArrayBuffer).
|
||||
|
||||
|
||||
**Returns**: An ArrayBuffer containing the NOTA-encoded data.
|
||||
|
||||
|
||||
### decode(buffer) <sub>function</sub>
|
||||
|
||||
Decode a NOTA-encoded ArrayBuffer into a JavaScript value.
|
||||
|
||||
This function deserializes a NOTA-formatted ArrayBuffer into its corresponding JavaScript representation, such as a number, string, boolean, array, object, or ArrayBuffer. If the input is invalid or empty, it returns undefined.
|
||||
|
||||
|
||||
|
||||
**buffer**: An ArrayBuffer containing NOTA-encoded data to decode.
|
||||
|
||||
|
||||
**Returns**: The decoded JavaScript value (e.g., number, string, boolean, array, object, or ArrayBuffer), or undefined if no argument is provided.
|
||||
|
||||
66
docs/api/types/enet_host.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# enet_host
|
||||
|
||||
### service(callback, timeout) <sub>function</sub>
|
||||
|
||||
|
||||
Poll for and process any available network events (connect, receive, disconnect, or none)
|
||||
from this host, calling the provided callback for each event. This function loops until
|
||||
no more events are available in the current timeframe.
|
||||
|
||||
Event object properties:
|
||||
- type: String, one of "connect", "receive", "disconnect", or "none".
|
||||
- peer: (present if type = "connect") The ENetPeer object for the new connection.
|
||||
- channelID: (present if type = "receive") The channel on which the data was received.
|
||||
- data: (present if type = "receive") The received data as a *plain JavaScript object*.
|
||||
If the JSON parse fails or the data isn't an object, a JavaScript error is thrown.
|
||||
|
||||
object as its single argument.
|
||||
|
||||
|
||||
**callback**: A function called once for each available event, receiving an event
|
||||
|
||||
**timeout**: (optional) Timeout in milliseconds. Defaults to 0 (non-blocking).
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### connect(host, port) <sub>function</sub>
|
||||
|
||||
|
||||
Initiate a connection from this host to a remote server. Throws an error if the
|
||||
connection cannot be started.
|
||||
|
||||
|
||||
|
||||
**host**: The hostname or IP address of the remote server (e.g. "example.com" or "127.0.0.1").
|
||||
|
||||
**port**: The port number to connect to.
|
||||
|
||||
|
||||
**Returns**: An ENetPeer object representing the connection.
|
||||
|
||||
|
||||
### flush() <sub>function</sub>
|
||||
|
||||
|
||||
Flush all pending outgoing packets for this host immediately.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### broadcast(data) <sub>function</sub>
|
||||
|
||||
|
||||
Broadcast a JavaScript object to all connected peers on channel 0. The object is
|
||||
serialized to JSON, and the packet is sent reliably. Throws an error if serialization fails.
|
||||
|
||||
|
||||
|
||||
**data**: A JavaScript object to broadcast to all peers.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
102
docs/api/types/enet_peer.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# enet_peer
|
||||
|
||||
### send(data) <sub>function</sub>
|
||||
|
||||
|
||||
Send a JavaScript object to this peer on channel 0. The object is serialized to JSON and
|
||||
sent reliably. Throws an error if serialization fails.
|
||||
|
||||
|
||||
|
||||
**data**: A JavaScript object to send.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### disconnect() <sub>function</sub>
|
||||
|
||||
|
||||
Request a graceful disconnection from this peer. The connection will close after
|
||||
pending data is sent.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### disconnect_now() <sub>function</sub>
|
||||
|
||||
|
||||
Immediately terminate the connection to this peer, discarding any pending data.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### disconnect_later() <sub>function</sub>
|
||||
|
||||
|
||||
Request a disconnection from this peer after all queued packets are sent.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### reset() <sub>function</sub>
|
||||
|
||||
|
||||
Reset this peer's connection, immediately dropping it and clearing its internal state.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### ping() <sub>function</sub>
|
||||
|
||||
|
||||
Send a ping request to this peer to measure latency.
|
||||
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### throttle_configure(interval, acceleration, deceleration) <sub>function</sub>
|
||||
|
||||
|
||||
Configure the throttling behavior for this peer, controlling how ENet adjusts its sending
|
||||
rate based on packet loss or congestion.
|
||||
|
||||
|
||||
|
||||
**interval**: The interval (ms) between throttle adjustments.
|
||||
|
||||
**acceleration**: The factor to increase sending speed when conditions improve.
|
||||
|
||||
**deceleration**: The factor to decrease sending speed when conditions worsen.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
|
||||
### timeout(timeout_limit, timeout_min, timeout_max) <sub>function</sub>
|
||||
|
||||
|
||||
Set timeout parameters for this peer, determining how long ENet waits before considering
|
||||
the connection lost.
|
||||
|
||||
|
||||
|
||||
**timeout_limit**: The total time (ms) before the peer is disconnected.
|
||||
|
||||
**timeout_min**: The minimum timeout (ms) used for each timeout attempt.
|
||||
|
||||
**timeout_max**: The maximum timeout (ms) used for each timeout attempt.
|
||||
|
||||
|
||||
**Returns**: None
|
||||
|
||||
@@ -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'
|
||||
endian = 'little'
|
||||
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
|
||||
}
|
||||
})
|
||||
352
meson.build
@@ -1,39 +1,43 @@
|
||||
project('prosperon', ['c', 'cpp'],
|
||||
version: '0.9.2',
|
||||
project('cell', ['c', 'cpp'],
|
||||
version: '0.9.3',
|
||||
meson_version: '>=1.4',
|
||||
default_options : [ 'cpp_std=c++11'])
|
||||
default_options : [ 'cpp_std=c++11']
|
||||
)
|
||||
|
||||
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
|
||||
# Pass version/commit defines to C
|
||||
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,177 +68,261 @@ endif
|
||||
|
||||
cmake = import('cmake')
|
||||
|
||||
# mbedtls (either system or subproject)
|
||||
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',
|
||||
'ENABLE_TESTING': 'OFF',
|
||||
'CMAKE_BUILD_TYPE': 'Release',
|
||||
'MBEDTLS_FATAL_WARNINGS': 'ON',
|
||||
'USE_STATIC_MBEDTLS_LIBRARY': 'ON',
|
||||
'USE_SHARED_MBEDTLS_LIBRARY': 'OFF'
|
||||
})
|
||||
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 (system or subproject)
|
||||
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')
|
||||
|
||||
if host_machine.system() == 'darwin'
|
||||
deps += dependency('appleframeworks', modules: 'accelerate')
|
||||
add_project_arguments('-DACCELERATE_NEW_LAPACK=1', language:'c')
|
||||
add_project_arguments('-DACCELERATE_LAPACK_ILP64=1', language:'c')
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'linux'
|
||||
deps += cc.find_library('asound', required:true)
|
||||
deps += [dependency('x11'), dependency('xi'), dependency('xcursor'), dependency('egl'), dependency('gl')]
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
deps += cc.find_library('d3d11')
|
||||
deps += cc.find_library('ws2_32', required:true)
|
||||
deps += cc.find_library('dbghelp')
|
||||
deps += cc.find_library('winmm')
|
||||
deps += cc.find_library('setupapi')
|
||||
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
|
||||
link += '-static'
|
||||
endif
|
||||
|
||||
if host_machine.system() == 'emscripten'
|
||||
link += '-sUSE_WEBGPU'
|
||||
deps += dependency('sdl3', static : true, method : 'pkg-config', required : true)
|
||||
else
|
||||
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'
|
||||
if get_option('buildtype') != 'release'
|
||||
quickjs_opts += 'leaks=true'
|
||||
endif
|
||||
quickjs_dep = dependency('quickjs', static: true, required: false)
|
||||
if not quickjs_dep.found()
|
||||
message('⚙ System quickjs not found, building subproject...')
|
||||
deps += dependency('quickjs', static:true, default_options:quickjs_opts)
|
||||
else
|
||||
deps += quickjs_dep
|
||||
endif
|
||||
|
||||
deps += dependency('quickjs', static:true, default_options:quickjs_opts)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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')
|
||||
|
||||
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'
|
||||
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'])
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 = get_option('storefront')
|
||||
if storefront == 'steam'
|
||||
deps += dependency('qjs-steam',static:false)
|
||||
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
|
||||
|
||||
deps += dependency('qjs-layout',static:true)
|
||||
deps += dependency('qjs-miniz',static:true)
|
||||
|
||||
deps += dependency('physfs', static:true)
|
||||
|
||||
#deps += dependency('opencv4')
|
||||
#deps += cc.find_library('opencv')
|
||||
|
||||
deps += dependency('threads')
|
||||
deps += dependency('chipmunk', static:true)
|
||||
deps += dependency('enet', static:true)
|
||||
deps += dependency('soloud', static:true)
|
||||
|
||||
#deps += dependency('qjs-chipmunk', static:false)
|
||||
# Any extra linker flags
|
||||
link_args = link
|
||||
|
||||
# === COLLECT ALL SOURCE FILES ===
|
||||
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_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'
|
||||
]
|
||||
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']
|
||||
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
|
||||
|
||||
# === NEW: build a STATIC library "cell_core" from exactly the same sources ===
|
||||
cell_core = static_library(
|
||||
'cell_core', # → libcell_core.a
|
||||
sources,
|
||||
dependencies: deps,
|
||||
include_directories: includers
|
||||
)
|
||||
|
||||
prosperon_dep = declare_dependency(
|
||||
link_with:prosperon
|
||||
# === Now build the "cell" executable by linking against that static lib ===
|
||||
cell = executable('cell',
|
||||
link_with : [ cell_core ],
|
||||
dependencies : deps,
|
||||
include_directories: includers,
|
||||
link_args : link_args,
|
||||
build_rpath : '$ORIGIN',
|
||||
install : true
|
||||
)
|
||||
|
||||
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
|
||||
# === Declare a Meson dependency so that other subprojects can do `dependency('cell')` if needed ===
|
||||
cell_dep = declare_dependency(
|
||||
link_with: [ cell_core ],
|
||||
include_directories: includers
|
||||
)
|
||||
|
||||
|
||||
|
||||
# === TESTS (unchanged) ===
|
||||
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')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
site_name: Prosperon Documentation
|
||||
edit_uri: edit/master/doc/docs
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- awesome-pages
|
||||
- mike
|
||||
|
||||
extra_css:
|
||||
- style.css
|
||||
@@ -32,6 +32,9 @@ extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-85ECSFGCBV
|
||||
version:
|
||||
default: latest
|
||||
provider: mike
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
|
||||
827
prosperon/_sdl_video.ce
Normal file
@@ -0,0 +1,827 @@
|
||||
// SDL Video Actor
|
||||
// This actor runs on the main thread and handles all SDL video operations
|
||||
log.console("TO HERE")
|
||||
var surface = use('surface')
|
||||
|
||||
// Default window configuration - documents all available window options
|
||||
var default_window = {
|
||||
// Basic properties
|
||||
title: "Prosperon Window",
|
||||
width: 640,
|
||||
height: 480,
|
||||
|
||||
// Position - can be numbers or "centered"
|
||||
x: undefined, // SDL_WINDOWPOS_UNDEFINED by default
|
||||
y: undefined, // SDL_WINDOWPOS_UNDEFINED by default
|
||||
|
||||
// Window behavior flags
|
||||
resizable: true,
|
||||
fullscreen: false,
|
||||
hidden: false,
|
||||
borderless: false,
|
||||
alwaysOnTop: false,
|
||||
minimized: false,
|
||||
maximized: false,
|
||||
|
||||
// Input grabbing
|
||||
mouseGrabbed: false,
|
||||
keyboardGrabbed: false,
|
||||
|
||||
// Display properties
|
||||
highPixelDensity: false,
|
||||
transparent: false,
|
||||
opacity: 1.0, // 0.0 to 1.0
|
||||
|
||||
// Focus behavior
|
||||
notFocusable: false,
|
||||
|
||||
// Special window types (mutually exclusive)
|
||||
utility: false, // Utility window (not in taskbar)
|
||||
tooltip: false, // Tooltip window (requires parent)
|
||||
popupMenu: false, // Popup menu window (requires parent)
|
||||
|
||||
// Graphics API flags (let SDL choose if not specified)
|
||||
opengl: false, // Force OpenGL context
|
||||
vulkan: false, // Force Vulkan context
|
||||
metal: false, // Force Metal context (macOS)
|
||||
|
||||
// Advanced properties
|
||||
parent: undefined, // Parent window for tooltips/popups/modal
|
||||
modal: false, // Modal to parent window (requires parent)
|
||||
externalGraphicsContext: false, // Use external graphics context
|
||||
|
||||
// Input handling
|
||||
textInput: true, // Enable text input on creation
|
||||
};
|
||||
|
||||
// Resource tracking
|
||||
var resources = {
|
||||
window: {},
|
||||
renderer: {},
|
||||
texture: {},
|
||||
surface: {},
|
||||
cursor: {}
|
||||
};
|
||||
|
||||
// ID counter for resource allocation
|
||||
var next_id = 1;
|
||||
|
||||
// Helper to allocate new ID
|
||||
function allocate_id() {
|
||||
return next_id++;
|
||||
}
|
||||
|
||||
// Message handler
|
||||
$_.receiver(function(msg) {
|
||||
if (!msg.kind || !msg.op) {
|
||||
send(msg, {error: "Message must have 'kind' and 'op' fields"});
|
||||
return;
|
||||
}
|
||||
|
||||
var response = {};
|
||||
|
||||
try {
|
||||
switch (msg.kind) {
|
||||
case 'window':
|
||||
response = handle_window(msg);
|
||||
break;
|
||||
case 'renderer':
|
||||
response = handle_renderer(msg);
|
||||
break;
|
||||
case 'texture':
|
||||
response = handle_texture(msg);
|
||||
break;
|
||||
case 'surface':
|
||||
response = handle_surface(msg);
|
||||
break;
|
||||
case 'cursor':
|
||||
response = handle_cursor(msg);
|
||||
break;
|
||||
case 'mouse':
|
||||
response = handle_mouse(msg);
|
||||
break;
|
||||
case 'keyboard':
|
||||
response = handle_keyboard(msg);
|
||||
break;
|
||||
default:
|
||||
response = {error: "Unknown kind: " + msg.kind};
|
||||
}
|
||||
} catch (e) {
|
||||
response = {error: e.toString()};
|
||||
log.error(e)
|
||||
}
|
||||
|
||||
send(msg, response);
|
||||
});
|
||||
|
||||
// Window operations
|
||||
function handle_window(msg) {
|
||||
// Special case: create doesn't need an existing window
|
||||
if (msg.op === 'create') {
|
||||
var config = Object.assign({}, default_window, msg.data || {});
|
||||
var id = allocate_id();
|
||||
var window = new prosperon.endowments.window(config);
|
||||
resources.window[id] = window;
|
||||
return {id: id, data: {size: window.size}};
|
||||
}
|
||||
|
||||
// All other operations require a valid window ID
|
||||
if (!msg.id || !resources.window[msg.id]) {
|
||||
return {error: "Invalid window id: " + msg.id};
|
||||
}
|
||||
|
||||
var win = resources.window[msg.id];
|
||||
|
||||
switch (msg.op) {
|
||||
case 'destroy':
|
||||
win.destroy();
|
||||
delete resources.window[msg.id];
|
||||
return {success: true};
|
||||
|
||||
case 'show':
|
||||
win.visible = true;
|
||||
return {success: true};
|
||||
|
||||
case 'hide':
|
||||
win.visible = false;
|
||||
return {success: true};
|
||||
|
||||
case 'get':
|
||||
var prop = msg.data ? msg.data.property : null;
|
||||
if (!prop) return {error: "Missing property name"};
|
||||
|
||||
// Handle special cases
|
||||
if (prop === 'surface') {
|
||||
var surf = win.surface;
|
||||
if (!surf) return {data: null};
|
||||
var surf_id = allocate_id();
|
||||
resources.surface[surf_id] = surf;
|
||||
return {data: surf_id};
|
||||
}
|
||||
|
||||
return {data: win[prop]};
|
||||
|
||||
case 'set':
|
||||
var prop = msg.data ? msg.data.property : null;
|
||||
var value = msg.data ? msg.data.value : undefined;
|
||||
if (!prop) return {error: "Missing property name"};
|
||||
|
||||
// Validate property is settable
|
||||
var readonly = ['id', 'pixelDensity', 'displayScale', 'sizeInPixels', 'flags', 'surface'];
|
||||
if (readonly.indexOf(prop) !== -1) {
|
||||
return {error: "Property '" + prop + "' is read-only"};
|
||||
}
|
||||
|
||||
win[prop] = value;
|
||||
return {success: true};
|
||||
|
||||
case 'fullscreen':
|
||||
win.fullscreen();
|
||||
return {success: true};
|
||||
|
||||
case 'updateSurface':
|
||||
win.updateSurface();
|
||||
return {success: true};
|
||||
|
||||
case 'updateSurfaceRects':
|
||||
if (!msg.data || !msg.data.rects) return {error: "Missing rects array"};
|
||||
win.updateSurfaceRects(msg.data.rects);
|
||||
return {success: true};
|
||||
|
||||
case 'raise':
|
||||
win.raise();
|
||||
return {success: true};
|
||||
|
||||
case 'restore':
|
||||
win.restore();
|
||||
return {success: true};
|
||||
|
||||
case 'flash':
|
||||
win.flash(msg.data ? msg.data.operation : 'briefly');
|
||||
return {success: true};
|
||||
|
||||
case 'sync':
|
||||
win.sync();
|
||||
return {success: true};
|
||||
|
||||
case 'setIcon':
|
||||
if (!msg.data || !msg.data.surface_id) return {error: "Missing surface_id"};
|
||||
var surf = resources.surface[msg.data.surface_id];
|
||||
if (!surf) return {error: "Invalid surface id"};
|
||||
win.set_icon(surf);
|
||||
return {success: true};
|
||||
|
||||
case 'makeRenderer':
|
||||
var renderer = win.make_renderer();
|
||||
var renderer_id = allocate_id();
|
||||
resources.renderer[renderer_id] = renderer;
|
||||
return {id: renderer_id};
|
||||
|
||||
default:
|
||||
return {error: "Unknown window operation: " + msg.op};
|
||||
}
|
||||
}
|
||||
|
||||
// Renderer operations
|
||||
function handle_renderer(msg) {
|
||||
// Special case: createWindowAndRenderer creates both
|
||||
if (msg.op === 'createWindowAndRenderer') {
|
||||
var data = msg.data || {};
|
||||
var result = prosperon.endowments.createWindowAndRenderer(
|
||||
data.title || "Prosperon Window",
|
||||
data.width || 640,
|
||||
data.height || 480,
|
||||
data.flags || 0
|
||||
);
|
||||
var win_id = allocate_id();
|
||||
var ren_id = allocate_id();
|
||||
resources.window[win_id] = result.window;
|
||||
resources.renderer[ren_id] = result.renderer;
|
||||
return {window_id: win_id, renderer_id: ren_id};
|
||||
}
|
||||
|
||||
// All other operations require a valid renderer ID
|
||||
if (!msg.id || !resources.renderer[msg.id]) {
|
||||
return {error: "Invalid renderer id: " + msg.id};
|
||||
}
|
||||
|
||||
var ren = resources.renderer[msg.id];
|
||||
|
||||
switch (msg.op) {
|
||||
case 'destroy':
|
||||
delete resources.renderer[msg.id];
|
||||
// Renderer is automatically destroyed when all references are gone
|
||||
return {success: true};
|
||||
|
||||
case 'clear':
|
||||
ren.clear();
|
||||
return {success: true};
|
||||
|
||||
case 'present':
|
||||
ren.present();
|
||||
return {success: true};
|
||||
|
||||
case 'flush':
|
||||
ren.flush();
|
||||
return {success: true};
|
||||
|
||||
case 'get':
|
||||
var prop = msg.data ? msg.data.property : null;
|
||||
if (!prop) return {error: "Missing property name"};
|
||||
|
||||
// Handle special cases
|
||||
if (prop === 'window') {
|
||||
var win = ren.window;
|
||||
if (!win) return {data: null};
|
||||
// Find window ID
|
||||
for (var id in resources.window) {
|
||||
if (resources.window[id] === win) {
|
||||
return {data: id};
|
||||
}
|
||||
}
|
||||
// Window not tracked, add it
|
||||
var win_id = allocate_id();
|
||||
resources.window[win_id] = win;
|
||||
return {data: win_id};
|
||||
}
|
||||
|
||||
// Handle special getters that might return objects
|
||||
if (prop === 'drawColor') {
|
||||
var color = ren[prop];
|
||||
if (color && typeof color === 'object') {
|
||||
// Convert color object to array format [r,g,b,a]
|
||||
return {data: [color.r || 0, color.g || 0, color.b || 0, color.a || 255]};
|
||||
}
|
||||
}
|
||||
|
||||
return {data: ren[prop]};
|
||||
|
||||
case 'set':
|
||||
var prop = msg.prop
|
||||
var value = msg.value
|
||||
if (!prop) return {error: "Missing property name"};
|
||||
|
||||
// Validate property is settable
|
||||
var readonly = ['window', 'name', 'outputSize', 'currentOutputSize', 'logicalPresentationRect', 'safeArea'];
|
||||
if (readonly.indexOf(prop) !== -1) {
|
||||
return {error: "Property '" + prop + "' is read-only"};
|
||||
}
|
||||
|
||||
// Special handling for render target
|
||||
if (prop === 'target' && value !== null && value !== undefined) {
|
||||
var tex = resources.texture[value];
|
||||
if (!tex) return {error: "Invalid texture id"};
|
||||
value = tex;
|
||||
}
|
||||
|
||||
ren[prop] = value;
|
||||
return {success: true};
|
||||
|
||||
case 'line':
|
||||
if (!msg.data || !msg.data.points) return {error: "Missing points array"};
|
||||
ren.line(msg.data.points);
|
||||
return {success: true};
|
||||
|
||||
case 'point':
|
||||
if (!msg.data || !msg.data.points) return {error: "Missing points"};
|
||||
ren.point(msg.data.points);
|
||||
return {success: true};
|
||||
|
||||
case 'rect':
|
||||
if (!msg.data || !msg.data.rect) return {error: "Missing rect"};
|
||||
ren.rect(msg.data.rect);
|
||||
return {success: true};
|
||||
|
||||
case 'fillRect':
|
||||
if (!msg.data || !msg.data.rect) return {error: "Missing rect"};
|
||||
ren.fillRect(msg.data.rect);
|
||||
return {success: true};
|
||||
|
||||
case 'rects':
|
||||
if (!msg.data || !msg.data.rects) return {error: "Missing rects"};
|
||||
ren.rects(msg.data.rects);
|
||||
return {success: true};
|
||||
|
||||
case 'lineTo':
|
||||
if (!msg.data || !msg.data.a || !msg.data.b) return {error: "Missing points a and b"};
|
||||
ren.lineTo(msg.data.a, msg.data.b);
|
||||
return {success: true};
|
||||
|
||||
case 'texture':
|
||||
if (!msg.data) return {error: "Missing texture data"};
|
||||
var tex_id = msg.data.texture_id;
|
||||
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
|
||||
ren.texture(
|
||||
resources.texture[tex_id],
|
||||
msg.data.src,
|
||||
msg.data.dst,
|
||||
msg.data.angle || 0,
|
||||
msg.data.anchor || {x:0.5, y:0.5}
|
||||
);
|
||||
return {success: true};
|
||||
|
||||
case 'copyTexture':
|
||||
if (!msg.data) return {error: "Missing texture data"};
|
||||
var tex_id = msg.data.texture_id;
|
||||
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
|
||||
var tex = resources.texture[tex_id];
|
||||
|
||||
// Use the texture method with normalized coordinates
|
||||
ren.texture(
|
||||
tex,
|
||||
msg.data.src || {x:0, y:0, width:tex.width, height:tex.height},
|
||||
msg.data.dest || {x:0, y:0, width:tex.width, height:tex.height},
|
||||
0, // No rotation
|
||||
{x:0, y:0} // Top-left anchor
|
||||
);
|
||||
return {success: true};
|
||||
|
||||
case 'sprite':
|
||||
if (!msg.data || !msg.data.sprite) return {error: "Missing sprite data"};
|
||||
ren.sprite(msg.data.sprite);
|
||||
return {success: true};
|
||||
|
||||
case 'geometry':
|
||||
if (!msg.data) return {error: "Missing geometry data"};
|
||||
var tex_id = msg.data.texture_id;
|
||||
var tex = tex_id ? resources.texture[tex_id] : null;
|
||||
ren.geometry(tex, msg.data.geometry);
|
||||
return {success: true};
|
||||
|
||||
case 'debugText':
|
||||
if (!msg.data || !msg.data.text) return {error: "Missing text"};
|
||||
ren.debugText([msg.data.pos.x, msg.data.pos.y], msg.data.text);
|
||||
return {success: true};
|
||||
|
||||
case 'clipEnabled':
|
||||
return {data: ren.clipEnabled()};
|
||||
|
||||
case 'texture9Grid':
|
||||
if (!msg.data) return {error: "Missing data"};
|
||||
var tex_id = msg.data.texture_id;
|
||||
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
|
||||
ren.texture9Grid(
|
||||
resources.texture[tex_id],
|
||||
msg.data.src,
|
||||
msg.data.leftWidth,
|
||||
msg.data.rightWidth,
|
||||
msg.data.topHeight,
|
||||
msg.data.bottomHeight,
|
||||
msg.data.scale,
|
||||
msg.data.dst
|
||||
);
|
||||
return {success: true};
|
||||
|
||||
case 'textureTiled':
|
||||
if (!msg.data) return {error: "Missing data"};
|
||||
var tex_id = msg.data.texture_id;
|
||||
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
|
||||
ren.textureTiled(
|
||||
resources.texture[tex_id],
|
||||
msg.data.src,
|
||||
msg.data.scale || 1.0,
|
||||
msg.data.dst
|
||||
);
|
||||
return {success: true};
|
||||
|
||||
case 'readPixels':
|
||||
var surf = ren.readPixels(msg.data ? msg.data.rect : null);
|
||||
if (!surf) return {error: "Failed to read pixels"};
|
||||
var surf_id = allocate_id();
|
||||
resources.surface[surf_id] = surf;
|
||||
return {id: surf_id};
|
||||
|
||||
case 'loadTexture':
|
||||
if (!msg.data) throw new Error("Missing data")
|
||||
|
||||
var tex;
|
||||
// Direct surface data
|
||||
var surf = new surface(msg.data)
|
||||
|
||||
if (!surf)
|
||||
throw new Error("Must provide surface_id or surface data")
|
||||
|
||||
tex = ren.load_texture(surf);
|
||||
|
||||
if (!tex) throw new Error("Failed to load texture")
|
||||
var tex_id = allocate_id();
|
||||
resources.texture[tex_id] = tex;
|
||||
return {
|
||||
id: tex_id,
|
||||
};
|
||||
|
||||
case 'flush':
|
||||
ren.flush();
|
||||
return {success: true};
|
||||
|
||||
case 'coordsFromWindow':
|
||||
if (!msg.data || !msg.data.pos) return {error: "Missing pos"};
|
||||
return {data: ren.coordsFromWindow(msg.data.pos)};
|
||||
|
||||
case 'coordsToWindow':
|
||||
if (!msg.data || !msg.data.pos) return {error: "Missing pos"};
|
||||
return {data: ren.coordsToWindow(msg.data.pos)};
|
||||
|
||||
case 'batch':
|
||||
// Execute a batch of operations
|
||||
if (!msg.data || !Array.isArray(msg.data)) return {error: "Missing or invalid data array"};
|
||||
|
||||
var results = [];
|
||||
for (var i = 0; i < msg.data.length; i++) {
|
||||
var cmd = msg.data[i];
|
||||
if (!cmd.op) {
|
||||
results.push({error: "Command at index " + i + " missing op"});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create a temporary message object for the command
|
||||
var temp_msg = {
|
||||
kind: 'renderer',
|
||||
id: msg.id,
|
||||
op: cmd.op,
|
||||
prop: cmd.prop,
|
||||
value: cmd.value,
|
||||
data: cmd.data
|
||||
};
|
||||
|
||||
// Recursively call handle_renderer for each command
|
||||
var result = handle_renderer(temp_msg);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return {results: results};
|
||||
|
||||
default:
|
||||
return {error: "Unknown renderer operation: " + msg.op};
|
||||
}
|
||||
}
|
||||
|
||||
// Texture operations
|
||||
function handle_texture(msg) {
|
||||
// Special case: create needs a renderer
|
||||
if (msg.op === 'create') {
|
||||
if (!msg.data) return {error: "Missing texture data"};
|
||||
var ren_id = msg.data.renderer_id;
|
||||
if (!ren_id || !resources.renderer[ren_id]) return {error: "Invalid renderer id"};
|
||||
|
||||
var tex;
|
||||
var renderer = resources.renderer[ren_id];
|
||||
|
||||
// Create from surface
|
||||
if (msg.data.surface_id) {
|
||||
var surf = resources.surface[msg.data.surface_id];
|
||||
if (!surf) return {error: "Invalid surface id"};
|
||||
tex = new prosperon.endowments.texture(renderer, surf);
|
||||
}
|
||||
// Create from properties
|
||||
else if (msg.data.width && msg.data.height) {
|
||||
tex = new prosperon.endowments.texture(renderer, {
|
||||
width: msg.data.width,
|
||||
height: msg.data.height,
|
||||
format: msg.data.format || 'rgba8888',
|
||||
pixels: msg.data.pixels,
|
||||
pitch: msg.data.pitch
|
||||
});
|
||||
}
|
||||
else {
|
||||
log.console(json.encode(msg.data))
|
||||
return {error: "Must provide either surface_id or width/height"};
|
||||
}
|
||||
|
||||
var tex_id = allocate_id();
|
||||
resources.texture[tex_id] = tex;
|
||||
return {id: tex_id, data: {size: tex.size}};
|
||||
}
|
||||
|
||||
// All other operations require a valid texture ID
|
||||
if (!msg.id || !resources.texture[msg.id]) {
|
||||
return {error: "Invalid texture id: " + msg.id};
|
||||
}
|
||||
|
||||
var tex = resources.texture[msg.id];
|
||||
|
||||
switch (msg.op) {
|
||||
case 'destroy':
|
||||
delete resources.texture[msg.id];
|
||||
// Texture is automatically destroyed when all references are gone
|
||||
return {success: true};
|
||||
|
||||
case 'get':
|
||||
var prop = msg.data ? msg.data.property : null;
|
||||
if (!prop) return {error: "Missing property name"};
|
||||
return {data: tex[prop]};
|
||||
|
||||
case 'set':
|
||||
var prop = msg.data ? msg.data.property : null;
|
||||
var value = msg.data ? msg.data.value : undefined;
|
||||
if (!prop) return {error: "Missing property name"};
|
||||
|
||||
// Validate property is settable
|
||||
var readonly = ['size', 'width', 'height'];
|
||||
if (readonly.indexOf(prop) !== -1) {
|
||||
return {error: "Property '" + prop + "' is read-only"};
|
||||
}
|
||||
|
||||
tex[prop] = value;
|
||||
return {success: true};
|
||||
|
||||
case 'update':
|
||||
if (!msg.data) return {error: "Missing update data"};
|
||||
tex.update(
|
||||
msg.data.rect || null,
|
||||
msg.data.pixels,
|
||||
msg.data.pitch || 0
|
||||
);
|
||||
return {success: true};
|
||||
|
||||
case 'lock':
|
||||
var result = tex.lock(msg.data ? msg.data.rect : null);
|
||||
return {data: result};
|
||||
|
||||
case 'unlock':
|
||||
tex.unlock();
|
||||
return {success: true};
|
||||
|
||||
case 'query':
|
||||
return {data: tex.query()};
|
||||
|
||||
default:
|
||||
return {error: "Unknown texture operation: " + msg.op};
|
||||
}
|
||||
}
|
||||
|
||||
// Surface operations (mainly for cleanup)
|
||||
function handle_surface(msg) {
|
||||
switch (msg.op) {
|
||||
case 'destroy':
|
||||
if (!msg.id || !resources.surface[msg.id]) {
|
||||
return {error: "Invalid surface id: " + msg.id};
|
||||
}
|
||||
delete resources.surface[msg.id];
|
||||
return {success: true};
|
||||
|
||||
default:
|
||||
return {error: "Unknown surface operation: " + msg.op};
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor operations
|
||||
function handle_cursor(msg) {
|
||||
switch (msg.op) {
|
||||
case 'create':
|
||||
var surf = new surface(msg.data)
|
||||
|
||||
var hotspot = msg.data.hotspot || [0, 0];
|
||||
var cursor = prosperon.endowments.createCursor(surf, hotspot);
|
||||
|
||||
var cursor_id = allocate_id();
|
||||
resources.cursor[cursor_id] = cursor;
|
||||
return {id: cursor_id};
|
||||
|
||||
case 'set':
|
||||
var cursor = null;
|
||||
if (msg.id && resources.cursor[msg.id]) {
|
||||
cursor = resources.cursor[msg.id];
|
||||
}
|
||||
prosperon.endowments.setCursor(cursor);
|
||||
return {success: true};
|
||||
|
||||
case 'destroy':
|
||||
if (!msg.id || !resources.cursor[msg.id]) {
|
||||
return {error: "Invalid cursor id: " + msg.id};
|
||||
}
|
||||
delete resources.cursor[msg.id];
|
||||
return {success: true};
|
||||
|
||||
default:
|
||||
return {error: "Unknown cursor operation: " + msg.op};
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to create window and renderer
|
||||
prosperon.endowments = prosperon.endowments || {};
|
||||
|
||||
// Mouse operations
|
||||
function handle_mouse(msg) {
|
||||
var mouse = prosperon.endowments.mouse;
|
||||
|
||||
switch (msg.op) {
|
||||
case 'show':
|
||||
if (msg.data === undefined) return {error: "Missing show parameter"};
|
||||
mouse.show(msg.data);
|
||||
return {success: true};
|
||||
|
||||
case 'capture':
|
||||
if (msg.data === undefined) return {error: "Missing capture parameter"};
|
||||
mouse.capture(msg.data);
|
||||
return {success: true};
|
||||
|
||||
case 'get_state':
|
||||
return {data: mouse.get_state()};
|
||||
|
||||
case 'get_global_state':
|
||||
return {data: mouse.get_global_state()};
|
||||
|
||||
case 'get_relative_state':
|
||||
return {data: mouse.get_relative_state()};
|
||||
|
||||
case 'warp_global':
|
||||
if (!msg.data) return {error: "Missing position"};
|
||||
mouse.warp_global(msg.data);
|
||||
return {success: true};
|
||||
|
||||
case 'warp_in_window':
|
||||
if (!msg.data || !msg.data.window_id || !msg.data.pos)
|
||||
return {error: "Missing window_id or position"};
|
||||
var window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
mouse.warp_in_window(window, msg.data.pos);
|
||||
return {success: true};
|
||||
|
||||
case 'cursor_visible':
|
||||
return {data: mouse.cursor_visible()};
|
||||
|
||||
case 'get_cursor':
|
||||
var cursor = mouse.get_cursor();
|
||||
if (!cursor) return {data: null};
|
||||
// Find or create cursor ID
|
||||
for (var id in resources.cursor) {
|
||||
if (resources.cursor[id] === cursor) {
|
||||
return {data: id};
|
||||
}
|
||||
}
|
||||
// Not tracked, add it
|
||||
var cursor_id = allocate_id();
|
||||
resources.cursor[cursor_id] = cursor;
|
||||
return {data: cursor_id};
|
||||
|
||||
case 'get_default_cursor':
|
||||
var cursor = mouse.get_default_cursor();
|
||||
if (!cursor) return {data: null};
|
||||
// Find or create cursor ID
|
||||
for (var id in resources.cursor) {
|
||||
if (resources.cursor[id] === cursor) {
|
||||
return {data: id};
|
||||
}
|
||||
}
|
||||
// Not tracked, add it
|
||||
var cursor_id = allocate_id();
|
||||
resources.cursor[cursor_id] = cursor;
|
||||
return {data: cursor_id};
|
||||
|
||||
case 'create_system_cursor':
|
||||
if (msg.data === undefined) return {error: "Missing cursor type"};
|
||||
var cursor = mouse.create_system_cursor(msg.data);
|
||||
var cursor_id = allocate_id();
|
||||
resources.cursor[cursor_id] = cursor;
|
||||
return {id: cursor_id};
|
||||
|
||||
case 'get_focus':
|
||||
var window = mouse.get_focus();
|
||||
if (!window) return {data: null};
|
||||
// Find window ID
|
||||
for (var id in resources.window) {
|
||||
if (resources.window[id] === window) {
|
||||
return {data: id};
|
||||
}
|
||||
}
|
||||
// Not tracked, add it
|
||||
var win_id = allocate_id();
|
||||
resources.window[win_id] = window;
|
||||
return {data: win_id};
|
||||
|
||||
default:
|
||||
return {error: "Unknown mouse operation: " + msg.op};
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard operations
|
||||
function handle_keyboard(msg) {
|
||||
var keyboard = prosperon.endowments.keyboard;
|
||||
|
||||
switch (msg.op) {
|
||||
case 'get_state':
|
||||
return {data: keyboard.get_state()};
|
||||
|
||||
case 'get_focus':
|
||||
var window = keyboard.get_focus();
|
||||
if (!window) return {data: null};
|
||||
// Find window ID
|
||||
for (var id in resources.window) {
|
||||
if (resources.window[id] === window) {
|
||||
return {data: id};
|
||||
}
|
||||
}
|
||||
// Not tracked, add it
|
||||
var win_id = allocate_id();
|
||||
resources.window[win_id] = window;
|
||||
return {data: win_id};
|
||||
|
||||
case 'start_text_input':
|
||||
var window = null;
|
||||
if (msg.data && msg.data.window_id) {
|
||||
window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
}
|
||||
keyboard.start_text_input(window);
|
||||
return {success: true};
|
||||
|
||||
case 'stop_text_input':
|
||||
var window = null;
|
||||
if (msg.data && msg.data.window_id) {
|
||||
window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
}
|
||||
keyboard.stop_text_input(window);
|
||||
return {success: true};
|
||||
|
||||
case 'text_input_active':
|
||||
var window = null;
|
||||
if (msg.data && msg.data.window_id) {
|
||||
window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
}
|
||||
return {data: keyboard.text_input_active(window)};
|
||||
|
||||
case 'get_text_input_area':
|
||||
var window = null;
|
||||
if (msg.data && msg.data.window_id) {
|
||||
window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
}
|
||||
return {data: keyboard.get_text_input_area(window)};
|
||||
|
||||
case 'set_text_input_area':
|
||||
if (!msg.data || !msg.data.rect) return {error: "Missing rect"};
|
||||
var window = null;
|
||||
if (msg.data.window_id) {
|
||||
window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
}
|
||||
keyboard.set_text_input_area(msg.data.rect, msg.data.cursor || 0, window);
|
||||
return {success: true};
|
||||
|
||||
case 'clear_composition':
|
||||
var window = null;
|
||||
if (msg.data && msg.data.window_id) {
|
||||
window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
}
|
||||
keyboard.clear_composition(window);
|
||||
return {success: true};
|
||||
|
||||
case 'screen_keyboard_shown':
|
||||
if (!msg.data || !msg.data.window_id) return {error: "Missing window_id"};
|
||||
var window = resources.window[msg.data.window_id];
|
||||
if (!window) return {error: "Invalid window id"};
|
||||
return {data: keyboard.screen_keyboard_shown(window)};
|
||||
|
||||
case 'reset':
|
||||
keyboard.reset();
|
||||
return {success: true};
|
||||
|
||||
default:
|
||||
return {error: "Unknown keyboard operation: " + msg.op};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -295,7 +295,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 |
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
|
||||
397
prosperon/examples/chess/main.ce
Normal file
@@ -0,0 +1,397 @@
|
||||
/* 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()
|
||||
draw2d.text("HELL", {x: 100, y: 100}, 'fonts/c64.ttf', 16, [1,1,1,1])
|
||||
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)
|
||||
}
|
||||
})
|
||||
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,203 @@
|
||||
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 renderer_id = arg[1]
|
||||
|
||||
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",
|
||||
id: renderer_id,
|
||||
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 +218,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 +280,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 +289,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 +344,27 @@ 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",
|
||||
id: renderer_id,
|
||||
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 +382,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 +400,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 +408,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 |