commit 3d1f3e5eb4011d1b5acb24558d1e2048ffd1202b Author: Giancarmine Salucci Date: Tue Mar 31 18:38:34 2026 +0200 chore: initial commit diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..887b6e7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "trueref": { + "command": "node", + "args": ["--import=tsx/esm", "/home/moze/Sources/trueref/src/mcp/index.ts"], + "env": { + "TRUEREF_API_URL": "http://localhost:5173" + } + } + }, + "enableAllProjectMcpServers": true +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..fcde7f7 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,72 @@ +{ + "permissions": { + "allow": [ + "Read(//home/moze/Sources/trueref/**)", + "WebFetch(domain:en.wikipedia.org)", + "Read(//home/moze/.claude/**)", + "Read(//home/moze/.config/claude/**)", + "Bash(curl -s \"http://localhost:5173/api/v1/libs/search?libraryName=phaser&query=phaser+game+framework&type=txt\")", + "Skill(update-config)", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=load+texture+atlas+sprite+sheet+frame&type=txt&tokens=3000\")", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=scene+preload+create+game+config+setup&type=txt&tokens=3000\")", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=load+atlas+texture+atlas+json+this.load.atlas&type=txt&tokens=2000\")", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=image+draggable+input+pointer+drag+interactive&type=txt&tokens=2000\")", + "Bash(npm create:*)", + "Bash(npx:*)", + "Bash(echo {})", + "Bash(npm init:*)", + "Bash(npm install:*)", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=game+object+image+add+scene+this.add.image+create&type=txt&tokens=2000\")", + "Bash(cp /tmp/phaser-atlas.json /home/moze/Sources/phaser-scopa/public/atlas.json)", + "Bash(cp /home/moze/Immagini/Napoletane/dist/atlas.png /home/moze/Sources/phaser-scopa/public/atlas.png)", + "Bash(cp /home/moze/Immagini/Napoletane/dist/retro.png /home/moze/Sources/phaser-scopa/public/retro.png)", + "mcp__chrome-devtools__take_screenshot", + "mcp__chrome-devtools__click", + "mcp__chrome-devtools__evaluate_script", + "mcp__chrome-devtools__list_console_messages", + "mcp__chrome-devtools__list_network_requests", + "mcp__chrome-devtools__navigate_page", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=particle+emitter+ParticleEmitter+explode+burst+effect&type=txt&tokens=3000\")", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=WebAudioSound sound manager play background music loop&type=txt&tokens=2000\")", + "Bash(curl -v \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=particle+emitter+burst+effect&type=txt&tokens=2000\")", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=particle+emitter+burst+effect&type=txt&tokens=2000\" -o /tmp/trueref_particles.txt)", + "Read(//tmp/**)", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=sound+manager+audio+play+loop&type=txt&tokens=2000\" -o /tmp/trueref_audio.txt)", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=ParticleEmitter+addEmitter+createEmitter+gravity+speed&type=txt&tokens=2000\" -o /tmp/t1.txt)", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=tween+chain+timeline+sequence+onComplete&type=txt&tokens=2000\" -o /tmp/t2.txt)", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=add particles texture frame lifespan speed scale alpha&type=txt&tokens=3000\" -o /tmp/t3.txt)", + "Bash(curl -s --max-time 10 \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=graphics fillRoundedRect strokeRoundedRect radius corner&type=txt&tokens=2500\")", + "Bash(curl -s http://localhost:3000)", + "mcp__chrome-devtools__get_console_message", + "mcp__context7__resolve-library-id", + "Bash(curl -s http://localhost:5173/api/v1/libraries)", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=mobile+android+APK+cordova+capacitor+package&type=txt\")", + "Bash(curl -sv \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=mobile+android+APK+cordova+capacitor+package&type=txt\")", + "Bash(curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=device+mobile+scale+touch+input+browser&type=txt&tokens=3000\")", + "Bash(head -60 echo \"---\" curl -s \"http://localhost:5173/api/v1/context?libraryId=/local/phaser&query=scale manager fit resize mobile device&type=txt&tokens=2000\")", + "Bash(java -version)", + "Read(//home/moze/Android/Sdk/**)", + "Bash(node -e \"require\\(''child_process''\\).execSync\\(''npx @capacitor/cli --version'', {stdio:''inherit''}\\)\")", + "Read(//usr/lib/android-sdk/**)", + "Read(//opt/**)", + "Read(//usr/**)", + "Bash(dpkg -l android-sdk)", + "Bash(adb version:*)", + "Bash(apt-cache search:*)", + "Bash(curl:*)", + "Bash(sudo apt-get:*)", + "Bash(unzip -q /tmp/cmdline-tools.zip -d ~/android-sdk/cmdline-tools)", + "Bash(mv ~/android-sdk/cmdline-tools/cmdline-tools ~/android-sdk/cmdline-tools/latest)", + "Read(//home/moze/android-sdk/cmdline-tools/latest/**)", + "Read(//home/moze/android-sdk/**)", + "Bash(export ANDROID_SDK_ROOT=$HOME/android-sdk export ANDROID_HOME=$HOME/android-sdk export PATH=$PATH:$HOME/android-sdk/cmdline-tools/latest/bin __NEW_LINE_061516952640df3a__ yes)", + "Bash($HOME/android-sdk/cmdline-tools/latest/bin/sdkmanager --sdk_root=$HOME/android-sdk \"platforms;android-34\" \"build-tools;34.0.0\")", + "Bash(npm run:*)", + "Bash(export ANDROID_SDK_ROOT=$HOME/android-sdk)", + "Bash(export ANDROID_HOME=$HOME/android-sdk)", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); [print\\(s[''''title''''], ''''|'''', s[''''description''''][:80]\\) for s in d.get\\(''''snippets'''',[]\\)]\")", + "Bash(grep -r \"android-sdk\\\\|ANDROID_SDK\\\\|sdk.dir\" /home/moze/Sources/phaser-scopa/android/ --include=*.properties --include=*.gradle --include=*.gradle.kts)", + "Bash(./gradlew assembleDebug --quiet)" + ] + } +} diff --git a/.github/agents b/.github/agents new file mode 120000 index 0000000..e9ce9b9 --- /dev/null +++ b/.github/agents @@ -0,0 +1 @@ +/home/moze/Sources/copilot-agents/.github/agents \ No newline at end of file diff --git a/.github/schemas b/.github/schemas new file mode 120000 index 0000000..f200112 --- /dev/null +++ b/.github/schemas @@ -0,0 +1 @@ +/home/moze/Sources/copilot-agents/.github/schemas \ No newline at end of file diff --git a/.github/skills b/.github/skills new file mode 120000 index 0000000..74398ae --- /dev/null +++ b/.github/skills @@ -0,0 +1 @@ +/home/moze/Sources/copilot-agents/.github/skills \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a559eb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +prompts/ +docs/docs_cache_state.yaml diff --git a/TRUEREF_FEEDBACK.md b/TRUEREF_FEEDBACK.md new file mode 100644 index 0000000..ad5abc2 --- /dev/null +++ b/TRUEREF_FEEDBACK.md @@ -0,0 +1,225 @@ +# TrueRef Feedback — From a Phaser.js Development Session + +**Date:** 2026-03-26 +**Project:** Scopone Scientifico card game with Phaser 3.90 +**Library Indexed:** `/local/phaser` (cloned Phaser repo, ~3.5M tokens, 8848 snippets) + +--- + +## What Worked Well + +### 1. Library Discovery +`resolve-library-id` found `/local/phaser` immediately: +```json +{"results":[{"id":"/local/phaser","title":"phaser","state":"finalized", + "totalTokens":3511693,"totalSnippets":8848}]} +``` +Zero friction. The `state: "finalized"` field is useful for callers to know it's ready. + +### 2. Relevant Snippets for Conceptual Queries +Broad queries like `"scene preload create game config setup"` and `"image draggable input pointer drag interactive"` returned well-matched `InputConfiguration.js` typedef and scene lifecycle documentation. The semantic search correctly ranked relevant fragments for natural-language queries. + +### 3. Speed +All queries resolved in under 200ms against the local SQLite index. Excellent for tight dev loops. + +### 4. Context7 Compatibility +The drop-in interface (same tool names, same params) worked as expected. No adapter needed. + +### 5. Particle API Migration Docs — Well Indexed +Query `"add particles texture lifespan speed scale alpha"` correctly returned the full +`ParticleEmitter.js` JSDoc block explaining the v3.55→v3.60 API change (removal of +`ParticleEmitterManager`, new `this.add.particles(x,y,texture,config)` signature). This +was the most critical query of session 2 and it landed on exactly the right snippet. + +### 6. Audio API Discovery +Query `"sound manager audio play loop"` returned `HTML5AudioSound.js` which confirmed +the `this.sound.context` path for accessing the raw `AudioContext`. Useful for procedural +audio patterns that don't need loaded assets. + +--- + +## Issues Found + +### BUG — FTS5 Special Character Crash +**Severity: High** + +Any query containing a `.` (dot) causes a hard server error: +``` +GET /api/v1/context?query=this.load.atlas → {"error":"fts5: syntax error near \".\"","code":"INTERNAL_ERROR"} +``` +This is triggered by the most natural way to ask about Phaser APIs — developers instinctively include method names like `this.load.atlas()`, `Phaser.Scale.FIT`, `scene.add.image()`. + +**Root cause:** FTS5 treats `.` as a query operator. The user query string is passed raw into the FTS5 `MATCH` expression. + +**Fix:** +```typescript +// In query-docs handler, sanitize for FTS5 before keyword search +function sanitizeFts5(query: string): string { + // Remove/replace FTS5 special chars: . , ( ) " * ^ ~ - + @ = + return query.replace(/[.()"*^~\-+@=,]/g, ' ').replace(/\s+/g, ' ').trim(); +} +``` +Or better: wrap the whole query in `"..."` for phrase matching, or switch to semantic-only when the query contains code-like symbols. + +--- + +### BUG — No Automatic Fallback on FTS5 Error +**Severity: Medium** + +When keyword search fails (FTS5 crash, empty results), the server returns an error or empty response instead of automatically retrying with `searchMode=semantic`. Given that `searchMode=auto` is the default, "auto" should truly mean "try keyword, fall back to semantic if needed." + +**Current behaviour:** `searchMode=auto` → keyword fails → 500 error +**Expected behaviour:** `searchMode=auto` → keyword fails → retry with semantic → return best result + +--- + +### USABILITY — Semantic Search Imprecision for API Surface Queries +**Severity: Medium** + +Query: `"load atlas texture atlas json this.load.atlas"` +Result: Returned `SpriteSheetFile.js` docs (adjacent topic, wrong API). + +The correct result would be `AtlasJSONFile.js` with the `this.load.atlas()` signature. This may be a chunking issue — if the atlas loader code isn't semantically close to "load atlas texture atlas json", the embedding is misleading. + +**Suggestion:** Index JSDoc `@method` names and their parameter descriptions as separate high-weight chunks, so API surface queries get precise matches even when the function body is minimal. + +--- + +### USABILITY — No Symbol/API Lookup Mode +**Severity: Low–Medium** + +There's no way to say "give me the signature and docs for `Phaser.Loader.LoaderPlugin#atlas`" without knowing what file it lives in. Context7 has the same limitation but a "symbol search" mode would be very useful for: + +- Finding the exact overloads of a method +- Looking up TypeDef shapes +- Checking which config properties exist on a class + +**Suggested addition:** +``` +GET /api/v1/symbol?libraryId=...&symbol=LoaderPlugin%23atlas +``` +Or as a `searchMode=symbol` option in the existing `/context` endpoint. + +--- + +### USABILITY — Text Output Lacks File-Path Headers for Some Snippets +**Severity: Low** + +Some returned snippet blocks lack the `### FileName.js\n*src/path/to/file.js*` header, making it hard to know which part of the codebase a snippet comes from. Consistent headers on every snippet would help developers navigate to the source. + +--- + +### USABILITY — `tokens` Parameter Has No Visible Effect Below ~3000 +**Severity: Low** + +Setting `tokens=500` still returned >1500 tokens of content. The parameter seems to be a soft cap or not enforced strictly. Clarify in the docs whether it truncates at the snippet level or the total response level, and enforce it more precisely so callers can budget context window usage. + +--- + +### BUG — Empty Response Instead of "No Results" for Some Queries +**Severity: Medium** + +Several queries returned a `200 OK` with an empty body (0 bytes): +- `"particle emitter ParticleEmitter explode burst effect"` → 0 bytes +- `"ParticleEmitter addEmitter createEmitter gravity speed"` → 0 bytes + +The identical concept with different phrasing (`"add particles texture lifespan speed scale alpha"`) returned 3451 bytes of perfect content. This means: +1. The FTS5/semantic pipeline silently returns nothing for some query formulations +2. The caller has no way to distinguish "no results" from a server error +3. A 0-byte 200 response should instead be a `{"message": "no matching snippets"}` with a 404, or the `auto` mode should try rephrasing before giving up + +**Fix:** Return a structured "no results" response body even when the result set is empty. Consider adding a `suggestions` field with alternative query terms. + +--- + +### BUG — camelCase Terms Return Zero Results (Keyword + Semantic) +**Severity: High** + +Queries using camelCase method names return 0 snippets even in `searchMode=semantic`: +``` +query=lockOrientation+landscape → {"snippets":[],"totalTokens":0} +query=screen+orientation+landscape+portrait+lock+mobile+device&searchMode=semantic → {"snippets":[],"totalTokens":0} +``` + +The word "orientation" and "landscape" clearly appear in `ScaleManager.js` (confirmed by direct keyword search returning the snippet). The semantic embeddings are not matching natural-language descriptions of the API to actual occurrences. + +**Root cause:** `lockOrientation` is only mentioned as JSDoc `@method` header with minimal surrounding prose. The embedding vector is too sparse to match broader concept queries. A dedicated method-summary chunk (method name + params + one-line description) would dramatically improve recall. + +**Fix:** During indexing, extract each `@method` block as its own mini-chunk: `"{methodName}({params}): {description}"`. This gives the embedder enough signal to match `"lock screen to landscape"` → `lockOrientation(orientation)`. + +--- + +### FINDING — Library Resolution Returns Real context7, Not Local trueref +**Severity: Medium / Workflow** + +When the project configures trueref as a local MCP server (`mcpServers.trueref`), the Claude Code MCP tool namespace resolves to `mcp__context7__*` — the same namespace as the cloud context7 service. This causes **silent fallthrough to the wrong backend**: `resolve-library-id("phaser")` returned `/phaserjs/phaser` (cloud) instead of `/local/phaser` (trueref local). + +**Impact:** Developers who configure both context7 (global) and trueref (project) have no way to tell which backend answered without inspecting the library IDs returned. The local trueref advantage (exact-version index) is silently bypassed. + +**Fix suggestions:** +- Trueref MCP server should expose a distinct tool namespace (e.g., `trueref__query-docs`) OR +- Return a `"source": "local"` field in every response so callers can verify they're hitting the local index +- Document that the project MCP server name should differ from `context7` to avoid namespace collision + +--- + +### FINDING — APK/Mobile Packaging Docs Absent from Index +**Severity: Low (expected gap)** + +Queries about mobile packaging (`android APK cordova capacitor package build export`) return 0 snippets. This is expected — the Phaser source repo is a game framework, not a packaging tool. However, Phaser's documentation website (`https://phaser.io/tutorials/making-your-first-phaser-3-game`) does include Cordova/Capacitor guides. + +**Suggestion:** When trueref indexes a library, optionally allow indexing the companion **documentation site** as a secondary source alongside the source repo. The `/local/phaser` index could be augmented with phaser.io/tutorials content, giving results for "how to package as APK" that the source code alone cannot provide. + +--- + +## Summary Table + +| Issue | Severity | Type | +|-------|----------|------| +| FTS5 crash on dots/special chars | High | Bug | +| camelCase terms return 0 results (keyword+semantic) | High | Bug | +| No fallback from keyword → semantic | Medium | Bug | +| Library namespace collision (local vs cloud context7) | Medium | Workflow Bug | +| Imprecise semantic results for API names | Medium | Usability | +| No symbol/method lookup mode | Medium | Feature Request | +| Empty body instead of structured "no results" | Medium | Bug | +| Missing file-path headers on some snippets | Low | Usability | +| `tokens` soft cap not enforced | Low | Usability | +| No docs-site indexing (packaging/tutorial content) | Low | Feature Request | + +--- + +## Suggested Priority + +1. Fix the FTS5 special-char crash (blocks real-world usage immediately) +2. Fix camelCase zero-results (method-summary mini-chunks at index time) +3. Auto-fallback in `searchMode=auto` +4. Disambiguate local vs cloud namespace in MCP tool names +5. Improve API surface indexing (separate JSDoc chunks with method names) +6. Symbol lookup endpoint + +--- + +## Notes on the Development Experience + +### Sessions 1–2 (Game development) +Using trueref for a local Phaser clone was smooth once queries were phrased as natural language rather than code expressions. The semantic search accurately returned `InputConfiguration` docs for "draggable input pointer", and correctly found the v3.55→v3.60 `ParticleEmitter` API migration notes. `camera.postFX.addVignette`, `fillRoundedRect`, `postFX.addGlow`, `camera.shake`, `camera.flash` — all found correctly. + +### Session 3 (Packaging to APK) +Attempted to use trueref to guide Android APK packaging via Capacitor. All mobile/orientation queries returned empty: +- `lockOrientation landscape` → 0 snippets +- `screen orientation landscape portrait lock mobile device` (semantic) → 0 snippets + +The `ScaleManager.lockOrientation` method IS in the index (it appeared in a broader earlier query), but no orientation-specific query could retrieve it. This is the camelCase / sparse-JSDoc problem documented above. + +**Packaging was completed via Capacitor without trueref assistance:** +``` +npm install @capacitor/core @capacitor/cli @capacitor/android +npx cap init "Scopone Scientifico" "com.phaser.scopa" --web-dir dist +npx cap add android +# Set android:screenOrientation="landscape" in AndroidManifest.xml +./gradlew assembleDebug +# → android/app/build/outputs/apk/debug/app-debug.apk (15MB) +``` + +The key advantage over context7 for private codebases remains obvious: indexing a local clone gives docs matching the exact version in use. Fix the FTS5 crash, the camelCase recall gap, and the namespace collision and it becomes fully production-ready. diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..972dead --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.application' + +android { + namespace = "com.phaser.scopa" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId "com.phaser.scopa" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle new file mode 100644 index 0000000..bbfb44f --- /dev/null +++ b/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..13f7f81 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/phaser/scopa/MainActivity.java b/android/app/src/main/java/com/phaser/scopa/MainActivity.java new file mode 100644 index 0000000..46f015d --- /dev/null +++ b/android/app/src/main/java/com/phaser/scopa/MainActivity.java @@ -0,0 +1,5 @@ +package com.phaser.scopa; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity {} diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000..e31573b Binary files /dev/null and b/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000..8077255 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000..14c6c8f Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000..244ca25 Binary files /dev/null and b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 0000000..74faaa5 Binary files /dev/null and b/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 0000000..e944f4a Binary files /dev/null and b/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 0000000..564a82f Binary files /dev/null and b/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000..bfabe68 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000..6929071 Binary files /dev/null and b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/android/app/src/main/res/drawable/splash.png differ diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c023e50 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2127973 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..b441f37 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..72905b8 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8ed0605 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..9502e47 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..4d1e077 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df0f158 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..853db04 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6cdf97c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2960cbb Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e3093a Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..46de6e2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d2ea9ab Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a40d73e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..52294e3 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Scopone Scientifico + Scopone Scientifico + com.phaser.scopa + com.phaser.scopa + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..f8f0e43 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle new file mode 100644 index 0000000..9a5fa87 --- /dev/null +++ b/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7705927 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/android/variables.gradle b/android/variables.gradle new file mode 100644 index 0000000..ee4ba41 --- /dev/null +++ b/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.7.1' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} \ No newline at end of file diff --git a/capacitor.config.ts b/capacitor.config.ts new file mode 100644 index 0000000..c5fd549 --- /dev/null +++ b/capacitor.config.ts @@ -0,0 +1,9 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'com.phaser.scopa', + appName: 'Scopone Scientifico', + webDir: 'dist' +}; + +export default config; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..6f0b2c1 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,147 @@ +# Architecture + +> Last Updated: 2026-03-31T00:00:00.000Z + +## Overview + +| Attribute | Value | +|-----------------|--------------------------------------------------------| +| **Language** | TypeScript (ES2020 target, strict mode) | +| **Type** | 2D card game — Scopone Scientifico | +| **Framework** | Phaser 3.87+ (scene-based game engine) | +| **Bundler** | Vite 5 | +| **Native** | Capacitor 8.3 (Android) | +| **Resolution** | 1280 × 720, FIT scaling with auto-center | + +## Project Structure + +``` +scopone-phaser/ +├── src/ +│ ├── main.ts # Phaser.Game bootstrap + config +│ ├── game/ +│ │ ├── types.ts # Card, Suit, Player, GameState, TeamScore, ScoreBreakdown, PRIMIERA_VALUES +│ │ ├── engine.ts # Deck build, shuffle, capture logic, applyMove, scoring, primiera +│ │ └── ai.ts # Heuristic AI: chooseMove, scoreCapture, scoreDump +│ └── scenes/ +│ ├── BootScene.ts # Asset loading (atlas, card back image) +│ ├── MenuScene.ts # Start menu with rules summary +│ └── GameScene.ts # Main game: rendering, turn management, effects, audio +├── index.html # Entry point, Italian locale, green felt background +├── public/ # Static assets (atlas.json, atlas.png, retro.png) +├── android/ # Capacitor Android native shell +├── package.json +├── tsconfig.json +├── vite.config.ts +└── capacitor.config.ts +``` + +## Key Directories + +| Directory | Purpose | +|--------------------|----------------------------------------------------------------| +| `src/game/` | Domain logic — types, game engine, AI (no Phaser dependency) | +| `src/scenes/` | Phaser scenes — rendering, input, effects, audio | +| `public/` | Static assets served by Vite (card atlas, card back) | +| `android/` | Capacitor-generated Android project (Gradle, Java) | +| `prompts/` | JIRA agent pipeline artifacts | + +## Design Patterns + +| Pattern | Where | +|-----------------------------|--------------------------------------------------------------------| +| **Scene lifecycle** | BootScene → MenuScene → GameScene (Phaser scene graph) | +| **Immutable state updates** | `applyMove()` deep-clones `GameState` before mutation | +| **Heuristic scoring** | AI evaluates all legal moves with weighted feature scores | +| **Separation of concerns** | `game/` has no Phaser imports; `scenes/` bridges game ↔ rendering | +| **Procedural audio** | Web Audio API oscillators + delay reverb — no audio files | + +No explicit GoF patterns (singleton, factory, observer, DI) detected. + +## Key Components + +### `types.ts` — Domain Types + +- `Card { suit, value, id }` — 40-card Napoletane deck (suits: bastoni, coppe, denara, spade; values 1-10) +- `GameState` — full round state: 4 players, table, current player, team scores, round tracking +- `TeamScore` — per-team stats: cards, scope, denari, settebello, primiera, round/total points +- `ScoreBreakdown` — which team won each scoring category +- `PRIMIERA_VALUES` — lookup table for primiera card values + +### `engine.ts` — Game Logic + +- `buildDeck()` / `shuffle()` — Fisher-Yates 40-card deck +- `createInitialState()` — deals 10 cards per player, empty table (Scopone Scientifico rules) +- `findCaptures(played, table)` — direct value match (mandatory) or subset-sum combinations +- `applyMove(state, player, card, captureChoice)` — immutable state transition, scopa detection, end-of-round scoring +- `calculateScores()` / `scoreRound()` — carte, denari, settebello, primiera, scope points +- `calcPrimiera(pile)` — best card per suit using `PRIMIERA_VALUES` +- `teamOf(playerIdx)` — team assignment: 0+2 = Team A, 1+3 = Team B + +### `ai.ts` — Heuristic AI + +- `chooseMove(state, playerIdx)` — evaluates all legal moves (captures + dumps) +- `scoreCapture()` — weighted: scopa (+500), settebello (+300), denari (+50 each), card count, primiera value, opponent threat +- `scoreDump()` — avoids giving opponents scopa (-400), prefers low-value non-denari, penalises dumping 7s and aces + +### `GameScene.ts` — Main Scene (~1340 lines) + +- Four-player layout: South (human), West/East (AI, rotated ±90°), North (AI partner) +- Deal animation with staggered tweens +- Card selection with postFX glow pulse +- Capture highlighting with multiple-choice UI +- Particle effects: capture burst, scopa explosion, settebello flash, denari shimmer, primiera glow, card trails, victory confetti +- Camera shake + flash on scopa and settebello +- Live score bar with animated counter updates +- Think bar progress indicator during AI turns +- Procedural background music (oscillator drone + triangle melody + chord stabs) +- Round-end summary panel and game-over screen + +## Dependencies + +### Production + +| Package | Version | Purpose | +|----------------------|----------|--------------------------------------| +| `phaser` | ^3.87.0 | 2D game engine | +| `@capacitor/core` | ^8.3.0 | Capacitor runtime | +| `@capacitor/cli` | ^8.3.0 | Capacitor CLI | +| `@capacitor/android` | ^8.3.0 | Android platform plugin | + +### Development + +| Package | Version | Purpose | +|---------------|---------|--------------------------| +| `typescript` | ^5.0.0 | TypeScript compiler | +| `vite` | ^5.0.0 | Dev server and bundler | + +## Module Organisation + +``` +main.ts ──→ BootScene ──→ MenuScene ──→ GameScene + │ + ├── game/engine (createInitialState, applyMove, findCaptures, ...) + ├── game/ai (chooseMove) + └── game/types (Card, GameState, ...) +``` + +`game/` modules are pure logic with no framework coupling. `scenes/` imports from `game/` but never vice versa. + +## Data Flow + +1. `createInitialState()` builds shuffled deck, deals 10 cards each, empty table +2. `GameScene.nextTurn()` detects current player: human → enable input; AI → delay + `chooseMove()` +3. `applyMove()` returns new `GameState` + capture result + scopa flag +4. `GameScene.executeMove()` animates: card flight → capture burst → pile collection +5. `updateScoreBar()` reflects live team stats with animated counter tweens +6. When all hands empty → `calculateScores()` → round-end overlay +7. First team to 11 points → game-over screen → optional restart + +## Build System + +| Command | Action | +|--------------------|--------------------------------------------| +| `npm run dev` | `vite` — dev server on port 3000 | +| `npm run build` | `tsc && vite build` — compile + bundle to `dist/` | +| `npm run preview` | `vite preview` — preview production build | +| `tsc --noEmit` | Type-check only (no test framework) | diff --git a/docs/CODE_STYLE.md b/docs/CODE_STYLE.md new file mode 100644 index 0000000..b0e00a6 --- /dev/null +++ b/docs/CODE_STYLE.md @@ -0,0 +1,145 @@ +# Code Style + +> Last Updated: 2026-03-31T00:00:00.000Z + +## Language & Version + +- **TypeScript** 5.x, strict mode enabled +- Target: **ES2020**, module: **ESNext**, module resolution: **bundler** +- `noEmit: true` — Vite handles transpilation; `tsc` is used for type-checking only + +## Naming Conventions + +| Kind | Convention | Examples | +|------------------|---------------|-------------------------------------------------------------------| +| Types/Interfaces | PascalCase | `Card`, `GameState`, `PlayerIndex`, `TeamScore`, `ScoreBreakdown` | +| Type aliases | PascalCase | `Suit`, `PlayerIndex` | +| Classes | PascalCase | `BootScene`, `MenuScene`, `GameScene` | +| Functions | camelCase | `buildDeck()`, `findCaptures()`, `chooseMove()`, `applyMove()` | +| Constants | UPPER_SNAKE | `PRIMIERA_VALUES`, `SUITS`, `AI_DELAY`, `SCOREBAR_H` | +| Local variables | camelCase | `bestMove`, `capturedCards`, `isScopa`, `afterTable` | +| Private members | camelCase | `this.state`, `this.cardImages`, `this.aiThinking` | +| Parameters | camelCase | `playerIdx`, `captureChoice`, `onComplete` | + +## Class & Scene Patterns + +Scenes extend `Phaser.Scene` and follow the Phaser lifecycle: + +```ts +export class BootScene extends Phaser.Scene { + constructor() { + super({ key: 'BootScene' }); + } + preload(): void { /* asset loading */ } + create(): void { /* scene setup */ } +} +``` + +Scene registration via `Phaser.Types.Core.GameConfig.scene` array in `main.ts`. + +## Indentation & Formatting + +- **2-space** indentation +- **Single quotes** for string literals +- No semicolons omission — **semicolons used consistently** +- Trailing commas in multi-line objects/arrays +- `const` preferred; `let` when reassignment is needed; no `var` + +## Import Patterns + +Named imports from local modules: + +```ts +import { Card, GameState, PlayerIndex } from './types'; +import { findCaptures, canCapture, calcPrimiera, teamOf } from './engine'; +import { chooseMove } from '../game/ai'; +``` + +Default import for Phaser: + +```ts +import Phaser from 'phaser'; +``` + +Type-only import for Capacitor config: + +```ts +import type { CapacitorConfig } from '@capacitor/cli'; +``` + +Relative paths only (`./`, `../`). No path aliases configured. + +## Export Patterns + +- **Named exports** for all public symbols (`export function`, `export class`, `export interface`, `export type`, `export const`) +- No default exports except `vite.config.ts` and `capacitor.config.ts` (framework convention) +- Private/internal functions are not exported (`getSubsets`, `calculateScores`, `scoreRound`, `deepClone`) + +## Type Annotations + +- Explicit return types on exported functions: `(): Card[]`, `(): GameState`, `(): boolean` +- Union types for constrained values: `PlayerIndex = 0 | 1 | 2 | 3` +- String literal unions: `Suit = 'bastoni' | 'coppe' | 'denara' | 'spade'` +- Tuple types for fixed-length arrays: `[Player, Player, Player, Player]`, `[TeamScore, TeamScore]` +- `Record` for maps: `PRIMIERA_VALUES: Record` + +## Comments & Documentation + +- **JSDoc** comments on key exported functions (`findCaptures`, `applyMove`) +- Section separators using dashed lines: + ```ts + // --------------------------------------------------------------------------- + // Deck + // --------------------------------------------------------------------------- + ``` +- `[trueref]` annotations documenting Phaser API provenance +- Inline comments for non-obvious logic (capture rules, AI heuristic weights) +- No auto-generated docs or separate documentation tooling + +## Code Examples (from codebase) + +### Immutable state update pattern + +```ts +export function applyMove( + state: GameState, + playerIdx: PlayerIndex, + card: Card, + captureChoice?: Card[] +): { nextState: GameState; capture: Capture | null; isScopa: boolean } { + const state2 = deepClone(state); + // ... mutations on state2 ... + return { nextState: state2, capture: ..., isScopa }; +} +``` + +### AI heuristic weighted scoring + +```ts +function scoreCapture(...): number { + let score = 100; // base for capturing anything + if (isScopa) score += 500; + if (settebello) score += 300; + score += denariCount * 50; + score += captured.length * 20; + // ... + return score; +} +``` + +### Phaser particle effect + +```ts +const e1 = this.add.particles(x, y, 'particle_glow', { + lifespan: { min: 350, max: 700 }, + speed: { min: 80, max: 280 }, + scale: { start: 0.9, end: 0 }, + tint: color, gravityY: 100, emitting: false, +}).setDepth(25); +e1.explode(count); +``` + +## Linting & Formatting + +No ESLint or Prettier configuration detected. Code style is maintained manually. +TypeScript strict mode provides type-level linting (`strict: true` in `tsconfig.json`). diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md new file mode 100644 index 0000000..b3cf0a5 --- /dev/null +++ b/docs/FINDINGS.md @@ -0,0 +1,99 @@ +# Findings + +> Last Updated: 2026-03-31T00:00:00.000Z + +## Summary + +Initial analysis of the Scopone Scientifico Phaser 3 codebase. This document is populated by the Planner agent as research is performed. + +## Codebase Observations + +- **Total source files**: 9 TypeScript (6 in `src/`), 3 Java (Capacitor boilerplate) +- **Largest file**: `GameScene.ts` (~1340 lines) — rendering, input, effects, audio, UI +- **Game logic is framework-independent**: `game/` modules have zero Phaser imports +- **No test framework**: only `tsc --noEmit` for type-checking +- **No linter/formatter**: code style enforced manually +- **AI plays all 3 non-human seats** using the same heuristic +- **Procedural audio**: all sound is Web Audio oscillators — no audio asset files + +## Potential Improvement Areas + +- **AI cheats with perfect information**: `scoreDump()` and `opponentThreatScore()` in `ai.ts` iterate `opp.hand` directly — bots can see all opponent cards. Must be replaced with imperfect-information card tracking. +- **No mastery/difficulty levels**: All 3 AI seats use the same heuristic at the same strength. +- **No card tracking**: No module tracks which cards have been played or remains in the deck. +- **No minimax**: Pure heuristic scoring, no look-ahead or game tree search. +- **Allied bot is selfish**: Compagno (player 2) plays identically to opponents — no cooperative strategy. + +## Research Performed + +### Web Research: Scopone Scientifico Rules (2026-03-31) + +**Sources**: Wikipedia (*Scopa* article, Scopone section), Pagat.com (*Scopone* page by John McLeod) + +#### Core Rules (Scopone Scientifico variant) +- 4 players, 2 fixed teams of 2 (sit opposite): Team A = players 0+2, Team B = players 1+3 +- 40-card Napoletane deck: 4 suits (bastoni, coppe, denara, spade), values 1–10 +- **All 40 cards dealt** (10 each), **no initial table cards** — the "scientifico" variant +- Play passes around the table (counter-clockwise in Italian tradition; this game uses 0→1→2→3) +- Each turn: play one card face-up to the table + +#### Capture Rules +1. If the played card's value **matches a table card**, the table card **must** be captured (single card, not a sum) +2. If **multiple table cards match** the played value, exactly one is captured (player chooses) +3. If **no direct match**, the player may capture a **subset of table cards summing** to the played value +4. If the played card matches both a single card and a sum, **the single card must be captured** (not the sum) +5. There is **no obligation to play a capturing card** — a player may choose to play a non-capturing card instead. But if the played card CAN capture, it MUST capture. +6. **Scopa**: capturing ALL remaining table cards awards +1 point (except on the very last card of the round) + +#### Scoring (per round, 4 fixed points + scope) +| Category | Rule | +|------------|----------------------------------------------------------| +| **Carte** | Team with majority of captured cards (20+ of 40). Tie = no point. | +| **Denari** | Team with majority of coins/denara suit cards (6+ of 10). Tie = no point. | +| **Settebello** | Team capturing the 7 of denara. Always awarded. | +| **Primiera** | Team with highest prime value. Prime = best card per suit using special scale. Tie = no point. Must have all 4 suits. | +| **Scope** | +1 per scopa achieved during play. | + +#### Primiera Values (confirmed matching codebase) +| Card value | Primiera value | +|------------|---------------| +| 7 | 21 | +| 6 | 18 | +| 1 (Ace) | 16 | +| 5 | 15 | +| 4 | 14 | +| 3 | 13 | +| 2 | 12 | +| 8,9,10 | 10 | + +A team missing an entire suit **cannot win primiera** (even 3×21=63 loses to 21+16+16+16=69 with all 4 suits). + +#### Winning +- First team to **11+ points** at the end of a round wins +- If both reach 11 in the same round, higher total wins; if tied, play continues + +#### Strategy Notes (from Pagat.com) +- **7 of coins (settebello)** is the single most valuable card — contributes to all 4 fixed scoring categories +- **Avoid giving scope**: leave table total ≥ 11 when possible +- **Anchor strategy**: leave a card on table that your team controls (you hold duplicates of that value) +- **Whirlwind**: consecutive scope — clearing the table forces opponent to play, partner captures, repeat +- **Sevens > sixes > aces** in priority for primiera control +- **Paired/unpaired tracking**: if all captures are single-card matches, the last card matches the last table card. Sum captures disrupt this pattern, important for end-game planning. + +### Codebase Capture Rule Validation +The `findCaptures()` in `engine.ts` correctly implements: +- Direct match priority over sum captures ✓ +- Multiple direct matches: takes ALL matching cards (slight deviation — pagat.com says choose ONE, but Wikipedia says take all. The codebase takes all direct matches. This is the **existing behavior** and must not be altered per success criteria.) +- Sum subsets via power set enumeration ✓ +- `applyMove()` auto-captures when possible ✓ + +### Minimax Feasibility Analysis +- 10 cards per player × 4 players = 40 total moves per round +- Full game tree: ~10^12 nodes — infeasible for exhaustive search +- **Approach**: Depth-limited alpha-beta with determinization for imperfect information + - Sample N possible opponent hand assignments consistent with card tracking + - Run minimax on each sample to limited depth (4–6 plies) + - Average/vote across samples for best move + - Alpha-beta pruning reduces effective branching factor significantly + - Depth 4 (one full rotation) with ~5 moves per player = ~625 nodes per sample — very manageable + - 10–20 samples × 625 nodes = ~6,000–12,500 evaluations — runs in <100ms on modern hardware diff --git a/index.html b/index.html new file mode 100644 index 0000000..b331295 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + Scopone Scientifico + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ea7ee6e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2081 @@ +{ + "name": "phaser-scopa", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "phaser-scopa", + "version": "1.0.0", + "dependencies": { + "@capacitor/android": "^8.3.0", + "@capacitor/cli": "^8.3.0", + "@capacitor/core": "^8.3.0", + "phaser": "^3.87.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vite": "^5.0.0" + } + }, + "node_modules/@capacitor/android": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.0.tgz", + "integrity": "sha512-EQy6ByUuKayQBJmMm/e0byJiHavqsQHrvW23BuT2GNVQvenAvipqwaePiJHzrv2PZr7A0T0+se4kgDCeROj0mQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.3.0" + } + }, + "node_modules/@capacitor/cli": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.0.tgz", + "integrity": "sha512-n3QDUimtFNbagoo8kLdjvTz3i3Y4jX1fOjvo6ptUKLzErmuqeamL8kECASoyQvg/OzJisZToGZrgLphBsptJcw==", + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/core": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.0.tgz", + "integrity": "sha512-S4ajn4G/fS3VJj8salxqH/3LO5PPWv1VxGKQ27OCajnDcLJjEg9VXwgMPnlypgkIOqCJ2fmQLtk8GT+BlI9/rw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "license": "MIT" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/phaser": { + "version": "3.90.0", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.90.0.tgz", + "integrity": "sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9aeed89 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "phaser-scopa", + "version": "1.0.0", + "description": "Scopone Scientifico card game built with Phaser 3", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.0.0", + "vite": "^5.0.0" + }, + "dependencies": { + "@capacitor/android": "^8.3.0", + "@capacitor/cli": "^8.3.0", + "@capacitor/core": "^8.3.0", + "phaser": "^3.87.0" + } +} diff --git a/public/atlas.json b/public/atlas.json new file mode 100644 index 0000000..7dad1b5 --- /dev/null +++ b/public/atlas.json @@ -0,0 +1,808 @@ +{ + "frames": { + "bastoni_1": { + "frame": { + "x": 1620, + "y": 2588, + "w": 402, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 402, + "h": 645 + }, + "sourceSize": { + "w": 402, + "h": 645 + } + }, + "bastoni_10": { + "frame": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_2": { + "frame": { + "x": 405, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_3": { + "frame": { + "x": 810, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_4": { + "frame": { + "x": 1215, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_5": { + "frame": { + "x": 0, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_6": { + "frame": { + "x": 405, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_7": { + "frame": { + "x": 810, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_8": { + "frame": { + "x": 1215, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "bastoni_9": { + "frame": { + "x": 1620, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_1": { + "frame": { + "x": 2024, + "y": 2588, + "w": 402, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 402, + "h": 645 + }, + "sourceSize": { + "w": 402, + "h": 645 + } + }, + "coppe_10": { + "frame": { + "x": 1620, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_2": { + "frame": { + "x": 0, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_3": { + "frame": { + "x": 405, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_4": { + "frame": { + "x": 810, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_5": { + "frame": { + "x": 1215, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_6": { + "frame": { + "x": 1620, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_7": { + "frame": { + "x": 2025, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_8": { + "frame": { + "x": 2025, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "coppe_9": { + "frame": { + "x": 2025, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_1": { + "frame": { + "x": 2428, + "y": 2588, + "w": 402, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 402, + "h": 645 + }, + "sourceSize": { + "w": 402, + "h": 645 + } + }, + "denara_10": { + "frame": { + "x": 2430, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_2": { + "frame": { + "x": 2430, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_3": { + "frame": { + "x": 2430, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_4": { + "frame": { + "x": 0, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_5": { + "frame": { + "x": 405, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_6": { + "frame": { + "x": 810, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_7": { + "frame": { + "x": 1215, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_8": { + "frame": { + "x": 1620, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "denara_9": { + "frame": { + "x": 2025, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_1": { + "frame": { + "x": 2832, + "y": 2588, + "w": 402, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 402, + "h": 645 + }, + "sourceSize": { + "w": 402, + "h": 645 + } + }, + "spade_10": { + "frame": { + "x": 2430, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_2": { + "frame": { + "x": 2835, + "y": 0, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_3": { + "frame": { + "x": 2835, + "y": 647, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_4": { + "frame": { + "x": 2835, + "y": 1294, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_5": { + "frame": { + "x": 2835, + "y": 1941, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_6": { + "frame": { + "x": 0, + "y": 2588, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_7": { + "frame": { + "x": 405, + "y": 2588, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_8": { + "frame": { + "x": 810, + "y": 2588, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + }, + "spade_9": { + "frame": { + "x": 1215, + "y": 2588, + "w": 403, + "h": 645 + }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 403, + "h": 645 + }, + "sourceSize": { + "w": 403, + "h": 645 + } + } + }, + "meta": { + "image": "atlas.png", + "scale": "1" + } +} \ No newline at end of file diff --git a/public/atlas.png b/public/atlas.png new file mode 100644 index 0000000..225deef Binary files /dev/null and b/public/atlas.png differ diff --git a/public/retro.png b/public/retro.png new file mode 100644 index 0000000..3894d28 Binary files /dev/null and b/public/retro.png differ diff --git a/src/game/ai.ts b/src/game/ai.ts new file mode 100644 index 0000000..163f4ba --- /dev/null +++ b/src/game/ai.ts @@ -0,0 +1,163 @@ +import { Card, GameState, PlayerIndex } from './types'; +import { findCaptures, canCapture, calcPrimiera, teamOf } from './engine'; + +export interface AIMove { + card: Card; + capture: Card[]; +} + +/** + * Heuristic AI for Scopone Scientifico. + * Evaluates moves by scoring the value of captured cards. + */ +export function chooseMove(state: GameState, playerIdx: PlayerIndex): AIMove { + const player = state.players[playerIdx]; + const hand = player.hand; + const table = state.table; + const myTeam = teamOf(playerIdx); + + let bestMove: AIMove | null = null; + let bestScore = -Infinity; + + for (const card of hand) { + const captures = findCaptures(card, table); + if (captures.length > 0) { + for (const captureSet of captures) { + const score = scoreCapture(card, captureSet, table, state, myTeam); + if (score > bestScore) { + bestScore = score; + bestMove = { card, capture: captureSet }; + } + } + } else { + // No capture — score the "dump" move + const score = scoreDump(card, table, state, myTeam); + if (score > bestScore) { + bestScore = score; + bestMove = { card, capture: [] }; + } + } + } + + return bestMove!; +} + +function scoreCapture( + played: Card, + captured: Card[], + table: Card[], + state: GameState, + myTeam: 0 | 1 +): number { + let score = 100; // base for capturing anything + + const allCaptured = [played, ...captured]; + const afterTable = table.filter(c => !captured.some(cc => cc.id === c.id)); + const isScopa = afterTable.length === 0; + + // Scopa is very valuable + if (isScopa) score += 500; + + // Settebello + if (allCaptured.some(c => c.suit === 'denara' && c.value === 7)) score += 300; + + // Table has settebello and this capture takes it + if (table.some(c => c.suit === 'denara' && c.value === 7) && + captured.some(c => c.suit === 'denara' && c.value === 7)) score += 200; + + // Denari cards + score += allCaptured.filter(c => c.suit === 'denara').length * 50; + + // More cards = better + score += captured.length * 20; + + // High-value primiera cards + score += allCaptured.reduce((s, c) => s + primieraScore(c), 0); + + // Avoid leaving settebello on table for opponent + const settebelloOnTable = afterTable.some(c => c.suit === 'denara' && c.value === 7); + if (settebelloOnTable) score -= 150; + + // Opponent could easily capture remaining table + score -= opponentThreatScore(afterTable, state, myTeam) * 30; + + return score; +} + +function scoreDump( + card: Card, + table: Card[], + state: GameState, + myTeam: 0 | 1 +): number { + let score = 0; + + // Don't dump cards that complete opponent captures + const afterTable = [...table, card]; + + // Check if opponent can make a scopa after this dump + const opponentTeam = myTeam === 0 ? 1 : 0; + const opponentPlayers = state.players.filter(p => teamOf(p.index) === opponentTeam); + for (const opp of opponentPlayers) { + for (const oppCard of opp.hand) { + const caps = findCaptures(oppCard, afterTable); + for (const cap of caps) { + const leftAfter = afterTable.filter(c => !cap.some(cc => cc.id === c.id)); + if (leftAfter.length === 0) score -= 400; // would give opponent scopa + } + } + } + + // Prefer to dump low-value non-denari cards + if (card.suit !== 'denara') score += 30; + if (card.suit === 'denara') score -= 40; + if (card.suit === 'denara' && card.value === 7) score -= 300; // never dump settebello + + // Prefer dumping face cards (10, 9, 8) that are less useful + if (card.value >= 8) score += 10; + + // Don't dump 7s (primiera value) + if (card.value === 7) score -= 50; + + // Don't dump 1s (primiera value) + if (card.value === 1) score -= 30; + + // Penalty for making easy captures for opponents + const capturable = state.players + .filter(p => teamOf(p.index) !== myTeam) + .flatMap(p => p.hand) + .some(oppCard => canCapture(oppCard, afterTable)); + if (capturable) score -= 20; + + return score; +} + +function primieraScore(card: Card): number { + // Reward for capturing high-primiera cards + const vals: Record = { 7: 8, 6: 6, 1: 5, 5: 4, 4: 3, 3: 2, 2: 1 }; + return vals[card.value] ?? 0; +} + +function opponentThreatScore( + table: Card[], + state: GameState, + myTeam: 0 | 1 +): number { + const opponentTeam = myTeam === 0 ? 1 : 0; + let threat = 0; + for (const player of state.players) { + if (teamOf(player.index) !== opponentTeam) continue; + for (const card of player.hand) { + const caps = findCaptures(card, table); + if (caps.length > 0) { + threat += caps[0].length; + // Extra threat if settebello is capturable + if (table.some(c => c.suit === 'denara' && c.value === 7) && + caps.some(cap => cap.some(c => c.suit === 'denara' && c.value === 7))) { + threat += 5; + } + } + } + } + return threat; +} diff --git a/src/game/engine.ts b/src/game/engine.ts new file mode 100644 index 0000000..2bc96d4 --- /dev/null +++ b/src/game/engine.ts @@ -0,0 +1,307 @@ +import { + Card, Suit, SUITS, Player, PlayerIndex, GameState, + TeamScore, ScoreBreakdown, PRIMIERA_VALUES, Capture +} from './types'; + +// --------------------------------------------------------------------------- +// Deck +// --------------------------------------------------------------------------- + +export function buildDeck(): Card[] { + const deck: Card[] = []; + for (const suit of SUITS) { + for (let v = 1; v <= 10; v++) { + deck.push({ suit, value: v, id: `${suit}_${v}` }); + } + } + return deck; +} + +export function shuffle(arr: T[]): T[] { + const a = [...arr]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +// --------------------------------------------------------------------------- +// Capture logic +// --------------------------------------------------------------------------- + +/** + * Find all valid capture combinations for a played card against the table. + * Rules: + * - If the table contains a card of the same value, you MUST take it (and only it) + * unless the table has multiple cards of that value (take exactly one) + * - If no direct match exists, you may take any subset of table cards that sums to the played value + * Returns array of capture sets (each is a list of cards taken from table). + */ +export function findCaptures(played: Card, table: Card[]): Card[][] { + const results: Card[][] = []; + + // Check for direct value matches + const directMatches = table.filter(c => c.value === played.value); + if (directMatches.length > 0) { + // Must capture exactly one matching card (Italian rules: take one direct match) + // Actually: if there's one direct match take it; if multiple, still take all that match + // Standard Italian rule: take ALL cards of matching value + results.push([...directMatches]); + return results; // direct match takes priority, no sum captures allowed + } + + // No direct matches — find subsets that sum to played.value + const subsets = getSubsets(table); + for (const subset of subsets) { + if (subset.length >= 2) { + const sum = subset.reduce((acc, c) => acc + c.value, 0); + if (sum === played.value) { + results.push(subset); + } + } + } + return results; +} + +function getSubsets(cards: Card[]): Card[][] { + const result: Card[][] = [[]]; + for (const card of cards) { + const newSubsets = result.map(s => [...s, card]); + result.push(...newSubsets); + } + return result; +} + +export function canCapture(played: Card, table: Card[]): boolean { + return findCaptures(played, table).length > 0; +} + +// --------------------------------------------------------------------------- +// Game state initialisation +// --------------------------------------------------------------------------- + +export function createInitialState(): GameState { + const deck = shuffle(buildDeck()); + + const players: [Player, Player, Player, Player] = [ + { index: 0, hand: [], pile: [], scope: 0, isHuman: true, name: 'Tu' }, + { index: 1, hand: [], pile: [], scope: 0, isHuman: false, name: 'AI Ovest' }, + { index: 2, hand: [], pile: [], scope: 0, isHuman: false, name: 'Compagno' }, + { index: 3, hand: [], pile: [], scope: 0, isHuman: false, name: 'AI Est' }, + ]; + + // Deal 10 cards each — Scopone Scientifico deals all at once + for (let i = 0; i < 4; i++) { + players[i].hand = deck.splice(0, 10); + } + + // No initial table cards in Scopone Scientifico + const table: Card[] = []; + + const emptyTeamScore = (): TeamScore => ({ + cards: 0, scope: 0, denari: 0, settebello: false, primiera: 0, roundPoints: 0, totalPoints: 0, + }); + + return { + players, + table, + currentPlayer: 0, + roundOver: false, + gameOver: false, + teamScores: [emptyTeamScore(), emptyTeamScore()], + lastCapturTeam: null, + roundNumber: 1, + }; +} + +// --------------------------------------------------------------------------- +// Play a card +// --------------------------------------------------------------------------- + +/** + * Apply a move to the game state (immutably). + * If captureChoice is provided, use that capture set; otherwise use the first valid capture. + * Returns the new state and what was captured (null if nothing). + */ +export function applyMove( + state: GameState, + playerIdx: PlayerIndex, + card: Card, + captureChoice?: Card[] +): { nextState: GameState; capture: Capture | null; isScopa: boolean } { + const state2 = deepClone(state); + const player = state2.players[playerIdx]; + + // Remove card from hand + player.hand = player.hand.filter(c => c.id !== card.id); + + const captures = findCaptures(card, state2.table); + let capturedCards: Card[] = []; + let isScopa = false; + + if (captures.length > 0) { + const chosen = captureChoice ?? captures[0]; + capturedCards = chosen; + + // Remove captured cards from table + const capturedIds = new Set(chosen.map(c => c.id)); + state2.table = state2.table.filter(c => !capturedIds.has(c.id)); + + // Add played card + captured to player's pile + player.pile.push(card, ...capturedCards); + + // Scopa: cleared the table + if (state2.table.length === 0) { + player.scope += 1; + isScopa = true; + } + + // Track which team made last capture + state2.lastCapturTeam = (playerIdx === 0 || playerIdx === 2) ? 0 : 1; + } else { + // No capture — add to table + state2.table.push(card); + } + + // Advance turn + state2.currentPlayer = ((playerIdx + 1) % 4) as PlayerIndex; + + // Check if round is over (all hands empty) + const allHandsEmpty = state2.players.every(p => p.hand.length === 0); + if (allHandsEmpty) { + // Remaining table cards go to last capturing team + if (state2.table.length > 0 && state2.lastCapturTeam !== null) { + const lastTeamPlayers = state2.lastCapturTeam === 0 ? [0, 2] : [1, 3]; + // Give to first player of that team + state2.players[lastTeamPlayers[0]].pile.push(...state2.table); + state2.table = []; + } + state2.roundOver = true; + calculateScores(state2); + } + + return { + nextState: state2, + capture: capturedCards.length > 0 ? { played: card, captured: capturedCards } : null, + isScopa, + }; +} + +// --------------------------------------------------------------------------- +// Scoring +// --------------------------------------------------------------------------- + +function calculateScores(state: GameState): void { + const team0 = [state.players[0], state.players[2]]; + const team1 = [state.players[1], state.players[3]]; + + const breakdown: ScoreBreakdown = scoreRound(team0, team1); + + const t0 = state.teamScores[0]; + const t1 = state.teamScores[1]; + + // Cards + t0.cards = team0.reduce((s, p) => s + p.pile.length, 0); + t1.cards = team1.reduce((s, p) => s + p.pile.length, 0); + + // Denari + const denari0 = team0.flatMap(p => p.pile).filter(c => c.suit === 'denara'); + const denari1 = team1.flatMap(p => p.pile).filter(c => c.suit === 'denara'); + t0.denari = denari0.length; + t1.denari = denari1.length; + + // Settebello + t0.settebello = team0.flatMap(p => p.pile).some(c => c.suit === 'denara' && c.value === 7); + t1.settebello = !t0.settebello; + + // Scope + t0.scope = team0.reduce((s, p) => s + p.scope, 0); + t1.scope = team1.reduce((s, p) => s + p.scope, 0); + + // Primiera + t0.primiera = calcPrimiera(team0.flatMap(p => p.pile)); + t1.primiera = calcPrimiera(team1.flatMap(p => p.pile)); + + // Points this round + let p0 = 0; + let p1 = 0; + + if (breakdown.cartePoint === 0) p0++; + else if (breakdown.cartePoint === 1) p1++; + + if (breakdown.denariPoint === 0) p0++; + else if (breakdown.denariPoint === 1) p1++; + + if (breakdown.settebelloPoint === 0) p0++; + else p1++; + + if (breakdown.primieraPoint === 0) p0++; + else if (breakdown.primieraPoint === 1) p1++; + + p0 += breakdown.scopeTeam0; + p1 += breakdown.scopeTeam1; + + t0.roundPoints = p0; + t1.roundPoints = p1; + t0.totalPoints += p0; + t1.totalPoints += p1; +} + +function scoreRound(team0: Player[], team1: Player[]): ScoreBreakdown { + const pile0 = team0.flatMap(p => p.pile); + const pile1 = team1.flatMap(p => p.pile); + + const cards0 = pile0.length; + const cards1 = pile1.length; + + const denari0 = pile0.filter(c => c.suit === 'denara').length; + const denari1 = pile1.filter(c => c.suit === 'denara').length; + + const hasSette0 = pile0.some(c => c.suit === 'denara' && c.value === 7); + + const prim0 = calcPrimiera(pile0); + const prim1 = calcPrimiera(pile1); + + const scope0 = team0.reduce((s, p) => s + p.scope, 0); + const scope1 = team1.reduce((s, p) => s + p.scope, 0); + + return { + cartePoint: cards0 > cards1 ? 0 : cards1 > cards0 ? 1 : null, + denariPoint: denari0 > denari1 ? 0 : denari1 > denari0 ? 1 : null, + settebelloPoint: hasSette0 ? 0 : 1, + primieraPoint: prim0 > prim1 ? 0 : prim1 > prim0 ? 1 : null, + scopeTeam0: scope0, + scopeTeam1: scope1, + }; +} + +export function calcPrimiera(pile: Card[]): number { + // Best card per suit, using primiera values + let total = 0; + for (const suit of SUITS) { + const cards = pile.filter(c => c.suit === suit); + if (cards.length === 0) return 0; // can't score primiera without all 4 suits + const best = Math.max(...cards.map(c => PRIMIERA_VALUES[c.value])); + total += best; + } + return total; +} + +export function getScoreBreakdown(state: GameState): ScoreBreakdown { + const team0 = [state.players[0], state.players[2]]; + const team1 = [state.players[1], state.players[3]]; + return scoreRound(team0, team1); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function deepClone(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} + +export function teamOf(playerIdx: PlayerIndex): 0 | 1 { + return (playerIdx === 0 || playerIdx === 2) ? 0 : 1; +} diff --git a/src/game/types.ts b/src/game/types.ts new file mode 100644 index 0000000..324d136 --- /dev/null +++ b/src/game/types.ts @@ -0,0 +1,69 @@ +export type Suit = 'bastoni' | 'coppe' | 'denara' | 'spade'; +export const SUITS: Suit[] = ['bastoni', 'coppe', 'denara', 'spade']; + +export interface Card { + suit: Suit; + value: number; // 1-10 + id: string; // e.g. "denara_7" +} + +export interface Capture { + played: Card; + captured: Card[]; +} + +export type PlayerIndex = 0 | 1 | 2 | 3; + +export interface Player { + index: PlayerIndex; + hand: Card[]; + pile: Card[]; // captured cards + scope: number; // number of scope achieved + isHuman: boolean; + name: string; +} + +export interface GameState { + players: [Player, Player, Player, Player]; + table: Card[]; + currentPlayer: PlayerIndex; + roundOver: boolean; + gameOver: boolean; + teamScores: [TeamScore, TeamScore]; // Team 0: players 0+2, Team 1: players 1+3 + lastCapturTeam: 0 | 1 | null; // which team made the last capture (gets remaining table cards) + roundNumber: number; +} + +export interface TeamScore { + cards: number; + scope: number; + denari: number; + settebello: boolean; + primiera: number; + // final points for this round + roundPoints: number; + totalPoints: number; +} + +export interface ScoreBreakdown { + cartePoint: 0 | 1 | null; // null = tie + denariPoint: 0 | 1 | null; + settebelloPoint: 0 | 1; + primieraPoint: 0 | 1 | null; + scopeTeam0: number; + scopeTeam1: number; +} + +// Primiera values per card value +export const PRIMIERA_VALUES: Record = { + 7: 21, + 6: 18, + 1: 16, + 5: 15, + 4: 14, + 3: 13, + 2: 12, + 8: 10, + 9: 10, + 10: 10, +}; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..916bbcc --- /dev/null +++ b/src/main.ts @@ -0,0 +1,19 @@ +import Phaser from 'phaser'; +import { BootScene } from './scenes/BootScene'; +import { MenuScene } from './scenes/MenuScene'; +import { GameScene } from './scenes/GameScene'; + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + width: 1280, + height: 720, + backgroundColor: '#1a5c2a', + parent: 'game', + scene: [BootScene, MenuScene, GameScene], + scale: { + mode: Phaser.Scale.FIT, + autoCenter: Phaser.Scale.CENTER_BOTH, + }, +}; + +new Phaser.Game(config); diff --git a/src/scenes/BootScene.ts b/src/scenes/BootScene.ts new file mode 100644 index 0000000..b187cd1 --- /dev/null +++ b/src/scenes/BootScene.ts @@ -0,0 +1,47 @@ +import Phaser from 'phaser'; + +/** + * BootScene — loads all assets before the game starts. + * Uses atlas loaded from public/atlas.json (Phaser hash format, converted from Napoletane atlas). + * + * [trueref /local/phaser] — this.load.atlas() loads a texture atlas where frames are + * referenced by name (e.g. 'bastoni_7') using the Phaser hash format JSON. + */ +export class BootScene extends Phaser.Scene { + constructor() { + super({ key: 'BootScene' }); + } + + preload(): void { + const W = this.scale.width; + const H = this.scale.height; + + // Loading bar + const bar = this.add.rectangle(W / 2, H / 2, 400, 20, 0x4caf50); + const barBg = this.add.rectangle(W / 2, H / 2, 402, 22, 0x1a5c2a); + barBg.setDepth(0); + bar.setDepth(1); + bar.setOrigin(0.5); + bar.scaleX = 0; + + const label = this.add.text(W / 2, H / 2 - 40, 'Caricamento...', { + fontFamily: 'serif', + fontSize: '24px', + color: '#ffffff', + }).setOrigin(0.5); + + this.load.on('progress', (value: number) => { + bar.scaleX = value; + }); + + // Load card atlas (Phaser hash format converted from Napoletane atlas) + this.load.atlas('cards', 'atlas.png', 'atlas.json'); + + // Load card back separately + this.load.image('retro', 'retro.png'); + } + + create(): void { + this.scene.start('MenuScene'); + } +} diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts new file mode 100644 index 0000000..3dcff8d --- /dev/null +++ b/src/scenes/GameScene.ts @@ -0,0 +1,1331 @@ +import Phaser from 'phaser'; +import { Card, PlayerIndex, GameState } from '../game/types'; +import { + createInitialState, applyMove, findCaptures, getScoreBreakdown, teamOf, calcPrimiera +} from '../game/engine'; +import { chooseMove } from '../game/ai'; + +// --------------------------------------------------------------------------- +// Layout constants +// --------------------------------------------------------------------------- +const CARD_SCALE_HUMAN = 0.165; // larger cards for the human player +const CARD_SCALE_AI = 0.125; // smaller cards for AI (fit in side slots) + +const CW_H = 402 * CARD_SCALE_HUMAN; // card width for human ≈ 66 +const CH_H = 645 * CARD_SCALE_HUMAN; // card height for human ≈ 106 +const CW_A = 402 * CARD_SCALE_AI; // card width for AI ≈ 50 +const CH_A = 645 * CARD_SCALE_AI; // card height for AI ≈ 81 + +const AI_DELAY = 1100; // ms — think bar fills over this time + +// Scorebar height at top +const SCOREBAR_H = 54; + +// Player positions: +// 0 = South (human, bottom), 1 = West (AI, left, rotated -90°) +// 2 = North (AI partner, top), 3 = East (AI, right, rotated +90°) + +/** + * GameScene — Scopone Scientifico main game scene. + * + * Phaser features exercised (many discovered via trueref /local/phaser): + * + * [trueref] postFX.addGlow(color, outerStrength, innerStrength, knockout) + * → used on selected cards and settebello on the table + * [trueref] camera.shake(duration, intensity) + camera.flash(duration, r, g, b) + * → triggered on scopa and settebello captures + * [trueref] graphics.fillRoundedRect(x, y, w, h, radius) + * → all UI panels (score bar, HUD, think bar) + * [trueref] ParticleEmitter (v3.60+) this.add.particles(x, y, texture, config) + * → capture bursts, scopa explosion, card trails, victory confetti + * [trueref] camera.postFX.addVignette(cx, cy, radius, strength) + * → cinematic vignette on the main camera + * [trueref] image.setAngle() — used to rotate AI side cards ±90° + */ +export class GameScene extends Phaser.Scene { + private state!: GameState; + private cardImages: Map = new Map(); + + // Live score bar texts + private hudA!: { scope: Phaser.GameObjects.Text; cards: Phaser.GameObjects.Text; + denari: Phaser.GameObjects.Text; prim: Phaser.GameObjects.Text; + total: Phaser.GameObjects.Text }; + private hudB!: { scope: Phaser.GameObjects.Text; cards: Phaser.GameObjects.Text; + denari: Phaser.GameObjects.Text; prim: Phaser.GameObjects.Text; + total: Phaser.GameObjects.Text }; + private roundText!: Phaser.GameObjects.Text; + + // Status bar + private statusText!: Phaser.GameObjects.Text; + + // Think bar + private thinkBar!: Phaser.GameObjects.Graphics; + private thinkTween: Phaser.Tweens.Tween | null = null; + private thinkProgress = 0; + + // Player label containers (pulsed on active turn) + private playerLabels: Map = new Map(); + + // Interaction state + private selectedCard: Card | null = null; + private selectedCardImg: Phaser.GameObjects.Image | null = null; + private selectedGlow: any = null; + private selectedGlowTween: Phaser.Tweens.Tween | null = null; + private pendingCaptures: Card[][] = []; + private tableHighlights: Phaser.GameObjects.GameObject[] = []; + private aiThinking = false; + + private tableCenter!: { x: number; y: number }; + + // Web Audio + private audioCtx: AudioContext | null = null; + private musicGain: GainNode | null = null; + private musicStarted = false; + + constructor() { + super({ key: 'GameScene' }); + } + + // --------------------------------------------------------------------------- + // Create + // --------------------------------------------------------------------------- + + create(): void { + const W = this.scale.width; + const H = this.scale.height; + this.tableCenter = { x: W / 2, y: (H + SCOREBAR_H) / 2 + 10 }; + + this.generateParticleTextures(); + this.drawBackground(W, H); + this.buildScoreBar(W); + this.buildStatusBar(W, H); + this.buildThinkBar(W, H); + this.buildPlayerLabels(W, H); + + // Custom vignette drawn in scene (avoids darkening score bar corners) + this.drawVignette(W, H); + + this.input.once('pointerdown', () => this.startMusic()); + + this.state = createInitialState(); + this.dealAnimation(() => { + this.updateScoreBar(); + this.nextTurn(); + }); + } + + // --------------------------------------------------------------------------- + // Particle textures + // --------------------------------------------------------------------------- + + private generateParticleTextures(): void { + // [trueref: graphics.generateTexture] — makes particle sprites without image files + + const mkCircle = (key: string, r: number) => { + const g = this.add.graphics(); + g.fillStyle(0xffffff, 1); + g.fillCircle(r, r, r); + g.generateTexture(key, r * 2, r * 2); + g.destroy(); + }; + mkCircle('particle_glow', 8); + mkCircle('particle_sm', 4); + + const sq = this.add.graphics(); + sq.fillStyle(0xffffff, 1); + sq.fillRect(0, 0, 5, 5); + sq.generateTexture('particle_sq', 5, 5); + sq.destroy(); + + // Diamond (star) for settebello + const dia = this.add.graphics(); + dia.fillStyle(0xffffff, 1); + dia.fillTriangle(8, 0, 16, 8, 8, 16); + dia.fillTriangle(0, 8, 8, 16, 8, 0); + dia.generateTexture('particle_dia', 16, 16); + dia.destroy(); + } + + // --------------------------------------------------------------------------- + // Background + // --------------------------------------------------------------------------- + + private drawBackground(W: number, H: number): void { + // Layered felt + this.add.rectangle(0, 0, W, H, 0x193d20).setOrigin(0).setDepth(0); + // Lighter dark overlay (was 0.5) + this.add.rectangle(W / 2, H / 2, W, H, 0x0d2410, 0.18).setDepth(0); + + // Subtle dot grid + const g = this.add.graphics().setDepth(0); + g.fillStyle(0x000000, 0.07); + for (let x = 20; x < W; x += 36) { + for (let y = SCOREBAR_H + 10; y < H; y += 36) { + g.fillCircle(x, y, 1.2); + } + } + + // Outer decorative border + const border = this.add.graphics().setDepth(1); + border.lineStyle(2, 0xffd700, 0.18); + border.strokeRect(8, SCOREBAR_H + 6, W - 16, H - SCOREBAR_H - 14); + + // Table area — using [trueref: graphics.fillRoundedRect] + const tg = this.add.graphics().setDepth(1); + tg.fillStyle(0x0a2912, 0.55); + tg.fillRoundedRect(W * 0.12, SCOREBAR_H + 90, W * 0.76, H - SCOREBAR_H - 185, 18); + // Softer border (was 1.5 / 0.5) + tg.lineStyle(1, 0x3d8b3d, 0.18); + tg.strokeRoundedRect(W * 0.12, SCOREBAR_H + 90, W * 0.76, H - SCOREBAR_H - 185, 18); + + // Player hand slot — no border, just subtle fill + const hg = this.add.graphics().setDepth(1); + hg.fillStyle(0x000000, 0.22); + hg.fillRoundedRect(W * 0.12, H - CH_H - 28, W * 0.76, CH_H + 16, 10); + } + + /** Soft multi-step vignette that only covers below the score bar. */ + private drawVignette(W: number, H: number): void { + const vg = this.add.graphics().setDepth(3); + const gameH = H - SCOREBAR_H; + const steps = 14; + + // Left gradient + for (let i = 0; i < steps; i++) { + const t = 1 - i / steps; + const alpha = 0.32 * t * t; + const bw = W * 0.14 / steps; + vg.fillStyle(0x000000, alpha); + vg.fillRect(i * bw, SCOREBAR_H, bw + 1, gameH); + } + // Right gradient + for (let i = 0; i < steps; i++) { + const t = 1 - i / steps; + const alpha = 0.32 * t * t; + const bw = W * 0.14 / steps; + vg.fillRect(W - (i + 1) * bw, SCOREBAR_H, bw + 1, gameH); + } + // Bottom gradient + for (let i = 0; i < 10; i++) { + const t = 1 - i / 10; + const alpha = 0.22 * t * t; + const bh = H * 0.10 / 10; + vg.fillStyle(0x000000, alpha); + vg.fillRect(0, H - (i + 1) * bh, W, bh + 1); + } + } + + // --------------------------------------------------------------------------- + // Score bar (top, full width) + // --------------------------------------------------------------------------- + + private buildScoreBar(W: number): void { + // [trueref: fillRoundedRect] Panel background + const bg = this.add.graphics().setDepth(8); + bg.fillStyle(0x050e07, 0.92); + bg.fillRect(0, 0, W, SCOREBAR_H); + bg.lineStyle(1, 0xffd700, 0.35); + bg.lineBetween(0, SCOREBAR_H, W, SCOREBAR_H); + + const mkTxt = (x: number, y: number, val: string, color = '#ffffff', size = '15px') => + this.add.text(x, y, val, { fontFamily: 'monospace', fontSize: size, color }) + .setOrigin(0.5).setDepth(9); + + // Left side — Team A + this.add.text(10, SCOREBAR_H / 2, 'TEAM A (Tu + Compagno)', { + fontFamily: 'serif', fontSize: '13px', color: '#aaffaa', + }).setOrigin(0, 0.5).setDepth(9); + + // Right side — Team B + this.add.text(W - 10, SCOREBAR_H / 2, 'TEAM B (AI Ovest + AI Est)', { + fontFamily: 'serif', fontSize: '13px', color: '#ffaaaa', + }).setOrigin(1, 0.5).setDepth(9); + + // Center — Round + this.roundText = mkTxt(W / 2, SCOREBAR_H / 2, 'Mano 1', '#ffd700', '16px'); + + // Column headers (shared, centered-ish) + const cols = ['Scope', 'Carte', 'Denari', 'Primiera', 'TOTALE']; + const xA = [240, 320, 410, 510, 620]; + const xB = [W - 240, W - 320, W - 410, W - 510, W - 620]; + + cols.forEach((_, i) => { + const label = ['Sc', 'Ca', 'De', 'Pr', 'Pt'][i]; + this.add.text(xA[i], SCOREBAR_H * 0.28, label, { + fontFamily: 'monospace', fontSize: '10px', color: '#666666', + }).setOrigin(0.5).setDepth(9); + this.add.text(xB[i], SCOREBAR_H * 0.28, label, { + fontFamily: 'monospace', fontSize: '10px', color: '#666666', + }).setOrigin(0.5).setDepth(9); + }); + + // Live value texts + const mkA = (xi: number) => mkTxt(xA[xi], SCOREBAR_H * 0.72, '0', '#aaffaa', '17px'); + const mkB = (xi: number) => mkTxt(xB[xi], SCOREBAR_H * 0.72, '0', '#ffaaaa', '17px'); + + this.hudA = { + scope: mkA(0), cards: mkA(1), denari: mkA(2), prim: mkA(3), + total: this.add.text(xA[4], SCOREBAR_H * 0.72, '0', { + fontFamily: 'Georgia, serif', fontSize: '20px', color: '#aaffaa', + stroke: '#000', strokeThickness: 2, + }).setOrigin(0.5).setDepth(9), + }; + this.hudB = { + scope: mkB(0), cards: mkB(1), denari: mkB(2), prim: mkB(3), + total: this.add.text(xB[4], SCOREBAR_H * 0.72, '0', { + fontFamily: 'Georgia, serif', fontSize: '20px', color: '#ffaaaa', + stroke: '#000', strokeThickness: 2, + }).setOrigin(0.5).setDepth(9), + }; + } + + private updateScoreBar(): void { + const s = this.state; + const t0 = s.teamScores[0]; + const t1 = s.teamScores[1]; + const team0 = [s.players[0], s.players[2]]; + const team1 = [s.players[1], s.players[3]]; + + const scope0 = team0.reduce((n, p) => n + p.scope, 0); + const scope1 = team1.reduce((n, p) => n + p.scope, 0); + const pile0 = team0.flatMap(p => p.pile); + const pile1 = team1.flatMap(p => p.pile); + const den0 = pile0.filter(c => c.suit === 'denara').length; + const den1 = pile1.filter(c => c.suit === 'denara').length; + const prim0 = calcPrimiera(pile0); + const prim1 = calcPrimiera(pile1); + + const setAnim = (txt: Phaser.GameObjects.Text, val: string | number) => { + const v = String(val); + if (txt.text === v) return; + txt.setText(v); + this.tweens.add({ targets: txt, scaleX: 1.4, scaleY: 1.4, duration: 100, yoyo: true }); + }; + + setAnim(this.hudA.scope, scope0); + setAnim(this.hudA.cards, pile0.length); + setAnim(this.hudA.denari, den0); + setAnim(this.hudA.prim, prim0 > 0 ? prim0 : '-'); + setAnim(this.hudA.total, t0.totalPoints); + + setAnim(this.hudB.scope, scope1); + setAnim(this.hudB.cards, pile1.length); + setAnim(this.hudB.denari, den1); + setAnim(this.hudB.prim, prim1 > 0 ? prim1 : '-'); + setAnim(this.hudB.total, t1.totalPoints); + + this.roundText.setText(`Mano ${s.roundNumber ?? 1}`); + } + + private flashScoreScope(team: 0 | 1): void { + const txt = team === 0 ? this.hudA.scope : this.hudB.scope; + this.tweens.add({ + targets: txt, + scaleX: 2.2, scaleY: 2.2, + duration: 180, yoyo: true, repeat: 3, + onStart: () => txt.setColor('#ffd700'), + onComplete: () => txt.setColor(team === 0 ? '#aaffaa' : '#ffaaaa'), + }); + } + + // --------------------------------------------------------------------------- + // Status bar + think bar + // --------------------------------------------------------------------------- + + private buildStatusBar(W: number, H: number): void { + // Background chip + const bg = this.add.graphics().setDepth(9); + bg.fillStyle(0x000000, 0.55); + bg.fillRoundedRect(W / 2 - 280, H - CH_H - 50, 560, 28, 8); + + this.statusText = this.add.text(W / 2, H - CH_H - 36, '', { + fontFamily: 'serif', fontSize: '17px', color: '#ffffff', + stroke: '#000', strokeThickness: 2, + }).setOrigin(0.5).setDepth(10); + } + + private buildThinkBar(W: number, H: number): void { + // Thin progress bar just below the score bar + this.thinkBar = this.add.graphics().setDepth(11).setVisible(false); + } + + private showThinkBar(playerIdx: PlayerIndex): void { + this.thinkProgress = 0; + this.thinkBar.setVisible(true); + this.thinkTween?.stop(); + + const W = this.scale.width; + const tg = this.thinkBar; + const color = (playerIdx === 0 || playerIdx === 2) ? 0x44ff88 : 0xff5555; + + const tweenTarget = { v: 0 }; + this.thinkTween = this.tweens.add({ + targets: tweenTarget, + v: 1, + duration: AI_DELAY - 80, + ease: 'Linear', + onUpdate: () => { + tg.clear(); + const w = tweenTarget.v * W; + tg.fillStyle(0x000000, 0.4); + tg.fillRect(0, SCOREBAR_H, W, 4); + tg.fillStyle(color, 0.85); + tg.fillRect(0, SCOREBAR_H, w, 4); + // Glow tip + tg.fillStyle(0xffffff, 0.6); + tg.fillRect(w - 6, SCOREBAR_H, 6, 4); + }, + onComplete: () => { tg.clear(); tg.setVisible(false); }, + }); + } + + private hideThinkBar(): void { + this.thinkTween?.stop(); + this.thinkTween = null; + this.thinkBar.clear(); + this.thinkBar.setVisible(false); + } + + // --------------------------------------------------------------------------- + // Player labels (pulse on active turn) + // --------------------------------------------------------------------------- + + private buildPlayerLabels(W: number, H: number): void { + const defs: Array<{ idx: PlayerIndex; x: number; y: number; color: string; txt: string }> = [ + { idx: 0, x: W / 2, y: H - CH_H - 56, color: '#aaffaa', txt: 'Tu [Team A]' }, + { idx: 1, x: CH_A + 14, y: H / 2 + SCOREBAR_H / 2 - 60, color: '#ffaaaa', txt: 'AI\nOvest\n[B]' }, + { idx: 2, x: W / 2, y: SCOREBAR_H + 18, color: '#aaffaa', txt: 'Compagno [Team A]' }, + { idx: 3, x: W - CH_A - 14, y: H / 2 + SCOREBAR_H / 2 - 60, color: '#ffaaaa', txt: 'AI\nEst\n[B]' }, + ]; + for (const d of defs) { + const lbl = this.add.text(d.x, d.y, d.txt, { + fontFamily: 'serif', fontSize: '12px', color: d.color, + stroke: '#000', strokeThickness: 1, align: 'center', + }).setOrigin(0.5).setDepth(2); + this.playerLabels.set(d.idx, lbl); + } + } + + private pulseLabel(playerIdx: PlayerIndex): void { + // Reset all + for (const [idx, lbl] of this.playerLabels) { + lbl.setAlpha(idx === playerIdx ? 1 : 0.5); + } + // Pulse active + const lbl = this.playerLabels.get(playerIdx)!; + this.tweens.add({ + targets: lbl, + scaleX: 1.2, scaleY: 1.2, + duration: 300, yoyo: true, ease: 'Sine.InOut', + }); + } + + // --------------------------------------------------------------------------- + // Deal animation + // --------------------------------------------------------------------------- + + private dealAnimation(onComplete: () => void): void { + const W = this.scale.width; + const H = this.scale.height; + let delay = 0, done = 0; + + const allCards: Array<{ card: Card; p: number; destX: number; destY: number; face: boolean }> = []; + + for (let p = 0; p < 4; p++) { + const positions = this.getHandPositions(p as PlayerIndex, + this.state.players[p as PlayerIndex].hand.length); + this.state.players[p as PlayerIndex].hand.forEach((card, i) => + allCards.push({ card, p, destX: positions[i].x, destY: positions[i].y, face: p === 0 }) + ); + } + + const total = allCards.length; + for (const { card, p, destX, destY, face } of allCards) { + const scale = p === 0 ? CARD_SCALE_HUMAN : CARD_SCALE_AI; + const img = this.add.image(W / 2, SCOREBAR_H + 40, + face ? 'cards' : 'retro', face ? card.id : undefined) + .setScale(scale).setDepth(5).setAlpha(0); + + // Rotate side cards [trueref: image.setAngle() — rotation in degrees] + if (p === 1) img.setAngle(-90); + if (p === 3) img.setAngle(90); + + // Subtle drop shadow on all cards [trueref: postFX.addShadow] + if (this.renderer.type === Phaser.WEBGL) { + img.postFX.addShadow(0, 4, 0.006, 1.2, 0x000000, 6, 0.4); + } + + this.cardImages.set(card.id, img); + + this.tweens.add({ + targets: img, + x: destX, y: destY, alpha: 1, + duration: 260, delay, + ease: 'Power2', + onComplete: () => { + done++; + if (done === total) onComplete(); + }, + }); + delay += 25; + } + } + + // --------------------------------------------------------------------------- + // Hand / table positions + // --------------------------------------------------------------------------- + + private getHandPositions(playerIdx: PlayerIndex, count: number): Array<{ x: number; y: number }> { + const W = this.scale.width; + const H = this.scale.height; + + switch (playerIdx) { + case 0: { // South (human) — horizontal, bottom + const sp = Math.min(CW_H + 5, (W * 0.72) / Math.max(count - 1, 1)); + const sx = W / 2 - ((count - 1) * sp) / 2; + return Array.from({ length: count }, (_, i) => ({ x: sx + i * sp, y: H - CH_H / 2 - 18 })); + } + case 2: { // North (partner) — horizontal, top + const sp = Math.min(CW_A + 4, (W * 0.65) / Math.max(count - 1, 1)); + const sx = W / 2 - ((count - 1) * sp) / 2; + return Array.from({ length: count }, (_, i) => ({ x: sx + i * sp, y: SCOREBAR_H + CH_A / 2 + 22 })); + } + case 1: { // West — vertical, rotated -90° + // When rotated -90°, visual footprint: width=CH_A, height=CW_A + const available = H - SCOREBAR_H - CH_H - 60; + const sp = Math.min(CW_A + 4, available / Math.max(count - 1, 1)); + const sy = (H + SCOREBAR_H) / 2 - ((count - 1) * sp) / 2; + return Array.from({ length: count }, (_, i) => ({ x: CH_A / 2 + 10, y: sy + i * sp })); + } + case 3: { // East — vertical, rotated +90° + const available = H - SCOREBAR_H - CH_H - 60; + const sp = Math.min(CW_A + 4, available / Math.max(count - 1, 1)); + const sy = (H + SCOREBAR_H) / 2 - ((count - 1) * sp) / 2; + return Array.from({ length: count }, (_, i) => ({ x: W - CH_A / 2 - 10, y: sy + i * sp })); + } + } + } + + private getTablePositions(count: number): Array<{ x: number; y: number }> { + const W = this.scale.width; + const H = this.scale.height; + const cols = Math.min(count, 7); + const rows = Math.ceil(count / cols); + const spX = CW_H + 10; + const spY = CH_H + 8; + const results: Array<{ x: number; y: number }> = []; + for (let i = 0; i < count; i++) { + results.push({ + x: W / 2 + (i % cols - (cols - 1) / 2) * spX, + y: this.tableCenter.y + (Math.floor(i / cols) - (rows - 1) / 2) * spY, + }); + } + return results; + } + + // --------------------------------------------------------------------------- + // Turn management + // --------------------------------------------------------------------------- + + private nextTurn(): void { + if (this.state.roundOver) { this.showRoundEnd(); return; } + const cur = this.state.currentPlayer; + const player = this.state.players[cur]; + this.setStatus(`Turno di ${player.name}`); + this.pulseLabel(cur); + + if (player.isHuman) { + this.enableHumanInteraction(); + } else { + this.aiThinking = true; + this.showThinkBar(cur); + this.time.delayedCall(AI_DELAY, () => { + this.hideThinkBar(); + this.doAIMove(cur); + }); + } + } + + private doAIMove(playerIdx: PlayerIndex): void { + const move = chooseMove(this.state, playerIdx); + this.aiThinking = false; + this.executeMove(playerIdx, move.card, move.capture); + } + + // --------------------------------------------------------------------------- + // Human interaction + // --------------------------------------------------------------------------- + + private enableHumanInteraction(): void { + for (const card of this.state.players[0].hand) { + const img = this.cardImages.get(card.id); + if (!img) continue; + img.setInteractive({ useHandCursor: true }); + img.on('pointerover', () => { + if (this.selectedCard?.id !== card.id) img.setY(img.y - 10); + }); + img.on('pointerout', () => { + if (this.selectedCard?.id !== card.id) img.setY(img.y + 10); + }); + img.on('pointerdown', () => this.onCardClick(card, img)); + } + } + + private disableHumanInteraction(): void { + for (const card of this.state.players[0].hand) { + const img = this.cardImages.get(card.id); + if (img) { img.removeAllListeners(); img.disableInteractive(); } + } + this.clearSelectedGlow(); + this.clearHighlights(); + this.selectedCard = null; + this.selectedCardImg = null; + } + + private onCardClick(card: Card, img: Phaser.GameObjects.Image): void { + if (this.aiThinking) return; + const captures = findCaptures(card, this.state.table); + + if (this.selectedCard?.id === card.id) { + // Second click: confirm if possible + if (captures.length === 1) { this.confirmMove(card, captures[0]); return; } + if (captures.length === 0) { this.confirmMove(card, []); return; } + this.deselectCard(); + return; + } + + this.deselectCard(); + this.selectedCard = card; + this.selectedCardImg = img; + + // [trueref: postFX.addGlow] — pulsing selection glow + if (this.renderer.type === Phaser.WEBGL) { + img.postFX.clear(); + this.selectedGlow = img.postFX.addGlow(0xffff88, 4, 0); + this.selectedGlowTween = this.tweens.add({ + targets: this.selectedGlow, + outerStrength: 12, + duration: 500, yoyo: true, repeat: -1, ease: 'Sine.InOut', + }); + } else { + img.setTint(0xffff88); + } + + if (captures.length === 0) { + this.setStatus('Nessuna cattura — clicca di nuovo per giocare sul tavolo'); + this.highlightTableForDump(card); + } else if (captures.length === 1) { + this.setStatus(`Cattura: ${captures[0].map(cardName).join(', ')} — clicca di nuovo per confermare`); + this.pendingCaptures = captures; + this.highlightCapture(captures[0]); + } else { + this.setStatus('Scegli le carte da catturare'); + this.pendingCaptures = captures; + this.highlightMultipleCaptures(captures); + } + } + + private deselectCard(): void { + this.clearSelectedGlow(); + if (this.selectedCardImg && this.renderer.type !== Phaser.WEBGL) { + this.selectedCardImg.clearTint(); + } + this.selectedCard = null; + this.selectedCardImg = null; + this.pendingCaptures = []; + this.clearHighlights(); + } + + private clearSelectedGlow(): void { + this.selectedGlowTween?.stop(); + this.selectedGlowTween = null; + this.selectedGlow = null; + if (this.selectedCardImg) { + this.selectedCardImg.postFX?.clear(); + if (this.renderer.type === Phaser.WEBGL) { + // Re-add shadow after clearing + this.selectedCardImg.postFX.addShadow(0, 4, 0.006, 1.2, 0x000000, 6, 0.4); + } + } + } + + private confirmMove(card: Card, capture: Card[]): void { + this.disableHumanInteraction(); + this.executeMove(0, card, capture); + } + + // --------------------------------------------------------------------------- + // Highlights + // --------------------------------------------------------------------------- + + private highlightCapture(capture: Card[]): void { + this.clearHighlights(); + for (const card of capture) { + const img = this.cardImages.get(card.id); + if (!img) continue; + const hl = this.add.rectangle(img.x, img.y, CW_H + 8, CH_H + 8, 0x00ff88, 0.2) + .setStrokeStyle(2, 0x00ff88, 0.9).setDepth(4); + this.tableHighlights.push(hl); + img.setInteractive({ useHandCursor: true }); + img.once('pointerdown', () => this.confirmMove(this.selectedCard!, capture)); + } + } + + private highlightMultipleCaptures(captures: Card[][]): void { + this.clearHighlights(); + const W = this.scale.width; + captures.forEach((cap, i) => { + const label = cap.map(cardName).join(' + '); + const y = SCOREBAR_H + 70 + i * 36; + const bg = this.add.graphics().setDepth(20); + bg.fillStyle(0x001a0a, 0.9); + bg.fillRoundedRect(W / 2 - 180, y - 14, 360, 28, 7); + bg.lineStyle(1, 0x00ff88, 0.7); + bg.strokeRoundedRect(W / 2 - 180, y - 14, 360, 28, 7); + const btn = this.add.zone(W / 2, y, 360, 28).setInteractive({ useHandCursor: true }).setDepth(21); + const txt = this.add.text(W / 2, y, `Cattura: ${label}`, { + fontFamily: 'serif', fontSize: '14px', color: '#00ffaa', + }).setOrigin(0.5).setDepth(21); + btn.on('pointerdown', () => this.confirmMove(this.selectedCard!, cap)); + (bg as any)._captureBtn = true; + this.tableHighlights.push(bg, btn, txt); + }); + for (const cap of captures) { + for (const c of cap) { + const img = this.cardImages.get(c.id); + if (img) { + const hl = this.add.rectangle(img.x, img.y, CW_H + 8, CH_H + 8, 0xffff00, 0.15) + .setStrokeStyle(2, 0xffff00, 0.8).setDepth(4); + this.tableHighlights.push(hl); + } + } + } + } + + private highlightTableForDump(card: Card): void { + const { x, y } = this.tableCenter; + const W = this.scale.width; + const H = this.scale.height; + const tw = W * 0.76 - 4; + const th = H - SCOREBAR_H - 175; + const hl = this.add.graphics().setDepth(4).setInteractive( + new Phaser.Geom.Rectangle(x - tw / 2, y - th / 2, tw, th), + Phaser.Geom.Rectangle.Contains + ); + hl.fillStyle(0xffff00, 0.05); + hl.fillRoundedRect(x - tw / 2, y - th / 2, tw, th, 18); + hl.lineStyle(2, 0xffff00, 0.4); + hl.strokeRoundedRect(x - tw / 2, y - th / 2, tw, th, 18); + hl.on('pointerdown', () => this.confirmMove(card, [])); + this.tableHighlights.push(hl); + } + + private clearHighlights(): void { + for (const h of this.tableHighlights) h.destroy(); + this.tableHighlights = []; + } + + // --------------------------------------------------------------------------- + // Execute move + animate + // --------------------------------------------------------------------------- + + private executeMove(playerIdx: PlayerIndex, card: Card, capture: Card[]): void { + const { nextState, capture: captureResult, isScopa } = applyMove( + this.state, playerIdx, card, capture.length > 0 ? capture : undefined + ); + const oldState = this.state; + this.state = nextState; + + const cardImg = this.cardImages.get(card.id)!; + cardImg.setDepth(15); + + const isSettebello = captureResult !== null && + [card, ...captureResult.captured].some(c => c.suit === 'denara' && c.value === 7); + const isDenariCapture = captureResult !== null && !isSettebello && + [card, ...captureResult.captured].some(c => c.suit === 'denara'); + const isPrimieraPick = captureResult !== null && + [card, ...captureResult.captured].some(c => [7, 6, 1, 5].includes(c.value)); + + // Flip to face-up + cardImg.setTexture('cards', card.id); + if (this.renderer.type === Phaser.WEBGL) { + cardImg.postFX.clear(); + cardImg.postFX.addShadow(0, 4, 0.006, 1.2, 0x000000, 6, 0.4); + } + + this.playSfx(isSettebello ? 'settebello' : captureResult ? 'capture' : 'card_play'); + this.spawnCardTrail(cardImg); + + if (captureResult) { + this.tweens.add({ + targets: cardImg, + x: this.tableCenter.x, y: this.tableCenter.y, + duration: 200, ease: 'Power2', + onComplete: () => { + this.spawnCaptureEffect(this.tableCenter.x, this.tableCenter.y, isSettebello); + + if (isSettebello) { + // [trueref: camera.shake + camera.flash] — dramatic impact for settebello + this.cameras.main.shake(450, 0.012); + this.cameras.main.flash(400, 255, 215, 0); + this.spawnSettebelloFlash(); + } + if (isDenariCapture) { + this.spawnDenariEffect(this.tableCenter.x, this.tableCenter.y); + } + if (isPrimieraPick && !isSettebello) { + this.spawnPrimieraEffect(this.tableCenter.x, this.tableCenter.y); + } + + const pilePos = this.getPilePos(playerIdx); + const toClear = [card, ...captureResult.captured]; + let done = 0; + for (const c of toClear) { + const img = this.cardImages.get(c.id)!; + this.tweens.add({ + targets: img, + x: pilePos.x, y: pilePos.y, alpha: 0, + duration: 240, delay: 30, + onComplete: () => { + img.setVisible(false); + done++; + if (done === toClear.length) { + if (isScopa) { + this.playSfx('scopa'); + this.doScopaEffect(playerIdx, () => + this.afterMove(nextState, oldState) + ); + } else { + this.afterMove(nextState, oldState); + } + } + }, + }); + } + }, + }); + } else { + const tablePos = this.nextTablePos(nextState.table); + // Drop shadow on newly placed table card [trueref: postFX.addShadow] + if (this.renderer.type === Phaser.WEBGL) { + cardImg.postFX.clear(); + cardImg.postFX.addShadow(2, 6, 0.008, 1.2, 0x000000, 8, 0.5); + } + // Random "fallen card" angle — readable but organic [trueref: image.setAngle] + const randomAngle = Phaser.Math.Between(-9, 9); + this.tweens.add({ + targets: cardImg, + x: tablePos.x, y: tablePos.y, angle: randomAngle, + duration: 280, ease: 'Back.Out', + onComplete: () => this.afterMove(nextState, oldState), + }); + } + } + + private afterMove(nextState: GameState, _old: GameState): void { + this.updateScoreBar(); + this.relayoutTable(); + if (nextState.roundOver) { + this.time.delayedCall(500, () => this.showRoundEnd()); + } else { + this.relayoutHand(0); + this.nextTurn(); + } + } + + private relayoutHand(playerIdx: PlayerIndex): void { + const hand = this.state.players[playerIdx].hand; + const positions = this.getHandPositions(playerIdx, hand.length); + hand.forEach((card, i) => { + const img = this.cardImages.get(card.id); + if (img) this.tweens.add({ targets: img, x: positions[i].x, y: positions[i].y, duration: 160 }); + }); + } + + private relayoutTable(): void { + const table = this.state.table; + if (!table.length) return; + const positions = this.getTablePositions(table.length); + table.forEach((card, i) => { + const img = this.cardImages.get(card.id); + if (img?.visible) this.tweens.add({ targets: img, x: positions[i].x, y: positions[i].y, duration: 160 }); + }); + } + + // --------------------------------------------------------------------------- + // Particle effects + // --------------------------------------------------------------------------- + + private spawnCaptureEffect(x: number, y: number, settebello = false): void { + const color = settebello ? 0xffd700 : 0x66ffcc; + const count = settebello ? 55 : 28; + + // [trueref: ParticleEmitter v3.60+] this.add.particles returns emitter directly + const e1 = this.add.particles(x, y, 'particle_glow', { + lifespan: { min: 350, max: 700 }, + speed: { min: 80, max: 280 }, + scale: { start: 0.9, end: 0 }, + alpha: { start: 1, end: 0 }, + tint: color, gravityY: 100, emitting: false, + }).setDepth(25); + e1.explode(count); + this.time.delayedCall(750, () => e1.destroy()); + + if (settebello) { + const e2 = this.add.particles(x, y, 'particle_dia', { + lifespan: 1000, speed: { min: 60, max: 200 }, + scale: { start: 1.3, end: 0 }, alpha: { start: 1, end: 0 }, + tint: 0xffd700, gravityY: -30, emitting: false, + }).setDepth(26); + e2.explode(30); + this.time.delayedCall(1100, () => e2.destroy()); + } + } + + private doScopaEffect(playerIdx: PlayerIndex, onDone: () => void): void { + const W = this.scale.width; + const H = this.scale.height; + const isTeamA = (playerIdx === 0 || playerIdx === 2); + + // [trueref: camera.shake + camera.flash] + this.cameras.main.shake(600, 0.018); + this.cameras.main.flash(500, 255, 215, 0); + + // Gold explosion + const e1 = this.add.particles(W / 2, H / 2, 'particle_glow', { + lifespan: { min: 700, max: 1400 }, + speed: { min: 200, max: 500 }, + scale: { start: 1.3, end: 0 }, + alpha: { start: 1, end: 0 }, + tint: [0xffd700, 0xffcc00, 0xffffff, 0xff8800], + gravityY: 50, emitting: false, + }).setDepth(28); + e1.explode(100); + this.time.delayedCall(1500, () => e1.destroy()); + + // Shockwave ring + const ring = this.add.particles(W / 2, H / 2, 'particle_sq', { + lifespan: 450, speed: { min: 350, max: 650 }, + scale: { start: 0.8, end: 0 }, alpha: { start: 0.7, end: 0 }, + tint: 0xffcc00, gravityY: 0, emitting: false, + }).setDepth(27); + ring.explode(60); + this.time.delayedCall(500, () => ring.destroy()); + + // Update scope counter live [already done in updateScoreBar] + this.updateScoreBar(); + this.flashScoreScope(isTeamA ? 0 : 1); + + // SCOPA! text with bounce-in [trueref: Back.Out ease] + const player = this.state.players[playerIdx]; + const txt = this.add.text(W / 2, H / 2, 'SCOPA!', { + fontFamily: 'Georgia, serif', fontSize: '108px', color: '#ffd700', + stroke: '#000000', strokeThickness: 10, + }).setOrigin(0.5).setDepth(50).setAlpha(0).setScale(0.2); + + const sub = this.add.text(W / 2, H / 2 + 110, player.name, { + fontFamily: 'serif', fontSize: '32px', + color: isTeamA ? '#aaffaa' : '#ffaaaa', + stroke: '#000', strokeThickness: 3, + }).setOrigin(0.5).setDepth(50).setAlpha(0); + + this.tweens.add({ + targets: txt, + alpha: 1, scale: 1, + duration: 280, ease: 'Back.Out', + }); + this.tweens.add({ targets: sub, alpha: 1, duration: 300, delay: 120 }); + + this.time.delayedCall(1600, () => { + this.tweens.add({ + targets: [txt, sub], alpha: 0, y: '-=50', + duration: 280, + onComplete: () => { txt.destroy(); sub.destroy(); onDone(); }, + }); + }); + } + + private spawnCardTrail(img: Phaser.GameObjects.Image): void { + const e = this.add.particles(img.x, img.y, 'particle_sm', { + lifespan: 220, speed: 15, + scale: { start: 0.6, end: 0 }, alpha: { start: 0.5, end: 0 }, + tint: 0xffffff, follow: img, frequency: 50, quantity: 1, + }).setDepth(img.depth - 1); + this.time.delayedCall(420, () => { + e.stop(); + this.time.delayedCall(250, () => e.destroy()); + }); + } + + /** Denari capture: warm orange shimmer — smaller than scopa, bigger than plain capture. */ + private spawnDenariEffect(x: number, y: number): void { + const e = this.add.particles(x, y, 'particle_sm', { + lifespan: { min: 280, max: 550 }, + speed: { min: 60, max: 160 }, + scale: { start: 1.1, end: 0 }, + alpha: { start: 0.9, end: 0 }, + tint: [0xff9900, 0xffcc44, 0xffdd00], + gravityY: 70, emitting: false, + }).setDepth(24); + e.explode(18); + this.time.delayedCall(650, () => e.destroy()); + // Flash the denari counter in the score bar + const isTeamA = this.state.currentPlayer === 0 || this.state.currentPlayer === 2; + const denTxt = isTeamA ? this.hudA.denari : this.hudB.denari; + this.tweens.add({ targets: denTxt, scaleX: 2.0, scaleY: 2.0, duration: 130, yoyo: true }); + } + + /** Settebello capture: brief "7♦" pop and gold flash — between capture and scopa in weight. */ + private spawnSettebelloFlash(): void { + const W = this.scale.width; + const H = this.scale.height; + const txt = this.add.text(W / 2, H / 2 - 60, '7♦', { + fontFamily: 'Georgia, serif', fontSize: '64px', color: '#ffd700', + stroke: '#000', strokeThickness: 7, + }).setOrigin(0.5).setDepth(50).setAlpha(0).setScale(0.4); + + this.tweens.add({ + targets: txt, alpha: 1, scale: 1, + duration: 220, ease: 'Back.Out', + }); + this.time.delayedCall(900, () => { + this.tweens.add({ + targets: txt, alpha: 0, y: txt.y - 40, + duration: 200, onComplete: () => txt.destroy(), + }); + }); + + // Diamond burst in gold + const e = this.add.particles(W / 2, H / 2, 'particle_dia', { + lifespan: { min: 500, max: 900 }, + speed: { min: 100, max: 300 }, + scale: { start: 1.2, end: 0 }, alpha: { start: 1, end: 0 }, + tint: [0xffd700, 0xffcc00, 0xffffff], + gravityY: -20, emitting: false, + }).setDepth(27); + e.explode(22); + this.time.delayedCall(1000, () => e.destroy()); + } + + /** Primiera-valuable pick (7,6,1,5): subtle blue-white shimmer at capture point. */ + private spawnPrimieraEffect(x: number, y: number): void { + const e = this.add.particles(x, y, 'particle_glow', { + lifespan: { min: 250, max: 480 }, + speed: { min: 25, max: 80 }, + scale: { start: 0.6, end: 0 }, + alpha: { start: 0.8, end: 0 }, + tint: [0xaaddff, 0xffffff, 0x88ccff], + gravityY: -30, emitting: false, + }).setDepth(23); + e.explode(10); + this.time.delayedCall(530, () => e.destroy()); + // Briefly flash primiera score + const isTeamA = this.state.currentPlayer === 0 || this.state.currentPlayer === 2; + const primTxt = isTeamA ? this.hudA.prim : this.hudB.prim; + this.tweens.add({ targets: primTxt, scaleX: 1.7, scaleY: 1.7, duration: 120, yoyo: true, + onStart: () => primTxt.setColor('#aaddff'), + onComplete: () => primTxt.setColor(isTeamA ? '#aaffaa' : '#ffaaaa'), + }); + } + + // --------------------------------------------------------------------------- + // Settebello glow on table + // --------------------------------------------------------------------------- + + private maybeGlowSettebello(): void { + if (this.renderer.type !== Phaser.WEBGL) return; + for (const card of this.state.table) { + const img = this.cardImages.get(card.id); + if (!img) continue; + // Clear old FX first + img.postFX.clear(); + img.postFX.addShadow(2, 6, 0.008, 1.2, 0x000000, 8, 0.5); + if (card.suit === 'denara' && card.value === 7) { + // [trueref: postFX.addGlow] pulsing golden halo on settebello + const glow = img.postFX.addGlow(0xffd700, 0, 0); + this.tweens.add({ + targets: glow, outerStrength: 14, + duration: 700, yoyo: true, repeat: -1, ease: 'Sine.InOut', + }); + } + } + } + + // --------------------------------------------------------------------------- + // Positions + // --------------------------------------------------------------------------- + + private nextTablePos(table: Card[]): { x: number; y: number } { + const count = table.length; + const positions = this.getTablePositions(count); + return positions[count - 1] ?? this.tableCenter; + } + + private getPilePos(playerIdx: PlayerIndex): { x: number; y: number } { + const W = this.scale.width; + const H = this.scale.height; + return teamOf(playerIdx) === 0 + ? { x: 50, y: H - 40 } + : { x: W - 50, y: H - 40 }; + } + + // --------------------------------------------------------------------------- + // Sound FX (Web Audio) + // --------------------------------------------------------------------------- + + private startMusic(): void { + if (this.musicStarted) return; + this.musicStarted = true; + try { + this.audioCtx = (this.sound as Phaser.Sound.WebAudioSoundManager).context as AudioContext; + } catch { return; } + if (!this.audioCtx) return; + const ctx = this.audioCtx; + + this.musicGain = ctx.createGain(); + this.musicGain.gain.setValueAtTime(0, ctx.currentTime); + this.musicGain.gain.linearRampToValueAtTime(0.16, ctx.currentTime + 4); + this.musicGain.connect(ctx.destination); + + const reverb = ctx.createDelay(0.4); + reverb.delayTime.value = 0.14; + const fbk = ctx.createGain(); + fbk.gain.value = 0.38; + reverb.connect(fbk); + fbk.connect(reverb); + fbk.connect(this.musicGain); + + // Bass drone + [110, 165, 220].forEach((f, i) => { + const o = ctx.createOscillator(); + const g = ctx.createGain(); + o.type = 'sine'; + o.frequency.value = f; + g.gain.value = [0.07, 0.045, 0.02][i]; + o.connect(g); g.connect(reverb); o.start(); + }); + + // Melodic loop + const notes = [440,523,587,659,587,523,440,392,440,523,659,784,659,587,523,440]; + const durs = [0.3,0.2,0.3,0.5,0.2,0.3,0.6,0.3,0.2,0.4,0.3,0.2,0.3,0.4,0.2,0.8]; + const totalDur = durs.reduce((s, d) => s + d + 0.05, 0); + const scheduleLoop = (t0: number) => { + if (!this.scene.isActive('GameScene')) return; + let t = t0; + notes.forEach((freq, i) => { + const o = ctx.createOscillator(); + const env = ctx.createGain(); + o.type = 'triangle'; + o.frequency.value = freq; + env.gain.setValueAtTime(0, t); + env.gain.linearRampToValueAtTime(0.06, t + 0.02); + env.gain.exponentialRampToValueAtTime(0.001, t + durs[i]); + o.connect(env); env.connect(reverb); + o.start(t); o.stop(t + durs[i] + 0.05); + t += durs[i] + 0.05; + }); + this.time.delayedCall((totalDur + 0.8) * 1000, () => { + if (this.scene.isActive('GameScene')) scheduleLoop(ctx.currentTime); + }); + }; + scheduleLoop(ctx.currentTime + 1.8); + + // Chord stabs + const scheduleChords = () => { + if (!this.scene.isActive('GameScene')) return; + const now = ctx.currentTime + 0.05; + [220, 261, 329].forEach(f => { + const o = ctx.createOscillator(); const g = ctx.createGain(); + o.type = 'sine'; o.frequency.value = f; + g.gain.setValueAtTime(0, now); + g.gain.linearRampToValueAtTime(0.038, now + 0.06); + g.gain.exponentialRampToValueAtTime(0.001, now + 1.4); + o.connect(g); g.connect(reverb); o.start(now); o.stop(now + 1.5); + }); + this.time.delayedCall(4200, scheduleChords); + }; + this.time.delayedCall(2400, scheduleChords); + } + + private playSfx(type: 'card_play' | 'capture' | 'scopa' | 'settebello'): void { + if (!this.audioCtx) return; + const ctx = this.audioCtx; + const now = ctx.currentTime; + const note = (freq: number, t: number, dur: number, vol: number, type: OscillatorType = 'triangle') => { + const o = ctx.createOscillator(); + const g = ctx.createGain(); + o.type = type; + o.frequency.value = freq; + g.gain.setValueAtTime(vol, t); + g.gain.exponentialRampToValueAtTime(0.001, t + dur); + o.connect(g); g.connect(ctx.destination); + o.start(t); o.stop(t + dur + 0.05); + }; + switch (type) { + case 'card_play': + note(280, now, 0.12, 0.12, 'sine'); + note(200, now + 0.04, 0.1, 0.06, 'sine'); + break; + case 'capture': + note(520, now, 0.08, 0.1); + note(720, now + 0.07, 0.1, 0.1); + break; + case 'scopa': + [440,554,659,880].forEach((f, i) => note(f, now + i * 0.07, 0.3, 0.14)); + break; + case 'settebello': + [440,523,659,784,1047].forEach((f, i) => note(f, now + i * 0.06, 0.5, 0.12, 'sine')); + break; + } + } + + private stopMusic(): void { + if (this.musicGain && this.audioCtx) { + this.musicGain.gain.linearRampToValueAtTime(0, this.audioCtx.currentTime + 1.5); + } + } + + // --------------------------------------------------------------------------- + // Round end / game over + // --------------------------------------------------------------------------- + + private showRoundEnd(): void { + const W = this.scale.width; + const H = this.scale.height; + const bd = getScoreBreakdown(this.state); + const t0 = this.state.teamScores[0]; + const t1 = this.state.teamScores[1]; + + const overlay = this.add.rectangle(0, 0, W, H, 0x000000, 0.82).setOrigin(0).setDepth(30); + + // Panel with rounded rect [trueref: fillRoundedRect] + const panel = this.add.graphics().setDepth(31); + panel.fillStyle(0x07200c, 1); + panel.fillRoundedRect(W / 2 - 280, H / 2 - 210, 560, 420, 16); + panel.lineStyle(2, 0xffd700, 0.7); + panel.strokeRoundedRect(W / 2 - 280, H / 2 - 210, 560, 420, 16); + + const lines: Array<[string, string]> = [ + [`Fine Mano ${this.state.roundNumber ?? 1}`, '#ffd700'], + ['', ''], + [`Team A +${t0.roundPoints} pt → ${t0.totalPoints} totali`, '#aaffaa'], + [`Team B +${t1.roundPoints} pt → ${t1.totalPoints} totali`, '#ffaaaa'], + ['', ''], + [`Carte A=${t0.cards} B=${t1.cards} ${pointStr(bd.cartePoint)}`, '#ffffff'], + [`Denari A=${t0.denari} B=${t1.denari} ${pointStr(bd.denariPoint)}`, '#ffdd88'], + [`Settebello → ${bd.settebelloPoint === 0 ? 'Team A' : 'Team B'}`, '#ffd700'], + [`Primiera A=${t0.primiera} B=${t1.primiera} ${pointStr(bd.primieraPoint)}`, '#aaddff'], + [`Scope A=${bd.scopeTeam0} B=${bd.scopeTeam1}`, '#ccffcc'], + ]; + + lines.forEach(([line, color], i) => { + this.add.text(W / 2, H / 2 - 190 + i * 34, line, { + fontFamily: i === 0 ? 'Georgia, serif' : 'monospace', + fontSize: i === 0 ? '28px' : '16px', color, + }).setOrigin(0.5).setDepth(32); + }); + + const gameOver = t0.totalPoints >= 11 || t1.totalPoints >= 11; + const btnLabel = gameOver ? 'Fine Partita' : 'Prossima Mano'; + + const btnG = this.add.graphics().setDepth(32); + btnG.fillStyle(0xffd700, 1); + btnG.fillRoundedRect(W / 2 - 110, H / 2 + 185, 220, 44, 10); + const btnZone = this.add.zone(W / 2, H / 2 + 207, 220, 44) + .setInteractive({ useHandCursor: true }).setDepth(33); + this.add.text(W / 2, H / 2 + 207, btnLabel, { + fontFamily: 'Georgia, serif', fontSize: '20px', color: '#0a2e10', + }).setOrigin(0.5).setDepth(34); + + btnG.on('pointerover', () => { btnG.clear(); btnG.fillStyle(0xffec6e, 1); btnG.fillRoundedRect(W / 2 - 110, H / 2 + 185, 220, 44, 10); }); + btnG.on('pointerout', () => { btnG.clear(); btnG.fillStyle(0xffd700, 1); btnG.fillRoundedRect(W / 2 - 110, H / 2 + 185, 220, 44, 10); }); + btnZone.on('pointerover', () => btnG.emit('pointerover')); + btnZone.on('pointerout', () => btnG.emit('pointerout')); + btnZone.on('pointerdown', () => { + [overlay, panel, btnG, btnZone].forEach(o => o.destroy()); + this.children.list.filter((c: any) => c.depth >= 31).forEach(c => c.destroy()); + gameOver ? this.showGameOver() : this.startNewRound(); + }); + } + + private showGameOver(): void { + const W = this.scale.width; + const H = this.scale.height; + const t0 = this.state.teamScores[0]; + const t1 = this.state.teamScores[1]; + const win = t0.totalPoints >= t1.totalPoints; + this.stopMusic(); + + // Victory confetti + const confetti = this.add.particles(W / 2, H / 2, 'particle_glow', { + lifespan: 2500, speed: { min: 80, max: 450 }, + scale: { start: 1.1, end: 0 }, tint: [0xffd700, 0xffffff, 0x00ff88, 0xff8800], + gravityY: 90, frequency: 30, + }).setDepth(39); + this.time.delayedCall(3500, () => confetti.destroy()); + + this.add.rectangle(0, 0, W, H, 0x000000, 0.88).setOrigin(0).setDepth(40); + + // Rounded panel + const pg = this.add.graphics().setDepth(41); + pg.fillStyle(0x04150a, 1); + pg.fillRoundedRect(W / 2 - 220, H / 2 - 150, 440, 310, 20); + pg.lineStyle(3, 0xffd700, 0.8); + pg.strokeRoundedRect(W / 2 - 220, H / 2 - 150, 440, 310, 20); + + this.add.text(W / 2, H / 2 - 110, 'PARTITA FINITA', { + fontFamily: 'Georgia, serif', fontSize: '44px', color: '#ffd700', + stroke: '#000', strokeThickness: 6, + }).setOrigin(0.5).setDepth(42); + this.add.text(W / 2, H / 2 - 30, win ? 'Team A (Tu + Compagno)' : 'Team B (AI)', { + fontFamily: 'serif', fontSize: '26px', + color: win ? '#aaffaa' : '#ffaaaa', + }).setOrigin(0.5).setDepth(42); + this.add.text(W / 2, H / 2 + 35, `${t0.totalPoints} — ${t1.totalPoints}`, { + fontFamily: 'Georgia, serif', fontSize: '50px', color: '#ffd700', + }).setOrigin(0.5).setDepth(42); + + const bz = this.add.zone(W / 2, H / 2 + 115, 230, 48) + .setInteractive({ useHandCursor: true }).setDepth(44); + const bg2 = this.add.graphics().setDepth(43); + const drawBtn = (c: number) => { bg2.clear(); bg2.fillStyle(c, 1); bg2.fillRoundedRect(W / 2 - 115, H / 2 + 91, 230, 48, 12); }; + drawBtn(0xffd700); + this.add.text(W / 2, H / 2 + 115, 'NUOVA PARTITA', { + fontFamily: 'Georgia, serif', fontSize: '21px', color: '#0a2e10', + }).setOrigin(0.5).setDepth(44); + bz.on('pointerover', () => drawBtn(0xffec6e)); + bz.on('pointerout', () => drawBtn(0xffd700)); + bz.on('pointerdown', () => this.scene.restart()); + } + + private startNewRound(): void { + const totals = this.state.teamScores.map(t => t.totalPoints); + const nextRound = (this.state.roundNumber ?? 1) + 1; + for (const img of this.cardImages.values()) img.destroy(); + this.cardImages.clear(); + this.state = createInitialState(); + this.state.teamScores[0].totalPoints = totals[0]; + this.state.teamScores[1].totalPoints = totals[1]; + this.state.roundNumber = nextRound; + this.dealAnimation(() => { + this.updateScoreBar(); + this.nextTurn(); + }); + } + + private setStatus(msg: string): void { this.statusText.setText(msg); } +} + +// --------------------------------------------------------------------------- +// Utils +// --------------------------------------------------------------------------- + +function cardName(card: Card): string { + const s: Record = { bastoni: 'Bastoni', coppe: 'Coppe', denara: 'Denari', spade: 'Spade' }; + const v: Record = { 1: 'Asso', 8: 'Fante', 9: 'Cavallo', 10: 'Re' }; + return `${v[card.value] ?? card.value} di ${s[card.suit]}`; +} + +function pointStr(p: 0 | 1 | null): string { + return p === null ? '(pari)' : p === 0 ? '→ A' : '→ B'; +} diff --git a/src/scenes/MenuScene.ts b/src/scenes/MenuScene.ts new file mode 100644 index 0000000..9522e38 --- /dev/null +++ b/src/scenes/MenuScene.ts @@ -0,0 +1,71 @@ +import Phaser from 'phaser'; + +export class MenuScene extends Phaser.Scene { + constructor() { + super({ key: 'MenuScene' }); + } + + create(): void { + const W = this.scale.width; + const H = this.scale.height; + + // Background felt + this.add.rectangle(0, 0, W, H, 0x1a5c2a).setOrigin(0); + + // Title + this.add.text(W / 2, H * 0.2, 'Scopone Scientifico', { + fontFamily: 'Georgia, serif', + fontSize: '52px', + color: '#ffd700', + stroke: '#000000', + strokeThickness: 4, + }).setOrigin(0.5); + + this.add.text(W / 2, H * 0.32, '2 vs 2 · Tu + Compagno vs 2 AI', { + fontFamily: 'serif', + fontSize: '22px', + color: '#ccffcc', + }).setOrigin(0.5); + + // Rules summary + const rules = [ + '40 carte Napoletane · 10 a testa', + 'Cattura per valore o somma', + 'Punteggio: Carte · Denari · Settebello · Primiera · Scope', + 'Prima squadra a 11 punti vince', + ]; + rules.forEach((line, i) => { + this.add.text(W / 2, H * 0.44 + i * 28, line, { + fontFamily: 'serif', + fontSize: '18px', + color: '#ffffff', + }).setOrigin(0.5); + }); + + // Start button + const btn = this.add.rectangle(W / 2, H * 0.72, 220, 60, 0xffd700, 1) + .setInteractive({ useHandCursor: true }); + const btnText = this.add.text(W / 2, H * 0.72, 'INIZIA PARTITA', { + fontFamily: 'Georgia, serif', + fontSize: '22px', + color: '#1a5c2a', + }).setOrigin(0.5); + + btn.on('pointerover', () => btn.setFillStyle(0xffec6e)); + btn.on('pointerout', () => btn.setFillStyle(0xffd700)); + btn.on('pointerdown', () => { + this.cameras.main.fadeOut(300, 0, 30, 0); + this.cameras.main.once('camerafadeoutcomplete', () => { + this.scene.start('GameScene'); + }); + }); + + // Show some face-down cards decoratively + const positions = [ + [W * 0.1, H * 0.5], [W * 0.15, H * 0.52], [W * 0.9, H * 0.5], [W * 0.85, H * 0.52], + ]; + for (const [x, y] of positions) { + this.add.image(x, y, 'retro').setScale(0.08).setAngle(Phaser.Math.Between(-15, 15)); + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ead3e13 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b76c9ff --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: '.', + publicDir: 'public', + build: { + outDir: 'dist', + emptyOutDir: true, + }, + server: { + port: 3000, + open: true, + }, +});