├── .eslintignore ├── .eslintrc.cjs ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── lua ├── README.md ├── examples │ ├── .gitignore │ ├── 01-simple │ │ ├── README.md │ │ ├── build.sh │ │ └── src │ │ │ ├── main.lua │ │ │ └── pdxinfo │ ├── 02-net-tac-toe │ │ ├── README.md │ │ ├── build.sh │ │ ├── design │ │ │ ├── card.aseprite │ │ │ ├── concept.aseprite │ │ │ ├── grid-lines-h.aseprite │ │ │ ├── grid-lines-v.aseprite │ │ │ ├── hand.aseprite │ │ │ ├── itch-cover.aseprite │ │ │ └── itch-logo.aseprite │ │ └── src │ │ │ ├── en.strings │ │ │ ├── fonts │ │ │ ├── nico │ │ │ │ ├── OFL.txt │ │ │ │ ├── nico-bold-16-table-16-16.png │ │ │ │ ├── nico-bold-16.fnt │ │ │ │ ├── nico-clean-16-table-16-16.png │ │ │ │ ├── nico-clean-16.fnt │ │ │ │ ├── nico-paint-16-table-16-16.png │ │ │ │ └── nico-paint-16.fnt │ │ │ └── pinzelan │ │ │ │ ├── License.txt │ │ │ │ ├── pinzelan-48-table-44-55.png │ │ │ │ └── pinzelan-48.fnt │ │ │ ├── img │ │ │ ├── grid-lines-h-table-220-20.png │ │ │ ├── grid-lines-v-table-20-220.png │ │ │ ├── hand-table-48-48.png │ │ │ └── pdx │ │ │ │ └── card.png │ │ │ ├── lib │ │ │ ├── objects │ │ │ │ ├── Hand.lua │ │ │ │ ├── NttGame.lua │ │ │ │ ├── board │ │ │ │ │ ├── BoardCell.lua │ │ │ │ │ ├── BoardLines.lua │ │ │ │ │ └── BoardState.lua │ │ │ │ └── screens │ │ │ │ │ ├── GameplayScreen.lua │ │ │ │ │ └── SetupScreen.lua │ │ │ └── util │ │ │ │ ├── board.lua │ │ │ │ └── text.lua │ │ │ ├── main.lua │ │ │ └── pdxinfo │ ├── 03-panic-sign │ │ ├── README.md │ │ ├── build.sh │ │ ├── design │ │ │ ├── bottom-arrow.aseprite │ │ │ ├── card.aseprite │ │ │ ├── main-screen.aseprite │ │ │ ├── panic-logo.aseprite │ │ │ └── top-arrow.aseprite │ │ └── src │ │ │ ├── Example03PanicSign.lua │ │ │ ├── en.strings │ │ │ ├── fonts │ │ │ └── nico │ │ │ │ ├── OFL.txt │ │ │ │ ├── nico-bold-16-table-16-16.png │ │ │ │ ├── nico-bold-16.fnt │ │ │ │ ├── nico-clean-16-table-16-16.png │ │ │ │ ├── nico-clean-16.fnt │ │ │ │ ├── nico-paint-16-table-16-16.png │ │ │ │ └── nico-paint-16.fnt │ │ │ ├── img │ │ │ ├── bottom-arrow.png │ │ │ ├── panic-logo.png │ │ │ ├── pdx │ │ │ │ └── card.png │ │ │ └── top-arrow.png │ │ │ ├── lib │ │ │ └── util │ │ │ │ └── text.lua │ │ │ ├── main.lua │ │ │ └── pdxinfo │ └── 04-fetch │ │ ├── README.md │ │ ├── build.sh │ │ └── src │ │ ├── Example04Fetch.lua │ │ ├── main.lua │ │ └── pdxinfo └── pdportal.lua ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts ├── README.md └── uploadPdxToPlaydate.js ├── src ├── app.css ├── app.d.ts ├── app.html ├── lib │ ├── components │ │ ├── appHeader │ │ │ ├── AppHeader.svelte │ │ │ └── HeaderLink.svelte │ │ ├── dev │ │ │ ├── DeviceInfo.svelte │ │ │ ├── PeerDebugger.svelte │ │ │ └── Toasts.svelte │ │ ├── form │ │ │ ├── Button.svelte │ │ │ ├── TextInput.svelte │ │ │ └── Textarea.svelte │ │ ├── text │ │ │ ├── Heading.svelte │ │ │ └── Link.svelte │ │ └── toast │ │ │ ├── Toast.svelte │ │ │ └── ToastList.svelte │ ├── stores │ │ ├── pdDeviceStore.ts │ │ ├── peerStore.ts │ │ └── toastStore.ts │ ├── util │ │ ├── CommandBuffer.ts │ │ ├── EventEmitter.ts │ │ ├── PdCommunicator.ts │ │ ├── buffer.ts │ │ └── string.ts │ └── version.ts └── routes │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── about │ └── +page.svelte │ └── just-dev-things │ └── +page.svelte ├── static ├── favicon.png └── logo.jpg ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:svelte/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint'], 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020, 14 | extraFileExtensions: ['.svelte'] 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true 20 | }, 21 | overrides: [ 22 | { 23 | files: ['*.svelte'], 24 | parser: 'svelte-eslint-parser', 25 | parserOptions: { 26 | parser: '@typescript-eslint/parser' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Browser (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Browser [e.g. Chrome] 29 | - Version [e.g. 119] 30 | 31 | **Playdate (please complete the following information):** 32 | - OS: [e.g. 2.1.1] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | *.pdx 12 | *.pdz 13 | *.luac 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "semi": false, 6 | "printWidth": 80, 7 | "plugins": ["prettier-plugin-svelte"], 8 | "pluginSearchDirs": ["."], 9 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Paul Straw 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pdportal 2 | 3 | A magic portal for [Playdate](https://play.date) that enables online multiplayer using Web Serial and WebRTC. This is the web interface (available for use with any Playdate game at https://pdportal.net). If you want to make a multiplayer game that uses pdportal, check out the [lua](./lua) subfolder. 4 | 5 | This technique should now be compatible with Catalog games! 6 | 7 | ``` 8 | +------------+ +------------+ 9 | | | | | 10 | | Playdate 1 | | Playdate 2 | 11 | | | | | 12 | +------------+ WebRTC P2P conn +------------+ 13 | ^ established with ^ 14 | | PeerJS | 15 | Serial | Serial 16 | | v | 17 | v .-------. v 18 | +------------+ ,-' '-. +------------+ 19 | | Computer 1 | ; : | Computer 2 | 20 | | (Browser @ |<---->: Internet ;<---->| (Browser @ | 21 | | pdportal) | \ / | pdportal) | 22 | +------------+ '-. ,-' +------------+ 23 | `-----' 24 | ``` 25 | 26 | ## ⚠️ DISCLAIMER 27 | 28 | pdportal is not affiliated with or endorsed by [Panic](https://panic.com). Use it at your own risk. If you're having a problem with a game that uses pdportal, contact that game's author for support. 29 | 30 | Docs? Currently, the source code is the documentation. Limitations? Probably many. Check out the various README.md files for more details. 31 | 32 | ## Thanks 33 | 34 | pdportal uses [pd-usb](https://github.com/cranksters/pd-usb) and info from [playdate-reverse-engineering](https://github.com/cranksters/playdate-reverse-engineering) to communicate with the cheese. It's built with [Svelte](https://svelte.dev/) and [PeerJS](https://peerjs.com/). 35 | 36 | Inspiration and help with the Lua bytecode parts in the original version came from [pd-camera](https://github.com/t0mg/pd-camera), and code from [Eric Lewis](https://gist.github.com/ericlewis/43d07016275308de11a5519466deea85). 37 | 38 | ## Want to help? 39 | 40 | ### Contributing 41 | 42 | Thanks for your interest in contributing to pdportal! Before you get started: 43 | 44 | 1. Read and agree to follow the [code of conduct (Contributor Covenant 2.1)](./CODE_OF_CONDUCT.md). 45 | 2. Before you start work, check the [open issues](https://github.com/strawdynamics/pdportal/issues) to make sure there isn't an existing issue for the fix or feature you want to work on. 46 | 3. If there's not already a relevant issue, [open a new one](https://github.com/strawdynamics/pdportal/issues/new). Your new issue should describe the fix or feature, why you think it's necessary, and how you want to approach the work (please use one of the issue templates). 47 | 4. Project maintainers will review your proposal and work with you to figure out next steps! 48 | 49 | ### Running locally 50 | 51 | ```bash 52 | npm i 53 | 54 | npm run dev 55 | 56 | # or start the server and open the app in a new browser tab 57 | npm run dev -- --open 58 | ``` 59 | 60 | ### Building for production 61 | 62 | ```bash 63 | npm run build 64 | npm run preview 65 | ``` 66 | -------------------------------------------------------------------------------- /lua/README.md: -------------------------------------------------------------------------------- 1 | # pdportal Lua 2 | 3 | Copy `pdportal.lua` from this folder to your own project to get started adding pdportal compatibility. Subclass `PdPortal` and implement your own game logic by overriding the specified methods. 4 | 5 | There are also Lua example apps you can install to your device. See the README.md in each example folder for more details. 6 | -------------------------------------------------------------------------------- /lua/examples/.gitignore: -------------------------------------------------------------------------------- 1 | pdportal.lua 2 | -------------------------------------------------------------------------------- /lua/examples/01-simple/README.md: -------------------------------------------------------------------------------- 1 | # pdportal-01-simple 2 | 3 | This example responds to every event by telling pdportal to log a message to the browser console. This is just a demonstration of subclassing `PdPortal` and overriding the default empty implementations. 4 | 5 | To test the peer connection parts of this demo, open a second instance of the pdportal site on your computer. Follow the instructions in your browser console to access the pdportal development tools. From there, you can manually initialize a peer (without a second Playdate), connect to your existing peer, and exchange messages. 6 | 7 | ## Installation 8 | 9 | `./build.sh d` will compile and automatically install the app to your connected Playdate. I'm sorry, the script currently only works on macOS, and even then can be a bit janky. Pull requests welcome :) 10 | -------------------------------------------------------------------------------- /lua/examples/01-simple/build.sh: -------------------------------------------------------------------------------- 1 | pdxFile="pdportal-01-simple.pdx" 2 | 3 | cp ../../pdportal.lua ./src/pdportal.lua 4 | pdc src "$pdxFile" 5 | 6 | runChoice=$1 7 | 8 | if [ "$runChoice" = "s" ]; then 9 | open "$pdxFile" 10 | elif [ "$runChoice" = "d" ]; then 11 | node ../../../scripts/uploadPdxToPlaydate.js "$pdxFile" 12 | fi 13 | -------------------------------------------------------------------------------- /lua/examples/01-simple/src/main.lua: -------------------------------------------------------------------------------- 1 | -- Copied during build, you wouldn't normally have to do that 2 | import "./pdportal" 3 | 4 | import "CoreLibs/graphics" 5 | 6 | local graphics = playdate.graphics 7 | 8 | local PdPortal = PdPortal 9 | local PortalCommand = PdPortal.PortalCommand 10 | 11 | class('Example01SimplePortal').extends(PdPortal) 12 | local Example01SimplePortal = Example01SimplePortal 13 | 14 | playdate.display.setRefreshRate(50) 15 | 16 | function Example01SimplePortal:init() 17 | -- If your subclass overrides the init method, make sure to call super! 18 | Example01SimplePortal.super.init(self) 19 | 20 | self.connected = false 21 | self.peerId = nil 22 | self.updatingPeer = false 23 | end 24 | 25 | local portalInstance = Example01SimplePortal() 26 | 27 | playdate.update = function() 28 | -- Required for serial keepalive 29 | portalInstance:update() 30 | end 31 | 32 | function Example01SimplePortal:update() 33 | -- If your subclass overrides the update method, make sure to call super! 34 | Example01SimplePortal.super.update(self) 35 | 36 | graphics.clear() 37 | 38 | graphics.drawTextAligned( 39 | self.connected and 'Connected' or 'Disconnected', 40 | 200, 41 | 120, 42 | kTextAlignment.center 43 | ) 44 | 45 | playdate.drawFPS(10, 10) 46 | 47 | if self.connected then 48 | local peerText = self.peerId and 'Ⓐ Destroy peer ' .. self.peerId or 'Ⓐ Initialize peer' 49 | if self.updatingPeer then 50 | peerText = 'Updating peer…' 51 | end 52 | 53 | graphics.drawTextAligned(peerText, 200, 140, kTextAlignment.center) 54 | 55 | if not self.updatingPeer and playdate.buttonJustPressed(playdate.kButtonA) then 56 | self.updatingPeer = true 57 | 58 | if self.peerId == nil then 59 | self:sendCommand(PortalCommand.InitializePeer) 60 | else 61 | self:sendCommand(PortalCommand.DestroyPeer) 62 | end 63 | end 64 | end 65 | end 66 | 67 | function Example01SimplePortal:onConnect(portalVersion) 68 | self.connected = true 69 | self:log('connectEcho!', portalVersion) 70 | end 71 | 72 | function Example01SimplePortal:onDisconnect() 73 | self.connected = false 74 | self.peerId = nil 75 | self.updatingPeer = false 76 | end 77 | 78 | function Example01SimplePortal:onPeerOpen(peerId) 79 | self:log('peerOpenEcho!', peerId) 80 | self.updatingPeer = false 81 | self.peerId = peerId 82 | end 83 | 84 | function Example01SimplePortal:onPeerClose() 85 | self:log('peerCloseEcho!') 86 | self.updatingPeer = false 87 | self.peerId = nil 88 | end 89 | 90 | function Example01SimplePortal:onPeerConnection(remotePeerId) 91 | self:log('peerConnectionEcho!', remotePeerId) 92 | end 93 | 94 | function Example01SimplePortal:onPeerConnOpen(remotePeerId) 95 | self:log('peerConnOpenEcho!', remotePeerId) 96 | end 97 | 98 | function Example01SimplePortal:onPeerConnClose(remotePeerId) 99 | self:log('peerConnCloseEcho!', remotePeerId) 100 | end 101 | 102 | function Example01SimplePortal:onPeerConnData(remotePeerId, payload) 103 | self:log('peerConnDataEcho!', remotePeerId, payload) 104 | end 105 | -------------------------------------------------------------------------------- /lua/examples/01-simple/src/pdxinfo: -------------------------------------------------------------------------------- 1 | name=pdportal-01-simple 2 | bundleID=com.strawdynamics.pdportal-01-simple 3 | version=0.2.0 4 | buildNumber=2 5 | author=pdportal contributors 6 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/README.md: -------------------------------------------------------------------------------- 1 | # Net-tac-toe (pdportal-02-net-tac-toe) 2 | 3 | [Available for download on itch.io](https://paulstraw.itch.io/net-tac-toe) 4 | 5 | Currently the most complete pdportal example. This is an implementation of a (very simple) two player turn-based game. 6 | 7 | ## Testing against yourself 8 | 9 | Just like with 01-simple, you can use a second instance of the pdportal site on your computer to test Net-tac-toe. Follow the instructions in your browser console to access the pdportal development tools. From there, you can manually initialize a peer (without a second Playdate), connect to your existing peer, and exchange messages. 10 | 11 | Whichever browser you enter the peer ID into will act as the "client", while the other browser will be the "host". If you make the browser connected to the Playdate the "client", you'll need to manually send a `MatchEvent.Start` event from the other browser for the game to begin. 12 | 13 | Example messages: 14 | 15 | ```js 16 | // Sent from host to client to start game. `isHostX` determines who goes first. 17 | {"e": "s", "isHostX": false} 18 | 19 | // Sent by current player to visually move the cursor. Indices start at 1 top 20 | // left, then continue across and down (i.e. bottom left is 7) 21 | {"e": "m", "oldIndex": 1, "newIndex": 3} 22 | 23 | // Sent by current player to place a mark on their current square (ends turn) 24 | {"e": "p", "index": 3} 25 | ``` 26 | 27 | ## Installation 28 | 29 | `./build.sh d` will compile and automatically install the app to your connected Playdate. I'm sorry, the script currently only works on macOS, and even then can be a bit janky. Pull requests welcome :) 30 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/build.sh: -------------------------------------------------------------------------------- 1 | pdxFile="pdportal-02-net-tac-toe.pdx" 2 | 3 | cp ../../pdportal.lua ./src/pdportal.lua 4 | pdc -q src "$pdxFile" 5 | 6 | runChoice=$1 7 | 8 | if [ "$runChoice" = "s" ]; then 9 | open "$pdxFile" 10 | elif [ "$runChoice" = "d" ]; then 11 | node ../../../scripts/uploadPdxToPlaydate.js "$pdxFile" 12 | fi 13 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/design/card.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/design/card.aseprite -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/design/concept.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/design/concept.aseprite -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/design/grid-lines-h.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/design/grid-lines-h.aseprite -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/design/grid-lines-v.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/design/grid-lines-v.aseprite -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/design/hand.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/design/hand.aseprite -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/design/itch-cover.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/design/itch-cover.aseprite -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/design/itch-logo.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/design/itch-logo.aseprite -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/en.strings: -------------------------------------------------------------------------------- 1 | "setup.title" = "Net-tac-toe" 2 | "setup.disconnected" = "Visit https://pdportal.net on your computer to get started!" 3 | "setup.serialConnected" = "Connected to serial, connecting to peer server…" 4 | "setup.peerOpenPrefix" = "Connected to pdportal! Your ID is *" 5 | "setup.peerOpenSuffix" = "*. Waiting for someone to play with…" 6 | 7 | "gameplay.you" = "You" 8 | "gameplay.win" = "You win!" 9 | "gameplay.lose" = "You lose!" 10 | "gameplay.draw" = "Draw!" 11 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/nico/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Emily Huo (mlyhuo@gmail.com, emuhuo.itch.io, emhuo.com). 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/nico/nico-bold-16-table-16-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/fonts/nico/nico-bold-16-table-16-16.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/nico/nico-bold-16.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":12,"xHeight":-1,"capHeight":-1,"descent":-1} 2 | 3 | tracking=0 4 | 5 | space 5 6 | ! 5 7 | " 8 8 | # 14 9 | $ 10 10 | % 15 11 | & 13 12 | ' 4 13 | ( 6 14 | ) 6 15 | * 8 16 | + 7 17 | , 5 18 | - 8 19 | . 5 20 | / 10 21 | 0 11 22 | 1 9 23 | 2 10 24 | 3 10 25 | 4 11 26 | 5 10 27 | 6 10 28 | 7 10 29 | 8 10 30 | 9 11 31 | : 5 32 | ; 6 33 | < 10 34 | = 9 35 | > 11 36 | ? 10 37 | @ 14 38 | A 13 39 | B 11 40 | C 12 41 | D 13 42 | E 10 43 | F 10 44 | G 14 45 | H 12 46 | I 5 47 | J 9 48 | K 13 49 | L 9 50 | M 15 51 | N 14 52 | O 14 53 | P 11 54 | Q 14 55 | R 12 56 | S 10 57 | T 10 58 | U 12 59 | V 13 60 | W 15 61 | X 12 62 | Y 12 63 | Z 11 64 | [ 6 65 | \ 9 66 | ] 5 67 | ^ 13 68 | _ 10 69 | ` 4 70 | a 11 71 | b 11 72 | c 9 73 | d 11 74 | e 10 75 | f 7 76 | g 11 77 | h 10 78 | i 5 79 | j 5 80 | k 11 81 | l 5 82 | m 15 83 | n 10 84 | o 10 85 | p 11 86 | q 11 87 | r 8 88 | s 8 89 | t 8 90 | u 10 91 | v 10 92 | w 15 93 | x 10 94 | y 11 95 | z 9 96 | { 6 97 | | 4 98 | } 7 99 | ~ 10 100 |   5 101 | ¡ 5 102 | ¢ 9 103 | £ 11 104 | ¤ 10 105 | ¥ 12 106 | ¦ 5 107 | § 8 108 | ¨ 8 109 | © 13 110 | ª 7 111 | « 9 112 | ¬ 13 113 | ® 14 114 | ¯ 6 115 | ° 7 116 | ± 10 117 | ² 6 118 | ³ 7 119 | ´ 5 120 | µ 11 121 | ¶ 10 122 | · 5 123 | ¸ 5 124 | ¹ 6 125 | º 7 126 | » 10 127 | ¼ 15 128 | ½ 15 129 | ¾ 15 130 | ¿ 10 131 | À 11 132 | Á 11 133 |  11 134 | à 11 135 | Ä 11 136 | Å 13 137 | Æ 15 138 | Ç 12 139 | È 10 140 | É 10 141 | Ê 10 142 | Ë 10 143 | Ì 5 144 | Í 5 145 | Î 5 146 | Ï 5 147 | Ð 13 148 | Ñ 14 149 | Ò 14 150 | Ó 14 151 | Ô 14 152 | Õ 14 153 | Ö 14 154 | × 9 155 | Ø 14 156 | Ù 12 157 | Ú 12 158 | Û 12 159 | Ü 12 160 | Ý 12 161 | Þ 11 162 | ß 11 163 | à 11 164 | á 11 165 | â 11 166 | ã 11 167 | ä 11 168 | å 11 169 | æ 15 170 | ç 9 171 | è 11 172 | é 11 173 | ê 11 174 | ë 11 175 | ì 5 176 | í 6 177 | î 6 178 | ï 7 179 | ð 11 180 | ñ 10 181 | ò 11 182 | ó 11 183 | ô 11 184 | õ 11 185 | ö 11 186 | ÷ 9 187 | ø 11 188 | ù 10 189 | ú 10 190 | û 10 191 | ü 10 192 | ý 10 193 | þ 11 194 | ÿ 10 195 | Ā 13 196 | ā 11 197 | Ă 13 198 | ă 11 199 | Ą 13 200 | ą 11 201 | Ć 12 202 | ć 9 203 | Ċ 12 204 | ċ 9 205 | Č 12 206 | č 9 207 | Ď 13 208 | ď 15 209 | Đ 13 210 | đ 12 211 | Ē 10 212 | ē 11 213 | Ė 10 214 | ė 11 215 | Ę 10 216 | ę 11 217 | Ě 10 218 | ě 11 219 | Ğ 14 220 | ğ 11 221 | Ġ 14 222 | ġ 11 223 | Ģ 14 224 | ģ 11 225 | Ħ 13 226 | ħ 10 227 | Ĩ 5 228 | ĩ 6 229 | Ī 5 230 | ī 6 231 | Į 5 232 | į 5 233 | İ 5 234 | ı 5 235 | Ķ 13 236 | ķ 11 237 | Ĺ 9 238 | ĺ 5 239 | Ļ 9 240 | ļ 5 241 | Ľ 9 242 | ľ 9 243 | Ł 9 244 | ł 6 245 | Ń 14 246 | ń 10 247 | Ņ 14 248 | ņ 10 249 | Ň 14 250 | ň 10 251 | Ŋ 14 252 | ŋ 10 253 | Ō 14 254 | ō 11 255 | Ő 14 256 | ő 11 257 | Œ 15 258 | œ 15 259 | Ŕ 12 260 | ŕ 7 261 | Ŗ 12 262 | ŗ 7 263 | Ř 12 264 | ř 7 265 | Ś 10 266 | ś 8 267 | Ş 10 268 | ş 8 269 | Š 10 270 | š 8 271 | Ţ 10 272 | ţ 7 273 | Ť 10 274 | ť 9 275 | Ŧ 10 276 | ŧ 7 277 | Ũ 12 278 | ũ 10 279 | Ū 12 280 | ū 10 281 | Ů 12 282 | ů 10 283 | Ű 12 284 | ű 10 285 | Ų 12 286 | ų 10 287 | Ŵ 15 288 | ŵ 15 289 | Ŷ 12 290 | ŷ 10 291 | Ÿ 12 292 | Ź 11 293 | ź 9 294 | Ż 11 295 | ż 9 296 | Ž 11 297 | ž 9 298 | ƒ 9 299 | Ơ 15 300 | ơ 12 301 | Ư 14 302 | ư 12 303 | Ǎ 13 304 | ǎ 11 305 | Ǐ 5 306 | ǐ 6 307 | Ǒ 14 308 | ǒ 11 309 | Ǔ 12 310 | ǔ 10 311 | Ǖ 12 312 | ǖ 10 313 | Ǘ 12 314 | ǘ 10 315 | Ǚ 12 316 | ǚ 10 317 | Ǜ 12 318 | ǜ 10 319 | Ș 10 320 | ș 8 321 | Ț 10 322 | ț 7 323 | ȷ 5 324 | ˆ 7 325 | ˇ 7 326 | ˘ 7 327 | ˙ 4 328 | ˚ 5 329 | ˛ 4 330 | ˜ 7 331 | ˝ 8 332 | ̃ 1 333 | ̄ 1 334 | ̆ 1 335 | ̇ 1 336 | ̈ 1 337 | ̉ 1 338 | ̊ 1 339 | ̋ 1 340 | ̌ 1 341 | ̒ 1 342 | ̛ 1 343 | ̣ 1 344 | ̦ 1 345 | ̧ 1 346 | ̨ 1 347 | ̵ 1 348 | ̶ 1 349 | ̷ 1 350 | ̸ 1 351 | Δ 14 352 | Ω 14 353 | μ 10 354 | π 11 355 | Ẁ 15 356 | ẁ 15 357 | Ẃ 15 358 | ẃ 15 359 | Ẅ 15 360 | ẅ 15 361 | Ạ 13 362 | ạ 11 363 | Ả 13 364 | ả 11 365 | Ấ 13 366 | ấ 11 367 | Ầ 13 368 | ầ 11 369 | Ẩ 13 370 | ẩ 11 371 | Ẫ 13 372 | ẫ 11 373 | Ậ 13 374 | ậ 11 375 | Ắ 13 376 | ắ 11 377 | Ằ 13 378 | ằ 11 379 | Ẳ 13 380 | ẳ 11 381 | Ẵ 13 382 | ẵ 11 383 | Ặ 13 384 | ặ 11 385 | Ẹ 10 386 | ẹ 11 387 | Ẻ 10 388 | ẻ 11 389 | Ẽ 10 390 | ẽ 11 391 | Ế 10 392 | ế 11 393 | Ề 10 394 | ề 11 395 | Ể 10 396 | ể 11 397 | Ễ 10 398 | ễ 11 399 | Ệ 10 400 | ệ 11 401 | Ỉ 5 402 | ỉ 5 403 | Ị 5 404 | ị 5 405 | Ọ 14 406 | ọ 11 407 | Ỏ 14 408 | ỏ 11 409 | Ố 14 410 | ố 11 411 | Ồ 14 412 | ồ 11 413 | Ổ 14 414 | ổ 11 415 | Ỗ 14 416 | ỗ 11 417 | Ộ 14 418 | ộ 11 419 | Ớ 15 420 | ớ 12 421 | Ờ 15 422 | ờ 12 423 | Ở 15 424 | ở 12 425 | Ỡ 15 426 | ỡ 12 427 | Ợ 15 428 | ợ 12 429 | Ụ 12 430 | ụ 10 431 | Ủ 12 432 | ủ 10 433 | Ứ 14 434 | ứ 12 435 | Ừ 14 436 | ừ 12 437 | Ử 14 438 | ử 12 439 | Ữ 14 440 | ữ 12 441 | Ự 14 442 | ự 12 443 | Ỳ 12 444 | ỳ 10 445 | Ỵ 12 446 | ỵ 10 447 | Ỷ 12 448 | ỷ 10 449 | Ỹ 12 450 | ỹ 10 451 | – 10 452 | — 15 453 | ‘ 6 454 | ’ 6 455 | ‚ 5 456 | “ 8 457 | ” 8 458 | „ 9 459 | † 9 460 | ‡ 9 461 | • 9 462 | … 15 463 | ‰ 15 464 | ‹ 6 465 | › 6 466 | ⁄ 7 467 | ⁰ 7 468 | ⁴ 7 469 | ⁵ 7 470 | ⁶ 7 471 | ⁷ 6 472 | ⁸ 7 473 | ⁹ 7 474 | ₀ 7 475 | ₁ 6 476 | ₂ 6 477 | ₃ 7 478 | ₄ 7 479 | ₅ 7 480 | ₆ 7 481 | ₇ 7 482 | ₈ 7 483 | ₉ 7 484 | € 12 485 | № 14 486 | ™ 12 487 | ⅛ 15 488 | ⅜ 15 489 | ⅝ 15 490 | ⅞ 15 491 | ∂ 10 492 | ∅ 11 493 | ∏ 14 494 | ∑ 11 495 | − 10 496 | √ 12 497 | ∞ 13 498 | ∫ 8 499 | ≈ 11 500 | ≠ 10 501 | ≤ 11 502 | ≥ 11 503 | ◊ 10 504 |   16 505 |  13 506 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/nico/nico-clean-16-table-16-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/fonts/nico/nico-clean-16-table-16-16.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/nico/nico-clean-16.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":12,"xHeight":-1,"capHeight":-1,"descent":-1} 2 | 3 | tracking=0 4 | 5 | space 5 6 | ! 5 7 | " 7 8 | # 15 9 | $ 11 10 | % 15 11 | & 12 12 | ' 4 13 | ( 6 14 | ) 6 15 | * 8 16 | + 10 17 | , 5 18 | - 9 19 | . 5 20 | / 8 21 | 0 11 22 | 1 6 23 | 2 10 24 | 3 10 25 | 4 11 26 | 5 10 27 | 6 10 28 | 7 9 29 | 8 10 30 | 9 10 31 | : 5 32 | ; 5 33 | < 10 34 | = 10 35 | > 10 36 | ? 9 37 | @ 15 38 | A 12 39 | B 11 40 | C 13 41 | D 12 42 | E 10 43 | F 10 44 | G 13 45 | H 11 46 | I 4 47 | J 6 48 | K 12 49 | L 9 50 | M 14 51 | N 12 52 | O 14 53 | P 11 54 | Q 14 55 | R 12 56 | S 11 57 | T 10 58 | U 11 59 | V 12 60 | W 15 61 | X 12 62 | Y 11 63 | Z 10 64 | [ 7 65 | \ 9 66 | ] 6 67 | ^ 10 68 | _ 10 69 | ` 6 70 | a 10 71 | b 11 72 | c 10 73 | d 11 74 | e 10 75 | f 7 76 | g 10 77 | h 10 78 | i 4 79 | j 4 80 | k 10 81 | l 4 82 | m 14 83 | n 10 84 | o 11 85 | p 11 86 | q 11 87 | r 7 88 | s 8 89 | t 7 90 | u 10 91 | v 10 92 | w 15 93 | x 10 94 | y 11 95 | z 9 96 | { 6 97 | | 5 98 | } 7 99 | ~ 10 100 |   5 101 | ¡ 5 102 | ¢ 10 103 | £ 11 104 | ¤ 10 105 | ¥ 11 106 | ¦ 5 107 | § 9 108 | ¨ 8 109 | © 15 110 | ª 7 111 | « 11 112 | ¬ 11 113 | ® 11 114 | ¯ 7 115 | ° 8 116 | ± 10 117 | ² 7 118 | ³ 7 119 | ´ 6 120 | µ 10 121 | ¶ 8 122 | · 5 123 | ¸ 7 124 | ¹ 5 125 | º 8 126 | » 11 127 | ¼ 14 128 | ½ 14 129 | ¾ 15 130 | ¿ 9 131 | À 12 132 | Á 12 133 |  12 134 | à 12 135 | Ä 12 136 | Å 12 137 | Æ 15 138 | Ç 13 139 | È 10 140 | É 10 141 | Ê 10 142 | Ë 10 143 | Ì 4 144 | Í 4 145 | Î 4 146 | Ï 4 147 | Ð 12 148 | Ñ 12 149 | Ò 14 150 | Ó 14 151 | Ô 14 152 | Õ 14 153 | Ö 14 154 | × 9 155 | Ø 14 156 | Ù 11 157 | Ú 11 158 | Û 11 159 | Ü 11 160 | Ý 11 161 | Þ 11 162 | ß 11 163 | à 9 164 | á 9 165 | â 9 166 | ã 9 167 | ä 9 168 | å 9 169 | æ 15 170 | ç 10 171 | è 10 172 | é 10 173 | ê 10 174 | ë 10 175 | ì 4 176 | í 5 177 | î 5 178 | ï 6 179 | ð 11 180 | ñ 10 181 | ò 11 182 | ó 11 183 | ô 11 184 | õ 11 185 | ö 11 186 | ÷ 10 187 | ø 11 188 | ù 10 189 | ú 10 190 | û 10 191 | ü 10 192 | ý 10 193 | þ 11 194 | ÿ 10 195 | Ā 12 196 | ā 9 197 | Ă 12 198 | ă 9 199 | Ą 12 200 | ą 10 201 | Ć 13 202 | ć 10 203 | Ĉ 13 204 | ĉ 10 205 | Ċ 13 206 | ċ 10 207 | Č 13 208 | č 10 209 | Ď 12 210 | ď 14 211 | Đ 12 212 | đ 12 213 | Ē 10 214 | ē 10 215 | Ĕ 10 216 | ĕ 10 217 | Ė 10 218 | ė 10 219 | Ę 10 220 | ę 10 221 | Ě 10 222 | ě 10 223 | Ĝ 13 224 | ĝ 10 225 | Ğ 13 226 | ğ 10 227 | Ġ 13 228 | ġ 10 229 | Ģ 13 230 | ģ 10 231 | Ĥ 12 232 | ĥ 10 233 | Ħ 13 234 | ħ 10 235 | Ĩ 4 236 | ĩ 5 237 | Ī 4 238 | ī 5 239 | Ĭ 4 240 | ĭ 5 241 | Į 5 242 | į 4 243 | İ 4 244 | ı 4 245 | Ĵ 6 246 | ĵ 6 247 | Ķ 11 248 | ķ 10 249 | ĸ 10 250 | Ĺ 9 251 | ĺ 4 252 | Ļ 9 253 | ļ 4 254 | Ľ 9 255 | ľ 7 256 | Ŀ 9 257 | ŀ 7 258 | Ł 9 259 | ł 5 260 | Ń 12 261 | ń 10 262 | Ņ 12 263 | ņ 10 264 | Ň 12 265 | ň 10 266 | Ŋ 12 267 | ŋ 10 268 | Ō 14 269 | ō 11 270 | Ŏ 14 271 | ŏ 11 272 | Ő 14 273 | ő 11 274 | Œ 15 275 | œ 15 276 | Ŕ 11 277 | ŕ 7 278 | Ŗ 11 279 | ŗ 7 280 | Ř 11 281 | ř 7 282 | Ś 11 283 | ś 9 284 | Ŝ 11 285 | ŝ 9 286 | Ş 11 287 | ş 9 288 | Š 11 289 | š 9 290 | Ţ 10 291 | ţ 8 292 | Ť 10 293 | ť 8 294 | Ŧ 10 295 | ŧ 7 296 | Ũ 11 297 | ũ 10 298 | Ū 11 299 | ū 10 300 | Ŭ 11 301 | ŭ 10 302 | Ů 11 303 | ů 10 304 | Ű 11 305 | ű 10 306 | Ų 11 307 | ų 10 308 | Ŵ 15 309 | ŵ 15 310 | Ŷ 11 311 | ŷ 10 312 | Ÿ 11 313 | Ź 10 314 | ź 9 315 | Ż 10 316 | ż 9 317 | Ž 10 318 | ž 9 319 | Ə 13 320 | ƒ 9 321 | Ơ 14 322 | ơ 11 323 | Ư 14 324 | ư 11 325 | DŽ 15 326 | Dž 15 327 | dž 15 328 | LJ 15 329 | Lj 13 330 | lj 8 331 | NJ 15 332 | Nj 15 333 | nj 14 334 | Ǧ 13 335 | ǧ 10 336 | Ǫ 14 337 | ǫ 11 338 | Ǻ 12 339 | ǻ 9 340 | Ǽ 15 341 | ǽ 15 342 | Ǿ 14 343 | ǿ 11 344 | Ȁ 12 345 | ȁ 9 346 | Ȃ 12 347 | ȃ 9 348 | Ȅ 10 349 | ȅ 10 350 | Ȇ 10 351 | ȇ 10 352 | Ȉ 4 353 | ȉ 5 354 | Ȋ 4 355 | ȋ 5 356 | Ȍ 14 357 | ȍ 11 358 | Ȏ 14 359 | ȏ 11 360 | Ȑ 11 361 | ȑ 7 362 | Ȓ 11 363 | ȓ 7 364 | Ȕ 11 365 | ȕ 10 366 | Ȗ 11 367 | ȗ 10 368 | Ș 11 369 | ș 9 370 | Ț 10 371 | ț 7 372 | Ȫ 14 373 | ȫ 11 374 | Ȭ 14 375 | ȭ 11 376 | Ȱ 14 377 | ȱ 11 378 | Ȳ 11 379 | ȳ 10 380 | ȷ 4 381 | ə 10 382 | ʻ 5 383 | ʼ 5 384 | ʾ 5 385 | ʿ 5 386 | ˆ 8 387 | ˇ 8 388 | ˈ 5 389 | ˉ 7 390 | ˊ 6 391 | ˋ 6 392 | ˌ 5 393 | ˘ 8 394 | ˙ 5 395 | ˚ 7 396 | ˛ 7 397 | ˜ 8 398 | ˝ 9 399 | Δ 11 400 | Ω 14 401 | π 12 402 | ฿ 11 403 | Ḉ 13 404 | ḉ 10 405 | Ḍ 12 406 | ḍ 11 407 | Ḏ 12 408 | ḏ 11 409 | Ḕ 10 410 | ḕ 10 411 | Ḗ 10 412 | ḗ 10 413 | Ḝ 10 414 | ḝ 10 415 | Ḡ 13 416 | ḡ 10 417 | Ḥ 12 418 | ḥ 10 419 | Ḫ 12 420 | ḫ 10 421 | Ḯ 4 422 | ḯ 6 423 | Ḷ 9 424 | ḷ 4 425 | Ḻ 9 426 | ḻ 5 427 | Ṃ 14 428 | ṃ 15 429 | Ṅ 12 430 | ṅ 10 431 | Ṇ 12 432 | ṇ 10 433 | Ṉ 12 434 | ṉ 10 435 | Ṍ 14 436 | ṍ 11 437 | Ṏ 14 438 | ṏ 11 439 | Ṑ 14 440 | ṑ 11 441 | Ṓ 14 442 | ṓ 11 443 | Ṛ 11 444 | ṛ 7 445 | Ṟ 11 446 | ṟ 7 447 | Ṡ 11 448 | ṡ 9 449 | Ṣ 11 450 | ṣ 9 451 | Ṥ 11 452 | ṥ 9 453 | Ṧ 11 454 | ṧ 9 455 | Ṩ 11 456 | ṩ 9 457 | Ṭ 10 458 | ṭ 7 459 | Ṯ 10 460 | ṯ 8 461 | Ṹ 11 462 | ṹ 10 463 | Ṻ 11 464 | ṻ 10 465 | Ẁ 15 466 | ẁ 15 467 | Ẃ 15 468 | ẃ 15 469 | Ẅ 15 470 | ẅ 15 471 | Ẏ 11 472 | ẏ 10 473 | Ẓ 10 474 | ẓ 9 475 | ẗ 7 476 | ẞ 15 477 | Ạ 12 478 | ạ 9 479 | Ả 12 480 | ả 9 481 | Ấ 12 482 | ấ 9 483 | Ầ 12 484 | ầ 9 485 | Ẩ 12 486 | ẩ 9 487 | Ẫ 12 488 | ẫ 9 489 | Ậ 12 490 | ậ 9 491 | Ắ 12 492 | ắ 9 493 | Ằ 12 494 | ằ 9 495 | Ẳ 12 496 | ẳ 9 497 | Ẵ 12 498 | ẵ 9 499 | Ặ 12 500 | ặ 9 501 | Ẹ 10 502 | ẹ 10 503 | Ẻ 10 504 | ẻ 10 505 | Ẽ 10 506 | ẽ 10 507 | Ế 10 508 | ế 10 509 | Ề 10 510 | ề 10 511 | Ể 10 512 | ể 10 513 | Ễ 10 514 | ễ 10 515 | Ệ 10 516 | ệ 10 517 | Ỉ 4 518 | ỉ 4 519 | Ị 4 520 | ị 4 521 | Ọ 14 522 | ọ 11 523 | Ỏ 14 524 | ỏ 11 525 | Ố 14 526 | ố 11 527 | Ồ 14 528 | ồ 11 529 | Ổ 14 530 | ổ 11 531 | Ỗ 14 532 | ỗ 11 533 | Ộ 14 534 | ộ 11 535 | Ớ 14 536 | ớ 11 537 | Ờ 14 538 | ờ 11 539 | Ở 14 540 | ở 11 541 | Ỡ 14 542 | ỡ 11 543 | Ợ 14 544 | ợ 11 545 | Ụ 11 546 | ụ 10 547 | Ủ 11 548 | ủ 10 549 | Ứ 14 550 | ứ 11 551 | Ừ 14 552 | ừ 11 553 | Ử 14 554 | ử 11 555 | Ữ 14 556 | ữ 11 557 | Ự 14 558 | ự 11 559 | Ỳ 11 560 | ỳ 10 561 | Ỵ 11 562 | ỵ 11 563 | Ỷ 11 564 | ỷ 10 565 | Ỹ 11 566 | ỹ 10 567 | ‐ 10 568 | ‒ 10 569 | – 10 570 | — 15 571 | ― 15 572 | ‘ 5 573 | ’ 5 574 | ‚ 5 575 | “ 8 576 | ” 8 577 | „ 8 578 | † 11 579 | ‡ 11 580 | • 8 581 | … 12 582 | ‰ 15 583 | ′ 5 584 | ″ 8 585 | ‹ 6 586 | › 6 587 | ⁄ 7 588 | ⁰ 8 589 | ⁴ 8 590 | ⁵ 7 591 | ⁶ 7 592 | ⁷ 7 593 | ⁸ 8 594 | ⁹ 7 595 | ⁺ 8 596 | ⁻ 8 597 | ⁼ 8 598 | ⁽ 5 599 | ⁾ 5 600 | ₀ 8 601 | ₁ 5 602 | ₂ 7 603 | ₃ 7 604 | ₄ 8 605 | ₅ 7 606 | ₆ 7 607 | ₇ 7 608 | ₈ 8 609 | ₉ 7 610 | ₊ 8 611 | ₋ 8 612 | ₌ 8 613 | ₍ 5 614 | ₎ 5 615 | ₡ 13 616 | ₣ 11 617 | ₤ 11 618 | ₦ 13 619 | ₧ 12 620 | ₨ 15 621 | ₩ 15 622 | ₪ 10 623 | ₫ 10 624 | € 12 625 | ₭ 12 626 | ₮ 10 627 | ₱ 12 628 | ₲ 13 629 | ₳ 12 630 | ₴ 11 631 | ₵ 13 632 | ₸ 10 633 | ₹ 12 634 | ₺ 11 635 | ₼ 11 636 | ₽ 11 637 | ₾ 14 638 | ₿ 11 639 | ℓ 11 640 | № 15 641 | ™ 15 642 | Ω 14 643 | ℮ 14 644 | ⅓ 14 645 | ⅔ 15 646 | ⅛ 14 647 | ⅜ 15 648 | ⅝ 15 649 | ⅞ 15 650 | ← 11 651 | ↑ 11 652 | → 12 653 | ↓ 12 654 | ↔ 15 655 | ↕ 11 656 | ↖ 10 657 | ↗ 11 658 | ↘ 10 659 | ↙ 11 660 | ↩ 15 661 | ↪ 14 662 | ↰ 14 663 | ↱ 13 664 | ↲ 14 665 | ↳ 14 666 | ∂ 11 667 | ∅ 14 668 | ∆ 11 669 | ∏ 14 670 | ∑ 11 671 | − 10 672 | ∕ 9 673 | ∙ 8 674 | √ 15 675 | ∞ 14 676 | ∫ 9 677 | ≈ 11 678 | ≠ 10 679 | ≤ 10 680 | ≥ 10 681 | ■ 15 682 | □ 15 683 | ▲ 14 684 | △ 14 685 | ▶ 15 686 | ▷ 15 687 | ▼ 13 688 | ▽ 13 689 | ◀ 14 690 | ◁ 13 691 | ◆ 14 692 | ◇ 14 693 | ◊ 10 694 | ○ 15 695 | ● 15 696 | ★ 14 697 | ☆ 14 698 | ⬎ 14 699 | ⬐ 14 700 |   16 701 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/nico/nico-paint-16-table-16-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/fonts/nico/nico-paint-16-table-16-16.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/nico/nico-paint-16.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":12,"xHeight":-1,"capHeight":-1,"descent":-1} 2 | 3 | tracking=0 4 | 5 | space 5 6 | ! 5 7 | " 8 8 | # 14 9 | $ 9 10 | % 14 11 | & 13 12 | ' 5 13 | ( 6 14 | ) 7 15 | * 8 16 | + 10 17 | , 5 18 | - 8 19 | . 5 20 | / 9 21 | 0 11 22 | 1 7 23 | 2 11 24 | 3 10 25 | 4 10 26 | 5 9 27 | 6 10 28 | 7 10 29 | 8 10 30 | 9 10 31 | : 5 32 | ; 5 33 | < 11 34 | = 8 35 | > 11 36 | ? 9 37 | @ 15 38 | A 13 39 | B 11 40 | C 13 41 | D 12 42 | E 11 43 | F 11 44 | G 12 45 | H 12 46 | I 5 47 | J 9 48 | K 11 49 | L 10 50 | M 14 51 | N 12 52 | O 13 53 | P 11 54 | Q 14 55 | R 11 56 | S 10 57 | T 12 58 | U 12 59 | V 14 60 | W 15 61 | X 13 62 | Y 12 63 | Z 11 64 | [ 6 65 | \ 9 66 | ] 5 67 | ^ 9 68 | _ 10 69 | ` 7 70 | a 10 71 | b 10 72 | c 9 73 | d 10 74 | e 10 75 | f 9 76 | g 10 77 | h 10 78 | i 5 79 | j 5 80 | k 9 81 | l 6 82 | m 14 83 | n 10 84 | o 10 85 | p 10 86 | q 10 87 | r 9 88 | s 9 89 | t 9 90 | u 10 91 | v 11 92 | w 14 93 | x 10 94 | y 11 95 | z 10 96 | { 7 97 | | 6 98 | } 10 99 | ~ 10 100 |   5 101 | ¡ 5 102 | ¢ 9 103 | £ 12 104 | ¤ 12 105 | ¥ 12 106 | ¦ 4 107 | § 10 108 | ¨ 7 109 | © 11 110 | ª 8 111 | « 12 112 | ¬ 9 113 | ® 12 114 | ° 7 115 | ± 9 116 | ² 8 117 | ³ 8 118 | ´ 8 119 | µ 10 120 | ¶ 10 121 | · 4 122 | ¸ 7 123 | ¹ 6 124 | º 8 125 | » 12 126 | ¼ 13 127 | ½ 13 128 | ¾ 14 129 | ¿ 9 130 | À 13 131 | Á 13 132 |  13 133 | à 13 134 | Ä 13 135 | Å 13 136 | Æ 15 137 | Ç 11 138 | È 11 139 | É 11 140 | Ê 11 141 | Ë 11 142 | Ì 5 143 | Í 5 144 | Î 5 145 | Ï 5 146 | Ð 13 147 | Ñ 12 148 | Ò 13 149 | Ó 13 150 | Ô 13 151 | Õ 13 152 | Ö 13 153 | × 9 154 | Ø 13 155 | Ù 12 156 | Ú 12 157 | Û 12 158 | Ü 12 159 | Ý 12 160 | Þ 11 161 | ß 11 162 | à 10 163 | á 10 164 | â 10 165 | ã 10 166 | ä 10 167 | å 10 168 | æ 15 169 | ç 9 170 | è 10 171 | é 10 172 | ê 10 173 | ë 10 174 | ì 5 175 | í 6 176 | î 6 177 | ï 7 178 | ð 10 179 | ñ 10 180 | ò 10 181 | ó 10 182 | ô 10 183 | õ 10 184 | ö 10 185 | ÷ 9 186 | ø 10 187 | ù 10 188 | ú 10 189 | û 10 190 | ü 10 191 | ý 10 192 | þ 10 193 | ÿ 10 194 | ı 5 195 | Ł 12 196 | ł 8 197 | Œ 15 198 | œ 15 199 | Š 10 200 | š 9 201 | Ÿ 12 202 | Ž 11 203 | ž 10 204 | ƒ 9 205 | ˆ 9 206 | ˇ 9 207 | ˙ 4 208 | ˚ 6 209 | ˛ 7 210 | ˜ 9 211 | – 10 212 | — 12 213 | ‘ 4 214 | ’ 4 215 | ‚ 4 216 | “ 8 217 | ” 8 218 | „ 8 219 | † 9 220 | ‡ 9 221 | • 6 222 | … 15 223 | ‰ 14 224 | ‹ 7 225 | › 7 226 | ⁄ 9 227 | € 11 228 | ™ 14 229 | − 10 230 |   16 231 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/pinzelan/License.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2021 GGBot, with Reserved Font Name "Pinzelan". 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/pinzelan/pinzelan-48-table-44-55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/fonts/pinzelan/pinzelan-48-table-44-55.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/fonts/pinzelan/pinzelan-48.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":45,"xHeight":-1,"capHeight":-1,"descent":-1} 2 | 3 | tracking=0 4 | 5 | space 21 6 | ! 20 7 | " 23 8 | # 40 9 | $ 28 10 | % 32 11 | & 31 12 | ' 12 13 | ( 26 14 | ) 19 15 | * 27 16 | + 30 17 | , 15 18 | - 22 19 | . 15 20 | / 27 21 | 0 32 22 | 1 21 23 | 2 31 24 | 3 30 25 | 4 28 26 | 5 32 27 | 6 31 28 | 7 29 29 | 8 29 30 | 9 29 31 | : 13 32 | ; 14 33 | < 17 34 | = 25 35 | > 20 36 | ? 32 37 | @ 34 38 | A 32 39 | B 31 40 | C 29 41 | D 31 42 | E 30 43 | F 31 44 | G 36 45 | H 33 46 | I 13 47 | J 30 48 | K 36 49 | L 30 50 | M 38 51 | N 35 52 | O 38 53 | P 35 54 | Q 35 55 | R 29 56 | S 31 57 | T 32 58 | U 32 59 | V 31 60 | W 40 61 | X 31 62 | Y 31 63 | Z 32 64 | [ 28 65 | \ 27 66 | ] 26 67 | ^ 25 68 | _ 28 69 | ` 15 70 | a 32 71 | b 31 72 | c 29 73 | d 31 74 | e 30 75 | f 31 76 | g 36 77 | h 33 78 | i 13 79 | j 30 80 | k 36 81 | l 30 82 | m 38 83 | n 35 84 | o 38 85 | p 35 86 | q 35 87 | r 29 88 | s 31 89 | t 32 90 | u 32 91 | v 31 92 | w 40 93 | x 31 94 | y 31 95 | z 32 96 | | 17 97 | ~ 27 98 | ¤ 22 99 | ¦ 10 100 | ¨ 25 101 | © 38 102 | ¬ 19 103 | ° 25 104 | ´ 16 105 | · 11 106 | ¸ 14 107 | Á 28 108 | Ä 27 109 | Å 27 110 | Æ 31 111 | Ì 14 112 | Í 16 113 | Î 20 114 | Ï 19 115 | Õ 29 116 | Ö 30 117 | × 26 118 | Ø 33 119 | Ü 29 120 | ß 29 121 | á 28 122 | ä 27 123 | å 27 124 | æ 31 125 | ì 14 126 | í 16 127 | î 20 128 | ï 19 129 | õ 29 130 | ö 30 131 | ÷ 21 132 | ø 33 133 | ü 29 134 | Ā 32 135 | ā 32 136 | Č 30 137 | č 30 138 | Ē 26 139 | ē 26 140 | Ģ 38 141 | ģ 38 142 | Ī 20 143 | ī 20 144 | Ķ 30 145 | ķ 30 146 | Ļ 27 147 | ļ 27 148 | Ņ 30 149 | ņ 30 150 | Š 29 151 | š 29 152 | Ū 33 153 | ū 33 154 | Ž 19 155 | ž 19 156 | ʹ 10 157 | ˆ 20 158 | ˇ 16 159 | ˉ 15 160 | ˊ 16 161 | ˋ 16 162 | ˙ 13 163 | ẞ 29 164 | – 29 165 | — 44 166 | ‘ 13 167 | ’ 11 168 | ‚ 12 169 | ‛ 11 170 | “ 19 171 | ” 23 172 | „ 22 173 | ‟ 21 174 | • 20 175 | … 32 176 | € 37 177 | − 23 178 | ☡ 29 179 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/img/grid-lines-h-table-220-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/img/grid-lines-h-table-220-20.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/img/grid-lines-v-table-20-220.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/img/grid-lines-v-table-20-220.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/img/hand-table-48-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/img/hand-table-48-48.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/img/pdx/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/02-net-tac-toe/src/img/pdx/card.png -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/objects/Hand.lua: -------------------------------------------------------------------------------- 1 | class('Hand').extends() 2 | local Hand = Hand 3 | 4 | local graphics = playdate.graphics 5 | local geometry = playdate.geometry 6 | local screenWidth, screenHeight = playdate.display.getSize() 7 | local imageWithTextStroked = imageWithTextStroked 8 | local easings = playdate.easingFunctions 9 | 10 | local handImageTable = graphics.imagetable.new('img/hand') 11 | 12 | function Hand:init(peerId, displayName) 13 | self.peerId = peerId 14 | self.displayName = displayName 15 | self._targetPos = geometry.point.new( 16 | screenWidth * 0.5, 17 | screenHeight + 64 18 | ) 19 | self.pos = self._targetPos:copy() 20 | 21 | self._index = nil 22 | 23 | self._handSprite = graphics.sprite.new(handImageTable:getImage(1)) 24 | self._handSprite:setZIndex(10) 25 | self._handSprite:moveTo(self._targetPos) 26 | self._handSprite:add() 27 | 28 | self:_initDisplayNameSprite() 29 | 30 | self._animator = graphics.animator.new(0, self._targetPos:copy(), self._targetPos:copy()) 31 | end 32 | 33 | function Hand:update() 34 | local animPos = self._animator:currentValue() 35 | local x, y = animPos:unpack() 36 | self.pos.x = x 37 | self.pos.y = y 38 | self._handSprite:moveTo(x, y) 39 | self._displayNameSprite:moveTo(x + 2, y + 27) 40 | end 41 | 42 | function Hand:destroy() 43 | self._displayNameSprite:remove() 44 | self._handSprite:remove() 45 | end 46 | 47 | function Hand:getIndex() 48 | return self._index 49 | end 50 | 51 | function Hand:indexMoveUp() 52 | if not self._index then 53 | return 54 | end 55 | 56 | self._index -= 3 57 | if self._index < 1 then 58 | self._index += 9 59 | end 60 | 61 | self:setTargetIndex(self._index) 62 | return self._index 63 | end 64 | 65 | function Hand:indexMoveRight() 66 | if not self._index then 67 | return 68 | end 69 | 70 | if self._index % 3 == 0 then 71 | self._index -= 2 72 | else 73 | self._index += 1 74 | end 75 | 76 | self:setTargetIndex(self._index) 77 | return self._index 78 | end 79 | 80 | function Hand:indexMoveDown() 81 | if not self._index then 82 | return 83 | end 84 | 85 | self._index += 3 86 | if self._index > 9 then 87 | self._index -= 9 88 | end 89 | 90 | self:setTargetIndex(self._index) 91 | return self._index 92 | end 93 | 94 | function Hand:indexMoveLeft() 95 | if not self._index then 96 | return 97 | end 98 | 99 | self._index -= 1 100 | if self._index % 3 == 0 then 101 | self._index += 3 102 | end 103 | 104 | self:setTargetIndex(self._index) 105 | return self._index 106 | end 107 | 108 | function Hand:setTargetIndex(index) 109 | local indexPoint = getIndexPoint(index) 110 | indexPoint.x += 16 111 | indexPoint.y += 24 112 | self:setTarget(indexPoint) 113 | 114 | self._index = index 115 | end 116 | 117 | function Hand:setTarget(point) 118 | self._index = nil 119 | 120 | self._animator = graphics.animator.new( 121 | 500, 122 | self.pos:copy(), 123 | point:copy(), 124 | easings.outBack 125 | ) 126 | self._animator.s = 1.2 127 | self._targetPos.x = point.x 128 | self._targetPos.y = point.y 129 | self.pos.x = point.x 130 | self.pos.y = point.y 131 | end 132 | 133 | function Hand:_initDisplayNameSprite() 134 | local displayName = self.displayName 135 | 136 | graphics.pushContext() 137 | graphics.setFont(fonts.nicoPaint16) 138 | local displayNameImage = imageWithTextStroked( 139 | displayName, 140 | screenWidth, 141 | screenHeight, 142 | 1, 143 | '…', 144 | kTextAlignment.center, 145 | 2 146 | ) 147 | graphics.popContext() 148 | 149 | self._displayNameSprite = graphics.sprite.new(displayNameImage) 150 | self._displayNameSprite:setCenter(0.5, 0) 151 | self._displayNameSprite:moveTo(self._targetPos) 152 | self._displayNameSprite:setZIndex(10) 153 | self._displayNameSprite:add() 154 | end 155 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/objects/NttGame.lua: -------------------------------------------------------------------------------- 1 | class('NttGame').extends('PdPortal') 2 | local NttGame = NttGame 3 | 4 | local timer = playdate.timer 5 | local graphics = playdate.graphics 6 | local display = playdate.display 7 | local json = json 8 | local PdPortal = PdPortal 9 | local screenWidth, screenHeight = playdate.display.getSize() 10 | 11 | function NttGame:init() 12 | NttGame.super.init(self) 13 | 14 | self.isSerialConnected = false 15 | self._wasSerialConnected = false 16 | 17 | self.isPeerOpen = false 18 | self._wasPeerOpen = false 19 | 20 | self.peerId = nil 21 | self.remotePeerId = nil 22 | self.isSelfHost = false 23 | 24 | self:_initBackgroundDrawing() 25 | 26 | self._setupScreen = SetupScreen(self) 27 | self._gameplayScreen = GameplayScreen(self) 28 | 29 | self._currentScreen = self._setupScreen 30 | self._setupScreen:show() 31 | end 32 | 33 | function NttGame:_initBackgroundDrawing() 34 | graphics.sprite.setBackgroundDrawingCallback(function(x, y, w, h) 35 | graphics.pushContext() 36 | graphics.setPattern({0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA}) 37 | graphics.fillRect(0, 0, display.getSize()) 38 | graphics.popContext() 39 | end) 40 | end 41 | 42 | function NttGame:update() 43 | NttGame.super.update(self) 44 | 45 | self:_updateBasics() 46 | self:_updateConnectionState() 47 | 48 | self._currentScreen:update() 49 | end 50 | 51 | function NttGame:onConnect() 52 | self.isSerialConnected = true 53 | self:sendCommand(PdPortal.PortalCommand.InitializePeer) 54 | end 55 | 56 | function NttGame:onDisconnect() 57 | self.isSerialConnected = false 58 | self:onPeerClose() 59 | 60 | if self.remotePeerId then 61 | self:onPeerConnClose(self.remotePeerId) 62 | end 63 | end 64 | 65 | function NttGame:onPeerOpen(peerId) 66 | self.peerId = peerId 67 | self.isPeerOpen = true 68 | end 69 | 70 | -- This doesn't matter during gameplay, only matchmaking 71 | function NttGame:onPeerClose() 72 | self.peerId = nil 73 | self.isPeerOpen = false 74 | end 75 | 76 | -- Remote peer connected to us, we are host 77 | function NttGame:onPeerConnection(remotePeerId) 78 | self.isSelfHost = true 79 | self:_onPeerConn(remotePeerId) 80 | end 81 | 82 | -- We connected to remote peer, they are host 83 | function NttGame:onPeerConnOpen(remotePeerId) 84 | self.isSelfHost = false 85 | self:_onPeerConn(remotePeerId) 86 | end 87 | 88 | function NttGame:_onPeerConn(remotePeerId) 89 | if self.remotePeerId ~= nil then 90 | self:log('[NttGame] Only 2 players supported!') 91 | 92 | self:sendCommand( 93 | PdPortal.PortalCommand.ClosePeerConn, 94 | remotePeerId 95 | ) 96 | return 97 | end 98 | 99 | self.remotePeerId = remotePeerId 100 | 101 | self._currentScreen:hide(function () 102 | self._currentScreen = self._gameplayScreen 103 | self._gameplayScreen:show() 104 | end) 105 | end 106 | 107 | function NttGame:onPeerConnClose(remotePeerId) 108 | if remotePeerId ~= self.remotePeerId then 109 | return 110 | end 111 | 112 | self.remotePeerId = nil 113 | 114 | self:_showSetupScreen() 115 | end 116 | 117 | function NttGame:_showSetupScreen() 118 | if self._currentScreen == self._setupScreen then 119 | return 120 | end 121 | 122 | self._currentScreen:hide(function () 123 | self._currentScreen = self._setupScreen 124 | self._setupScreen:show() 125 | end) 126 | end 127 | 128 | function NttGame:_testSwitchScreen() 129 | self._currentScreen:hide(function () 130 | if self._currentScreen == self._setupScreen then 131 | self._currentScreen = self._gameplayScreen 132 | self._gameplayScreen:show() 133 | else 134 | self._currentScreen = self._setupScreen 135 | self._setupScreen:show() 136 | end 137 | end) 138 | end 139 | 140 | function NttGame:onPeerConnData(peerConnId, stringData) 141 | local payload = json.decode(stringData) 142 | 143 | if peerConnId ~= self.remotePeerId then 144 | return 145 | end 146 | 147 | self._gameplayScreen:handlePeerData(payload) 148 | end 149 | 150 | function NttGame:_updateBasics() 151 | -- Required for serial keepalive 152 | timer.updateTimers() 153 | 154 | graphics.sprite.update() 155 | 156 | playdate.drawFPS(screenWidth - 18, screenHeight - 15) 157 | end 158 | 159 | function NttGame:_updateConnectionState() 160 | if self.isSerialConnected and not self._wasSerialConnected then 161 | self._wasSerialConnected = true 162 | -- handle serial connected 163 | elseif not self.isSerialConnected and self._wasSerialConnected then 164 | self._wasSerialConnected = false 165 | -- handle serial disconnected 166 | end 167 | 168 | if self.isPeerOpen and not self._wasPeerOpen then 169 | self._wasPeerOpen = true 170 | -- handle peer opened 171 | elseif not self.isPeerOpen and self._wasPeerOpen then 172 | self._wasPeerOpen = false 173 | -- handle peer closed 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/objects/board/BoardCell.lua: -------------------------------------------------------------------------------- 1 | class('BoardCell').extends() 2 | local BoardCell = BoardCell 3 | 4 | local geometry = playdate.geometry 5 | local graphics = playdate.graphics 6 | local fonts = fonts 7 | 8 | local getIndexPoint = getIndexPoint 9 | 10 | local function makeCharImage(char, graphicsPre, graphicsPost) 11 | local charImage = graphics.image.new(60, 60) 12 | 13 | graphics.pushContext(charImage) 14 | graphics.setFont(fonts.pinzelan48) 15 | 16 | graphicsPre() 17 | 18 | graphics.drawTextAligned(char, 30, 0, kTextAlignment.center) 19 | 20 | if graphicsPost then 21 | graphicsPost(charImage) 22 | end 23 | 24 | graphics.popContext() 25 | 26 | return charImage 27 | end 28 | 29 | local function postFadedCharImage(charImage) 30 | -- https://devforum.play.date/t/drawing-faded-text-more-sustainably/7477/2 31 | graphics.setStencilImage(charImage) 32 | graphics.setPattern({0x0, 0x22, 0x0, 0x88, 0x0, 0x22, 0x0, 0x88}) 33 | graphics.fillRect(0, 0, 60, 60) 34 | end 35 | 36 | -- Prebuild images: empty, x, o, hoverX, hoverO 37 | local emptyImage = makeCharImage(' ', function() end) 38 | local xImage = makeCharImage('x', function() 39 | graphics.setImageDrawMode(graphics.kDrawModeFillBlack) 40 | end) 41 | local oImage = makeCharImage('o', function() 42 | graphics.setImageDrawMode(graphics.kDrawModeFillBlack) 43 | end) 44 | local hoverXImage = makeCharImage('x', function() 45 | graphics.setImageDrawMode(graphics.kDrawModeFillWhite) 46 | end, postFadedCharImage) 47 | local hoverOImage = makeCharImage('o', function() 48 | graphics.setImageDrawMode(graphics.kDrawModeFillWhite) 49 | end, postFadedCharImage) 50 | 51 | local BoardStates = BoardStates 52 | 53 | local stateImages = { 54 | emptyImage, 55 | xImage, 56 | oImage, 57 | hoverXImage, 58 | hoverOImage, 59 | } 60 | 61 | -- 1 empty, 2 x, 3 o, 4 hoverX, 5 hoverO 62 | function BoardCell:init(index) 63 | self._index = index 64 | self.state = BoardStates.Empty 65 | self._point = getIndexPoint(self._index):offsetBy( 66 | math.random(-1, 1), 67 | math.random(-1, 1) 68 | ) 69 | 70 | self._sprite = graphics.sprite.new(self:_getImage()) 71 | self._sprite:moveTo(self._point) 72 | self._sprite:add() 73 | end 74 | 75 | function BoardCell:destroy() 76 | self._sprite:remove() 77 | end 78 | 79 | function BoardCell:setState(newState) 80 | self.state = newState 81 | end 82 | 83 | function BoardCell:_getImage() 84 | return stateImages[self.state] 85 | end 86 | 87 | function BoardCell:update() 88 | local x, y = self._point:unpack() 89 | 90 | self._sprite:setImage(self:_getImage()) 91 | end 92 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/objects/board/BoardLines.lua: -------------------------------------------------------------------------------- 1 | class('BoardLines').extends() 2 | local BoardLines = BoardLines 3 | 4 | local geometry = playdate.geometry 5 | local graphics = playdate.graphics 6 | local screenWidth, screenHeight = playdate.display.getSize() 7 | 8 | local gridLinesVImageTable = graphics.imagetable.new('img/grid-lines-v') 9 | local gridLinesHImageTable = graphics.imagetable.new('img/grid-lines-h') 10 | 11 | -- leftV, rightV, topH, bottomH 12 | local lineAnchors = { 13 | geometry.point.new(158, 10), 14 | geometry.point.new(222, 10), 15 | geometry.point.new(85, 80), 16 | geometry.point.new(85, 140), 17 | } 18 | 19 | function BoardLines:init() 20 | local vSize = gridLinesVImageTable:getSize() 21 | local hSize = gridLinesHImageTable:getSize() 22 | 23 | self._image = graphics.image.new(screenWidth, screenHeight) 24 | self._sprite = graphics.sprite.new(self._image) 25 | self._sprite:setCenter(0, 0) 26 | self._sprite:moveTo(0, 0) 27 | self._sprite:add() 28 | 29 | self._lines = { 30 | { 31 | image = gridLinesVImageTable:getImage( 32 | math.random(1, vSize) 33 | ), 34 | anchor = lineAnchors[1]:offsetBy(math.random(-3, 3), math.random(-3, 3)), 35 | dir = 'v', 36 | }, 37 | { 38 | image = gridLinesVImageTable:getImage( 39 | math.random(1, vSize) 40 | ), 41 | anchor = lineAnchors[2]:offsetBy(math.random(-3, 3), math.random(-3, 3)), 42 | dir = 'v', 43 | }, 44 | { 45 | image = gridLinesHImageTable:getImage( 46 | math.random(1, hSize) 47 | ), 48 | anchor = lineAnchors[3]:offsetBy(math.random(-3, 3), math.random(-3, 3)), 49 | dir = 'h', 50 | }, 51 | { 52 | image = gridLinesHImageTable:getImage( 53 | math.random(1, hSize) 54 | ), 55 | anchor = lineAnchors[4]:offsetBy(math.random(-3, 3), math.random(-3, 3)), 56 | dir = 'h', 57 | }, 58 | } 59 | 60 | self._drawingLinesIn = true 61 | self._currentLineIndex = 1 62 | self._currentLineProgress = 0 63 | end 64 | 65 | function BoardLines:undraw() 66 | self._drawingLinesOut = true 67 | self._outProgress = 0 68 | end 69 | 70 | function BoardLines:update() 71 | if self._drawingLinesIn then 72 | self:_updateLinesIn() 73 | end 74 | 75 | if self._drawingLinesOut then 76 | self:_updateLinesOut() 77 | end 78 | end 79 | 80 | function BoardLines:_updateLinesOut() 81 | self._outProgress += 0.05 82 | local progress = self._outProgress 83 | 84 | graphics.pushContext(self._image) 85 | graphics.setColor(graphics.kColorClear) 86 | graphics.fillRect( 87 | (1 - progress) * screenWidth, 88 | 0, 89 | progress * screenWidth, 90 | screenHeight 91 | ) 92 | graphics.popContext() 93 | 94 | if progress >= 1 then 95 | self._sprite:remove() 96 | 97 | self._drawingLinesOut = false 98 | end 99 | end 100 | 101 | function BoardLines:_updateLinesIn() 102 | self._currentLineProgress += 0.12 103 | 104 | local progress = self._currentLineProgress 105 | local line = self._lines[self._currentLineIndex] 106 | 107 | graphics.pushContext(self._image) 108 | 109 | if line.dir == 'v' then 110 | graphics.setClipRect(0, 0, screenWidth, screenHeight * progress) 111 | else 112 | graphics.setClipRect(0, 0, screenWidth * progress, screenHeight) 113 | end 114 | 115 | line.image:draw(line.anchor) 116 | graphics.popContext() 117 | 118 | if self._currentLineProgress >= 1 then 119 | self._currentLineIndex += 1 120 | self._currentLineProgress = -0.6 121 | 122 | if self._currentLineIndex > #self._lines then 123 | self._drawingLinesIn = false 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/objects/board/BoardState.lua: -------------------------------------------------------------------------------- 1 | class('BoardState').extends() 2 | local BoardState = BoardState 3 | 4 | local BoardStates = BoardStates 5 | 6 | function BoardState:init() 7 | self.grid = {} 8 | for i = 1, 9 do 9 | table.insert(self.grid, BoardCell(i)) 10 | end 11 | end 12 | 13 | function BoardState:destroy() 14 | for i, boardCell in ipairs(self.grid) do 15 | boardCell:destroy() 16 | end 17 | end 18 | 19 | function BoardState:update() 20 | for i, boardCell in ipairs(self.grid) do 21 | boardCell:update() 22 | end 23 | end 24 | 25 | function BoardState:trySetCell(cellIndex, state) 26 | local currentCellState = self.grid[cellIndex].state 27 | 28 | if ( 29 | currentCellState == BoardStates.Empty or 30 | currentCellState == BoardStates.HoverX or 31 | currentCellState == BoardStates.HoverO 32 | ) then 33 | self:setCell(cellIndex, state) 34 | return true 35 | else 36 | return false 37 | end 38 | end 39 | 40 | function BoardState:setCell(cellIndex, state) 41 | self.grid[cellIndex]:setState(state) 42 | end 43 | 44 | function BoardState:unsetCellHover(cellIndex) 45 | local currentCellState = self.grid[cellIndex].state 46 | 47 | -- Hover state, unset 48 | if currentCellState == BoardStates.HoverX or currentCellState == BoardStates.HoverO then 49 | self.grid[cellIndex]:setState(BoardStates.Empty) 50 | return true 51 | end 52 | 53 | return false 54 | end 55 | 56 | function BoardState:getFirstEmptyCellIndex() 57 | for i, cell in ipairs(self.grid) do 58 | if cell.state == BoardStates.Empty then 59 | return i 60 | end 61 | end 62 | 63 | error('No empty cells!') 64 | end 65 | 66 | function BoardState:reset() 67 | for i, cell in ipairs(self.grid) do 68 | cell:setState(BoardStates.Empty) 69 | end 70 | end 71 | 72 | function BoardState:_getCellIndex(row, col) 73 | return ((row - 1) * 3) + col 74 | end 75 | 76 | -- returns true if given state has a win, false if not, -1 if draw 77 | function BoardState:checkWinState(s) 78 | local g = self.grid 79 | 80 | -- Check rows, columns 81 | for i = 1, 3 do 82 | if 83 | ( 84 | g[self:_getCellIndex(i, 1)].state == s and 85 | g[self:_getCellIndex(i, 2)].state == s and 86 | g[self:_getCellIndex(i, 3)].state == s 87 | ) or 88 | ( 89 | g[i].state == s 90 | and g[i + 3].state == s 91 | and g[i + 6].state == s 92 | ) 93 | then 94 | return true 95 | end 96 | end 97 | 98 | -- Check diagonals 99 | if 100 | ( 101 | g[1].state == s and 102 | g[5].state == s and 103 | g[9].state == s 104 | ) or 105 | ( 106 | g[3].state == s and 107 | g[5].state == s and 108 | g[7].state == s 109 | ) 110 | then 111 | return true 112 | end 113 | 114 | local filledCount = 0 115 | for i = 1, 9 do 116 | if g[i].state ~= BoardStates.Empty then 117 | filledCount += 1 118 | end 119 | end 120 | 121 | if filledCount == 9 then 122 | return -1 123 | else 124 | return false 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/objects/screens/GameplayScreen.lua: -------------------------------------------------------------------------------- 1 | class('GameplayScreen').extends() 2 | local GameplayScreen = GameplayScreen 3 | 4 | local graphics = playdate.graphics 5 | local geometry = playdate.geometry 6 | local fonts = fonts 7 | local drawTextAlignedStroked = drawTextAlignedStroked 8 | local imageWithTextStroked = imageWithTextStroked 9 | local screenWidth, screenHeight = playdate.display.getSize() 10 | local timer = playdate.timer 11 | local BoardStates = BoardStates 12 | 13 | local ownHandRestPoint = geometry.point.new( 14 | 40, 15 | screenHeight * 0.7 16 | ) 17 | local otherHandRestPoint = geometry.point.new( 18 | screenWidth - 40, 19 | screenHeight * 0.7 20 | ) 21 | 22 | local MatchEvent = { 23 | Start = 's', 24 | HandMoved = 'm', 25 | Placed = 'p', 26 | } 27 | 28 | local MatchEventHandlerNames = { 29 | [MatchEvent.Start] = '_netHandleMatchStart', 30 | [MatchEvent.HandMoved] = '_netHandleHandMoved', 31 | [MatchEvent.Placed] = '_netHandlePlaced', 32 | } 33 | 34 | local function makeGameOverImage(str) 35 | graphics.pushContext() 36 | graphics.setFont(fonts.pinzelan48) 37 | local img = imageWithTextStroked( 38 | str, 39 | screenWidth, 40 | screenHeight, 41 | 1, 42 | '…', 43 | kTextAlignment.center, 44 | 3 45 | ) 46 | graphics.popContext() 47 | 48 | return img 49 | end 50 | 51 | local winImg = makeGameOverImage(graphics.getLocalizedText('gameplay.win')) 52 | local loseImg = makeGameOverImage(graphics.getLocalizedText('gameplay.lose')) 53 | local drawImg = makeGameOverImage(graphics.getLocalizedText('gameplay.draw')) 54 | 55 | function GameplayScreen:init(nttGame) 56 | self.game = nttGame 57 | self._isShowing = false 58 | self._isX = nil 59 | end 60 | 61 | function GameplayScreen:update() 62 | self._boardLines:update() 63 | self._boardState:update() 64 | 65 | self._ownHand:update() 66 | self._otherHand:update() 67 | 68 | self:_updateMatchOver() 69 | end 70 | 71 | function GameplayScreen:_updateMatchOver() 72 | if not self._isMatchOver or not self._matchOverImg then 73 | return 74 | end 75 | 76 | self._matchOverImg:drawAnchored( 77 | screenWidth * 0.5, 78 | screenHeight * 0.5, 79 | 0.5, 80 | 0.5 81 | ) 82 | end 83 | 84 | function GameplayScreen:show() 85 | self._isShowing = true 86 | self._isOwnTurn = false 87 | self._isMatchOver = false 88 | self._matchOverImg = nil 89 | 90 | self.game:log('[GameplayScreen] show') 91 | self._boardState = BoardState() 92 | self._boardLines = BoardLines() 93 | 94 | self._ownHand = Hand( 95 | self.game.peerId, 96 | graphics.getLocalizedText('gameplay.you') 97 | ) 98 | self._otherHand = Hand(self.game.remotePeerId, self.game.remotePeerId) 99 | 100 | timer.performAfterDelay(300, function() 101 | if not self._isShowing then 102 | return 103 | end 104 | 105 | self._ownHand:setTarget(ownHandRestPoint) 106 | end) 107 | 108 | timer.performAfterDelay(600, function() 109 | if not self._isShowing then 110 | return 111 | end 112 | 113 | self._otherHand:setTarget(otherHandRestPoint) 114 | end) 115 | 116 | if self.game.isSelfHost then 117 | timer.performAfterDelay(1500, function() 118 | if not self._isShowing then 119 | return 120 | end 121 | 122 | self:_restartMatch() 123 | end) 124 | end 125 | 126 | timer.performAfterDelay(2000, function() 127 | if not self._isShowing then 128 | return 129 | end 130 | self:_enableControls() 131 | end) 132 | end 133 | 134 | function GameplayScreen:_sendToPeer(payload) 135 | self.game:sendToPeerConn(self.game.remotePeerId, payload) 136 | end 137 | 138 | function GameplayScreen:handlePeerData(payload) 139 | local handlerName = MatchEventHandlerNames[payload.e] 140 | 141 | if handlerName == nil then 142 | self.game:log('[GameplayScreen] Received unknown command ' .. payload.e) 143 | return 144 | end 145 | 146 | self[handlerName](self, payload) 147 | end 148 | 149 | function GameplayScreen:_setIsX(isX) 150 | self._isX = isX 151 | end 152 | 153 | function GameplayScreen:_enableControls() 154 | if not self._isShowing then 155 | return 156 | end 157 | 158 | playdate.inputHandlers.push({ 159 | upButtonDown = function() 160 | self:_handleUpPressed() 161 | end, 162 | rightButtonDown = function() 163 | self:_handleRightPressed() 164 | end, 165 | downButtonDown = function() 166 | self:_handleDownPressed() 167 | end, 168 | leftButtonDown = function() 169 | self:_handleLeftPressed() 170 | end, 171 | AButtonDown = function() 172 | self:_handleAPressed() 173 | end, 174 | }) 175 | end 176 | 177 | function GameplayScreen:_handleHandChangeIndex(oldIndex, newIndex, whichHand) 178 | if oldIndex then 179 | self._boardState:unsetCellHover(oldIndex) 180 | end 181 | 182 | local ownChanged = whichHand == self._ownHand 183 | if ownChanged then 184 | self._boardState:trySetCell( 185 | newIndex, 186 | self._isX and BoardStates.HoverX or BoardStates.HoverO 187 | ) 188 | 189 | self:_sendToPeer({ 190 | e = MatchEvent.HandMoved, 191 | oldIndex = oldIndex, 192 | newIndex = newIndex 193 | }) 194 | else 195 | self._boardState:trySetCell( 196 | newIndex, 197 | self._isX and BoardStates.HoverO or BoardStates.HoverX 198 | ) 199 | end 200 | end 201 | 202 | function GameplayScreen:_handleUpPressed() 203 | if not self._isOwnTurn then 204 | return 205 | end 206 | 207 | local oldIndex = self._ownHand:getIndex() 208 | local newIndex = self._ownHand:indexMoveUp() 209 | 210 | self:_handleHandChangeIndex(oldIndex, newIndex, self._ownHand) 211 | end 212 | 213 | function GameplayScreen:_handleRightPressed() 214 | if not self._isOwnTurn then 215 | return 216 | end 217 | 218 | local oldIndex = self._ownHand:getIndex() 219 | local newIndex = self._ownHand:indexMoveRight() 220 | 221 | self:_handleHandChangeIndex(oldIndex, newIndex, self._ownHand) 222 | end 223 | 224 | function GameplayScreen:_handleDownPressed() 225 | if not self._isOwnTurn then 226 | return 227 | end 228 | 229 | local oldIndex = self._ownHand:getIndex() 230 | local newIndex = self._ownHand:indexMoveDown() 231 | 232 | self:_handleHandChangeIndex(oldIndex, newIndex, self._ownHand) 233 | end 234 | 235 | function GameplayScreen:_handleLeftPressed() 236 | if not self._isOwnTurn then 237 | return 238 | end 239 | 240 | local oldIndex = self._ownHand:getIndex() 241 | local newIndex = self._ownHand:indexMoveLeft() 242 | 243 | self:_handleHandChangeIndex(oldIndex, newIndex, self._ownHand) 244 | end 245 | 246 | function GameplayScreen:_handleAPressed() 247 | local index = self._ownHand:getIndex() 248 | 249 | if not self._isOwnTurn or index == nil then 250 | return 251 | end 252 | 253 | local ownState = self._isX and BoardStates.X or BoardStates.O 254 | 255 | local didSet = self._boardState:trySetCell(index, ownState) 256 | 257 | if didSet then 258 | self:_sendToPeer({ 259 | e = MatchEvent.Placed, 260 | index = index, 261 | }) 262 | 263 | local didWin = self._boardState:checkWinState(ownState) 264 | 265 | self.game:log( 266 | '[GameplayScreen] didWin???', 267 | json.encode({didWin = didWin}) 268 | ) 269 | 270 | if didWin == true then 271 | self:_handleWin() 272 | elseif didWin == -1 then 273 | self:_handleDraw() 274 | else 275 | self:_handleTurnEnd() 276 | end 277 | end 278 | end 279 | 280 | function GameplayScreen:_handleTurnStart() 281 | self._isOwnTurn = true 282 | 283 | self._otherHand:setTarget(otherHandRestPoint) 284 | 285 | local targetIndex = self._boardState:getFirstEmptyCellIndex() 286 | self._ownHand:setTargetIndex(targetIndex) 287 | self:_handleHandChangeIndex(nil, targetIndex, self._ownHand) 288 | end 289 | 290 | function GameplayScreen:_handleTurnEnd() 291 | self._isOwnTurn = false 292 | self._ownHand:setTarget(ownHandRestPoint) 293 | end 294 | 295 | function GameplayScreen:hide(hideCompleteCallback) 296 | self._isShowing = false 297 | 298 | self.game:log('[GameplayScreen] hide') 299 | 300 | self._boardState:destroy() 301 | self._boardLines:undraw() 302 | self._ownHand:destroy() 303 | self._otherHand:destroy() 304 | 305 | timer.performAfterDelay(900, hideCompleteCallback) 306 | 307 | playdate.inputHandlers.pop() 308 | end 309 | 310 | function GameplayScreen:_resetBoard() 311 | self._boardState:reset() 312 | end 313 | 314 | function GameplayScreen:_handleMatchEnd() 315 | self._isMatchOver = true 316 | self._isOwnTurn = false 317 | self._ownHand:setTarget(ownHandRestPoint) 318 | self._otherHand:setTarget(otherHandRestPoint) 319 | end 320 | 321 | function GameplayScreen:_restartMatch() 322 | self._isMatchOver = false 323 | self._matchOverImg = drawImg 324 | self:_resetBoard() 325 | 326 | -- Decide who starts first (i.e. who is x) 327 | self._isOwnTurn = math.random(1, 2) == 1 328 | 329 | self:_setIsX(self._isOwnTurn) 330 | 331 | self:_sendToPeer({e = MatchEvent.Start, isHostX = self._isOwnTurn}) 332 | 333 | if self._isOwnTurn then 334 | self:_handleTurnStart() 335 | end 336 | end 337 | 338 | function GameplayScreen:_maybeScheduleRestart() 339 | if self.game.isSelfHost then 340 | timer.performAfterDelay(2000, function() 341 | if self._isShowing then 342 | self:_restartMatch() 343 | end 344 | end) 345 | end 346 | end 347 | 348 | function GameplayScreen:_handleWin() 349 | self._matchOverImg = winImg 350 | 351 | self:_handleMatchEnd() 352 | self:_maybeScheduleRestart() 353 | end 354 | 355 | function GameplayScreen:_handleLose() 356 | self._matchOverImg = loseImg 357 | 358 | self:_handleMatchEnd() 359 | self:_maybeScheduleRestart() 360 | end 361 | 362 | function GameplayScreen:_handleDraw() 363 | self._matchOverImg = drawImg 364 | 365 | self:_handleMatchEnd() 366 | self:_maybeScheduleRestart() 367 | end 368 | 369 | function GameplayScreen:_netHandleMatchStart(matchStartData) 370 | self._isMatchOver = false 371 | self._matchOverImg = drawImg 372 | self:_resetBoard() 373 | 374 | local isSelfX = not matchStartData.isHostX 375 | 376 | self:_setIsX(isSelfX) 377 | 378 | if isSelfX then 379 | self:_handleTurnStart() 380 | end 381 | end 382 | 383 | function GameplayScreen:_netHandleHandMoved(handMovedData) 384 | local oldIndex = handMovedData.oldIndex 385 | local newIndex = handMovedData.newIndex 386 | 387 | self._otherHand:setTargetIndex(newIndex) 388 | self:_handleHandChangeIndex(oldIndex, newIndex, self._otherHand) 389 | end 390 | 391 | function GameplayScreen:_netHandlePlaced(placeData) 392 | local otherState = self._isX and BoardStates.O or BoardStates.X 393 | 394 | self._boardState:setCell(placeData.index, otherState) 395 | 396 | local didLose = self._boardState:checkWinState(otherState) 397 | 398 | if didLose == true then 399 | self:_handleLose() 400 | elseif didLose == -1 then 401 | self:_handleDraw() 402 | else 403 | self:_handleTurnStart() 404 | end 405 | end 406 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/objects/screens/SetupScreen.lua: -------------------------------------------------------------------------------- 1 | class('SetupScreen').extends() 2 | local SetupScreen = SetupScreen 3 | 4 | local graphics = playdate.graphics 5 | local fonts = fonts 6 | local fontFamilies = fontFamilies 7 | local drawTextAlignedStroked = drawTextAlignedStroked 8 | local screenWidth, screenHeight = playdate.display.getSize() 9 | local PdPortal = PdPortal 10 | local easings = playdate.easingFunctions 11 | local timer = playdate.timer 12 | 13 | graphics.pushContext() 14 | graphics.setFont(fonts.pinzelan48) 15 | local titleString = graphics.getLocalizedText('setup.title') 16 | local titleImage = imageWithTextStroked( 17 | titleString, 18 | screenWidth, 19 | screenHeight, 20 | 1, 21 | '…', 22 | kTextAlignment.center, 23 | 3 24 | ) 25 | graphics.popContext() 26 | 27 | function SetupScreen:init(nttGame) 28 | self.game = nttGame 29 | end 30 | 31 | function SetupScreen:update() 32 | titleImage:drawAnchored(screenWidth * 0.5, 50 + self._titleTextAnimator:currentValue(), 0.5, 0) 33 | 34 | local setupText = graphics.getLocalizedText('setup.disconnected') 35 | if self.game.isSerialConnected then 36 | if self.game.isPeerOpen then 37 | setupText = table.concat({ 38 | graphics.getLocalizedText('setup.peerOpenPrefix'), 39 | self.game.peerId, 40 | graphics.getLocalizedText('setup.peerOpenSuffix'), 41 | }, '') 42 | else 43 | setupText = graphics.getLocalizedText('setup.serialConnected') 44 | end 45 | end 46 | 47 | if self._lastSetupText ~= setupText then 48 | self._lastSetupText = setupText 49 | 50 | graphics.pushContext() 51 | graphics.setFontFamily(fontFamilies.nico) 52 | self._setupTextImage = imageWithTextStroked( 53 | setupText, 54 | screenWidth * 0.7, 55 | screenHeight, 56 | 8, 57 | '…', 58 | kTextAlignment.center, 59 | 2 60 | ) 61 | graphics.popContext() 62 | end 63 | 64 | self._setupTextImage:drawAnchored(screenWidth * 0.5, 140 + self._setupTextAnimator:currentValue(), 0.5, 0) 65 | end 66 | 67 | function SetupScreen:show() 68 | self._lastSetupText = '' 69 | self.game:log('[SetupScreen] show') 70 | 71 | self._titleTextAnimator = graphics.animator.new( 72 | 700, 73 | -150, 74 | 0, 75 | easings.outBack 76 | ) 77 | self._titleTextAnimator.s = 1.3 78 | 79 | self._setupTextAnimator = graphics.animator.new( 80 | 600, 81 | 150, 82 | 0, 83 | easings.outBack, 84 | 240 85 | ) 86 | self._setupTextAnimator.s = 1.1 87 | 88 | self:_enableControls() 89 | end 90 | 91 | function SetupScreen:_enableControls() 92 | playdate.inputHandlers.push({ 93 | }) 94 | end 95 | 96 | function SetupScreen:hide(hideCompleteCallback) 97 | self.game:log('[SetupScreen] hide') 98 | 99 | self._titleTextAnimator = graphics.animator.new( 100 | 400, 101 | 0, 102 | -150, 103 | easings.inBack, 104 | 150 105 | ) 106 | self._titleTextAnimator.s = 1.3 107 | 108 | self._setupTextAnimator = graphics.animator.new( 109 | 400, 110 | 0, 111 | 150, 112 | easings.inBack 113 | ) 114 | self._setupTextAnimator.s = 1.1 115 | 116 | playdate.inputHandlers.pop() 117 | 118 | timer.performAfterDelay(900, hideCompleteCallback) 119 | end 120 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/util/board.lua: -------------------------------------------------------------------------------- 1 | local geometry = playdate.geometry 2 | 3 | local indexPoints = { 4 | geometry.point.new(134, 56), 5 | geometry.point.new(200, 56), 6 | geometry.point.new(260, 56), 7 | 8 | geometry.point.new(134, 120), 9 | geometry.point.new(200, 120), 10 | geometry.point.new(260, 120), 11 | 12 | geometry.point.new(134, 184), 13 | geometry.point.new(200, 184), 14 | geometry.point.new(260, 184), 15 | } 16 | 17 | function getIndexPoint(index) 18 | return indexPoints[index]:copy() 19 | end 20 | 21 | BoardStates = { 22 | Empty = 1, 23 | X = 2, 24 | O = 3, 25 | HoverX = 4, 26 | HoverO = 5, 27 | } 28 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/lib/util/text.lua: -------------------------------------------------------------------------------- 1 | local graphics = playdate.graphics 2 | 3 | fonts = { 4 | nicoClean16 = graphics.font.new('fonts/nico/nico-clean-16'), 5 | nicoBold16 = graphics.font.new('fonts/nico/nico-bold-16'), 6 | nicoPaint16 = graphics.font.new('fonts/nico/nico-paint-16'), 7 | pinzelan48 = graphics.font.new('fonts/pinzelan/pinzelan-48'), 8 | } 9 | 10 | fontFamilies = { 11 | nico = graphics.font.newFamily({ 12 | [graphics.font.kVariantNormal] = 'fonts/nico/nico-clean-16', 13 | [graphics.font.kVariantBold] = 'fonts/nico/nico-bold-16', 14 | [graphics.font.kVariantItalic] = 'fonts/nico/nico-paint-16', 15 | }) 16 | } 17 | 18 | local function strokeOffsets(radius) 19 | local offsets = {} 20 | for x = -radius, radius do 21 | for y = -radius, radius do 22 | if x * x + y * y <= radius * radius then 23 | table.insert(offsets, { x, y }) 24 | end 25 | end 26 | end 27 | return offsets 28 | end 29 | 30 | function drawTextAlignedStroked(text, x, y, alignment, stroke, strokeColor) 31 | local offsets = strokeOffsets(stroke) 32 | 33 | if strokeColor == nil then 34 | strokeColor = graphics.kDrawModeFillWhite 35 | end 36 | 37 | graphics.pushContext() 38 | graphics.setImageDrawMode(strokeColor) 39 | for _, o in pairs(offsets) do 40 | graphics.drawTextAligned(text, x + o[1], y + o[2], alignment) 41 | end 42 | graphics.popContext() 43 | 44 | graphics.drawTextAligned(text, x, y, alignment) 45 | end 46 | 47 | function imageWithTextStroked( 48 | text, 49 | maxWidth, 50 | maxHeight, 51 | leadingAdjustment, 52 | truncationString, 53 | alignment, 54 | stroke, 55 | strokeColor 56 | ) 57 | if text == nil then 58 | text = 'nil' 59 | end 60 | 61 | if strokeColor == nil then 62 | strokeColor = graphics.kDrawModeFillWhite 63 | end 64 | 65 | local foregroundImage, textWasTruncated = graphics.imageWithText( 66 | text, 67 | maxWidth, 68 | maxHeight, 69 | graphics.kColorClear, 70 | leadingAdjustment, 71 | truncationString, 72 | alignment 73 | ) 74 | 75 | graphics.pushContext() 76 | graphics.setImageDrawMode(strokeColor) 77 | local strokeImage = graphics.imageWithText( 78 | text, 79 | maxWidth, 80 | maxHeight, 81 | graphics.kColorClear, 82 | leadingAdjustment, 83 | truncationString, 84 | alignment 85 | ) 86 | graphics.popContext() 87 | 88 | local outImageWidth, outImageHeight = foregroundImage:getSize() 89 | outImageWidth += stroke * 2 90 | outImageHeight += stroke * 2 91 | local outImage = graphics.image.new(outImageWidth, outImageHeight) 92 | 93 | graphics.pushContext(outImage) 94 | local offsets = strokeOffsets(stroke) 95 | for _, o in pairs(offsets) do 96 | strokeImage:draw(stroke + o[1], stroke + o[2]) 97 | end 98 | 99 | foregroundImage:draw(stroke, stroke) 100 | graphics.popContext() 101 | 102 | 103 | return outImage, textWasTruncated 104 | end 105 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/main.lua: -------------------------------------------------------------------------------- 1 | -- Copied during build, you wouldn't normally have to do that 2 | import "./pdportal" 3 | 4 | import "CoreLibs/animator" 5 | import "CoreLibs/graphics" 6 | import "CoreLibs/object" 7 | import "CoreLibs/sprites" 8 | 9 | import "lib/util/board" 10 | import "lib/util/text" 11 | 12 | import "lib/objects/board/BoardCell" 13 | import "lib/objects/board/BoardLines" 14 | import "lib/objects/board/BoardState" 15 | 16 | import "lib/objects/screens/GameplayScreen" 17 | import "lib/objects/screens/SetupScreen" 18 | 19 | import "lib/objects/Hand" 20 | import "lib/objects/NttGame" 21 | 22 | local PdPortal = PdPortal 23 | 24 | local nttGame = NttGame() 25 | 26 | playdate.display.setRefreshRate(50) 27 | 28 | playdate.update = function() 29 | nttGame:update() 30 | end 31 | -------------------------------------------------------------------------------- /lua/examples/02-net-tac-toe/src/pdxinfo: -------------------------------------------------------------------------------- 1 | name=Net-tac-toe 2 | bundleID=com.strawdynamics.net-tac-toe 3 | version=0.2.0 4 | buildNumber=3 5 | imagePath=img/pdx 6 | author=pdportal contributors 7 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/README.md: -------------------------------------------------------------------------------- 1 | # pdportal-03-panic-sign 2 | 3 | Control the world-famous [Panic sign](https://sign.panic.com/) right from your Playdate! This incredibly convenient and wildly useful example demonstrates pdportal's support for making arbitrary HTTP requests via `fetch`. 4 | 5 | ## Installation 6 | 7 | `./build.sh d` will compile and automatically install the app to your connected Playdate. I'm sorry, the script currently only works on macOS, and even then can be a bit janky. Pull requests welcome :) 8 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/build.sh: -------------------------------------------------------------------------------- 1 | pdxFile="pdportal-03-panic-sign.pdx" 2 | 3 | cp ../../pdportal.lua ./src/pdportal.lua 4 | pdc src "$pdxFile" 5 | 6 | runChoice=$1 7 | 8 | if [ "$runChoice" = "s" ]; then 9 | open "$pdxFile" 10 | elif [ "$runChoice" = "d" ]; then 11 | node ../../../scripts/uploadPdxToPlaydate.js "$pdxFile" 12 | fi 13 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/design/bottom-arrow.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/design/bottom-arrow.aseprite -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/design/card.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/design/card.aseprite -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/design/main-screen.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/design/main-screen.aseprite -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/design/panic-logo.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/design/panic-logo.aseprite -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/design/top-arrow.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/design/top-arrow.aseprite -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/Example03PanicSign.lua: -------------------------------------------------------------------------------- 1 | local graphics = playdate.graphics 2 | 3 | local PdPortal = PdPortal 4 | local PortalCommand = PdPortal.PortalCommand 5 | 6 | class('Example03PanicSign').extends(PdPortal) 7 | local Example03PanicSign = Example03PanicSign 8 | 9 | local screenWidth , screenHeight = playdate.display.getSize() 10 | local screenCenterX = screenWidth * 0.5 11 | local panicSpriteY = math.floor(screenHeight * 0.333) 12 | 13 | local topArrowImage = graphics.image.new("img/top-arrow") 14 | local bottomArrowImage = graphics.image.new("img/bottom-arrow") 15 | 16 | local colors = { 17 | 'red', 18 | 'orange', 19 | 'yellow', 20 | 'green', 21 | 'green2', 22 | 'teal', 23 | 'lightblue', 24 | 'blue', 25 | 'purple', 26 | 'pink' 27 | } 28 | 29 | local colorIndices = {} 30 | for i, v in ipairs(colors) do 31 | colorIndices[v] = i 32 | end 33 | 34 | function Example03PanicSign:init() 35 | Example03PanicSign.super.init(self) 36 | 37 | playdate.display.setRefreshRate(50) 38 | graphics.setBackgroundColor(graphics.kColorBlack) 39 | 40 | local panicImage = graphics.image.new("img/panic-logo") 41 | self.panicSprite = graphics.sprite.new(panicImage) 42 | self.panicSprite:add() 43 | self.panicSprite:moveTo(screenCenterX, panicSpriteY) 44 | 45 | self.currentScreen = '' 46 | self.connected = false 47 | self.isUpdatingSign = false 48 | self.errorMessage = nil 49 | self.topColor = 'lightblue' 50 | self.bottomColor = 'blue' 51 | self.selectedSegment = 'top' 52 | end 53 | 54 | function Example03PanicSign:update() 55 | Example03PanicSign.super.update(self) 56 | 57 | graphics.clear() 58 | 59 | graphics.sprite.update() 60 | 61 | playdate.drawFPS(screenWidth - 15, screenHeight - 12) 62 | 63 | if not self.connected then 64 | self:_maybeEnterScreen('disconnected') 65 | self:_updateDisconnectedScreen() 66 | elseif self.errorMessage then 67 | self:_maybeEnterScreen('error') 68 | self:_updateErrorScreen() 69 | elseif self.isUpdatingSign then 70 | self:_maybeEnterScreen('updating') 71 | self:_updateUpdatingScreen() 72 | else 73 | self:_maybeEnterScreen('picker') 74 | self:_updatePickerScreen() 75 | end 76 | end 77 | 78 | function Example03PanicSign:_maybeEnterScreen(newScreen) 79 | if self.currentScreen ~= newScreen then 80 | self.currentScreen = newScreen 81 | self['_enter' .. newScreen:lower():gsub("^%l", newScreen.upper) .. 'Screen'](self) 82 | end 83 | end 84 | 85 | function Example03PanicSign:_enterDisconnectedScreen() 86 | graphics.pushContext() 87 | graphics.setImageDrawMode(graphics.kDrawModeFillWhite) 88 | self.disconnectedTextImage = graphics.imageWithText( 89 | graphics.getLocalizedText('disconnected.instructions'), 90 | screenWidth * 0.7, -- max width 91 | fonts.nicoClean16:getHeight() * 2 + 4, -- max height 92 | graphics.kColorClear, -- background 93 | 4, -- leading 94 | '…', -- truncation 95 | kTextAlignment.center, -- text align 96 | fonts.nicoClean16 -- font 97 | ) 98 | graphics.popContext() 99 | end 100 | 101 | function Example03PanicSign:_enterErrorScreen() 102 | graphics.pushContext() 103 | graphics.setImageDrawMode(graphics.kDrawModeFillWhite) 104 | self.errorMessageImage = graphics.imageWithText( 105 | 'Error (Ⓐ to continue): ' .. self.errorMessage, 106 | screenWidth * 0.7, -- max width 107 | fonts.nicoClean16:getHeight() * 2 + 4, -- max height 108 | graphics.kColorClear, -- background 109 | 4, -- leading 110 | '…', -- truncation 111 | kTextAlignment.center, -- text align 112 | fonts.nicoClean16 -- font 113 | ) 114 | graphics.popContext() 115 | end 116 | 117 | function Example03PanicSign:_enterUpdatingScreen() 118 | -- 119 | end 120 | 121 | function Example03PanicSign:_enterPickerScreen() 122 | graphics.pushContext() 123 | graphics.setImageDrawMode(graphics.kDrawModeFillWhite) 124 | self.pickerInstructionsImage = graphics.imageWithText( 125 | graphics.getLocalizedText('picker.instructions'), 126 | screenWidth * 0.7, -- max width 127 | fonts.nicoClean16:getHeight() * 2 + 4, -- max height 128 | graphics.kColorClear, -- background 129 | 4, -- leading 130 | '…', -- truncation 131 | kTextAlignment.center, -- text align 132 | fonts.nicoClean16 -- font 133 | ) 134 | graphics.popContext() 135 | end 136 | 137 | local disconnectedTextY = math.floor(screenHeight * 0.78) 138 | 139 | function Example03PanicSign:_updateDisconnectedScreen() 140 | self.disconnectedTextImage:drawAnchored(screenCenterX, disconnectedTextY, 0.5, 0.5) 141 | end 142 | 143 | local errorTextY = math.floor(screenHeight * 0.78) 144 | function Example03PanicSign:_updateErrorScreen() 145 | self.errorMessageImage:drawAnchored(screenCenterX, errorTextY, 0.5, 0.5) 146 | 147 | if playdate.buttonJustPressed(playdate.kButtonA) then 148 | self.errorMessage = nil 149 | end 150 | end 151 | 152 | local updatingTextY = math.floor(screenHeight * 0.78) 153 | function Example03PanicSign:_updateUpdatingScreen() 154 | graphics.pushContext() 155 | graphics.setImageDrawMode(graphics.kDrawModeFillWhite) 156 | graphics.setFont(fonts.nicoPaint16) 157 | graphics.drawTextAligned( 158 | graphics.getLocalizedText('updating.description'), 159 | screenCenterX, 160 | updatingTextY, 161 | kTextAlignment.center 162 | ) 163 | graphics.popContext() 164 | end 165 | 166 | local pickerInstructionsY = math.floor(screenHeight * 0.78) 167 | 168 | local topPickerX = math.floor(screenWidth * 0.18) 169 | local topPickerY = math.floor(screenHeight * 0.18) 170 | 171 | local topArrowX = math.floor(screenWidth * 0.30) 172 | local topArrowY = math.floor(screenHeight * 0.14) 173 | 174 | local bottomPickerX = math.floor(screenWidth * 0.80) 175 | local bottomPickerY = math.floor(screenHeight * 0.43) 176 | 177 | local bottomArrowX = math.floor(screenWidth * 0.68) 178 | local bottomArrowY = math.floor(screenHeight * 0.55) 179 | 180 | function Example03PanicSign:_updatePickerScreen() 181 | self.pickerInstructionsImage:drawAnchored(screenCenterX, pickerInstructionsY, 0.5, 0.5) 182 | 183 | -- Draw swoopy arrows pointing to sign 184 | topArrowImage:drawAnchored(topArrowX, topArrowY, 0.5, 0.5) 185 | bottomArrowImage:drawAnchored(bottomArrowX, bottomArrowY, 0.5, 0.5) 186 | 187 | graphics.pushContext() 188 | graphics.setImageDrawMode(graphics.kDrawModeFillWhite) 189 | graphics.setFont(fonts.nicoPaint16) 190 | -- Draw selected colors 191 | graphics.drawTextAligned( 192 | graphics.getLocalizedText('colors.' .. self.topColor), 193 | topPickerX, 194 | topPickerY, 195 | kTextAlignment.center 196 | ) 197 | graphics.drawTextAligned( 198 | graphics.getLocalizedText('colors.' .. self.bottomColor), 199 | bottomPickerX, 200 | bottomPickerY, 201 | kTextAlignment.center 202 | ) 203 | 204 | -- Draw up/down arrows 205 | graphics.setFont(fonts.nicoClean16) 206 | graphics.drawTextAligned( 207 | '⬆️', 208 | self.selectedSegment == 'top' and topPickerX or bottomPickerX, 209 | (self.selectedSegment == 'top' and topPickerY or bottomPickerY) - 20, 210 | kTextAlignment.center 211 | ) 212 | graphics.drawTextAligned( 213 | '⬇️', 214 | self.selectedSegment == 'top' and topPickerX or bottomPickerX, 215 | (self.selectedSegment == 'top' and topPickerY or bottomPickerY) + 20, 216 | kTextAlignment.center 217 | ) 218 | graphics.popContext() 219 | 220 | if playdate.buttonJustPressed(playdate.kButtonLeft) or playdate.buttonJustPressed(playdate.kButtonRight) then 221 | self.selectedSegment = self.selectedSegment == 'top' and 'bottom' or 'top' 222 | end 223 | if playdate.buttonJustPressed(playdate.kButtonUp) then 224 | self:_changeColor('up') 225 | elseif playdate.buttonJustPressed(playdate.kButtonDown) then 226 | self:_changeColor('down') 227 | end 228 | 229 | if playdate.buttonJustPressed(playdate.kButtonA) then 230 | self:_updateSign() 231 | end 232 | end 233 | 234 | function Example03PanicSign:_changeColor(direction) 235 | local currentColor = self.selectedSegment == 'top' and self.topColor or self.bottomColor 236 | local currentIndex = colorIndices[currentColor] 237 | local newIndex = direction == 'up' and currentIndex + 1 or currentIndex - 1 238 | 239 | if newIndex < 1 then 240 | newIndex = #colors 241 | elseif newIndex > #colors then 242 | newIndex = 1 243 | end 244 | 245 | if self.selectedSegment == 'top' then 246 | self.topColor = colors[newIndex] 247 | else 248 | self.bottomColor = colors[newIndex] 249 | end 250 | end 251 | 252 | function Example03PanicSign:_updateSign() 253 | self.isUpdatingSign = true 254 | 255 | local url = 'https://signserver.panic.com/set/' .. self.topColor .. '/' .. self.bottomColor 256 | 257 | self:fetch( 258 | url, 259 | {}, 260 | function (responseText, responseDetails) 261 | self.isUpdatingSign = false 262 | 263 | if responseText ~= 'Done.\n' then 264 | self.errorMessage = responseText 265 | end 266 | end, 267 | function (errorDetails) 268 | self.isUpdatingSign = false 269 | self.errorMessage = errorDetails.message 270 | end 271 | ) 272 | end 273 | 274 | function Example03PanicSign:onConnect(portalVersion) 275 | self.connected = true 276 | end 277 | 278 | function Example03PanicSign:onDisconnect() 279 | self.connected = false 280 | end 281 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/en.strings: -------------------------------------------------------------------------------- 1 | "disconnected.instructions" = "Visit https://pdportal.net on your computer to get started!" 2 | 3 | "picker.instructions" = "⬅️ ➡️ Pick Segment\nⒶ Change the Sign!" 4 | "picker.upArrow" = "⬆️" 5 | "picker.downArrow" = "⬇️" 6 | 7 | "colors.red" = "Red" 8 | "colors.orange" = "Orange" 9 | "colors.yellow" = "Yellow" 10 | "colors.green" = "Lime" 11 | "colors.green2" = "Green" 12 | "colors.teal" = "Teal" 13 | "colors.lightblue" = "Light blue" 14 | "colors.blue" = "Blue" 15 | "colors.purple" = "Purple" 16 | "colors.pink" = "Pink" 17 | 18 | "updating.description" = "Updating sign…" 19 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/fonts/nico/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Emily Huo (mlyhuo@gmail.com, emuhuo.itch.io, emhuo.com). 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/fonts/nico/nico-bold-16-table-16-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/src/fonts/nico/nico-bold-16-table-16-16.png -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/fonts/nico/nico-bold-16.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":12,"xHeight":-1,"capHeight":-1,"descent":-1} 2 | 3 | tracking=0 4 | 5 | space 5 6 | ! 5 7 | " 8 8 | # 14 9 | $ 10 10 | % 15 11 | & 13 12 | ' 4 13 | ( 6 14 | ) 6 15 | * 8 16 | + 7 17 | , 5 18 | - 8 19 | . 5 20 | / 10 21 | 0 11 22 | 1 9 23 | 2 10 24 | 3 10 25 | 4 11 26 | 5 10 27 | 6 10 28 | 7 10 29 | 8 10 30 | 9 11 31 | : 5 32 | ; 6 33 | < 10 34 | = 9 35 | > 11 36 | ? 10 37 | @ 14 38 | A 13 39 | B 11 40 | C 12 41 | D 13 42 | E 10 43 | F 10 44 | G 14 45 | H 12 46 | I 5 47 | J 9 48 | K 13 49 | L 9 50 | M 15 51 | N 14 52 | O 14 53 | P 11 54 | Q 14 55 | R 12 56 | S 10 57 | T 10 58 | U 12 59 | V 13 60 | W 15 61 | X 12 62 | Y 12 63 | Z 11 64 | [ 6 65 | \ 9 66 | ] 5 67 | ^ 13 68 | _ 10 69 | ` 4 70 | a 11 71 | b 11 72 | c 9 73 | d 11 74 | e 10 75 | f 7 76 | g 11 77 | h 10 78 | i 5 79 | j 5 80 | k 11 81 | l 5 82 | m 15 83 | n 10 84 | o 10 85 | p 11 86 | q 11 87 | r 8 88 | s 8 89 | t 8 90 | u 10 91 | v 10 92 | w 15 93 | x 10 94 | y 11 95 | z 9 96 | { 6 97 | | 4 98 | } 7 99 | ~ 10 100 |   5 101 | ¡ 5 102 | ¢ 9 103 | £ 11 104 | ¤ 10 105 | ¥ 12 106 | ¦ 5 107 | § 8 108 | ¨ 8 109 | © 13 110 | ª 7 111 | « 9 112 | ¬ 13 113 | ® 14 114 | ¯ 6 115 | ° 7 116 | ± 10 117 | ² 6 118 | ³ 7 119 | ´ 5 120 | µ 11 121 | ¶ 10 122 | · 5 123 | ¸ 5 124 | ¹ 6 125 | º 7 126 | » 10 127 | ¼ 15 128 | ½ 15 129 | ¾ 15 130 | ¿ 10 131 | À 11 132 | Á 11 133 |  11 134 | à 11 135 | Ä 11 136 | Å 13 137 | Æ 15 138 | Ç 12 139 | È 10 140 | É 10 141 | Ê 10 142 | Ë 10 143 | Ì 5 144 | Í 5 145 | Î 5 146 | Ï 5 147 | Ð 13 148 | Ñ 14 149 | Ò 14 150 | Ó 14 151 | Ô 14 152 | Õ 14 153 | Ö 14 154 | × 9 155 | Ø 14 156 | Ù 12 157 | Ú 12 158 | Û 12 159 | Ü 12 160 | Ý 12 161 | Þ 11 162 | ß 11 163 | à 11 164 | á 11 165 | â 11 166 | ã 11 167 | ä 11 168 | å 11 169 | æ 15 170 | ç 9 171 | è 11 172 | é 11 173 | ê 11 174 | ë 11 175 | ì 5 176 | í 6 177 | î 6 178 | ï 7 179 | ð 11 180 | ñ 10 181 | ò 11 182 | ó 11 183 | ô 11 184 | õ 11 185 | ö 11 186 | ÷ 9 187 | ø 11 188 | ù 10 189 | ú 10 190 | û 10 191 | ü 10 192 | ý 10 193 | þ 11 194 | ÿ 10 195 | Ā 13 196 | ā 11 197 | Ă 13 198 | ă 11 199 | Ą 13 200 | ą 11 201 | Ć 12 202 | ć 9 203 | Ċ 12 204 | ċ 9 205 | Č 12 206 | č 9 207 | Ď 13 208 | ď 15 209 | Đ 13 210 | đ 12 211 | Ē 10 212 | ē 11 213 | Ė 10 214 | ė 11 215 | Ę 10 216 | ę 11 217 | Ě 10 218 | ě 11 219 | Ğ 14 220 | ğ 11 221 | Ġ 14 222 | ġ 11 223 | Ģ 14 224 | ģ 11 225 | Ħ 13 226 | ħ 10 227 | Ĩ 5 228 | ĩ 6 229 | Ī 5 230 | ī 6 231 | Į 5 232 | į 5 233 | İ 5 234 | ı 5 235 | Ķ 13 236 | ķ 11 237 | Ĺ 9 238 | ĺ 5 239 | Ļ 9 240 | ļ 5 241 | Ľ 9 242 | ľ 9 243 | Ł 9 244 | ł 6 245 | Ń 14 246 | ń 10 247 | Ņ 14 248 | ņ 10 249 | Ň 14 250 | ň 10 251 | Ŋ 14 252 | ŋ 10 253 | Ō 14 254 | ō 11 255 | Ő 14 256 | ő 11 257 | Œ 15 258 | œ 15 259 | Ŕ 12 260 | ŕ 7 261 | Ŗ 12 262 | ŗ 7 263 | Ř 12 264 | ř 7 265 | Ś 10 266 | ś 8 267 | Ş 10 268 | ş 8 269 | Š 10 270 | š 8 271 | Ţ 10 272 | ţ 7 273 | Ť 10 274 | ť 9 275 | Ŧ 10 276 | ŧ 7 277 | Ũ 12 278 | ũ 10 279 | Ū 12 280 | ū 10 281 | Ů 12 282 | ů 10 283 | Ű 12 284 | ű 10 285 | Ų 12 286 | ų 10 287 | Ŵ 15 288 | ŵ 15 289 | Ŷ 12 290 | ŷ 10 291 | Ÿ 12 292 | Ź 11 293 | ź 9 294 | Ż 11 295 | ż 9 296 | Ž 11 297 | ž 9 298 | ƒ 9 299 | Ơ 15 300 | ơ 12 301 | Ư 14 302 | ư 12 303 | Ǎ 13 304 | ǎ 11 305 | Ǐ 5 306 | ǐ 6 307 | Ǒ 14 308 | ǒ 11 309 | Ǔ 12 310 | ǔ 10 311 | Ǖ 12 312 | ǖ 10 313 | Ǘ 12 314 | ǘ 10 315 | Ǚ 12 316 | ǚ 10 317 | Ǜ 12 318 | ǜ 10 319 | Ș 10 320 | ș 8 321 | Ț 10 322 | ț 7 323 | ȷ 5 324 | ˆ 7 325 | ˇ 7 326 | ˘ 7 327 | ˙ 4 328 | ˚ 5 329 | ˛ 4 330 | ˜ 7 331 | ˝ 8 332 | ̃ 1 333 | ̄ 1 334 | ̆ 1 335 | ̇ 1 336 | ̈ 1 337 | ̉ 1 338 | ̊ 1 339 | ̋ 1 340 | ̌ 1 341 | ̒ 1 342 | ̛ 1 343 | ̣ 1 344 | ̦ 1 345 | ̧ 1 346 | ̨ 1 347 | ̵ 1 348 | ̶ 1 349 | ̷ 1 350 | ̸ 1 351 | Δ 14 352 | Ω 14 353 | μ 10 354 | π 11 355 | Ẁ 15 356 | ẁ 15 357 | Ẃ 15 358 | ẃ 15 359 | Ẅ 15 360 | ẅ 15 361 | Ạ 13 362 | ạ 11 363 | Ả 13 364 | ả 11 365 | Ấ 13 366 | ấ 11 367 | Ầ 13 368 | ầ 11 369 | Ẩ 13 370 | ẩ 11 371 | Ẫ 13 372 | ẫ 11 373 | Ậ 13 374 | ậ 11 375 | Ắ 13 376 | ắ 11 377 | Ằ 13 378 | ằ 11 379 | Ẳ 13 380 | ẳ 11 381 | Ẵ 13 382 | ẵ 11 383 | Ặ 13 384 | ặ 11 385 | Ẹ 10 386 | ẹ 11 387 | Ẻ 10 388 | ẻ 11 389 | Ẽ 10 390 | ẽ 11 391 | Ế 10 392 | ế 11 393 | Ề 10 394 | ề 11 395 | Ể 10 396 | ể 11 397 | Ễ 10 398 | ễ 11 399 | Ệ 10 400 | ệ 11 401 | Ỉ 5 402 | ỉ 5 403 | Ị 5 404 | ị 5 405 | Ọ 14 406 | ọ 11 407 | Ỏ 14 408 | ỏ 11 409 | Ố 14 410 | ố 11 411 | Ồ 14 412 | ồ 11 413 | Ổ 14 414 | ổ 11 415 | Ỗ 14 416 | ỗ 11 417 | Ộ 14 418 | ộ 11 419 | Ớ 15 420 | ớ 12 421 | Ờ 15 422 | ờ 12 423 | Ở 15 424 | ở 12 425 | Ỡ 15 426 | ỡ 12 427 | Ợ 15 428 | ợ 12 429 | Ụ 12 430 | ụ 10 431 | Ủ 12 432 | ủ 10 433 | Ứ 14 434 | ứ 12 435 | Ừ 14 436 | ừ 12 437 | Ử 14 438 | ử 12 439 | Ữ 14 440 | ữ 12 441 | Ự 14 442 | ự 12 443 | Ỳ 12 444 | ỳ 10 445 | Ỵ 12 446 | ỵ 10 447 | Ỷ 12 448 | ỷ 10 449 | Ỹ 12 450 | ỹ 10 451 | – 10 452 | — 15 453 | ‘ 6 454 | ’ 6 455 | ‚ 5 456 | “ 8 457 | ” 8 458 | „ 9 459 | † 9 460 | ‡ 9 461 | • 9 462 | … 15 463 | ‰ 15 464 | ‹ 6 465 | › 6 466 | ⁄ 7 467 | ⁰ 7 468 | ⁴ 7 469 | ⁵ 7 470 | ⁶ 7 471 | ⁷ 6 472 | ⁸ 7 473 | ⁹ 7 474 | ₀ 7 475 | ₁ 6 476 | ₂ 6 477 | ₃ 7 478 | ₄ 7 479 | ₅ 7 480 | ₆ 7 481 | ₇ 7 482 | ₈ 7 483 | ₉ 7 484 | € 12 485 | № 14 486 | ™ 12 487 | ⅛ 15 488 | ⅜ 15 489 | ⅝ 15 490 | ⅞ 15 491 | ∂ 10 492 | ∅ 11 493 | ∏ 14 494 | ∑ 11 495 | − 10 496 | √ 12 497 | ∞ 13 498 | ∫ 8 499 | ≈ 11 500 | ≠ 10 501 | ≤ 11 502 | ≥ 11 503 | ◊ 10 504 |   16 505 |  13 506 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/fonts/nico/nico-clean-16-table-16-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/src/fonts/nico/nico-clean-16-table-16-16.png -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/fonts/nico/nico-clean-16.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":12,"xHeight":-1,"capHeight":-1,"descent":-1} 2 | 3 | tracking=0 4 | 5 | space 5 6 | ! 5 7 | " 7 8 | # 15 9 | $ 11 10 | % 15 11 | & 12 12 | ' 4 13 | ( 6 14 | ) 6 15 | * 8 16 | + 10 17 | , 5 18 | - 9 19 | . 5 20 | / 8 21 | 0 11 22 | 1 6 23 | 2 10 24 | 3 10 25 | 4 11 26 | 5 10 27 | 6 10 28 | 7 9 29 | 8 10 30 | 9 10 31 | : 5 32 | ; 5 33 | < 10 34 | = 10 35 | > 10 36 | ? 9 37 | @ 15 38 | A 12 39 | B 11 40 | C 13 41 | D 12 42 | E 10 43 | F 10 44 | G 13 45 | H 11 46 | I 4 47 | J 6 48 | K 12 49 | L 9 50 | M 14 51 | N 12 52 | O 14 53 | P 11 54 | Q 14 55 | R 12 56 | S 11 57 | T 10 58 | U 11 59 | V 12 60 | W 15 61 | X 12 62 | Y 11 63 | Z 10 64 | [ 7 65 | \ 9 66 | ] 6 67 | ^ 10 68 | _ 10 69 | ` 6 70 | a 10 71 | b 11 72 | c 10 73 | d 11 74 | e 10 75 | f 7 76 | g 10 77 | h 10 78 | i 4 79 | j 4 80 | k 10 81 | l 4 82 | m 14 83 | n 10 84 | o 11 85 | p 11 86 | q 11 87 | r 7 88 | s 8 89 | t 7 90 | u 10 91 | v 10 92 | w 15 93 | x 10 94 | y 11 95 | z 9 96 | { 6 97 | | 5 98 | } 7 99 | ~ 10 100 |   5 101 | ¡ 5 102 | ¢ 10 103 | £ 11 104 | ¤ 10 105 | ¥ 11 106 | ¦ 5 107 | § 9 108 | ¨ 8 109 | © 15 110 | ª 7 111 | « 11 112 | ¬ 11 113 | ® 11 114 | ¯ 7 115 | ° 8 116 | ± 10 117 | ² 7 118 | ³ 7 119 | ´ 6 120 | µ 10 121 | ¶ 8 122 | · 5 123 | ¸ 7 124 | ¹ 5 125 | º 8 126 | » 11 127 | ¼ 14 128 | ½ 14 129 | ¾ 15 130 | ¿ 9 131 | À 12 132 | Á 12 133 |  12 134 | à 12 135 | Ä 12 136 | Å 12 137 | Æ 15 138 | Ç 13 139 | È 10 140 | É 10 141 | Ê 10 142 | Ë 10 143 | Ì 4 144 | Í 4 145 | Î 4 146 | Ï 4 147 | Ð 12 148 | Ñ 12 149 | Ò 14 150 | Ó 14 151 | Ô 14 152 | Õ 14 153 | Ö 14 154 | × 9 155 | Ø 14 156 | Ù 11 157 | Ú 11 158 | Û 11 159 | Ü 11 160 | Ý 11 161 | Þ 11 162 | ß 11 163 | à 9 164 | á 9 165 | â 9 166 | ã 9 167 | ä 9 168 | å 9 169 | æ 15 170 | ç 10 171 | è 10 172 | é 10 173 | ê 10 174 | ë 10 175 | ì 4 176 | í 5 177 | î 5 178 | ï 6 179 | ð 11 180 | ñ 10 181 | ò 11 182 | ó 11 183 | ô 11 184 | õ 11 185 | ö 11 186 | ÷ 10 187 | ø 11 188 | ù 10 189 | ú 10 190 | û 10 191 | ü 10 192 | ý 10 193 | þ 11 194 | ÿ 10 195 | Ā 12 196 | ā 9 197 | Ă 12 198 | ă 9 199 | Ą 12 200 | ą 10 201 | Ć 13 202 | ć 10 203 | Ĉ 13 204 | ĉ 10 205 | Ċ 13 206 | ċ 10 207 | Č 13 208 | č 10 209 | Ď 12 210 | ď 14 211 | Đ 12 212 | đ 12 213 | Ē 10 214 | ē 10 215 | Ĕ 10 216 | ĕ 10 217 | Ė 10 218 | ė 10 219 | Ę 10 220 | ę 10 221 | Ě 10 222 | ě 10 223 | Ĝ 13 224 | ĝ 10 225 | Ğ 13 226 | ğ 10 227 | Ġ 13 228 | ġ 10 229 | Ģ 13 230 | ģ 10 231 | Ĥ 12 232 | ĥ 10 233 | Ħ 13 234 | ħ 10 235 | Ĩ 4 236 | ĩ 5 237 | Ī 4 238 | ī 5 239 | Ĭ 4 240 | ĭ 5 241 | Į 5 242 | į 4 243 | İ 4 244 | ı 4 245 | Ĵ 6 246 | ĵ 6 247 | Ķ 11 248 | ķ 10 249 | ĸ 10 250 | Ĺ 9 251 | ĺ 4 252 | Ļ 9 253 | ļ 4 254 | Ľ 9 255 | ľ 7 256 | Ŀ 9 257 | ŀ 7 258 | Ł 9 259 | ł 5 260 | Ń 12 261 | ń 10 262 | Ņ 12 263 | ņ 10 264 | Ň 12 265 | ň 10 266 | Ŋ 12 267 | ŋ 10 268 | Ō 14 269 | ō 11 270 | Ŏ 14 271 | ŏ 11 272 | Ő 14 273 | ő 11 274 | Œ 15 275 | œ 15 276 | Ŕ 11 277 | ŕ 7 278 | Ŗ 11 279 | ŗ 7 280 | Ř 11 281 | ř 7 282 | Ś 11 283 | ś 9 284 | Ŝ 11 285 | ŝ 9 286 | Ş 11 287 | ş 9 288 | Š 11 289 | š 9 290 | Ţ 10 291 | ţ 8 292 | Ť 10 293 | ť 8 294 | Ŧ 10 295 | ŧ 7 296 | Ũ 11 297 | ũ 10 298 | Ū 11 299 | ū 10 300 | Ŭ 11 301 | ŭ 10 302 | Ů 11 303 | ů 10 304 | Ű 11 305 | ű 10 306 | Ų 11 307 | ų 10 308 | Ŵ 15 309 | ŵ 15 310 | Ŷ 11 311 | ŷ 10 312 | Ÿ 11 313 | Ź 10 314 | ź 9 315 | Ż 10 316 | ż 9 317 | Ž 10 318 | ž 9 319 | Ə 13 320 | ƒ 9 321 | Ơ 14 322 | ơ 11 323 | Ư 14 324 | ư 11 325 | DŽ 15 326 | Dž 15 327 | dž 15 328 | LJ 15 329 | Lj 13 330 | lj 8 331 | NJ 15 332 | Nj 15 333 | nj 14 334 | Ǧ 13 335 | ǧ 10 336 | Ǫ 14 337 | ǫ 11 338 | Ǻ 12 339 | ǻ 9 340 | Ǽ 15 341 | ǽ 15 342 | Ǿ 14 343 | ǿ 11 344 | Ȁ 12 345 | ȁ 9 346 | Ȃ 12 347 | ȃ 9 348 | Ȅ 10 349 | ȅ 10 350 | Ȇ 10 351 | ȇ 10 352 | Ȉ 4 353 | ȉ 5 354 | Ȋ 4 355 | ȋ 5 356 | Ȍ 14 357 | ȍ 11 358 | Ȏ 14 359 | ȏ 11 360 | Ȑ 11 361 | ȑ 7 362 | Ȓ 11 363 | ȓ 7 364 | Ȕ 11 365 | ȕ 10 366 | Ȗ 11 367 | ȗ 10 368 | Ș 11 369 | ș 9 370 | Ț 10 371 | ț 7 372 | Ȫ 14 373 | ȫ 11 374 | Ȭ 14 375 | ȭ 11 376 | Ȱ 14 377 | ȱ 11 378 | Ȳ 11 379 | ȳ 10 380 | ȷ 4 381 | ə 10 382 | ʻ 5 383 | ʼ 5 384 | ʾ 5 385 | ʿ 5 386 | ˆ 8 387 | ˇ 8 388 | ˈ 5 389 | ˉ 7 390 | ˊ 6 391 | ˋ 6 392 | ˌ 5 393 | ˘ 8 394 | ˙ 5 395 | ˚ 7 396 | ˛ 7 397 | ˜ 8 398 | ˝ 9 399 | Δ 11 400 | Ω 14 401 | π 12 402 | ฿ 11 403 | Ḉ 13 404 | ḉ 10 405 | Ḍ 12 406 | ḍ 11 407 | Ḏ 12 408 | ḏ 11 409 | Ḕ 10 410 | ḕ 10 411 | Ḗ 10 412 | ḗ 10 413 | Ḝ 10 414 | ḝ 10 415 | Ḡ 13 416 | ḡ 10 417 | Ḥ 12 418 | ḥ 10 419 | Ḫ 12 420 | ḫ 10 421 | Ḯ 4 422 | ḯ 6 423 | Ḷ 9 424 | ḷ 4 425 | Ḻ 9 426 | ḻ 5 427 | Ṃ 14 428 | ṃ 15 429 | Ṅ 12 430 | ṅ 10 431 | Ṇ 12 432 | ṇ 10 433 | Ṉ 12 434 | ṉ 10 435 | Ṍ 14 436 | ṍ 11 437 | Ṏ 14 438 | ṏ 11 439 | Ṑ 14 440 | ṑ 11 441 | Ṓ 14 442 | ṓ 11 443 | Ṛ 11 444 | ṛ 7 445 | Ṟ 11 446 | ṟ 7 447 | Ṡ 11 448 | ṡ 9 449 | Ṣ 11 450 | ṣ 9 451 | Ṥ 11 452 | ṥ 9 453 | Ṧ 11 454 | ṧ 9 455 | Ṩ 11 456 | ṩ 9 457 | Ṭ 10 458 | ṭ 7 459 | Ṯ 10 460 | ṯ 8 461 | Ṹ 11 462 | ṹ 10 463 | Ṻ 11 464 | ṻ 10 465 | Ẁ 15 466 | ẁ 15 467 | Ẃ 15 468 | ẃ 15 469 | Ẅ 15 470 | ẅ 15 471 | Ẏ 11 472 | ẏ 10 473 | Ẓ 10 474 | ẓ 9 475 | ẗ 7 476 | ẞ 15 477 | Ạ 12 478 | ạ 9 479 | Ả 12 480 | ả 9 481 | Ấ 12 482 | ấ 9 483 | Ầ 12 484 | ầ 9 485 | Ẩ 12 486 | ẩ 9 487 | Ẫ 12 488 | ẫ 9 489 | Ậ 12 490 | ậ 9 491 | Ắ 12 492 | ắ 9 493 | Ằ 12 494 | ằ 9 495 | Ẳ 12 496 | ẳ 9 497 | Ẵ 12 498 | ẵ 9 499 | Ặ 12 500 | ặ 9 501 | Ẹ 10 502 | ẹ 10 503 | Ẻ 10 504 | ẻ 10 505 | Ẽ 10 506 | ẽ 10 507 | Ế 10 508 | ế 10 509 | Ề 10 510 | ề 10 511 | Ể 10 512 | ể 10 513 | Ễ 10 514 | ễ 10 515 | Ệ 10 516 | ệ 10 517 | Ỉ 4 518 | ỉ 4 519 | Ị 4 520 | ị 4 521 | Ọ 14 522 | ọ 11 523 | Ỏ 14 524 | ỏ 11 525 | Ố 14 526 | ố 11 527 | Ồ 14 528 | ồ 11 529 | Ổ 14 530 | ổ 11 531 | Ỗ 14 532 | ỗ 11 533 | Ộ 14 534 | ộ 11 535 | Ớ 14 536 | ớ 11 537 | Ờ 14 538 | ờ 11 539 | Ở 14 540 | ở 11 541 | Ỡ 14 542 | ỡ 11 543 | Ợ 14 544 | ợ 11 545 | Ụ 11 546 | ụ 10 547 | Ủ 11 548 | ủ 10 549 | Ứ 14 550 | ứ 11 551 | Ừ 14 552 | ừ 11 553 | Ử 14 554 | ử 11 555 | Ữ 14 556 | ữ 11 557 | Ự 14 558 | ự 11 559 | Ỳ 11 560 | ỳ 10 561 | Ỵ 11 562 | ỵ 11 563 | Ỷ 11 564 | ỷ 10 565 | Ỹ 11 566 | ỹ 10 567 | ‐ 10 568 | ‒ 10 569 | – 10 570 | — 15 571 | ― 15 572 | ‘ 5 573 | ’ 5 574 | ‚ 5 575 | “ 8 576 | ” 8 577 | „ 8 578 | † 11 579 | ‡ 11 580 | • 8 581 | … 12 582 | ‰ 15 583 | ′ 5 584 | ″ 8 585 | ‹ 6 586 | › 6 587 | ⁄ 7 588 | ⁰ 8 589 | ⁴ 8 590 | ⁵ 7 591 | ⁶ 7 592 | ⁷ 7 593 | ⁸ 8 594 | ⁹ 7 595 | ⁺ 8 596 | ⁻ 8 597 | ⁼ 8 598 | ⁽ 5 599 | ⁾ 5 600 | ₀ 8 601 | ₁ 5 602 | ₂ 7 603 | ₃ 7 604 | ₄ 8 605 | ₅ 7 606 | ₆ 7 607 | ₇ 7 608 | ₈ 8 609 | ₉ 7 610 | ₊ 8 611 | ₋ 8 612 | ₌ 8 613 | ₍ 5 614 | ₎ 5 615 | ₡ 13 616 | ₣ 11 617 | ₤ 11 618 | ₦ 13 619 | ₧ 12 620 | ₨ 15 621 | ₩ 15 622 | ₪ 10 623 | ₫ 10 624 | € 12 625 | ₭ 12 626 | ₮ 10 627 | ₱ 12 628 | ₲ 13 629 | ₳ 12 630 | ₴ 11 631 | ₵ 13 632 | ₸ 10 633 | ₹ 12 634 | ₺ 11 635 | ₼ 11 636 | ₽ 11 637 | ₾ 14 638 | ₿ 11 639 | ℓ 11 640 | № 15 641 | ™ 15 642 | Ω 14 643 | ℮ 14 644 | ⅓ 14 645 | ⅔ 15 646 | ⅛ 14 647 | ⅜ 15 648 | ⅝ 15 649 | ⅞ 15 650 | ← 11 651 | ↑ 11 652 | → 12 653 | ↓ 12 654 | ↔ 15 655 | ↕ 11 656 | ↖ 10 657 | ↗ 11 658 | ↘ 10 659 | ↙ 11 660 | ↩ 15 661 | ↪ 14 662 | ↰ 14 663 | ↱ 13 664 | ↲ 14 665 | ↳ 14 666 | ∂ 11 667 | ∅ 14 668 | ∆ 11 669 | ∏ 14 670 | ∑ 11 671 | − 10 672 | ∕ 9 673 | ∙ 8 674 | √ 15 675 | ∞ 14 676 | ∫ 9 677 | ≈ 11 678 | ≠ 10 679 | ≤ 10 680 | ≥ 10 681 | Ⓐ 12 682 | Ⓑ 12 683 | ■ 15 684 | □ 15 685 | ▲ 14 686 | △ 14 687 | ▶ 15 688 | ▷ 15 689 | ▼ 13 690 | ▽ 13 691 | ◀ 14 692 | ◁ 13 693 | ◆ 14 694 | ◇ 14 695 | ◊ 10 696 | ○ 15 697 | ● 15 698 | ★ 14 699 | ☆ 14 700 | ✛ 12 701 | ➡ 12 702 | ⬅ 12 703 | ⬆ 12 704 | ⬇ 12 705 | ⬎ 14 706 | ⬐ 14 707 |   16 708 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/fonts/nico/nico-paint-16-table-16-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/src/fonts/nico/nico-paint-16-table-16-16.png -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/fonts/nico/nico-paint-16.fnt: -------------------------------------------------------------------------------- 1 | --metrics={"baseline":12,"xHeight":-1,"capHeight":-1,"descent":-1} 2 | 3 | tracking=0 4 | 5 | space 5 6 | ! 5 7 | " 8 8 | # 14 9 | $ 9 10 | % 14 11 | & 13 12 | ' 5 13 | ( 6 14 | ) 7 15 | * 8 16 | + 10 17 | , 5 18 | - 8 19 | . 5 20 | / 9 21 | 0 11 22 | 1 7 23 | 2 11 24 | 3 10 25 | 4 10 26 | 5 9 27 | 6 10 28 | 7 10 29 | 8 10 30 | 9 10 31 | : 5 32 | ; 5 33 | < 11 34 | = 8 35 | > 11 36 | ? 9 37 | @ 15 38 | A 13 39 | B 11 40 | C 13 41 | D 12 42 | E 11 43 | F 11 44 | G 12 45 | H 12 46 | I 5 47 | J 9 48 | K 11 49 | L 10 50 | M 14 51 | N 12 52 | O 13 53 | P 11 54 | Q 14 55 | R 11 56 | S 10 57 | T 12 58 | U 12 59 | V 14 60 | W 15 61 | X 13 62 | Y 12 63 | Z 11 64 | [ 6 65 | \ 9 66 | ] 5 67 | ^ 9 68 | _ 10 69 | ` 7 70 | a 10 71 | b 10 72 | c 9 73 | d 10 74 | e 10 75 | f 9 76 | g 10 77 | h 10 78 | i 5 79 | j 5 80 | k 9 81 | l 6 82 | m 14 83 | n 10 84 | o 10 85 | p 10 86 | q 10 87 | r 9 88 | s 9 89 | t 9 90 | u 10 91 | v 11 92 | w 14 93 | x 10 94 | y 11 95 | z 10 96 | { 7 97 | | 6 98 | } 10 99 | ~ 10 100 |   5 101 | ¡ 5 102 | ¢ 9 103 | £ 12 104 | ¤ 12 105 | ¥ 12 106 | ¦ 4 107 | § 10 108 | ¨ 7 109 | © 11 110 | ª 8 111 | « 12 112 | ¬ 9 113 | ® 12 114 | ° 7 115 | ± 9 116 | ² 8 117 | ³ 8 118 | ´ 8 119 | µ 10 120 | ¶ 10 121 | · 4 122 | ¸ 7 123 | ¹ 6 124 | º 8 125 | » 12 126 | ¼ 13 127 | ½ 13 128 | ¾ 14 129 | ¿ 9 130 | À 13 131 | Á 13 132 |  13 133 | à 13 134 | Ä 13 135 | Å 13 136 | Æ 15 137 | Ç 11 138 | È 11 139 | É 11 140 | Ê 11 141 | Ë 11 142 | Ì 5 143 | Í 5 144 | Î 5 145 | Ï 5 146 | Ð 13 147 | Ñ 12 148 | Ò 13 149 | Ó 13 150 | Ô 13 151 | Õ 13 152 | Ö 13 153 | × 9 154 | Ø 13 155 | Ù 12 156 | Ú 12 157 | Û 12 158 | Ü 12 159 | Ý 12 160 | Þ 11 161 | ß 11 162 | à 10 163 | á 10 164 | â 10 165 | ã 10 166 | ä 10 167 | å 10 168 | æ 15 169 | ç 9 170 | è 10 171 | é 10 172 | ê 10 173 | ë 10 174 | ì 5 175 | í 6 176 | î 6 177 | ï 7 178 | ð 10 179 | ñ 10 180 | ò 10 181 | ó 10 182 | ô 10 183 | õ 10 184 | ö 10 185 | ÷ 9 186 | ø 10 187 | ù 10 188 | ú 10 189 | û 10 190 | ü 10 191 | ý 10 192 | þ 10 193 | ÿ 10 194 | ı 5 195 | Ł 12 196 | ł 8 197 | Œ 15 198 | œ 15 199 | Š 10 200 | š 9 201 | Ÿ 12 202 | Ž 11 203 | ž 10 204 | ƒ 9 205 | ˆ 9 206 | ˇ 9 207 | ˙ 4 208 | ˚ 6 209 | ˛ 7 210 | ˜ 9 211 | – 10 212 | — 12 213 | ‘ 4 214 | ’ 4 215 | ‚ 4 216 | “ 8 217 | ” 8 218 | „ 8 219 | † 9 220 | ‡ 9 221 | • 6 222 | … 15 223 | ‰ 14 224 | ‹ 7 225 | › 7 226 | ⁄ 9 227 | € 11 228 | ™ 14 229 | − 10 230 |   16 231 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/img/bottom-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/src/img/bottom-arrow.png -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/img/panic-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/src/img/panic-logo.png -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/img/pdx/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/src/img/pdx/card.png -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/img/top-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strawdynamics/pdportal/a23cf0affd185b521b95198cf92521f1cfdc9faf/lua/examples/03-panic-sign/src/img/top-arrow.png -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/lib/util/text.lua: -------------------------------------------------------------------------------- 1 | local graphics = playdate.graphics 2 | 3 | fonts = { 4 | nicoClean16 = graphics.font.new('fonts/nico/nico-clean-16'), 5 | nicoBold16 = graphics.font.new('fonts/nico/nico-bold-16'), 6 | nicoPaint16 = graphics.font.new('fonts/nico/nico-paint-16'), 7 | } 8 | 9 | fontFamilies = { 10 | nico = graphics.font.newFamily({ 11 | [graphics.font.kVariantNormal] = 'fonts/nico/nico-clean-16', 12 | [graphics.font.kVariantBold] = 'fonts/nico/nico-bold-16', 13 | [graphics.font.kVariantItalic] = 'fonts/nico/nico-paint-16', 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/main.lua: -------------------------------------------------------------------------------- 1 | -- Copied during build, you wouldn't normally have to do that 2 | import './pdportal' 3 | 4 | import 'CoreLibs/graphics' 5 | import 'CoreLibs/sprites' 6 | 7 | import './lib/util/text' 8 | import './Example03PanicSign' 9 | 10 | local app = Example03PanicSign() 11 | 12 | function playdate.update() 13 | app:update() 14 | end 15 | -------------------------------------------------------------------------------- /lua/examples/03-panic-sign/src/pdxinfo: -------------------------------------------------------------------------------- 1 | name=pdportal-03-panic-sign 2 | bundleID=com.strawdynamics.pdportal-03-panic-sign 3 | version=0.2.0 4 | buildNumber=3 5 | imagePath=img/pdx 6 | author=pdportal contributors 7 | -------------------------------------------------------------------------------- /lua/examples/04-fetch/README.md: -------------------------------------------------------------------------------- 1 | # pdportal-04-fetch 2 | 3 | A slightly more practical example than 03-panic-sign, using GET and POST to integrate with https://restful-api.dev. It will create objects in this testing API. You can then view them on Playdate or your computer at https://api.restful-api.dev/objects/idLoggedToBrowserConsole. 4 | 5 | ## Installation 6 | 7 | `./build.sh d` will compile and automatically install the app to your connected Playdate. I'm sorry, the script currently only works on macOS, and even then can be a bit janky. Pull requests welcome :) 8 | -------------------------------------------------------------------------------- /lua/examples/04-fetch/build.sh: -------------------------------------------------------------------------------- 1 | pdxFile="pdportal-04-fetch.pdx" 2 | 3 | cp ../../pdportal.lua ./src/pdportal.lua 4 | pdc src "$pdxFile" 5 | 6 | runChoice=$1 7 | 8 | if [ "$runChoice" = "s" ]; then 9 | open "$pdxFile" 10 | elif [ "$runChoice" = "d" ]; then 11 | node ../../../scripts/uploadPdxToPlaydate.js "$pdxFile" 12 | fi 13 | -------------------------------------------------------------------------------- /lua/examples/04-fetch/src/Example04Fetch.lua: -------------------------------------------------------------------------------- 1 | local graphics = playdate.graphics 2 | 3 | local PdPortal = PdPortal 4 | local PortalCommand = PdPortal.PortalCommand 5 | 6 | class('Example04Fetch').extends(PdPortal) 7 | local Example04Fetch = Example04Fetch 8 | 9 | function Example04Fetch:init() 10 | -- If your subclass overrides the init method, make sure to call super! 11 | Example04Fetch.super.init(self) 12 | 13 | playdate.display.setRefreshRate(50) 14 | 15 | self:_initOwnProps() 16 | end 17 | 18 | function Example04Fetch:_initOwnProps() 19 | self.objectId = '-1' 20 | self.connected = false 21 | self.isSendingRequest = false 22 | self.responseImage = nil 23 | self.portalVersion = nil 24 | end 25 | 26 | function Example04Fetch:update() 27 | -- If your subclass overrides the update method, make sure to call super! 28 | Example04Fetch.super.update(self) 29 | 30 | graphics.clear() 31 | 32 | playdate.drawFPS(10, 225) 33 | 34 | if self.connected then 35 | if self.isSendingRequest then 36 | graphics.drawTextAligned( 37 | 'Fetching…', 38 | 200, 39 | 100, 40 | kTextAlignment.center 41 | ) 42 | elseif self.responseImage ~= nil then 43 | self.responseImage:draw(10, 10) 44 | 45 | if playdate.buttonJustPressed(playdate.kButtonA) then 46 | self.responseImage = nil 47 | end 48 | else 49 | self:_updateReady() 50 | end 51 | else 52 | graphics.drawTextAligned( 53 | 'Disconnected', 54 | 200, 55 | 100, 56 | kTextAlignment.center 57 | ) 58 | end 59 | end 60 | 61 | function Example04Fetch:_updateReady() 62 | 63 | graphics.drawTextAligned('Ⓐ POST expecting success', 200, 120, kTextAlignment.center) 64 | graphics.drawTextAligned('Ⓑ POST invalid body', 200, 140, kTextAlignment.center) 65 | graphics.drawTextAligned('⬆️ GET expecting success/404 if no post first', 200, 160, kTextAlignment.center) 66 | 67 | if playdate.buttonJustPressed(playdate.kButtonA) then 68 | self:_createObject(json.encode({ 69 | name = '04-fetch', 70 | data = { 71 | hello = 'from pdportal ' .. self.portalVersion, 72 | -- Make sure largeish payloads (over 255 chars) can be sent/received 73 | padding = 'abcdefghijklmnopanicqrstuvwxyzABCDEFGHIJKLMNOPANICQRSTUVWXYZ0123456789abcdefghijklmnopanicqrstuvwxyzABCDEFGHIJKLMNOPANICQRSTUVWXYZ0123456789abcdefghijklmnopanicqrstuvwxyzABCDEFGHIJKLMNOPANICQRSTUVWXYZ0123456789abcdefghijklmnopanicqrstuvwxyzABCDEFGHIJKLMNOPANICQRSTUVWXYZ0123456789abcdefghijklmnopanicqrstuvwxyzABCDEFGHIJKLMNOPANICQRSTUVWXYZ0123456789' 74 | } 75 | })) 76 | elseif playdate.buttonJustPressed(playdate.kButtonB) then 77 | self:_createObject('{invalidJson}') 78 | elseif playdate.buttonJustPressed(playdate.kButtonUp) then 79 | self:_getObject() 80 | end 81 | end 82 | 83 | function Example04Fetch:_setResponseImage(text) 84 | self.responseImage = graphics.imageWithText( 85 | '(Ⓐ continue) ' .. text, 86 | 380, 87 | 220, 88 | graphics.kColorClear, 89 | 0, 90 | '…', 91 | kTextAlignment.left 92 | ) 93 | end 94 | 95 | function Example04Fetch:_getObject() 96 | self.isSendingRequest = true 97 | 98 | self:fetch( 99 | 'https://api.restful-api.dev/objects/' .. self.objectId, 100 | {}, 101 | function (responseText, responseDetails) 102 | self.isSendingRequest = false 103 | self:_setResponseImage(responseText) 104 | end, 105 | function (errorDetails) 106 | self:_handleError(errorDetails.message) 107 | end 108 | ) 109 | end 110 | 111 | function Example04Fetch:_createObject(body) 112 | self.isSendingRequest = true 113 | 114 | self:fetch( 115 | 'https://api.restful-api.dev/objects', 116 | { 117 | body = body, 118 | method = 'POST', 119 | headers = { 120 | ['Content-Type'] = 'application/json' 121 | }, 122 | }, 123 | function (responseText, responseDetails) 124 | self.isSendingRequest = false 125 | 126 | local responseJson = json.decode(responseText) 127 | 128 | if responseDetails.status == 200 then 129 | self:_setResponseImage(responseText) 130 | self.objectId = responseJson.id 131 | else 132 | self:_handleError(responseText) 133 | end 134 | end, 135 | function (errorDetails) 136 | self:_handleError(errorDetails.message) 137 | end 138 | ) 139 | end 140 | 141 | function Example04Fetch:_handleError(errorMessage) 142 | self.isSendingRequest = false 143 | self:_setResponseImage('Error: ' .. errorMessage) 144 | self.objectId = '-1' 145 | end 146 | 147 | function Example04Fetch:onConnect(portalVersion) 148 | self.connected = true 149 | self.portalVersion = portalVersion 150 | self:log('connectEcho!', portalVersion) 151 | end 152 | 153 | function Example04Fetch:onDisconnect() 154 | self:_initOwnProps() 155 | end 156 | -------------------------------------------------------------------------------- /lua/examples/04-fetch/src/main.lua: -------------------------------------------------------------------------------- 1 | -- Copied during build, you wouldn't normally have to do that 2 | import './pdportal' 3 | 4 | import 'CoreLibs/graphics' 5 | 6 | import 'Example04Fetch' 7 | 8 | local app = Example04Fetch() 9 | 10 | playdate.update = function() 11 | app:update() 12 | end 13 | -------------------------------------------------------------------------------- /lua/examples/04-fetch/src/pdxinfo: -------------------------------------------------------------------------------- 1 | name=pdportal-04-fetch 2 | bundleID=com.strawdynamics.pdportal-04-fetch 3 | version=0.2.0 4 | buildNumber=1 5 | -------------------------------------------------------------------------------- /lua/pdportal.lua: -------------------------------------------------------------------------------- 1 | import 'CoreLibs/object' 2 | import 'CoreLibs/timer' 3 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 4 | -- The PdPortal class is used to communicate from the Playdate back to the 5 | -- browser (and other Playdates). Typically, you'll subclass this 6 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 7 | 8 | class('PdPortal').extends() 9 | local PdPortal = PdPortal 10 | 11 | local jsonEncode = json.encode 12 | local jsonDecode = json.decode 13 | local timer = playdate.timer 14 | 15 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 16 | -- These functions should be implemented by each app, and will be called by the 17 | -- pdportal web app as appropriate. 18 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 19 | 20 | --- Called by pdportal web app after serial connection established. Example 21 | -- handler: Update UI to show that the serial connection was successful. 22 | function PdPortal:onConnect(portalVersion) 23 | -- 24 | end 25 | 26 | --- Called by Lua code when serial keepalive fails. Example 27 | -- handler: Update UI to show that the serial has been disconnected. 28 | function PdPortal:onDisconnect() 29 | -- 30 | end 31 | 32 | --- Peer connection established, remote peer connection possible. Example 33 | -- handler: Update UI to show this Playdate's peer ID. Other users can enter 34 | -- the ID to connect. 35 | -- @see https://peerjs.com/docs/#peeron-open 36 | function PdPortal:onPeerOpen(peerId) 37 | -- 38 | end 39 | 40 | --- Peer connection destroyed, remote peer connection no longer possible. 41 | -- Example handler: Update UI to show that peer connection is no longer 42 | -- possible. Prompt user to reconnect? 43 | -- @see https://peerjs.com/docs/#peeron-close 44 | function PdPortal:onPeerClose() 45 | -- 46 | end 47 | 48 | --- A remote peer has connected to us via the peer server. The given 49 | -- `remotePeerId` can be used to send data to this peer with 50 | -- `PdPortal.PortalCommand.SendToPeerConn`. Example handler: Start game! 51 | -- @see https://peerjs.com/docs/#peeron-connection 52 | function PdPortal:onPeerConnection(remotePeerId) 53 | -- 54 | end 55 | 56 | -- We've connected to a remote peer via the peer server. The given 57 | -- `remotePeerId` can be used to send data to this peer with 58 | -- `PdPortal.PortalCommand.SendToPeerConn`. Example handler: Start game! 59 | -- @see https://peerjs.com/docs/#dataconnection-on-open 60 | function PdPortal:onPeerConnOpen(remotePeerId) 61 | -- 62 | end 63 | 64 | -- Connection to a remote peer has been lost. Example handler: End game. 65 | -- @see https://peerjs.com/docs/#dataconnection-on-close 66 | function PdPortal:onPeerConnClose(remotePeerId) 67 | -- 68 | end 69 | 70 | --- We've received data from a remote peer. Example handler: Read `payload` and 71 | -- act appropriately based on the `remotePeerId` and current app state. Note 72 | -- that `payload` is JSON-encoded, so you probably want to 73 | -- `local decodedPayload = json.decode(payload)` before use. 74 | -- @see https://peerjs.com/docs/#dataconnection-on-data 75 | -- @see https://sdk.play.date/inside-playdate#M-json 76 | function PdPortal:onPeerConnData(remotePeerId, payload) 77 | -- 78 | end 79 | 80 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 81 | -- Use these methods to communicate with the pdportal web app and/or other 82 | -- connected Playdates. 83 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 84 | 85 | --- Commands Playdate can send to the pdportal website 86 | PdPortal.PortalCommand = { 87 | --- Log the given arguments to the browser console 88 | Log = 'l', 89 | --- Keepalive command, automatically sent by the Lua side of pdportal 90 | Keepalive = 'k', 91 | -- Tell pdportal to initialize connection to the peer server for 92 | -- communicating with another Playdate. Not required for `fetch`. 93 | InitializePeer = 'ip', 94 | -- Tell pdportal to disconnect from the peer server. 95 | DestroyPeer = 'dp', 96 | --- Takes two strings, the destination peer ID, and a JSON string. Sends the 97 | -- JSON to that peer. 98 | SendToPeerConn = 'p', 99 | --- Takes one string, the peer ID to close the connection to. 100 | ClosePeerConn = 'cpc', 101 | --- HTTP fetch 102 | Fetch = 'f', 103 | } 104 | 105 | --- Send the given PortalCommand and arguments to the pdportal site e.g. 106 | -- `portalInstance:sendCommand(PdPortal.PortalCommand.Log, 'hello', 'world!')` 107 | -- will log those two strings to the browser console. 108 | function PdPortal:sendCommand(portalCommand, ...) 109 | local cmd = {portalCommand} 110 | 111 | for i = 1, select('#', ...) do 112 | local arg = select(i, ...) 113 | table.insert(cmd, PdPortal.portalArgumentSeparator) 114 | table.insert(cmd, arg) 115 | end 116 | 117 | table.insert(cmd, PdPortal.portalCommandSeparator) 118 | 119 | print(table.concat(cmd, '')) 120 | end 121 | 122 | --- Convenience method instead of calling 123 | -- `:sendCommand(PdPortal.PortalCommand.Log, 'hello', 'world')` 124 | function PdPortal:log(...) 125 | self:sendCommand( 126 | PdPortal.PortalCommand.Log, 127 | table.unpack({...}) 128 | ) 129 | end 130 | 131 | --- Convenience method instead of calling 132 | -- `:sendCommand(PdPortal.PortalCommand.SendToPeerConn, peerConnId, encodedPayload)` 133 | function PdPortal:sendToPeerConn(peerConnId, payload) 134 | local jsonPayload = jsonEncode(payload) 135 | self:sendCommand( 136 | PdPortal.PortalCommand.SendToPeerConn, 137 | peerConnId, 138 | jsonPayload 139 | ) 140 | end 141 | 142 | --- Send an HTTP request using `fetch`. 143 | -- @see https://developer.mozilla.org/en-US/docs/Web/API/fetch 144 | function PdPortal:fetch(url, options, successCallback, errorCallback) 145 | local encodedOptions = jsonEncode(options) 146 | local requestId = tostring(self._nextRequestId) 147 | self._nextRequestId += 1 148 | 149 | self._fetchCallbacks[requestId] = {successCallback, errorCallback} 150 | 151 | self:sendCommand(PdPortal.PortalCommand.Fetch, requestId, url, encodedOptions) 152 | end 153 | 154 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 155 | -- Most apps should _not_ need to use, override, or change things below here. 156 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 157 | 158 | --- Things the pdportal site can tell Playdate to do 159 | PdPortal.PlaydateCommand = { 160 | OnConnect = 'oc', 161 | OnPeerConnData = 'opcd', 162 | OnPeerOpen = 'opo', 163 | OnPeerClose = 'opc', 164 | OnPeerConnection = 'opconn', 165 | OnPeerConnOpen = 'opco', 166 | OnPeerConnClose = 'opcc', 167 | Keepalive = 'k', 168 | OnFetchSuccess = 'ofs', 169 | OnFetchError = 'ofe', 170 | } 171 | 172 | --- Map of PlaydateCommands to method names that should be called 173 | PdPortal.playdateCommandMethods = { 174 | [PdPortal.PlaydateCommand.OnConnect] = {'_onConnect', 'onConnect'}, 175 | [PdPortal.PlaydateCommand.OnPeerConnData] = {'onPeerConnData'}, 176 | [PdPortal.PlaydateCommand.OnPeerOpen] = {'onPeerOpen'}, 177 | [PdPortal.PlaydateCommand.OnPeerClose] = {'onPeerClose'}, 178 | [PdPortal.PlaydateCommand.OnPeerConnection] = {'onPeerConnection'}, 179 | [PdPortal.PlaydateCommand.OnPeerConnOpen] = {'onPeerConnOpen'}, 180 | [PdPortal.PlaydateCommand.OnPeerConnClose] = {'onPeerConnClose'}, 181 | [PdPortal.PlaydateCommand.Keepalive] = {'_onKeepalive'}, 182 | [PdPortal.PlaydateCommand.OnFetchSuccess] = {'_onFetchSuccess'}, 183 | [PdPortal.PlaydateCommand.OnFetchError] = {'_onFetchError'}, 184 | } 185 | 186 | --- Sent from Playdate to serial host at the end of each command 187 | PdPortal.portalCommandSeparator = string.char(30) -- RS (␞) 188 | --- Sent from Playdate to serial host between commands and their arguments, and each argument 189 | PdPortal.portalArgumentSeparator = string.char(31) -- US (␟) 190 | 191 | --- Sent from serial host to Playdate at the end of each command. 192 | PdPortal.playdateCommandSeparator = '~|~' 193 | PdPortal.playdateCommandPattern = PdPortal.playdateCommandSeparator .. "()" 194 | --- Sent from serial host to Playdate between commands and their arguments, and each argument 195 | PdPortal.playdateArgumentSeparator = '~,~' 196 | PdPortal.playdateArgumentPattern = '(.-)' .. PdPortal.playdateArgumentSeparator .. "()" 197 | 198 | -- Partial commands can be sent from serial host to PD due to `msg` length limit. Add to this buffer until a complete command has been received (playdateCommandSeparator) 199 | PdPortal.incomingCommandBuffer = '' 200 | 201 | 202 | --- The PdPortal class (and by extension, your subclass) should be treated as a 203 | -- singleton. On init, it sets the system `playdate.serialMessageReceived` 204 | -- callback to its own method, replacing any existing handler. 205 | -- @see https://sdk.play.date/2.4.1/Inside%20Playdate.html#c-serialMessageReceived 206 | function PdPortal:init() 207 | playdate.serialMessageReceived = function (msg) 208 | self:_onSerialMessageReceived(msg) 209 | end 210 | 211 | self.serialKeepaliveTimer = nil 212 | 213 | self._fetchCallbacks = {} 214 | self._nextRequestId = 1 215 | end 216 | 217 | function PdPortal:update() 218 | -- Required for serial keepalive 219 | timer.updateTimers() 220 | end 221 | 222 | --- Handle input from the `msg` serial command, and distribute appropriately 223 | function PdPortal:_onSerialMessageReceived(msgString) 224 | 225 | -- Convert `~n~` back to actual `\n` newlines 226 | msgString = msgString:gsub('~n~', '\n') 227 | 228 | local completeCommandStrings, trailingCommand = PdPortal._splitCommandBuffer( 229 | PdPortal.incomingCommandBuffer .. msgString, 230 | PdPortal.playdateCommandPattern 231 | ) 232 | 233 | PdPortal.incomingCommandBuffer = trailingCommand 234 | 235 | for _i, commandString in ipairs(completeCommandStrings) do 236 | local cmdArgs = PdPortal._splitString(commandString, PdPortal.playdateArgumentPattern) 237 | 238 | local methodsToCall = PdPortal.playdateCommandMethods[cmdArgs[1]] 239 | 240 | if methodsToCall == nil then 241 | self:log('Unknown command received', cmdArgs[1], msgString, string.len(msgString)) 242 | return 243 | end 244 | 245 | for i, methodName in ipairs(methodsToCall) do 246 | self[methodName](self, table.unpack(cmdArgs, 2)) 247 | end 248 | end 249 | end 250 | 251 | function PdPortal:_onConnect() 252 | self:_onKeepalive() 253 | end 254 | 255 | --- Called by self on connect, then automatically re-triggered by responses from 256 | -- pdportal. This is required so the Playdate can know when it loses connection 257 | -- with the portal. 258 | function PdPortal:_onKeepalive() 259 | -- We've received a keepalive message, cancel the keepalive timer. We live for 260 | -- at least another few hundred milliseconds. 261 | if self.serialKeepaliveTimer ~= nil then 262 | self.serialKeepaliveTimer:pause() 263 | self.serialKeepaliveTimer:remove() 264 | self.serialKeepaliveTimer = nil 265 | end 266 | 267 | -- Schedule another keepalive cycle in 500ms 268 | timer.performAfterDelay(500, function () 269 | -- If pdportal doesn't respond within 100ms, disconnect 270 | self.serialKeepaliveTimer = playdate.timer.new(100, function () 271 | self:onDisconnect() 272 | end) 273 | self:sendCommand(PdPortal.PortalCommand.Keepalive) 274 | end) 275 | end 276 | 277 | function PdPortal:_onFetchSuccess(requestId, responseText, responseDetails) 278 | local callbacks = self._fetchCallbacks[requestId] 279 | 280 | if callbacks == nil then 281 | self:log('Success, but no callbacks found for request ID', requestId) 282 | return 283 | end 284 | 285 | callbacks[1](responseText, jsonDecode(responseDetails)) 286 | 287 | self._fetchCallbacks[requestId] = nil 288 | end 289 | 290 | function PdPortal:_onFetchError(requestId, errorDetails) 291 | local callbacks = self._fetchCallbacks[requestId] 292 | 293 | if callbacks == nil then 294 | self:log('Error, but no callbacks found for request ID', requestId) 295 | return 296 | end 297 | 298 | callbacks[2](jsonDecode(errorDetails)) 299 | 300 | self._fetchCallbacks[requestId] = nil 301 | end 302 | 303 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 304 | -- Utility functions 305 | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 306 | 307 | --- Splits a string on the given pattern. Returns a table of complete commands, and a string with any trailing incomplete command info 308 | PdPortal._splitCommandBuffer = function(cmdBufferString, pattern) 309 | local completeCommands = {} 310 | local start = 1 311 | local first, last = string.find(cmdBufferString, pattern, start) 312 | local trailingCommand = cmdBufferString 313 | 314 | while first do 315 | local command = string.sub(cmdBufferString, start, first - 1) 316 | if command ~= "" then 317 | table.insert(completeCommands, command) 318 | end 319 | start = last + 1 320 | first, last = string.find(cmdBufferString, pattern, start) 321 | end 322 | 323 | if start <= string.len(cmdBufferString) then 324 | trailingCommand = string.sub(cmdBufferString, start) 325 | else 326 | trailingCommand = "" 327 | end 328 | 329 | return completeCommands, trailingCommand 330 | end 331 | 332 | PdPortal._splitString = function(inputStr, pattern) 333 | local result = {} 334 | local lastEnd = 1 335 | 336 | for part, endPos in string.gmatch(inputStr, pattern) do 337 | table.insert(result, part) 338 | lastEnd = endPos 339 | end 340 | 341 | -- Add the last part if there is any 342 | if lastEnd <= #inputStr then 343 | table.insert(result, string.sub(inputStr, lastEnd)) 344 | end 345 | 346 | return result 347 | end 348 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdportal", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 12 | "format": "prettier --plugin-search-dir . --write ." 13 | }, 14 | "devDependencies": { 15 | "@sveltejs/adapter-auto": "^2.0.0", 16 | "@sveltejs/adapter-static": "^2.0.3", 17 | "@sveltejs/kit": "^1.20.4", 18 | "@typescript-eslint/eslint-plugin": "^6.0.0", 19 | "@typescript-eslint/parser": "^6.0.0", 20 | "autoprefixer": "^10.4.16", 21 | "eslint": "^8.28.0", 22 | "eslint-config-prettier": "^8.5.0", 23 | "eslint-plugin-svelte": "^2.30.0", 24 | "node-watch": "^0.7.4", 25 | "postcss": "^8.4.31", 26 | "prettier": "^2.8.0", 27 | "prettier-plugin-svelte": "^2.10.1", 28 | "serialport": "^12.0.0", 29 | "svelte": "^4.0.5", 30 | "svelte-check": "^3.4.3", 31 | "tailwindcss": "^3.3.5", 32 | "tslib": "^2.4.1", 33 | "typescript": "^5.0.0", 34 | "vite": "^4.4.2" 35 | }, 36 | "type": "module", 37 | "dependencies": { 38 | "lucide-svelte": "^0.292.0", 39 | "pd-usb": "^2.0.1", 40 | "peerjs": "^1.5.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Support and build related scripts 2 | -------------------------------------------------------------------------------- /scripts/uploadPdxToPlaydate.js: -------------------------------------------------------------------------------- 1 | import { SerialPort } from 'serialport' 2 | import Watch from 'node-watch' 3 | import { exec } from 'child_process' 4 | import fs from 'fs' 5 | import path from 'path' 6 | 7 | const pdxFilePath = path.resolve(process.argv[2]) 8 | const playdateVolumePath = '/Volumes/PLAYDATE/Games' 9 | const baudRate = 115200 10 | 11 | console.log('Copying PDX to Playdate') 12 | 13 | if (!fs.existsSync(pdxFilePath)) { 14 | console.error('PDX file not found.') 15 | process.exit(1) 16 | } 17 | 18 | SerialPort.list().then((ports) => { 19 | const playdatePortInfo = ports.find((port) => { 20 | return ( 21 | port.path.includes('tty.usbmodemPD') && port.manufacturer === 'Panic Inc' 22 | ) 23 | }) 24 | if (!playdatePortInfo) { 25 | console.error('Playdate serial port not found') 26 | process.exit(1) 27 | } 28 | 29 | const playdatePort = new SerialPort({ 30 | path: playdatePortInfo.path, 31 | baudRate 32 | }) 33 | 34 | playdatePort.write('datadisk\r\n', (err) => { 35 | if (err) { 36 | console.error('Error sending command:', err.message) 37 | process.exit(1) 38 | } 39 | playdatePort.close() 40 | }) 41 | }) 42 | 43 | function waitForMount(path, callback) { 44 | const interval = setInterval(() => { 45 | fs.access(path, fs.constants.F_OK | fs.constants.W_OK, (err) => { 46 | if (!err) { 47 | clearInterval(interval) 48 | callback() 49 | } 50 | }) 51 | }, 100) 52 | } 53 | 54 | Watch('/Volumes', { recursive: false }, (evt, name) => { 55 | if (name.includes('PLAYDATE')) { 56 | waitForMount('/Volumes/PLAYDATE/Games', () => { 57 | const destPath = path.join(playdateVolumePath, path.basename(pdxFilePath)) 58 | fs.cp(pdxFilePath, destPath, { recursive: true }, (err) => { 59 | if (err) { 60 | console.error('Error copying PDX:', err) 61 | } else { 62 | console.log('PDX copied successfully') 63 | ejectDisk() 64 | } 65 | }) 66 | }) 67 | } 68 | }) 69 | 70 | function ejectDisk() { 71 | exec(`diskutil eject '/Volumes/PLAYDATE'`, (err, stdout, stderr) => { 72 | if (err) { 73 | console.error('Error ejecting disk:', stderr) 74 | process.exit(1) 75 | } 76 | console.log('Playdate disk ejected') 77 | process.exit(0) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pdportal 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/appHeader/AppHeader.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 |

