Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d7cd6182f | ||
|
|
b58b4251dc | ||
|
|
5c797c8807 | ||
|
|
05c443a4b5 |
@@ -69,7 +69,7 @@ jobs:
|
|||||||
- name: Upload debug APK
|
- name: Upload debug APK
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: PilgrimsOfKatan-debug-apk
|
name: ProLibertateGames-debug-apk
|
||||||
path: app/build/outputs/apk/debug/app-debug.apk
|
path: app/build/outputs/apk/debug/app-debug.apk
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@@ -79,7 +79,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: PilgrimsOfKatan-reports
|
name: ProLibertateGames-reports
|
||||||
path: |
|
path: |
|
||||||
app/build/reports/
|
app/build/reports/
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
stamp="$(( ${{ github.run_number }} + offset ))"
|
stamp="$(( ${{ github.run_number }} + offset ))"
|
||||||
version="v${series}.${stamp}-dev"
|
version="v${series}.${stamp}-dev"
|
||||||
src="app/build/outputs/apk/debug/app-debug.apk"
|
src="app/build/outputs/apk/debug/app-debug.apk"
|
||||||
dest="PilgrimsOfKatan-${version}.apk"
|
dest="ProLibertateGames-${version}.apk"
|
||||||
cp "$src" "$dest"
|
cp "$src" "$dest"
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
echo "apk=$dest" >> "$GITHUB_OUTPUT"
|
echo "apk=$dest" >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
stamp="$(( ${{ github.run_number }} + offset ))"
|
stamp="$(( ${{ github.run_number }} + offset ))"
|
||||||
version="v${series}.${stamp}"
|
version="v${series}.${stamp}"
|
||||||
src="app/build/outputs/apk/debug/app-debug.apk"
|
src="app/build/outputs/apk/debug/app-debug.apk"
|
||||||
dest="PilgrimsOfKatan-${version}.apk"
|
dest="ProLibertateGames-${version}.apk"
|
||||||
cp "$src" "$dest"
|
cp "$src" "$dest"
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
echo "apk=$dest" >> "$GITHUB_OUTPUT"
|
echo "apk=$dest" >> "$GITHUB_OUTPUT"
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
- name: Upload APK as workflow artifact
|
- name: Upload APK as workflow artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: PilgrimsOfKatan-apk
|
name: ProLibertateGames-apk
|
||||||
path: ${{ steps.stage.outputs.apk }}
|
path: ${{ steps.stage.outputs.apk }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
# Working on this repo (agent notes)
|
# Working on this repo (agent notes)
|
||||||
|
|
||||||
Pilgrims of Katan — an Android collection of card and board games, playable
|
ProLibertateGames — Wallacenet's build of an Android collection of card and
|
||||||
against AI or over Wi-Fi (LAN). Bootstrapped from
|
board games, playable against AI or over Wi-Fi (LAN). Upstream:
|
||||||
[adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
|
[adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
|
||||||
(GPL-3.0, kept as the `upstream` remote) for Wallacenet. Canonical repo:
|
(GPL-3.0, kept as the `upstream` remote). Canonical repo:
|
||||||
<https://git.wallacenet.ca/devadmin/PilgrimsOfKatan>.
|
<https://git.wallacenet.ca/devadmin/ProLibertateGames>.
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
- **Licences travel.** `LICENSE` is GPL-3.0 and must stay. The Uncial Antiqua
|
- **Licences travel.** `LICENSE` is GPL-3.0 and must stay. The Uncial Antiqua
|
||||||
font ships with its OFL licence at `docs/licenses/UncialAntiqua-OFL.txt`;
|
font ships with its OFL licence at `docs/licenses/UncialAntiqua-OFL.txt`;
|
||||||
never delete or edit either. Attribution to the upstream lives in the README.
|
never delete or edit either. Attribution to the upstream lives in the README.
|
||||||
- **No Catan trademarks or brand assets**, and none of the upstream's Wallace
|
- **No Catan trademarks or brand assets** — the one hard line. The app's own
|
||||||
clan branding (it was removed in the rebrand; see README → Branding). The
|
branding is the upstream's, restored (see README → Branding): tartan launcher
|
||||||
brand is: *Pilgrims of Katan*, `ca.wallacenet.pilgrimsofkatan` (+ `.dev`),
|
icon and splash, Wallace clan red on production and hunting green on dev,
|
||||||
slate blue / dev green, hexagon emblem.
|
launcher names *Pro Libertate Games* / *Pro Libertate Games DEV*. The
|
||||||
|
installed identity is `ca.wallacenet.prolibertategames` (+ `.dev`).
|
||||||
- **The existing catalogue is the regression safety net.** Keep every game
|
- **The existing catalogue is the regression safety net.** Keep every game
|
||||||
green; new games (Catan) arrive as separate tasks, not edits to old ones.
|
green; new games (Katan) arrive as separate tasks, not edits to old ones.
|
||||||
All 22 games are `available = true`.
|
The 22 reference games are `available = true`; Katan is listed with
|
||||||
|
`available = false` (*Coming soon*) until its game task lands.
|
||||||
- **Docs conventions**: every game gets `docs/RULES-<id>.md` (index and gaps in
|
- **Docs conventions**: every game gets `docs/RULES-<id>.md` (index and gaps in
|
||||||
`docs/README.md`); build/CI/release decisions go in `docs/CI.md`. Update the
|
`docs/README.md`); build/CI/release decisions go in `docs/CI.md`. Update the
|
||||||
docs when behaviour changes — they are the check against the rules code.
|
docs when behaviour changes — they are the check against the rules code.
|
||||||
@@ -44,7 +46,7 @@ One flag — `isDevBuild` in `app/build.gradle.kts` — drives applicationId,
|
|||||||
launcher name and brand colour together. Never set them individually.
|
launcher name and brand colour together. Never set them individually.
|
||||||
|
|
||||||
In-app OTA points at this repo's own Gitea releases
|
In-app OTA points at this repo's own Gitea releases
|
||||||
(`/api/v1/repos/devadmin/PilgrimsOfKatan/releases…`) and is **on**:
|
(`/api/v1/repos/devadmin/ProLibertateGames/releases…`) and is **on**:
|
||||||
`BuildConfig.OTA_ENABLED` is `true`. The channel is a decision already made and
|
`BuildConfig.OTA_ENABLED` is `true`. The channel is a decision already made and
|
||||||
recorded — `docs/CI.md` documents the endpoints, the build-number scheme, and
|
recorded — `docs/CI.md` documents the endpoints, the build-number scheme, and
|
||||||
what publishing requires. Setting the flag to `false` is the kill switch.
|
what publishing requires. Setting the flag to `false` is the kill switch.
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
# Pilgrims of Katan
|
# ProLibertateGames
|
||||||
|
|
||||||
A collection of card and board games for Android, playable against the computer
|
A collection of card and board games for Android, playable against the computer
|
||||||
or against other people over Wi-Fi (LAN).
|
or against other people over Wi-Fi (LAN).
|
||||||
|
|
||||||
Derived from [ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
|
Wallacenet's build of [ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
|
||||||
by adrianoftyriel, used under the GNU GPL v3 — see [LICENSE](LICENSE). Game
|
by adrianoftyriel (GNU GPL v3 — see [LICENSE](LICENSE)), hosted at
|
||||||
rules documentation lives under [docs/](docs/README.md); build, CI and release
|
<https://git.wallacenet.ca/devadmin/ProLibertateGames>. Game rules documentation
|
||||||
decisions live in [docs/CI.md](docs/CI.md). Catan itself arrives in later work;
|
lives under [docs/](docs/README.md); build, CI and release decisions live in
|
||||||
this app ships the reference catalogue unchanged as the regression safety net.
|
[docs/CI.md](docs/CI.md). Katan itself arrives in later work — it is listed under
|
||||||
|
Board Games as *Coming soon* — and this app ships the reference catalogue
|
||||||
|
unchanged as the regression safety net.
|
||||||
|
|
||||||
## What works today
|
## What works today
|
||||||
|
|
||||||
@@ -15,16 +17,16 @@ The app shell is complete and every game in the catalogue is playable end to end
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Cards** | Euchre, Kaiser, President, Golf, Wizard, Crazy 8s, Cribbage |
|
| **Cards** | Euchre, Kaiser, President, Golf, Wizard, Crazy 8s, Cribbage, Hearts, Klondike, FreeCell, Spider, Pyramid |
|
||||||
| **Board** | Sequence, Chess, Checkers, Backgammon, Mastermind, Nine Men's Morris, Pirates and Bulgars, Ta Yü |
|
| **Table** | Sequence, Chess, Checkers, Backgammon, Mastermind, Nine Men's Morris, Peg Solitaire, Yahtzee, Pirates and Bulgars, Ta Yü, Katan *(coming soon)* |
|
||||||
|
|
||||||
Nothing is marked *Coming soon* at the moment, because nothing is waiting. The
|
One entry is marked *Coming soon*: Katan, the island game, whose engine and
|
||||||
machinery for it is still there and still tested: a game listed in `GameCatalog`
|
screen land with their own task. The machinery is still there and still tested:
|
||||||
with `available = false` appears in the menu marked *Coming soon* **on dev builds
|
a game listed in `GameCatalog` with `available = false` appears in the menu
|
||||||
only**, so a production release lists just what can actually be played and nobody
|
marked *Coming soon* **on dev builds only**, so a production release lists just
|
||||||
installs it and taps into a dead end. The menu decides this from the installed
|
what can actually be played and nobody installs it and taps into a dead end. The
|
||||||
APK's own version name — the same source the updater uses to know which channel
|
menu decides this from the installed APK's own version name — the same source
|
||||||
it is on.
|
the updater uses to know which channel it is on.
|
||||||
|
|
||||||
Adding a game means writing a `GameRules` implementation, a `GameAi` and a
|
Adding a game means writing a `GameRules` implementation, a `GameAi` and a
|
||||||
screen, then flipping `available` — the menu, lobby, networking, settings and OTA
|
screen, then flipping `available` — the menu, lobby, networking, settings and OTA
|
||||||
@@ -135,21 +137,21 @@ it: every rule above is a pure function and unit-tested on the JVM.
|
|||||||
|
|
||||||
## Branding
|
## Branding
|
||||||
|
|
||||||
Neutral, original, and deliberately *not* Catan's: no third-party marks or
|
The app wears the upstream's own branding, restored (WALA-118 review): the
|
||||||
trade dress ship in this app (rebrand scope: WALA-118). The launcher icon is a
|
Wallace tartan is the launcher icon and the splash, drawn as vector drawables and
|
||||||
hexagonal ring around a solid core — a game tile, not any game's logo — drawn
|
Compose code so it stays sharp at every density and needs no PNGs. The app opens
|
||||||
as vector drawables so it stays sharp at every density and needs no PNGs. The
|
on the tartan held under the name, set in a Celtic uncial hand for two seconds
|
||||||
app opens on the brand field fading to its deep shade, the name set in a Celtic
|
before it fades into the menu.
|
||||||
uncial hand for two seconds before it fades into the menu.
|
|
||||||
|
|
||||||
A dev build wears the **dev green** field where production wears **slate
|
A dev build wears the **hunting green** sett where production wears the **clan
|
||||||
blue**, and installs as *Pilgrims of Katan DEV*. That is what tells the two
|
red**, and installs as *Pro Libertate Games DEV*. That is what tells the two
|
||||||
copies apart when both are on the phone; see
|
copies apart when both are on the phone; see
|
||||||
[Both channels on one device](#both-channels-on-one-device). The palette lives
|
[Both channels on one device](#both-channels-on-one-device). The tartan is drawn
|
||||||
in `ui/theme/Theme.kt` (`BrandField`, `BrandDeep`, `BrandAccent`) and the two
|
by `ui/theme/Tartan.kt` (`TartanBackground`, `threadWidthFor`) off
|
||||||
channel colours are generated per build by `resValue` in `app/build.gradle.kts`
|
`BuildConfig.DEV_BUILD`; the matching vector colours are generated per build by
|
||||||
— the icon vectors reference `@color/brand_field` rather than a literal, which
|
`resValue` in `app/build.gradle.kts` — the icon vectors reference
|
||||||
is how one set of vectors serves both colourways.
|
`@color/tartan_field` rather than a literal, which is how one set of vectors
|
||||||
|
serves both colourways.
|
||||||
|
|
||||||
Two honest caveats:
|
Two honest caveats:
|
||||||
|
|
||||||
@@ -158,9 +160,10 @@ Two honest caveats:
|
|||||||
the SIL Open Font License 1.1. The full licence is at
|
the SIL Open Font License 1.1. The full licence is at
|
||||||
[docs/licenses/UncialAntiqua-OFL.txt](docs/licenses/UncialAntiqua-OFL.txt) and
|
[docs/licenses/UncialAntiqua-OFL.txt](docs/licenses/UncialAntiqua-OFL.txt) and
|
||||||
must stay with the font — the OFL requires it to be distributed alongside.
|
must stay with the font — the OFL requires it to be distributed alongside.
|
||||||
- **The upstream's clan tartan is gone; its code is not.** The Wallace tartan
|
- **Katan is the one place this app deliberately diverges.** No Catan
|
||||||
was the upstream's brand and left with the rebrand. The GPL-licensed history
|
trademarks or brand assets ship anywhere in it; the island game is listed
|
||||||
that produced this app remains in git, and the source stays at
|
under its own name with original art only. The GPL-licensed history that
|
||||||
|
produced this app remains in git, and the source stays at
|
||||||
[adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
|
[adrianoftyriel/ProLibertateGames](https://github.com/adrianoftyriel/ProLibertateGames)
|
||||||
as the `upstream` remote.
|
as the `upstream` remote.
|
||||||
|
|
||||||
@@ -187,10 +190,10 @@ smaller than a fingertip.
|
|||||||
Requires JDK 17 and the Android SDK (compileSdk 34).
|
Requires JDK 17 and the Android SDK (compileSdk 34).
|
||||||
|
|
||||||
A local build is a dev build: it installs as
|
A local build is a dev build: it installs as
|
||||||
`ca.wallacenet.pilgrimsofkatan.dev` under the name *Pilgrims of Katan DEV* on
|
`ca.wallacenet.prolibertategames.dev` under the name *Pro Libertate Games DEV* on
|
||||||
the dev green field, so it goes on beside a production copy rather than over it.
|
the hunting-green sett, so it goes on beside a production copy rather than over
|
||||||
Set `PLG_CHANNEL=production` to build the production package
|
it. Set `PLG_CHANNEL=production` to build the production package
|
||||||
(`ca.wallacenet.pilgrimsofkatan`, slate blue).
|
(`ca.wallacenet.prolibertategames`, clan red).
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
@@ -262,7 +265,7 @@ unit-tested on the JVM like the rules engines are.
|
|||||||
### Both channels on one device
|
### Both channels on one device
|
||||||
|
|
||||||
A dev build installs under its own `applicationId`,
|
A dev build installs under its own `applicationId`,
|
||||||
`ca.wallacenet.pilgrimsofkatan.dev`, so it is a separate app as far as Android
|
`ca.wallacenet.prolibertategames.dev`, so it is a separate app as far as Android
|
||||||
is concerned and can sit beside the production copy rather than replacing it. Each channel still updates itself
|
is concerned and can sit beside the production copy rather than replacing it. Each channel still updates itself
|
||||||
normally, because an update within a channel keeps the same package.
|
normally, because an update within a channel keeps the same package.
|
||||||
|
|
||||||
@@ -270,9 +273,9 @@ Three things move together, all off `isDevBuild` in `app/build.gradle.kts`:
|
|||||||
|
|
||||||
| | Production | Dev |
|
| | Production | Dev |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `applicationId` | `ca.wallacenet.pilgrimsofkatan` | `ca.wallacenet.pilgrimsofkatan.dev` |
|
| `applicationId` | `ca.wallacenet.prolibertategames` | `ca.wallacenet.prolibertategames.dev` |
|
||||||
| Launcher name | Pilgrims of Katan | Pilgrims of Katan DEV |
|
| Launcher name | Pro Libertate Games | Pro Libertate Games DEV |
|
||||||
| Brand field | slate blue | deep green |
|
| Tartan sett | clan red | hunting green |
|
||||||
|
|
||||||
Anything that is not an explicit `PLG_CHANNEL=production` build is a dev build,
|
Anything that is not an explicit `PLG_CHANNEL=production` build is a dev build,
|
||||||
so a local `assembleDebug` is a dev build too and installs alongside a release
|
so a local `assembleDebug` is a dev build too and installs alongside a release
|
||||||
|
|||||||
+18
-16
@@ -40,9 +40,9 @@ val versionSeries = (project.findProperty("plgVersionSeries") as String?) ?: "1.
|
|||||||
//
|
//
|
||||||
// This one flag drives everything that makes a dev build a separate app rather
|
// This one flag drives everything that makes a dev build a separate app rather
|
||||||
// than a replacement for the production one — its own applicationId, its own
|
// than a replacement for the production one — its own applicationId, its own
|
||||||
// launcher name, and its own brand colour. They have to move together: a dev
|
// launcher name, and the hunting sett in place of the clan sett. They have to
|
||||||
// package wearing the production name and colours is exactly the confusion
|
// move together: a dev package wearing the production name and colours is
|
||||||
// having both installed is meant to avoid.
|
// exactly the confusion having both installed is meant to avoid.
|
||||||
val isDevBuild = buildChannel != "production"
|
val isDevBuild = buildChannel != "production"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -58,7 +58,7 @@ android {
|
|||||||
// A dev build installs under its own package, so a dev copy and a
|
// A dev build installs under its own package, so a dev copy and a
|
||||||
// production copy can sit side by side on one device instead of one
|
// production copy can sit side by side on one device instead of one
|
||||||
// replacing the other.
|
// replacing the other.
|
||||||
applicationId = if (isDevBuild) "ca.wallacenet.pilgrimsofkatan.dev" else "ca.wallacenet.pilgrimsofkatan"
|
applicationId = if (isDevBuild) "ca.wallacenet.prolibertategames.dev" else "ca.wallacenet.prolibertategames"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
// The -dev suffix in versionName is what the installed app reads to
|
// The -dev suffix in versionName is what the installed app reads to
|
||||||
@@ -83,19 +83,21 @@ android {
|
|||||||
resValue(
|
resValue(
|
||||||
"string",
|
"string",
|
||||||
"app_name",
|
"app_name",
|
||||||
if (isDevBuild) "Pilgrims of Katan DEV" else "Pilgrims of Katan",
|
if (isDevBuild) "Pro Libertate Games DEV" else "Pro Libertate Games",
|
||||||
)
|
)
|
||||||
|
|
||||||
// The one colour that separates the two installs: slate blue on
|
// The one colour that separates the two setts: Wallace red for
|
||||||
// production, deep green on dev. The launcher icon and the splash are
|
// production, Wallace Hunting green for dev. The icon vectors reference
|
||||||
// both built from it, so the drawer shows at a glance which of the two
|
// this rather than a literal, and Tartan.kt picks the matching Compose
|
||||||
// copies is which — the two have to stay in step or the icon and the
|
// colour off DEV_BUILD below — the two have to stay in step or the
|
||||||
// splash it opens on would be different brands.
|
// launcher icon and the splash it opens on would be different cloth.
|
||||||
resValue("color", "brand_field", if (isDevBuild) "#2A6B54" else "#2B4A78")
|
resValue("color", "tartan_field", if (isDevBuild) "#1E7A3C" else "#E01B24")
|
||||||
// A darker shade of the same hue, held behind the window for the
|
// Shades of the same field, so a launch does not flash the wrong colour
|
||||||
// instant before the splash draws, so a launch never flashes the wrong
|
// before the tartan is drawn: splash_background is held behind the
|
||||||
// colour.
|
// window for that instant, ic_launcher_background is the icon's flat
|
||||||
resValue("color", "splash_background", if (isDevBuild) "#153B2F" else "#182A44")
|
// fallback.
|
||||||
|
resValue("color", "splash_background", if (isDevBuild) "#0D2E17" else "#4A0B10")
|
||||||
|
resValue("color", "ic_launcher_background", if (isDevBuild) "#145C2D" else "#B3121B")
|
||||||
|
|
||||||
buildConfigField("boolean", "DEV_BUILD", isDevBuild.toString())
|
buildConfigField("boolean", "DEV_BUILD", isDevBuild.toString())
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
// For DEV_BUILD, which is how the Compose splash picks its brand colour.
|
// For DEV_BUILD, which is how the Compose tartan picks its sett.
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.PilgrimsOfKatan">
|
android:theme="@style/Theme.ProLibertateGames">
|
||||||
|
|
||||||
<!-- No fixed orientation: the UI adapts to portrait, landscape and
|
<!-- No fixed orientation: the UI adapts to portrait, landscape and
|
||||||
tablet widths. configChanges keeps Compose state across rotation
|
tablet widths. configChanges keeps Compose state across rotation
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.prolibertate.games.score.ScorekeeperRepository
|
|||||||
import org.prolibertate.games.settings.SettingsRepository
|
import org.prolibertate.games.settings.SettingsRepository
|
||||||
import org.prolibertate.games.ui.AppEnv
|
import org.prolibertate.games.ui.AppEnv
|
||||||
import org.prolibertate.games.ui.AppRoot
|
import org.prolibertate.games.ui.AppRoot
|
||||||
import org.prolibertate.games.ui.theme.PilgrimsTheme
|
import org.prolibertate.games.ui.theme.ProLibertateTheme
|
||||||
import org.prolibertate.games.update.Updater
|
import org.prolibertate.games.update.Updater
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
PilgrimsTheme {
|
ProLibertateTheme {
|
||||||
AppRoot(env)
|
AppRoot(env)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ object GameCatalog {
|
|||||||
const val MORRIS = "morris"
|
const val MORRIS = "morris"
|
||||||
const val PIRATES = "pirates"
|
const val PIRATES = "pirates"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The island game the collection is named for. Listed under Board Games
|
||||||
|
* and marked *Coming soon*; the engine and its screen land with the game
|
||||||
|
* task, and `available` flips on once two-device play is verified.
|
||||||
|
*/
|
||||||
|
const val CATAN = "catan"
|
||||||
|
|
||||||
val all: List<GameDescriptor> = listOf(
|
val all: List<GameDescriptor> = listOf(
|
||||||
GameDescriptor(
|
GameDescriptor(
|
||||||
id = EUCHRE,
|
id = EUCHRE,
|
||||||
@@ -333,6 +340,19 @@ object GameCatalog {
|
|||||||
"because your score multiplies them.",
|
"because your score multiplies them.",
|
||||||
available = true,
|
available = true,
|
||||||
),
|
),
|
||||||
|
GameDescriptor(
|
||||||
|
id = CATAN,
|
||||||
|
title = "Katan",
|
||||||
|
menus = setOf(GameMenu.BOARD),
|
||||||
|
minPlayers = 3,
|
||||||
|
maxPlayers = 4,
|
||||||
|
teamBased = false,
|
||||||
|
blurb = "Trade, build, and settle the island.",
|
||||||
|
// Listed, not playable: the engine and screen land with the Katan
|
||||||
|
// game task. Dev builds show the entry as *Coming soon*; a
|
||||||
|
// production build lists only what can actually be played.
|
||||||
|
available = false,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun byId(id: String): GameDescriptor? = all.firstOrNull { it.id == id }
|
fun byId(id: String): GameDescriptor? = all.firstOrNull { it.id == id }
|
||||||
|
|||||||
@@ -308,6 +308,6 @@ class LanTransport(private val context: Context) : Transport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val CONNECT_TIMEOUT_MS = 8_000
|
const val CONNECT_TIMEOUT_MS = 8_000
|
||||||
const val WIFI_LOCK_TAG = "PilgrimsOfKatan:lan"
|
const val WIFI_LOCK_TAG = "ProLibertateGames:lan"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const val PURPOSE_SCOREKEEPER = "scorekeeper"
|
|||||||
|
|
||||||
/** Service type and name advertised over mDNS. */
|
/** Service type and name advertised over mDNS. */
|
||||||
const val SERVICE_TYPE = "_plgames._tcp"
|
const val SERVICE_TYPE = "_plgames._tcp"
|
||||||
const val SERVICE_NAME = "PilgrimsOfKatan"
|
const val SERVICE_NAME = "ProLibertateGames"
|
||||||
|
|
||||||
val protocolJson: Json = Json {
|
val protocolJson: Json = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -19,7 +20,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shadow
|
import androidx.compose.ui.graphics.Shadow
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
@@ -30,9 +30,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.prolibertate.games.R
|
import org.prolibertate.games.R
|
||||||
import org.prolibertate.games.ui.theme.BrandAccent
|
import org.prolibertate.games.ui.theme.TartanBackground
|
||||||
import org.prolibertate.games.ui.theme.BrandDeep
|
import org.prolibertate.games.ui.theme.threadWidthFor
|
||||||
import org.prolibertate.games.ui.theme.BrandField
|
|
||||||
|
|
||||||
/** How long the splash holds at full opacity before fading. */
|
/** How long the splash holds at full opacity before fading. */
|
||||||
private const val SPLASH_HOLD_MILLIS = 2_000L
|
private const val SPLASH_HOLD_MILLIS = 2_000L
|
||||||
@@ -46,11 +45,7 @@ private const val SPLASH_FADE_MILLIS = 600
|
|||||||
val CelticFontFamily = FontFamily(Font(R.font.uncial_antiqua))
|
val CelticFontFamily = FontFamily(Font(R.font.uncial_antiqua))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The launch screen: the brand field under the name of the game.
|
* The launch screen: Wallace tartan under the name of the house.
|
||||||
*
|
|
||||||
* The gradient runs from the same hue as the window's `splash_background`
|
|
||||||
* held behind it, so the first frame and the splash are one brand in two
|
|
||||||
* shades rather than a colour change.
|
|
||||||
*
|
*
|
||||||
* Drawn as an overlay rather than a separate destination, so the menu is
|
* Drawn as an overlay rather than a separate destination, so the menu is
|
||||||
* already composed and ready behind it and the fade reveals a live screen
|
* already composed and ready behind it and the fade reveals a live screen
|
||||||
@@ -75,17 +70,30 @@ fun SplashScreen(
|
|||||||
onFinished()
|
onFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(modifier = modifier.fillMaxSize().alpha(alpha)) {
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.alpha(alpha)
|
|
||||||
.background(Brush.verticalGradient(listOf(BrandField, BrandDeep))),
|
|
||||||
) {
|
|
||||||
// Read the constraints here and hold them in locals: inside Column the
|
// Read the constraints here and hold them in locals: inside Column the
|
||||||
// implicit receiver becomes ColumnScope, and maxWidth/maxHeight are no
|
// implicit receiver becomes ColumnScope, and maxWidth/maxHeight are no
|
||||||
// longer in scope.
|
// longer in scope.
|
||||||
val shortestSide = minOf(maxWidth, maxHeight).value
|
val shortestSide = minOf(maxWidth, maxHeight).value
|
||||||
|
|
||||||
|
// Weave scale is derived from the sett, not a fixed fraction, so a
|
||||||
|
// couple of full repeats are always on screen whatever the threadcount
|
||||||
|
// says and whatever the display is.
|
||||||
|
val threadWidth = threadWidthFor(across = shortestSide.dp, repeats = 2f)
|
||||||
val titleSize = (shortestSide * 0.13f).sp
|
val titleSize = (shortestSide * 0.13f).sp
|
||||||
|
TartanBackground(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
threadWidth = threadWidth,
|
||||||
|
// On the bias, matching the icon.
|
||||||
|
rotationDegrees = 45f,
|
||||||
|
)
|
||||||
|
|
||||||
|
// A scrim keeps the lettering legible over the busiest part of the sett.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.45f))
|
||||||
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -95,7 +103,7 @@ fun SplashScreen(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Pilgrims",
|
text = "Pro Libertate",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontFamily = CelticFontFamily,
|
fontFamily = CelticFontFamily,
|
||||||
fontSize = titleSize,
|
fontSize = titleSize,
|
||||||
@@ -109,11 +117,11 @@ fun SplashScreen(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "of Katan",
|
text = "Games",
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontFamily = CelticFontFamily,
|
fontFamily = CelticFontFamily,
|
||||||
fontSize = titleSize,
|
fontSize = titleSize,
|
||||||
color = BrandAccent,
|
color = Color(0xFFF2C200),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
shadow = Shadow(
|
shadow = Shadow(
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import org.prolibertate.games.ui.common.CARD_ASPECT
|
import org.prolibertate.games.ui.common.CARD_ASPECT
|
||||||
import org.prolibertate.games.ui.theme.FeltGreenDark
|
import org.prolibertate.games.ui.theme.FeltGreenDark
|
||||||
import org.prolibertate.games.ui.theme.Parchment
|
import org.prolibertate.games.ui.theme.Parchment
|
||||||
import org.prolibertate.games.ui.theme.AccentGold
|
import org.prolibertate.games.ui.theme.WallaceGold
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The furniture every patience table needs.
|
* The furniture every patience table needs.
|
||||||
@@ -48,7 +48,7 @@ fun TableSlot(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(width)
|
.width(width)
|
||||||
.border(
|
.border(
|
||||||
border = BorderStroke(if (highlighted) 3.dp else 0.dp, AccentGold),
|
border = BorderStroke(if (highlighted) 3.dp else 0.dp, WallaceGold),
|
||||||
shape = RoundedCornerShape(4.dp),
|
shape = RoundedCornerShape(4.dp),
|
||||||
)
|
)
|
||||||
.clickable { onClick() },
|
.clickable { onClick() },
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import org.prolibertate.games.ui.common.CardBackView
|
|||||||
import org.prolibertate.games.ui.common.PlayingCardView
|
import org.prolibertate.games.ui.common.PlayingCardView
|
||||||
import org.prolibertate.games.ui.theme.FeltGreenDark
|
import org.prolibertate.games.ui.theme.FeltGreenDark
|
||||||
import org.prolibertate.games.ui.theme.Parchment
|
import org.prolibertate.games.ui.theme.Parchment
|
||||||
import org.prolibertate.games.ui.theme.AccentGold
|
import org.prolibertate.games.ui.theme.WallaceGold
|
||||||
|
|
||||||
private const val TITLE = "Klondike"
|
private const val TITLE = "Klondike"
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ private fun Slot(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(CARD_WIDTH)
|
.width(CARD_WIDTH)
|
||||||
.border(
|
.border(
|
||||||
border = BorderStroke(if (highlighted) 3.dp else 0.dp, AccentGold),
|
border = BorderStroke(if (highlighted) 3.dp else 0.dp, WallaceGold),
|
||||||
shape = RoundedCornerShape(4.dp),
|
shape = RoundedCornerShape(4.dp),
|
||||||
)
|
)
|
||||||
.clickable { onClick() },
|
.clickable { onClick() },
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ import org.prolibertate.games.net.MatchController
|
|||||||
import org.prolibertate.games.ui.ScreenScaffold
|
import org.prolibertate.games.ui.ScreenScaffold
|
||||||
import org.prolibertate.games.ui.theme.FeltGreenDark
|
import org.prolibertate.games.ui.theme.FeltGreenDark
|
||||||
import org.prolibertate.games.ui.theme.Parchment
|
import org.prolibertate.games.ui.theme.Parchment
|
||||||
import org.prolibertate.games.ui.theme.AccentGold
|
import org.prolibertate.games.ui.theme.WallaceGold
|
||||||
import org.prolibertate.games.ui.theme.AccentRed
|
import org.prolibertate.games.ui.theme.WallaceRed
|
||||||
|
|
||||||
private const val TITLE = "Peg Solitaire"
|
private const val TITLE = "Peg Solitaire"
|
||||||
|
|
||||||
@@ -214,15 +214,15 @@ private fun HoleView(
|
|||||||
Box(modifier = modifier.size(size), contentAlignment = Alignment.Center) {
|
Box(modifier = modifier.size(size), contentAlignment = Alignment.Center) {
|
||||||
val diameter = size * 0.72f
|
val diameter = size * 0.72f
|
||||||
val ring = when {
|
val ring = when {
|
||||||
selected -> AccentGold
|
selected -> WallaceGold
|
||||||
isDestination -> AccentGold
|
isDestination -> WallaceGold
|
||||||
else -> Parchment.copy(alpha = 0.35f)
|
else -> Parchment.copy(alpha = 0.35f)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(diameter)
|
.size(diameter)
|
||||||
.background(
|
.background(
|
||||||
color = if (filled) AccentRed else FeltGreenDark,
|
color = if (filled) WallaceRed else FeltGreenDark,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
)
|
)
|
||||||
.border(
|
.border(
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ import org.prolibertate.games.game.yahtzee.scoreOf
|
|||||||
import org.prolibertate.games.net.MatchController
|
import org.prolibertate.games.net.MatchController
|
||||||
import org.prolibertate.games.ui.ScreenScaffold
|
import org.prolibertate.games.ui.ScreenScaffold
|
||||||
import org.prolibertate.games.ui.theme.Parchment
|
import org.prolibertate.games.ui.theme.Parchment
|
||||||
import org.prolibertate.games.ui.theme.AccentGold
|
import org.prolibertate.games.ui.theme.WallaceGold
|
||||||
import org.prolibertate.games.ui.theme.AccentRed
|
import org.prolibertate.games.ui.theme.WallaceRed
|
||||||
|
|
||||||
private const val TITLE = "Yahtzee"
|
private const val TITLE = "Yahtzee"
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ private fun Die(face: Int?, held: Boolean, enabled: Boolean, onClick: () -> Unit
|
|||||||
.background(if (face == null) Parchment.copy(alpha = 0.3f) else Parchment, RoundedCornerShape(8.dp))
|
.background(if (face == null) Parchment.copy(alpha = 0.3f) else Parchment, RoundedCornerShape(8.dp))
|
||||||
.border(
|
.border(
|
||||||
width = if (held) 3.dp else 1.dp,
|
width = if (held) 3.dp else 1.dp,
|
||||||
color = if (held) AccentGold else AccentRed.copy(alpha = 0.4f),
|
color = if (held) WallaceGold else WallaceRed.copy(alpha = 0.4f),
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
)
|
)
|
||||||
.clickable(enabled = enabled) { onClick() },
|
.clickable(enabled = enabled) { onClick() },
|
||||||
@@ -193,7 +193,7 @@ private fun Die(face: Int?, held: Boolean, enabled: Boolean, onClick: () -> Unit
|
|||||||
text = face?.toString() ?: "·",
|
text = face?.toString() ?: "·",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = AccentRed,
|
color = WallaceRed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,7 +263,7 @@ private fun CardRow(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = if (written == null && open) AccentRed else MaterialTheme.colorScheme.onSurface,
|
color = if (written == null && open) WallaceRed else MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package org.prolibertate.games.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.rotate
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.prolibertate.games.BuildConfig
|
||||||
|
|
||||||
|
/** One band of a tartan sett, measured in threads. */
|
||||||
|
data class TartanStripe(val color: Color, val threads: Int)
|
||||||
|
|
||||||
|
val WallaceRed = Color(0xFFE01B24)
|
||||||
|
val WallaceBlack = Color(0xFF0C0C0C)
|
||||||
|
val WallaceGold = Color(0xFFF2C518)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The green the hunting sett puts where the clan sett puts red.
|
||||||
|
*
|
||||||
|
* Must match `tartan_field` for a dev build in app/build.gradle.kts, which is
|
||||||
|
* what the launcher icon is woven from — otherwise the icon and the splash it
|
||||||
|
* opens on would be visibly different cloth.
|
||||||
|
*/
|
||||||
|
val WallaceHuntingGreen = Color(0xFF1E7A3C)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands a half-sett into a full repeat by reflecting it.
|
||||||
|
*
|
||||||
|
* Tartans are specified as a half-sett between two pivots and woven out to
|
||||||
|
* both sides, so `A B C` becomes `A B C B` — the pivot stripes are not
|
||||||
|
* doubled. Writing the data this way keeps it in the same form a threadcount
|
||||||
|
* is published in, instead of a flat list that has to be kept symmetric by
|
||||||
|
* hand.
|
||||||
|
*/
|
||||||
|
fun List<TartanStripe>.pivoted(): List<TartanStripe> =
|
||||||
|
if (size < 3) this else this + drop(1).dropLast(1).reversed()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Wallace sett, from its published threadcount: **K/4 R32 K32 Y/4**.
|
||||||
|
*
|
||||||
|
* The slashes mark the pivots, so this is the half-sett and it reflects out to
|
||||||
|
* `K4 R32 K32 Y4 K32 R32` — 136 threads, roughly half black, half red, with a
|
||||||
|
* yellow line worth 3%. Two consequences that are easy to get backwards and
|
||||||
|
* that define the pattern:
|
||||||
|
*
|
||||||
|
* - the yellow overstripe runs down the **centre of a wide black band**, not
|
||||||
|
* between red and black;
|
||||||
|
* - the narrow black guard sits **between two red blocks**, splitting the red
|
||||||
|
* into pairs.
|
||||||
|
*
|
||||||
|
* The threadcount itself lives in [wallaceSett], because the hunting colourway
|
||||||
|
* below is the same cloth in a different colour and must not drift from it.
|
||||||
|
*/
|
||||||
|
val WallaceSett: List<TartanStripe> = wallaceSett(WallaceRed)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wallace Hunting: the same threadcount woven in green.
|
||||||
|
*
|
||||||
|
* Hunting setts are the clan cloth in muted, outdoor colours, and for Wallace
|
||||||
|
* that is the one substitution — green for the red — with the black ground and
|
||||||
|
* the yellow overstripe untouched. Deriving it from the same builder rather
|
||||||
|
* than writing the numbers out twice is what keeps that true: a change to the
|
||||||
|
* threadcount cannot land in one colourway and miss the other.
|
||||||
|
*/
|
||||||
|
val WallaceHuntingSett: List<TartanStripe> = wallaceSett(WallaceHuntingGreen)
|
||||||
|
|
||||||
|
/** The Wallace threadcount woven with [field] in place of the red. */
|
||||||
|
private fun wallaceSett(field: Color): List<TartanStripe> = listOf(
|
||||||
|
TartanStripe(WallaceBlack, 4),
|
||||||
|
TartanStripe(field, 32),
|
||||||
|
TartanStripe(WallaceBlack, 32),
|
||||||
|
TartanStripe(WallaceGold, 4),
|
||||||
|
).pivoted()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cloth this build wears: the clan sett on production, the hunting sett on
|
||||||
|
* dev.
|
||||||
|
*
|
||||||
|
* A dev build installs as its own app beside the production one, so it has to
|
||||||
|
* be recognisable at a glance in the app drawer and on the splash — the green
|
||||||
|
* says which of the two copies just opened without anything having to be read.
|
||||||
|
*/
|
||||||
|
val AppSett: List<TartanStripe> =
|
||||||
|
if (BuildConfig.DEV_BUILD) WallaceHuntingSett else WallaceSett
|
||||||
|
|
||||||
|
/** Threads in one full repeat of a sett. */
|
||||||
|
val List<TartanStripe>.threadsPerRepeat: Int get() = sumOf { it.threads }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The thread width that fits [repeats] repeats of [sett] across [across].
|
||||||
|
*
|
||||||
|
* Scale has to be derived from the sett rather than fixed, because changing a
|
||||||
|
* threadcount changes how many threads a repeat contains — a fixed width and a
|
||||||
|
* larger sett silently zooms in until the cloth is a couple of enormous blocks
|
||||||
|
* and stops reading as tartan at all.
|
||||||
|
*/
|
||||||
|
fun threadWidthFor(
|
||||||
|
sett: List<TartanStripe> = AppSett,
|
||||||
|
across: Dp,
|
||||||
|
repeats: Float = 2f,
|
||||||
|
): Dp = across / (sett.threadsPerRepeat * repeats)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a tartan by laying the sett down vertically as warp, then across as
|
||||||
|
* weft at half opacity. The overlap is what produces the blended squares and
|
||||||
|
* the sense of a woven cloth rather than a printed grid — red warp under black
|
||||||
|
* weft comes out a darker red, exactly as it does on the loom.
|
||||||
|
*
|
||||||
|
* [rotationDegrees] turns the cloth on the bias. The stripes are drawn well
|
||||||
|
* past the edges of the canvas so the corners stay covered once it is turned.
|
||||||
|
*/
|
||||||
|
fun DrawScope.drawTartan(
|
||||||
|
sett: List<TartanStripe> = AppSett,
|
||||||
|
threadSize: Float,
|
||||||
|
rotationDegrees: Float = 0f,
|
||||||
|
) {
|
||||||
|
if (sett.isEmpty() || threadSize <= 0f) return
|
||||||
|
val repeat = sett.sumOf { it.threads } * threadSize
|
||||||
|
if (repeat <= 0f) return
|
||||||
|
|
||||||
|
// Turning a square by 45 degrees needs about 0.21 of its size spare on
|
||||||
|
// every edge; three quarters is generous enough for any angle.
|
||||||
|
val pad = if (rotationDegrees == 0f) 0f else size.maxDimension * 0.75f
|
||||||
|
val left = -pad
|
||||||
|
val top = -pad
|
||||||
|
val right = size.width + pad
|
||||||
|
val bottom = size.height + pad
|
||||||
|
|
||||||
|
rotate(degrees = rotationDegrees) {
|
||||||
|
// Warp: full-height bands.
|
||||||
|
var x = left - (left.mod(repeat))
|
||||||
|
while (x < right) {
|
||||||
|
var offset = x
|
||||||
|
for (stripe in sett) {
|
||||||
|
val width = stripe.threads * threadSize
|
||||||
|
drawRect(
|
||||||
|
color = stripe.color,
|
||||||
|
topLeft = Offset(offset, top),
|
||||||
|
size = Size(width, bottom - top),
|
||||||
|
)
|
||||||
|
offset += width
|
||||||
|
}
|
||||||
|
x += repeat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weft: full-width bands, half opacity so the warp shows through.
|
||||||
|
var y = top - (top.mod(repeat))
|
||||||
|
while (y < bottom) {
|
||||||
|
var offset = y
|
||||||
|
for (stripe in sett) {
|
||||||
|
val height = stripe.threads * threadSize
|
||||||
|
drawRect(
|
||||||
|
color = stripe.color.copy(alpha = 0.5f),
|
||||||
|
topLeft = Offset(left, offset),
|
||||||
|
size = Size(right - left, height),
|
||||||
|
)
|
||||||
|
offset += height
|
||||||
|
}
|
||||||
|
y += repeat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tartan fill. [threadWidth] sets the weave scale, so the same cloth can be
|
||||||
|
* a fine weave on a phone and a coarse one on a tablet without redrawing it.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TartanBackground(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
sett: List<TartanStripe> = AppSett,
|
||||||
|
threadWidth: Dp = 2.dp,
|
||||||
|
rotationDegrees: Float = 0f,
|
||||||
|
) {
|
||||||
|
Canvas(modifier = modifier) {
|
||||||
|
drawTartan(
|
||||||
|
sett = sett,
|
||||||
|
threadSize = threadWidth.toPx(),
|
||||||
|
rotationDegrees = rotationDegrees,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import androidx.compose.material3.darkColorScheme
|
|||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import org.prolibertate.games.BuildConfig
|
|
||||||
|
|
||||||
/** Card-table green, which the board and table surfaces are drawn on. */
|
/** Card-table green, which the board and table surfaces are drawn on. */
|
||||||
val FeltGreen = Color(0xFF1B5E20)
|
val FeltGreen = Color(0xFF1B5E20)
|
||||||
@@ -15,31 +14,6 @@ val Parchment = Color(0xFFF4EFE4)
|
|||||||
val CardRed = Color(0xFFB3261E)
|
val CardRed = Color(0xFFB3261E)
|
||||||
val CardBlack = Color(0xFF1C1B1F)
|
val CardBlack = Color(0xFF1C1B1F)
|
||||||
|
|
||||||
/**
|
|
||||||
* The brand palette for Pilgrims of Katan — original and neutral, shared by
|
|
||||||
* every game in the catalogue. Slate blue on production, deep green on dev, so
|
|
||||||
* the two installs are told apart at a glance; both must stay in step with the
|
|
||||||
* resValues in app/build.gradle.kts or the icon and the splash it opens on
|
|
||||||
* would be different brands.
|
|
||||||
*
|
|
||||||
* The upstream shipped a clan tartan here. This project carries no clan or
|
|
||||||
* third-party trade dress (WALA-118).
|
|
||||||
*/
|
|
||||||
val BrandField: Color = if (BuildConfig.DEV_BUILD) Color(0xFF2A6B54) else Color(0xFF2B4A78)
|
|
||||||
val BrandDeep: Color = if (BuildConfig.DEV_BUILD) Color(0xFF153B2F) else Color(0xFF182A44)
|
|
||||||
|
|
||||||
/** The parchment accent: the emblem on the icon, the second line of the splash. */
|
|
||||||
val BrandAccent: Color = Color(0xFFEFD9A4)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accents used *inside* a game table — a highlight, a held die, a danger mark.
|
|
||||||
* Deliberately channel-free: what a game shows must not change with which
|
|
||||||
* install it is running in. Values unchanged from the upstream, so the games
|
|
||||||
* themselves look exactly as they did.
|
|
||||||
*/
|
|
||||||
val AccentRed = Color(0xFFE01B24)
|
|
||||||
val AccentGold = Color(0xFFF2C518)
|
|
||||||
|
|
||||||
val TeamColours: List<Color> = listOf(
|
val TeamColours: List<Color> = listOf(
|
||||||
Color(0xFF1565C0), // blue
|
Color(0xFF1565C0), // blue
|
||||||
Color(0xFFC62828), // red
|
Color(0xFFC62828), // red
|
||||||
@@ -65,7 +39,7 @@ private val LightColours = lightColorScheme(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PilgrimsTheme(
|
fun ProLibertateTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import java.net.URI
|
|||||||
* meaning to be had from comparing across them, so nothing does.
|
* meaning to be had from comparing across them, so nothing does.
|
||||||
*
|
*
|
||||||
* They also carry different applicationIds — a dev build installs as
|
* They also carry different applicationIds — a dev build installs as
|
||||||
* `…pilgrimsofkatan.dev` — so both can sit on one device, each updating
|
* `…prolibertategames.dev` — so both can sit on one device, each updating
|
||||||
* itself down its own line and leaving the other alone.
|
* itself down its own line and leaving the other alone.
|
||||||
*/
|
*/
|
||||||
enum class UpdateChannel(val label: String, val blurb: String) {
|
enum class UpdateChannel(val label: String, val blurb: String) {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class Updater(private val activity: Activity) {
|
|||||||
(URL(url).openConnection() as HttpURLConnection).apply {
|
(URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
requestMethod = "GET"
|
requestMethod = "GET"
|
||||||
setRequestProperty("Accept", "application/json")
|
setRequestProperty("Accept", "application/json")
|
||||||
setRequestProperty("User-Agent", "PilgrimsOfKatan-Updater")
|
setRequestProperty("User-Agent", "ProLibertateGames-Updater")
|
||||||
connectTimeout = 15_000
|
connectTimeout = 15_000
|
||||||
readTimeout = 15_000
|
readTimeout = 15_000
|
||||||
}
|
}
|
||||||
@@ -206,7 +206,7 @@ class Updater(private val activity: Activity) {
|
|||||||
const val SERVER = "https://git.wallacenet.ca"
|
const val SERVER = "https://git.wallacenet.ca"
|
||||||
const val SERVER_HOST = "git.wallacenet.ca"
|
const val SERVER_HOST = "git.wallacenet.ca"
|
||||||
const val OWNER = "devadmin"
|
const val OWNER = "devadmin"
|
||||||
const val REPO = "PilgrimsOfKatan"
|
const val REPO = "ProLibertateGames"
|
||||||
|
|
||||||
/** Release routes, Gitea's `/api/v1` spelling of GitHub's API. */
|
/** Release routes, Gitea's `/api/v1` spelling of GitHub's API. */
|
||||||
const val API = "$SERVER/api/v1/repos/$OWNER/$REPO"
|
const val API = "$SERVER/api/v1/repos/$OWNER/$REPO"
|
||||||
|
|||||||
@@ -1,16 +1,70 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
The flat field behind the launcher emblem: brand_field, slate blue on
|
A single square of the Wallace tartan, set on the bias.
|
||||||
production and deep green on dev, generated by the build (see resValue in
|
|
||||||
app/build.gradle.kts) so the two installs are told apart in the app drawer
|
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
|
||||||
by the icon alone.
|
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
|
||||||
|
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
|
||||||
|
centre of a wide black band, and the narrow black guard splits the red into
|
||||||
|
pairs.
|
||||||
|
|
||||||
|
The stripes reference @color/tartan_field rather than a literal, because the
|
||||||
|
same threadcount is woven in two colourways: Wallace red on a production
|
||||||
|
build, Wallace Hunting green on a dev one, so the two installs are told apart
|
||||||
|
in the app drawer by the icon alone. The build generates that colour; see
|
||||||
|
resValue in app/build.gradle.kts.
|
||||||
|
|
||||||
|
Warp is opaque, weft laid over it at half opacity, which is what gives the
|
||||||
|
blended squares rather than a flat grid. Paths run well past the viewport
|
||||||
|
because the group is rotated 45 degrees; the overspill keeps the corners
|
||||||
|
covered. One full repeat fills the adaptive icon's safe zone.
|
||||||
-->
|
-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<group
|
||||||
android:fillColor="@color/brand_field"
|
android:rotation="45"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pivotX="54.0"
|
||||||
|
android:pivotY="54.0">
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
The brand emblem: a hexagonal ring around a solid hexagonal core, in
|
Deliberately empty. The icon is the cloth itself, so the adaptive icon's
|
||||||
parchment on the brand field behind it (see ic_launcher_background).
|
foreground layer carries nothing — anything drawn here would sit on top of the
|
||||||
|
tartan and break the "single square of tartan" idea.
|
||||||
The hexagon is a game tile, not any game's mark — deliberately generic
|
|
||||||
branding for Pilgrims of Katan, carrying no third-party logo or trade dress.
|
|
||||||
Everything sits inside the adaptive icon's safe zone (a centred 66dp circle
|
|
||||||
on this 108dp canvas) so no launcher mask can clip it.
|
|
||||||
-->
|
-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<!-- Ring and core in one even-odd path: outer hexagon, inner hexagon,
|
|
||||||
core hexagon — odd coverage fills, so the ring and the core are drawn
|
|
||||||
and the gap between them stays open to the field showing through. -->
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="@color/brand_accent"
|
android:fillColor="#00000000"
|
||||||
android:fillType="evenOdd"
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,22 +1,70 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
The legacy (pre-API 26) launcher icon: the same brand emblem as the adaptive
|
A single square of the Wallace tartan, set on the bias.
|
||||||
icon's foreground, over the same brand field, in the one layer those
|
|
||||||
launchers get.
|
|
||||||
|
|
||||||
Field first, emblem second: two paths, because the emblem's ring relies on
|
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
|
||||||
even-odd parity and a filled background inside the same path would flip it.
|
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
|
||||||
|
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
|
||||||
|
centre of a wide black band, and the narrow black guard splits the red into
|
||||||
|
pairs.
|
||||||
|
|
||||||
|
The stripes reference @color/tartan_field rather than a literal, because the
|
||||||
|
same threadcount is woven in two colourways: Wallace red on a production
|
||||||
|
build, Wallace Hunting green on a dev one, so the two installs are told apart
|
||||||
|
in the app drawer by the icon alone. The build generates that colour; see
|
||||||
|
resValue in app/build.gradle.kts.
|
||||||
|
|
||||||
|
Warp is opaque, weft laid over it at half opacity, which is what gives the
|
||||||
|
blended squares rather than a flat grid. Paths run well past the viewport
|
||||||
|
because the group is rotated 45 degrees; the overspill keeps the corners
|
||||||
|
covered. One full repeat fills the adaptive icon's safe zone.
|
||||||
-->
|
-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="48dp"
|
android:width="48dp"
|
||||||
android:height="48dp"
|
android:height="48dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<group
|
||||||
android:fillColor="@color/brand_field"
|
android:rotation="45"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pivotX="54.0"
|
||||||
<path
|
android:pivotY="54.0">
|
||||||
android:fillColor="@color/brand_accent"
|
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
|
||||||
android:fillType="evenOdd"
|
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
|
||||||
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
|
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,22 +1,70 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
The legacy (pre-API 26) launcher icon: the same brand emblem as the adaptive
|
A single square of the Wallace tartan, set on the bias.
|
||||||
icon's foreground, over the same brand field, in the one layer those
|
|
||||||
launchers get.
|
|
||||||
|
|
||||||
Field first, emblem second: two paths, because the emblem's ring relies on
|
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
|
||||||
even-odd parity and a filled background inside the same path would flip it.
|
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
|
||||||
|
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
|
||||||
|
centre of a wide black band, and the narrow black guard splits the red into
|
||||||
|
pairs.
|
||||||
|
|
||||||
|
The stripes reference @color/tartan_field rather than a literal, because the
|
||||||
|
same threadcount is woven in two colourways: Wallace red on a production
|
||||||
|
build, Wallace Hunting green on a dev one, so the two installs are told apart
|
||||||
|
in the app drawer by the icon alone. The build generates that colour; see
|
||||||
|
resValue in app/build.gradle.kts.
|
||||||
|
|
||||||
|
Warp is opaque, weft laid over it at half opacity, which is what gives the
|
||||||
|
blended squares rather than a flat grid. Paths run well past the viewport
|
||||||
|
because the group is rotated 45 degrees; the overspill keeps the corners
|
||||||
|
covered. One full repeat fills the adaptive icon's safe zone.
|
||||||
-->
|
-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="48dp"
|
android:width="48dp"
|
||||||
android:height="48dp"
|
android:height="48dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<group
|
||||||
android:fillColor="@color/brand_field"
|
android:rotation="45"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pivotX="54.0"
|
||||||
<path
|
android:pivotY="54.0">
|
||||||
android:fillColor="@color/brand_accent"
|
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
|
||||||
android:fillType="evenOdd"
|
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
|
||||||
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
|
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
|
||||||
|
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
The colours the brand shares between channels. The two that differ —
|
The colours the sett shares between channels. The one that differs —
|
||||||
brand_field (slate blue on production, deep green on dev) and its darker
|
tartan_field, red on production and hunting green on dev — is generated by the
|
||||||
shade splash_background — are generated by the build. See resValue in
|
build, along with splash_background and ic_launcher_background, which are
|
||||||
app/build.gradle.kts.
|
shades of it. See resValue in app/build.gradle.kts.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
|
||||||
<!-- The parchment accent drawn on the brand field: the emblem on the
|
<!-- The ground the sett is woven on, and the overstripe down its centre.
|
||||||
launcher icon and the second line of the splash. Same number as the
|
Same numbers as WallaceBlack and WallaceGold in Tartan.kt. -->
|
||||||
accent in Theme.kt. -->
|
<color name="tartan_ground">#0C0C0C</color>
|
||||||
<color name="brand_accent">#EFD9A4</color>
|
<color name="tartan_over">#F2C518</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
app_name is generated by the build rather than declared here, because it
|
app_name is generated by the build rather than declared here, because it
|
||||||
differs by channel: a dev build installs as "Pilgrims of Katan DEV"
|
differs by channel: a dev build installs as "Pro Libertate Games DEV"
|
||||||
alongside the production copy. See resValue in app/build.gradle.kts.
|
alongside the production copy. See resValue in app/build.gradle.kts.
|
||||||
-->
|
-->
|
||||||
<resources>
|
<resources>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<!-- Compose supplies the real theme; this only needs to be a Material 3
|
<!-- Compose supplies the real theme; this only needs to be a Material 3
|
||||||
base with no action bar so the app draws its own top bars. -->
|
base with no action bar so the app draws its own top bars. -->
|
||||||
<style name="Theme.PilgrimsOfKatan" parent="android:Theme.Material.NoActionBar">
|
<style name="Theme.ProLibertateGames" parent="android:Theme.Material.NoActionBar">
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
<item name="android:windowContentOverlay">@null</item>
|
<item name="android:windowContentOverlay">@null</item>
|
||||||
<item name="android:windowBackground">@color/splash_background</item>
|
<item name="android:windowBackground">@color/splash_background</item>
|
||||||
|
|||||||
@@ -106,17 +106,19 @@ class CatalogTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `every game in the catalogue can be played`() {
|
fun `every reference game in the catalogue can be played`() {
|
||||||
// There is nothing left marked "coming soon". The machinery that hides
|
// Every game the app ships is playable. The one exception is Katan,
|
||||||
// unfinished games from a production build is still there and still
|
// listed so the slot exists and marked *Coming soon* until its engine
|
||||||
// tested above — it simply has nothing to hide at the moment, and this
|
// and screen land — `available = false` is what keeps an unfinished
|
||||||
// is what will fail the day something is added to the list before its
|
// game out of a production build while leaving it visible on a dev one.
|
||||||
// engine is written.
|
|
||||||
assertTrue(GameCatalog.all.all { it.available })
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = false),
|
listOf(GameCatalog.CATAN),
|
||||||
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = true),
|
GameCatalog.all.filterNot { it.available }.map { it.id },
|
||||||
)
|
)
|
||||||
|
// It is also the only difference the two channels see in a menu.
|
||||||
|
val hiddenFromProduction = GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = true) -
|
||||||
|
GameCatalog.inMenu(GameMenu.BOARD, includeComingSoon = false)
|
||||||
|
assertEquals(listOf(GameCatalog.byId(GameCatalog.CATAN)!!), hiddenFromProduction)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class UpdatePolicyTest {
|
|||||||
tag = tag,
|
tag = tag,
|
||||||
versionCode = versionCode,
|
versionCode = versionCode,
|
||||||
apkUrl = "https://example.invalid/$tag.apk",
|
apkUrl = "https://example.invalid/$tag.apk",
|
||||||
apkName = "PilgrimsOfKatan-$tag.apk",
|
apkName = "ProLibertateGames-$tag.apk",
|
||||||
channel = channel,
|
channel = channel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,16 +60,16 @@ class UpdatePolicyTest {
|
|||||||
// The instance reports its canonical host; the app was told to trust
|
// The instance reports its canonical host; the app was told to trust
|
||||||
// another. Same bytes, one host, whatever the server names itself.
|
// another. Same bytes, one host, whatever the server names itself.
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/" +
|
"https://git.wallacenet.ca/devadmin/ProLibertateGames/releases/download/" +
|
||||||
"v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk",
|
"v1.4.1001-dev/ProLibertateGames-v1.4.1001-dev.apk",
|
||||||
onReportedHost(
|
onReportedHost(
|
||||||
"https://git.saskvalleytech.com/devadmin/PilgrimsOfKatan/releases/download/" +
|
"https://git.saskvalleytech.com/devadmin/ProLibertateGames/releases/download/" +
|
||||||
"v1.4.1001-dev/PilgrimsOfKatan-v1.4.1001-dev.apk",
|
"v1.4.1001-dev/ProLibertateGames-v1.4.1001-dev.apk",
|
||||||
"git.wallacenet.ca",
|
"git.wallacenet.ca",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// A URL that already says the right host is left exactly as it is.
|
// A URL that already says the right host is left exactly as it is.
|
||||||
val already = "https://git.wallacenet.ca/devadmin/PilgrimsOfKatan/releases/download/v1.4.1/a.apk"
|
val already = "https://git.wallacenet.ca/devadmin/ProLibertateGames/releases/download/v1.4.1/a.apk"
|
||||||
assertEquals(already, onReportedHost(already, "git.wallacenet.ca"))
|
assertEquals(already, onReportedHost(already, "git.wallacenet.ca"))
|
||||||
// Anything unparseable survives untouched rather than being dropped.
|
// Anything unparseable survives untouched rather than being dropped.
|
||||||
assertEquals("not a url", onReportedHost("not a url", "git.wallacenet.ca"))
|
assertEquals("not a url", onReportedHost("not a url", "git.wallacenet.ca"))
|
||||||
|
|||||||
+18
-13
@@ -1,11 +1,11 @@
|
|||||||
# CI, releases, and OTA — decisions and how to build
|
# CI, releases, and OTA — decisions and how to build
|
||||||
|
|
||||||
Status as of the Pilgrims of Katan bootstrap (WALA-118).
|
Status as of the ProLibertateGames bootstrap (WALA-118).
|
||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
1. **The release channel is this instance's own releases.** The app ships from
|
1. **The release channel is this instance's own releases.** The app ships from
|
||||||
<https://git.wallacenet.ca/devadmin/PilgrimsOfKatan>, and the in-app updater
|
<https://git.wallacenet.ca/devadmin/ProLibertateGames>, and the in-app updater
|
||||||
reads that repo's releases over Gitea's REST API. GitHub is not a channel for
|
reads that repo's releases over Gitea's REST API. GitHub is not a channel for
|
||||||
this project; the only GitHub URLs left are the `upstream` remote and the
|
this project; the only GitHub URLs left are the `upstream` remote and the
|
||||||
attribution link in the README.
|
attribution link in the README.
|
||||||
@@ -80,15 +80,15 @@ release — the honest answer for a build nobody stamped.
|
|||||||
export JAVA_HOME=... # a JDK 17 install
|
export JAVA_HOME=... # a JDK 17 install
|
||||||
export ANDROID_HOME=... # SDK root (or set sdk.dir in local.properties)
|
export ANDROID_HOME=... # SDK root (or set sdk.dir in local.properties)
|
||||||
|
|
||||||
./gradlew assembleDebug # dev-channel app (default: ca.wallacenet.pilgrimsofkatan.dev)
|
./gradlew assembleDebug # dev-channel app (default: ca.wallacenet.prolibertategames.dev)
|
||||||
PLG_CHANNEL=production ./gradlew assembleDebug # production app (ca.wallacenet.pilgrimsofkatan)
|
PLG_CHANNEL=production ./gradlew assembleDebug # production app (ca.wallacenet.prolibertategames)
|
||||||
./gradlew testDebugUnitTest # JVM unit tests
|
./gradlew testDebugUnitTest # JVM unit tests
|
||||||
./gradlew lintDebug # Android lint (CI runs this too)
|
./gradlew lintDebug # Android lint (CI runs this too)
|
||||||
```
|
```
|
||||||
|
|
||||||
The two variants install side by side: different applicationId, different
|
The two variants install side by side: different applicationId, different
|
||||||
launcher name (`Pilgrims of Katan DEV` vs `Pilgrims of Katan`), different brand
|
launcher name (`Pro Libertate Games DEV` vs `Pro Libertate Games`), different
|
||||||
colour.
|
tartan sett.
|
||||||
|
|
||||||
## Manual release (works today, no runner needed)
|
## Manual release (works today, no runner needed)
|
||||||
|
|
||||||
@@ -97,10 +97,15 @@ GITEA_TOKEN=… ./scripts/publish-release.sh v1.4.1001 app/build/outputs/apk/deb
|
|||||||
```
|
```
|
||||||
|
|
||||||
The script takes `<tag> <apk> <prerelease|release> [target-sha]`, creates the
|
The script takes `<tag> <apk> <prerelease|release> [target-sha]`, creates the
|
||||||
release through `POST /api/v1/repos/{owner}/{repo}/releases`, and attaches the
|
release through `POST /api/v1/repos/{owner}/{repo}/releases`, attaches the APK
|
||||||
APK through `POST …/releases/{id}/assets` — which is the upload route Gitea's
|
through `POST …/releases/{id}/assets` — which is the upload route Gitea's
|
||||||
Swagger declares for this instance (1.27.3). If the tag already exists it
|
Swagger declares for this instance (1.27.3) — and then reads the release back
|
||||||
publishes into the existing release instead of failing, so a retry is safe.
|
and corrects its state if it did not stick. That last step is not decoration:
|
||||||
|
this instance has been observed to accept `draft: false` at create time and
|
||||||
|
store the release as a draft after the attachment lands, and a draft is
|
||||||
|
invisible to the updater, so the script verifies rather than assumes. If the
|
||||||
|
tag already exists it publishes into the existing release instead of failing,
|
||||||
|
so a retry is safe.
|
||||||
|
|
||||||
## What the in-app updater reads
|
## What the in-app updater reads
|
||||||
|
|
||||||
@@ -108,8 +113,8 @@ publishes into the existing release instead of failing, so a retry is safe.
|
|||||||
|
|
||||||
| Channel | Endpoint |
|
| Channel | Endpoint |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| production | `GET /api/v1/repos/devadmin/PilgrimsOfKatan/releases/latest` |
|
| production | `GET /api/v1/repos/devadmin/ProLibertateGames/releases/latest` |
|
||||||
| dev | `GET /api/v1/repos/devadmin/PilgrimsOfKatan/releases?limit=30` |
|
| dev | `GET /api/v1/repos/devadmin/ProLibertateGames/releases?limit=30` |
|
||||||
|
|
||||||
- `/releases/latest` is documented by Gitea as *the most recent non-prerelease,
|
- `/releases/latest` is documented by Gitea as *the most recent non-prerelease,
|
||||||
non-draft release* — the same rule GitHub implements, so the production
|
non-draft release* — the same rule GitHub implements, so the production
|
||||||
@@ -121,7 +126,7 @@ publishes into the existing release instead of failing, so a retry is safe.
|
|||||||
`prerelease`, `draft`, and assets carrying `browser_download_url`. Gitea says
|
`prerelease`, `draft`, and assets carrying `browser_download_url`. Gitea says
|
||||||
`limit` where GitHub says `per_page`.
|
`limit` where GitHub says `per_page`.
|
||||||
- Each asset must be named so it contains its tag —
|
- Each asset must be named so it contains its tag —
|
||||||
`PilgrimsOfKatan-v1.4.1001.apk` for tag `v1.4.1001` — because the updater
|
`ProLibertateGames-v1.4.1001.apk` for tag `v1.4.1001` — because the updater
|
||||||
picks the versioned APK out of the release's assets by that rule.
|
picks the versioned APK out of the release's assets by that rule.
|
||||||
|
|
||||||
`BuildConfig.OTA_ENABLED` is a kill switch, not a channel decision: it is
|
`BuildConfig.OTA_ENABLED` is a kill switch, not a channel decision: it is
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
# Environment:
|
# Environment:
|
||||||
# GITEA_TOKEN required — a token that may create releases on the repo.
|
# GITEA_TOKEN required — a token that may create releases on the repo.
|
||||||
# GITEA_API API base, default https://git.wallacenet.ca/api/v1
|
# GITEA_API API base, default https://git.wallacenet.ca/api/v1
|
||||||
# GITEA_REPO owner/name, default devadmin/PilgrimsOfKatan
|
# GITEA_REPO owner/name, default devadmin/ProLibertateGames
|
||||||
#
|
#
|
||||||
# The four arguments can also arrive as RELEASE_TAG, RELEASE_APK,
|
# The four arguments can also arrive as RELEASE_TAG, RELEASE_APK,
|
||||||
# RELEASE_KIND and RELEASE_TARGET, which is how the workflows pass them.
|
# RELEASE_KIND and RELEASE_TARGET, which is how the workflows pass them.
|
||||||
@@ -34,7 +34,7 @@ esac
|
|||||||
[ -f "$apk" ] || { echo "publish-release: no such APK: ${apk:-<unset>}" >&2; exit 2; }
|
[ -f "$apk" ] || { echo "publish-release: no such APK: ${apk:-<unset>}" >&2; exit 2; }
|
||||||
[ -n "${GITEA_TOKEN:-}" ] || { echo "publish-release: GITEA_TOKEN is not set" >&2; exit 2; }
|
[ -n "${GITEA_TOKEN:-}" ] || { echo "publish-release: GITEA_TOKEN is not set" >&2; exit 2; }
|
||||||
|
|
||||||
api="${GITEA_API:-${GITHUB_API_URL:-https://git.wallacenet.ca/api/v1}}/repos/${GITEA_REPO:-${GITHUB_REPOSITORY:-devadmin/PilgrimsOfKatan}}"
|
api="${GITEA_API:-${GITHUB_API_URL:-https://git.wallacenet.ca/api/v1}}/repos/${GITEA_REPO:-${GITHUB_REPOSITORY:-devadmin/ProLibertateGames}}"
|
||||||
|
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||||
@@ -43,7 +43,7 @@ trap 'rm -rf "$tmp"' EXIT INT TERM
|
|||||||
# updater picks the versioned APK out of a release's assets by finding the one
|
# updater picks the versioned APK out of a release's assets by finding the one
|
||||||
# whose name contains the tag, so an asset called app-debug.apk would only be
|
# whose name contains the tag, so an asset called app-debug.apk would only be
|
||||||
# found by the fallback rule.
|
# found by the fallback rule.
|
||||||
apk_name="PilgrimsOfKatan-${tag}.apk"
|
apk_name="ProLibertateGames-${tag}.apk"
|
||||||
|
|
||||||
# The tag and the file name are both derived from gradle.properties and a run
|
# The tag and the file name are both derived from gradle.properties and a run
|
||||||
# number, so neither can carry a quote or a backslash into this JSON.
|
# number, so neither can carry a quote or a backslash into this JSON.
|
||||||
@@ -56,7 +56,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cat > "$tmp/release.json" <<JSON
|
cat > "$tmp/release.json" <<JSON
|
||||||
{"tag_name":"${tag}","target_commitish":"${target}","name":"Pilgrims of Katan ${tag}","body":"${note}","prerelease":${prerelease},"draft":false}
|
{"tag_name":"${tag}","target_commitish":"${target}","name":"Pro Libertate Games ${tag}","body":"${note}","prerelease":${prerelease},"draft":false}
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
code="$(curl -sS -o "$tmp/create.json" -w '%{http_code}' -X POST \
|
code="$(curl -sS -o "$tmp/create.json" -w '%{http_code}' -X POST \
|
||||||
@@ -86,6 +86,10 @@ fi
|
|||||||
id="$(grep -o '"id":[0-9]*' "$tmp/create.json" | head -n1 | tr -dc '0-9' || true)"
|
id="$(grep -o '"id":[0-9]*' "$tmp/create.json" | head -n1 | tr -dc '0-9' || true)"
|
||||||
[ -n "$id" ] || { echo "publish-release: no release id in response" >&2; cat "$tmp/create.json" >&2; echo >&2; exit 1; }
|
[ -n "$id" ] || { echo "publish-release: no release id in response" >&2; cat "$tmp/create.json" >&2; echo >&2; exit 1; }
|
||||||
|
|
||||||
|
# Attach the APK first, then check the stored state: uploading has been
|
||||||
|
# observed to leave a freshly created release as a draft, whatever the create
|
||||||
|
# response said, so the check has to be the last thing that happens rather
|
||||||
|
# than a step in the middle.
|
||||||
code="$(curl -sS -o "$tmp/asset.json" -w '%{http_code}' -X POST \
|
code="$(curl -sS -o "$tmp/asset.json" -w '%{http_code}' -X POST \
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
-F "attachment=@${apk};filename=${apk_name}" \
|
-F "attachment=@${apk};filename=${apk_name}" \
|
||||||
@@ -98,4 +102,25 @@ if [ "$code" != "201" ] && [ "$code" != "200" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Read back what the database actually stored before declaring success. A draft
|
||||||
|
# is invisible to the updater — exactly what a channel must not silently become
|
||||||
|
# — and this instance has been seen to report one as published at create time
|
||||||
|
# and store it as a draft by the time it is read.
|
||||||
|
curl -sS -o "$tmp/stored.json" -H "Authorization: token ${GITEA_TOKEN}" "$api/releases/${id}"
|
||||||
|
if grep -q '"draft":true' "$tmp/stored.json" ||
|
||||||
|
! grep -q "\"prerelease\":${prerelease}" "$tmp/stored.json"; then
|
||||||
|
state="$(curl -sS -o "$tmp/fix.json" -w '%{http_code}' -X PATCH \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"draft\":false,\"prerelease\":${prerelease}}" \
|
||||||
|
"$api/releases/${id}")"
|
||||||
|
if [ "$state" != "200" ]; then
|
||||||
|
echo "publish-release: release ${tag} is stored with the wrong state (HTTP ${state})" >&2
|
||||||
|
cat "$tmp/fix.json" >&2
|
||||||
|
echo >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "publish-release: corrected the stored state of ${tag} (it was saved as a draft)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "publish-release: ${tag} -> release ${id} on ${api#*/repos/}, asset ${apk_name}"
|
echo "publish-release: ${tag} -> release ${id} on ${api#*/repos/}, asset ${apk_name}"
|
||||||
|
|||||||
+1
-1
@@ -14,5 +14,5 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "PilgrimsOfKatan"
|
rootProject.name = "ProLibertateGames"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
Reference in New Issue
Block a user