15 | 20 | pdportal 21 | {version} 22 |

23 | 24 | {#if $pdDeviceStore.device} 25 |
36 | {/if} 37 |
38 | 39 | 62 |
63 | -------------------------------------------------------------------------------- /src/lib/components/appHeader/HeaderLink.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
  • 10 | 17 | 18 | 19 |
  • 20 | -------------------------------------------------------------------------------- /src/lib/components/dev/DeviceInfo.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
    19 | Device info 20 | 21 | {#if $pdDeviceStore.device} 22 |
    {JSON.stringify(
    23 | 				$pdDeviceStore,
    24 | 				null,
    25 | 				2,
    26 | 			)}
    27 | 28 |
    29 | 30 | 31 |
    32 | {:else} 33 |

    No device connected

    34 | 35 | 36 | {/if} 37 |
    38 | -------------------------------------------------------------------------------- /src/lib/components/dev/PeerDebugger.svelte: -------------------------------------------------------------------------------- 1 | 65 | 66 |
    67 | P2P 68 | 69 | {#if $peerStore.peerId} 70 |

    71 | Connected to peer server. Your ID is {$peerStore.peerId}. 72 | Wait for someone to connect to you, or enter the ID you want to connect 73 | to: 74 |

    75 | 76 | 82 | 85 | 86 |
    87 | Peers 88 |
      89 | {#each Object.keys($peerStore.peerConnections) as pcId} 90 |
    • {pcId}
    • 91 | {/each} 92 |
    93 |
    94 | 95 |
    96 |
    97 | 102 |