Compare commits
101 Commits
41b9a14c0c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cecd89f6d | |||
| 410182cf7d | |||
| 42deeaf242 | |||
| 1695684fc9 | |||
| fa2784519b | |||
| 412cc08167 | |||
| 1a028b9852 | |||
| b8bfa00196 | |||
| 1ba30028b5 | |||
| a04bb3b847 | |||
| 0517fabdbb | |||
| d0a2be15ba | |||
| fa4247cd9b | |||
| ba4d4a662b | |||
| 638a177fbb | |||
| c185bd0886 | |||
| 544a2b8be4 | |||
| 221ea8346e | |||
| 6592b44387 | |||
| e7cd079a5d | |||
| bbf8cf3f08 | |||
| 5aee503c09 | |||
| 4cda34f0b7 | |||
| 9f1e0d2cd5 | |||
| 02021b540e | |||
| cec4c3e427 | |||
| 395e4a189b | |||
| 4df30c3f40 | |||
| c78dea71e0 | |||
| 8b5bbd9531 | |||
| e9d7f7cef6 | |||
| 3e136dc288 | |||
| ea01da9b62 | |||
| 8e3ed2951d | |||
| b79960c949 | |||
| 08372995bc | |||
| 30572e3912 | |||
| c7c558cb96 | |||
| 7c7c7afad0 | |||
| 1682578501 | |||
| 8947221b27 | |||
| 2310aabdb9 | |||
| a9380a7b27 | |||
| 40abc26f5d | |||
| 734c8a4cf7 | |||
| b31228d987 | |||
| 219da8677a | |||
| ac49168c02 | |||
| 23f90d5b89 | |||
| bc62127d1a | |||
| b0839aba2a | |||
| 7fe2bc2776 | |||
| 536807e7c8 | |||
| e1098949be | |||
| 099ced377d | |||
| 498fb6af83 | |||
| 3ca038ec65 | |||
| 93397988a2 | |||
| ea42c76f84 | |||
| 84a6749b5e | |||
| 6a0b543970 | |||
| c5c6af0313 | |||
| 7bea9c1456 | |||
| 640b23804f | |||
| 70f87ab8f2 | |||
| 40c2204d7b | |||
| 1ec59ba80d | |||
| 297449ccce | |||
| 3943653a23 | |||
| 6c4304cfd8 | |||
| 662ee55e1d | |||
| 539afd1dad | |||
| 6d2ffebb92 | |||
| e43fda8551 | |||
| 4744aa167a | |||
| eb258c7324 | |||
| 947d5566a2 | |||
| f560b9d4c8 | |||
| 2422a94b9a | |||
| 245a065ac7 | |||
| 0781e817d0 | |||
| 3a83c0a2b1 | |||
| 74fe1858d4 | |||
| 41b0180b44 | |||
| e3fb5959c5 | |||
| 5cd9d0ab10 | |||
| 3e25e3b9ca | |||
| af7bb83fb9 | |||
| b5134ef86d | |||
| 8555397c43 | |||
| ba70db3e60 | |||
| 9763968db9 | |||
| 074a21ea2b | |||
| 6d2d97bfcc | |||
| 4fa5564975 | |||
| 73a56aeeb1 | |||
| e722fe2ce1 | |||
| a820e97237 | |||
| bfa9c09b28 | |||
| c7bdb9b9c9 | |||
| 63ac8c6b9e |
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "block-seasons"
|
||||
}
|
||||
}
|
||||
@@ -48,3 +48,16 @@ app.*.map.json
|
||||
lib/l10n/gen/
|
||||
.superpowers/
|
||||
CLAUDE.md
|
||||
*.pid
|
||||
|
||||
# Firebase Hosting (CLI deploy cache + generated content payload)
|
||||
.firebase/
|
||||
/deploy/content/
|
||||
|
||||
# Android release signing — NEVER commit (owner backs these up out-of-band)
|
||||
android/key.properties
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Kotlin/Gradle build caches
|
||||
android/.kotlin/
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// START: FlutterFire Configuration
|
||||
id("com.google.gms.google-services")
|
||||
id("com.google.firebase.crashlytics")
|
||||
// END: FlutterFire Configuration
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// Release signing is read from android/key.properties (gitignored). When that
|
||||
// file is absent (CI, a fresh clone, another machine) the release build falls
|
||||
// back to debug signing so `flutter build`/`flutter run --release` still works.
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.airkjw.block_seasons"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -31,11 +47,24 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = if (keystorePropertiesFile.exists()) {
|
||||
signingConfigs.getByName("release")
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "190209969950",
|
||||
"project_id": "block-seasons",
|
||||
"storage_bucket": "block-seasons.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:190209969950:android:e08dc30877b1821b44c30f",
|
||||
"android_client_info": {
|
||||
"package_name": "com.airkjw.blockseasons"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCd_3Tw5IxlO6ysp3XMQ9tBsOM1yMYl2MU"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="block_seasons"
|
||||
android:label="Block Seasons"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
@@ -25,6 +25,9 @@
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
||||
android:value="ca-app-pub-5605900229781491~8257495040"/>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -20,6 +20,10 @@ pluginManagement {
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id("com.google.gms.google-services") version("4.4.4") apply false
|
||||
id("com.google.firebase.crashlytics") version("3.0.3") apply false
|
||||
// END: FlutterFire Configuration
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Drop background-music tracks here as MP3, named by theme key:
|
||||
menu.mp3 — home/menu (SeasonTheme.fallback, bgm="menu")
|
||||
season_001.mp3 — Season 1 "First Bloom"
|
||||
season_002.mp3 — Season 2 "Summer Tide"
|
||||
|
||||
Use CC0 / royalty-free, commercial-safe tracks (see docs). The app plays
|
||||
whatever is present and stays silent (no error) for any missing track.
|
||||
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 115 KiB |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"minAppBuild": 1,
|
||||
"current": "season_002",
|
||||
"seasons": [
|
||||
{
|
||||
"seasonId": "season_001",
|
||||
"version": 1,
|
||||
"packUrl": "seasons/season_001/pack.json",
|
||||
"sha256": "8d09f85bf2d2af8c80d7de20b17b4f65a7df5587e8728a7182fed82b9e507f4a"
|
||||
},
|
||||
{
|
||||
"seasonId": "season_002",
|
||||
"version": 1,
|
||||
"packUrl": "seasons/season_002/pack.json",
|
||||
"sha256": "7be1d0082d9fa81b25938c340801baf9cc0deecdbbd0cdc2d75af443e9fb8552"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,66 +1,66 @@
|
||||
# season_001 difficulty report
|
||||
|
||||
60 stages, 80 bot runs each, generated in 10s.
|
||||
60 stages, 80 bot runs each, generated in 9s.
|
||||
|
||||
| stage | objective | moves | bot win rate | 2★/3★ movesLeft |
|
||||
|---|---|---|---|---|
|
||||
| season_001_001 | clearGems 2 | 11 | 89% | 4/5 |
|
||||
| season_001_002 | clearGems 1 | 7 | 79% | 3/4 |
|
||||
| season_001_003 | clearGems 1 | 6 | 90% | 2/3 |
|
||||
| season_001_004 | clearGems 2 | 13 | 84% | 5/6 |
|
||||
| season_001_005 | reachScore 888 | 28 | 99% | 9/12 |
|
||||
| season_001_006 | clearGems 3 | 11 | 79% | 4/5 |
|
||||
| season_001_007 | clearLines 4 | 19 | 100% | 6/8 |
|
||||
| season_001_008 | clearGems 2 | 6 | 75% | 2/3 |
|
||||
| season_001_009 | clearGems 2 | 8 | 75% | 3/4 |
|
||||
| season_001_010 | reachScore 1017 | 27 | 99% | 8/10 |
|
||||
| season_001_011 | clearGems 2 | 7 | 79% | 2/3 |
|
||||
| season_001_012 | clearGems 3 | 22 | 79% | 9/12 |
|
||||
| season_001_013 | clearGems 2 | 12 | 79% | 2/5 |
|
||||
| season_001_014 | clearLines 5 | 24 | 100% | 7/9 |
|
||||
| season_001_015 | reachScore 1243 | 30 | 100% | 8/11 |
|
||||
| season_001_016 | clearGems 4 | 23 | 73% | 7/10 |
|
||||
| season_001_017 | clearGems 3 | 18 | 73% | 10/11 |
|
||||
| season_001_018 | clearGems 4 | 14 | 83% | 4/7 |
|
||||
| season_001_019 | clearGems 3 | 16 | 78% | 5/6 |
|
||||
| season_001_020 | reachScore 1478 | 32 | 99% | 7/11 |
|
||||
| season_001_021 | clearLines 5 | 22 | 100% | 6/7 |
|
||||
| season_001_022 | clearGems 4 | 26 | 86% | 10/13 |
|
||||
| season_001_023 | clearGems 3 | 10 | 70% | 3/4 |
|
||||
| season_001_024 | clearGems 3 | 18 | 80% | 5/8 |
|
||||
| season_001_025 | reachScore 1707 | 28 | 85% | 3/6 |
|
||||
| season_001_026 | clearGems 5 | 19 | 76% | 3/7 |
|
||||
| season_001_027 | clearGems 5 | 17 | 86% | 4/8 |
|
||||
| season_001_028 | clearLines 6 | 20 | 95% | 3/4 |
|
||||
| season_001_029 | clearGems 4 | 23 | 88% | 7/10 |
|
||||
| season_001_030 | reachScore 1838 | 28 | 86% | 3/6 |
|
||||
| season_001_031 | clearGems 5 | 28 | 81% | 8/12 |
|
||||
| season_001_032 | clearGems 5 | 23 | 74% | 5/9 |
|
||||
| season_001_001 | clearGems 2 | 16 | 99% | 9/10 |
|
||||
| season_001_002 | clearGems 1 | 11 | 96% | 7/8 |
|
||||
| season_001_003 | clearGems 1 | 7 | 98% | 3/4 |
|
||||
| season_001_004 | clearGems 2 | 20 | 96% | 10/13 |
|
||||
| season_001_005 | clearGems 1 | 13 | 96% | 8/9 |
|
||||
| season_001_006 | clearGems 3 | 16 | 94% | 9/10 |
|
||||
| season_001_007 | clearGems 3 | 20 | 98% | 10/13 |
|
||||
| season_001_008 | clearGems 2 | 10 | 95% | 6/7 |
|
||||
| season_001_009 | clearGems 2 | 11 | 91% | 4/7 |
|
||||
| season_001_010 | reachScore 1017 | 37 | 99% | 18/20 |
|
||||
| season_001_011 | clearGems 2 | 11 | 90% | 6/7 |
|
||||
| season_001_012 | clearGems 3 | 29 | 90% | 14/19 |
|
||||
| season_001_013 | clearGems 2 | 15 | 93% | 5/8 |
|
||||
| season_001_014 | clearLines 5 | 32 | 100% | 15/17 |
|
||||
| season_001_015 | reachScore 1243 | 39 | 100% | 17/20 |
|
||||
| season_001_016 | clearGems 4 | 30 | 91% | 11/16 |
|
||||
| season_001_017 | clearGems 3 | 24 | 89% | 14/17 |
|
||||
| season_001_018 | clearGems 4 | 19 | 94% | 9/12 |
|
||||
| season_001_019 | clearGems 3 | 20 | 90% | 7/10 |
|
||||
| season_001_020 | reachScore 1478 | 42 | 99% | 17/21 |
|
||||
| season_001_021 | clearLines 5 | 29 | 100% | 13/14 |
|
||||
| season_001_022 | clearGems 4 | 34 | 93% | 18/21 |
|
||||
| season_001_023 | clearGems 3 | 13 | 84% | 6/7 |
|
||||
| season_001_024 | clearGems 3 | 23 | 90% | 10/12 |
|
||||
| season_001_025 | reachScore 1707 | 46 | 100% | 21/24 |
|
||||
| season_001_026 | clearGems 5 | 25 | 95% | 9/12 |
|
||||
| season_001_027 | clearGems 5 | 21 | 93% | 8/12 |
|
||||
| season_001_028 | clearLines 6 | 29 | 100% | 12/13 |
|
||||
| season_001_029 | clearGems 4 | 29 | 98% | 12/16 |
|
||||
| season_001_030 | reachScore 1838 | 41 | 100% | 15/19 |
|
||||
| season_001_031 | clearGems 5 | 36 | 90% | 14/20 |
|
||||
| season_001_032 | clearGems 5 | 29 | 89% | 10/15 |
|
||||
| season_001_033 | clearGems 4 | 24 | 73% | 11/14 |
|
||||
| season_001_034 | clearGems 4 | 21 | 74% | 5/8 |
|
||||
| season_001_034 | clearGems 4 | 27 | 94% | 8/14 |
|
||||
| season_001_035 | clearLines 8 | 24 | 88% | 2/4 |
|
||||
| season_001_036 | clearGems 6 | 25 | 65% | 5/8 |
|
||||
| season_001_037 | clearGems 6 | 17 | 86% | 6/9 |
|
||||
| season_001_038 | clearGems 6 | 29 | 78% | 10/15 |
|
||||
| season_001_039 | clearGems 6 | 29 | 73% | 6/12 |
|
||||
| season_001_040 | reachScore 2328 | 32 | 80% | 2/6 |
|
||||
| season_001_041 | clearGems 5 | 17 | 73% | 5/8 |
|
||||
| season_001_042 | clearLines 9 | 25 | 78% | 1/4 |
|
||||
| season_001_043 | clearGems 6 | 22 | 79% | 5/9 |
|
||||
| season_001_044 | clearGems 6 | 26 | 75% | 6/10 |
|
||||
| season_001_036 | clearGems 6 | 31 | 85% | 9/13 |
|
||||
| season_001_037 | clearGems 6 | 21 | 95% | 8/12 |
|
||||
| season_001_038 | clearGems 6 | 36 | 85% | 16/22 |
|
||||
| season_001_039 | clearGems 6 | 35 | 83% | 10/17 |
|
||||
| season_001_040 | reachScore 2328 | 33 | 81% | 3/7 |
|
||||
| season_001_041 | clearGems 5 | 21 | 81% | 8/12 |
|
||||
| season_001_042 | clearLines 9 | 27 | 95% | 2/5 |
|
||||
| season_001_043 | clearGems 6 | 27 | 88% | 9/14 |
|
||||
| season_001_044 | clearGems 6 | 31 | 91% | 9/15 |
|
||||
| season_001_045 | reachScore 2451 | 34 | 88% | 4/6 |
|
||||
| season_001_046 | clearGems 6 | 22 | 74% | 6/9 |
|
||||
| season_001_047 | clearGems 7 | 21 | 79% | 5/8 |
|
||||
| season_001_048 | clearGems 7 | 26 | 71% | 6/11 |
|
||||
| season_001_049 | clearLines 9 | 24 | 68% | 1/2 |
|
||||
| season_001_050 | reachScore 2726 | 37 | 93% | 4/8 |
|
||||
| season_001_051 | clearGems 6 | 24 | 78% | 5/10 |
|
||||
| season_001_052 | clearGems 6 | 21 | 65% | 5/8 |
|
||||
| season_001_053 | clearGems 6 | 28 | 83% | 9/14 |
|
||||
| season_001_054 | clearGems 7 | 21 | 78% | 6/8 |
|
||||
| season_001_055 | reachScore 2978 | 39 | 91% | 5/8 |
|
||||
| season_001_056 | clearLines 11 | 30 | 83% | 2/4 |
|
||||
| season_001_057 | clearGems 7 | 16 | 74% | 5/7 |
|
||||
| season_001_058 | clearGems 8 | 20 | 85% | 7/10 |
|
||||
| season_001_059 | clearGems 8 | 23 | 59% | 6/10 |
|
||||
| season_001_060 | reachScore 3145 | 37 | 60% | 1/5 |
|
||||
| season_001_046 | clearGems 6 | 26 | 83% | 9/13 |
|
||||
| season_001_047 | clearGems 7 | 24 | 88% | 8/11 |
|
||||
| season_001_048 | clearGems 7 | 30 | 75% | 10/15 |
|
||||
| season_001_049 | clearLines 9 | 25 | 86% | 2/3 |
|
||||
| season_001_050 | reachScore 2726 | 33 | 60% | 2/6 |
|
||||
| season_001_051 | clearGems 6 | 28 | 90% | 9/13 |
|
||||
| season_001_052 | clearGems 6 | 25 | 83% | 8/12 |
|
||||
| season_001_053 | clearGems 6 | 32 | 88% | 13/18 |
|
||||
| season_001_054 | clearGems 7 | 22 | 84% | 6/9 |
|
||||
| season_001_055 | reachScore 2978 | 38 | 89% | 4/7 |
|
||||
| season_001_056 | clearLines 11 | 28 | 69% | 1/2 |
|
||||
| season_001_057 | clearGems 7 | 18 | 76% | 7/8 |
|
||||
| season_001_058 | clearGems 8 | 22 | 88% | 9/12 |
|
||||
| season_001_059 | clearGems 8 | 26 | 71% | 7/13 |
|
||||
| season_001_060 | reachScore 3145 | 38 | 71% | 2/5 |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"seasonId": "season_001",
|
||||
"version": 1,
|
||||
"title": { "en": "First Bloom", "ko": "첫 개화" },
|
||||
"theme": { "tileSet": "spring", "background": "background.webp" },
|
||||
"theme": { "tileSet": "spring", "background": "background.webp", "bgm": "season_001" },
|
||||
"stageCount": 60,
|
||||
"baseSeed": 20260611,
|
||||
"runsPerStage": 80,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# season_002 difficulty report
|
||||
|
||||
30 stages, 80 bot runs each, generated in 4s.
|
||||
|
||||
| stage | objective | moves | bot win rate | 2★/3★ movesLeft |
|
||||
|---|---|---|---|---|
|
||||
| season_002_001 | clearGems 1 | 7 | 83% | 3/4 |
|
||||
| season_002_002 | clearGems 1 | 8 | 79% | 4/5 |
|
||||
| season_002_003 | clearGems 2 | 11 | 91% | 4/5 |
|
||||
| season_002_004 | clearGems 2 | 12 | 86% | 5/6 |
|
||||
| season_002_005 | reachScore 990 | 25 | 100% | 6/9 |
|
||||
| season_002_006 | clearGems 3 | 24 | 74% | 11/17 |
|
||||
| season_002_007 | clearLines 5 | 23 | 100% | 6/8 |
|
||||
| season_002_008 | clearGems 2 | 6 | 75% | 2/3 |
|
||||
| season_002_009 | clearGems 4 | 14 | 86% | 6/7 |
|
||||
| season_002_010 | reachScore 1476 | 31 | 100% | 9/11 |
|
||||
| season_002_011 | clearGems 4 | 16 | 95% | 6/9 |
|
||||
| season_002_012 | clearGems 4 | 15 | 70% | 5/7 |
|
||||
| season_002_013 | clearGems 4 | 20 | 75% | 9/11 |
|
||||
| season_002_014 | clearLines 7 | 27 | 99% | 5/7 |
|
||||
| season_002_015 | reachScore 1766 | 30 | 96% | 4/8 |
|
||||
| season_002_016 | clearGems 5 | 10 | 73% | 3/5 |
|
||||
| season_002_017 | clearGems 5 | 21 | 65% | 7/10 |
|
||||
| season_002_018 | clearGems 6 | 27 | 74% | 9/13 |
|
||||
| season_002_019 | clearGems 6 | 20 | 73% | 4/7 |
|
||||
| season_002_020 | reachScore 2185 | 33 | 93% | 5/8 |
|
||||
| season_002_021 | clearLines 8 | 23 | 79% | 1/3 |
|
||||
| season_002_022 | clearGems 5 | 28 | 85% | 6/12 |
|
||||
| season_002_023 | clearGems 7 | 26 | 76% | 6/10 |
|
||||
| season_002_024 | clearGems 7 | 21 | 83% | 5/10 |
|
||||
| season_002_025 | reachScore 2692 | 39 | 93% | 7/11 |
|
||||
| season_002_026 | clearGems 6 | 31 | 59% | 9/15 |
|
||||
| season_002_027 | clearGems 6 | 13 | 65% | 2/5 |
|
||||
| season_002_028 | clearLines 10 | 27 | 89% | 2/4 |
|
||||
| season_002_029 | clearGems 7 | 27 | 69% | 8/10 |
|
||||
| season_002_030 | reachScore 3006 | 37 | 80% | 2/6 |
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"seasonId": "season_002",
|
||||
"version": 1,
|
||||
"title": { "en": "Summer Tide", "ko": "여름 파도" },
|
||||
"theme": {
|
||||
"tileSet": "summer",
|
||||
"background": "",
|
||||
"backgroundGradient": [4278854704, 4279253322, 4280179302],
|
||||
"accentColor": 4285517301,
|
||||
"particleType": "petals"
|
||||
},
|
||||
"stageCount": 30,
|
||||
"baseSeed": 20260612,
|
||||
"runsPerStage": 80
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Page Not Found</title>
|
||||
|
||||
<style media="screen">
|
||||
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
|
||||
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
|
||||
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
|
||||
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
|
||||
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
|
||||
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
|
||||
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
|
||||
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
|
||||
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
|
||||
@media (max-width: 600px) {
|
||||
body, #message { margin-top: 0; background: white; box-shadow: none; }
|
||||
body { border-top: 16px solid #ffa100; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message">
|
||||
<h2>404</h2>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
|
||||
<h3>Why am I seeing this?</h3>
|
||||
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
google.com, pub-5605900229781491, DIRECT, f08c47fec0942fa0
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — 시즌마다 새로워지는 블록 퍼즐</title>
|
||||
<meta name="description" content="Block Seasons는 8×8 보드에 블록을 놓아 줄을 지우고, 몇 주마다 새 테마 시즌을 즐기는 편안한 블록 퍼즐입니다.">
|
||||
<style>
|
||||
:root{ --navy:#0E1430; --navy2:#1B2350; --accent:#5B7FFF; --ink:#EAF0FF; --muted:#9DA9C7; }
|
||||
*{ box-sizing:border-box; }
|
||||
body{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans KR",sans-serif;
|
||||
background:linear-gradient(160deg,var(--navy),var(--navy2)); color:var(--ink); line-height:1.6; }
|
||||
.wrap{ max-width:680px; margin:0 auto; padding:56px 24px 72px; }
|
||||
.mark{ display:flex; gap:6px; margin-bottom:28px; }
|
||||
.mark span{ width:30px; height:30px; border-radius:8px; box-shadow:inset 0 -3px 0 rgba(0,0,0,.18), 0 2px 6px rgba(0,0,0,.3); }
|
||||
.b1{ background:#6E8BFF; } .b2{ background:#F4B6C2; } .b3{ background:#7FD4C0; } .b4{ background:#F6CF76; }
|
||||
h1{ font-size:2.2rem; margin:0 0 6px; letter-spacing:-.5px; }
|
||||
.tag{ color:var(--accent); font-weight:600; margin:0 0 28px; font-size:1.05rem; }
|
||||
p{ color:var(--ink); }
|
||||
.lead{ font-size:1.05rem; }
|
||||
ul{ padding-left:1.1rem; } li{ margin:.3rem 0; color:var(--ink); }
|
||||
.muted{ color:var(--muted); }
|
||||
h2{ font-size:1.1rem; margin:2.4rem 0 .6rem; color:#fff; }
|
||||
.links{ display:flex; flex-wrap:wrap; gap:12px; margin:30px 0 8px; }
|
||||
.links a{ display:inline-block; text-decoration:none; padding:12px 20px; border-radius:10px;
|
||||
background:var(--accent); color:#fff; font-weight:600; }
|
||||
.links a.alt{ background:transparent; border:1px solid rgba(255,255,255,.25); color:var(--ink); }
|
||||
hr{ border:none; border-top:1px solid rgba(255,255,255,.12); margin:40px 0 24px; }
|
||||
footer{ color:var(--muted); font-size:.86rem; }
|
||||
a.inline{ color:var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<div class="mark"><span class="b1"></span><span class="b2"></span><span class="b3"></span><span class="b4"></span></div>
|
||||
|
||||
<h1>Block Seasons</h1>
|
||||
<p class="tag">시즌마다 새로워지는 블록 퍼즐 · A seasonal block puzzle</p>
|
||||
|
||||
<p class="lead">8×8 보드에 세 조각을 드래그해 가로·세로 줄을 지우는, 편안하고 예쁜 블록 퍼즐입니다.
|
||||
몇 주마다 새 테마의 시즌과 스테이지가 앱 업데이트 없이 도착하고, 시즌 1은 오프라인으로도 즐길 수 있어요.</p>
|
||||
|
||||
<ul>
|
||||
<li>시즌제 — 몇 주마다 새 테마와 스테이지</li>
|
||||
<li>일러스트 여정 맵 + 엔드리스 모드</li>
|
||||
<li>광고 강요 없는 공정한 설계, 일회성 ‘광고 제거’ 지원</li>
|
||||
<li>오프라인 플레이 (시즌 1 내장)</li>
|
||||
</ul>
|
||||
|
||||
<p class="muted">A cozy 8×8 block puzzle. Drop three pieces, clear lines, and enjoy a fresh themed
|
||||
season every few weeks — no app update needed. Season 1 plays fully offline.</p>
|
||||
|
||||
<div class="links">
|
||||
<a href="mailto:airkjw@gmail.com">문의 / Contact</a>
|
||||
<a class="alt" href="/privacy-policy.html">개인정보처리방침 / Privacy</a>
|
||||
</div>
|
||||
|
||||
<h2>지원 / Support</h2>
|
||||
<p class="muted">문의 사항은 <a class="inline" href="mailto:airkjw@gmail.com">airkjw@gmail.com</a> 으로 보내주세요.
|
||||
보통 2~3일 내에 답변드립니다. · For support, email
|
||||
<a class="inline" href="mailto:airkjw@gmail.com">airkjw@gmail.com</a>.</p>
|
||||
|
||||
<hr>
|
||||
<footer>© 2026 Joungwook Kwon · Block Seasons</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — Privacy Policy / 개인정보처리방침</title>
|
||||
<style>
|
||||
body{max-width:760px;margin:0 auto;padding:24px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.6;color:#1a1a2e}
|
||||
h1{font-size:1.5rem} h2{font-size:1.15rem;margin-top:2rem;border-bottom:1px solid #ddd;padding-bottom:4px}
|
||||
h3{font-size:1rem;margin-top:1.4rem} code{background:#f0f0f5;padding:1px 5px;border-radius:4px}
|
||||
.meta{color:#666;font-size:.9rem} hr{margin:3rem 0;border:none;border-top:2px solid #eee}
|
||||
a{color:#3a5fcd}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ========================= KOREAN ========================= -->
|
||||
<h1>Block Seasons 개인정보처리방침</h1>
|
||||
<p class="meta">최종 업데이트: 2026년 6월 14일 · 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>본 방침은 모바일 게임 <strong>Block Seasons</strong>(이하 "본 앱")의 개인정보 처리에 관한 내용을 설명합니다. 본 앱은 계정 가입이 필요 없으며, 이름·이메일 등 직접적인 개인 식별 정보를 수집하지 않습니다.</p>
|
||||
|
||||
<h2>1. 수집하는 정보</h2>
|
||||
<ul>
|
||||
<li><strong>광고 식별자</strong> (Android 광고 ID / Apple IDFA): 광고 게재 및 측정을 위해 광고 파트너(Google AdMob)가 사용합니다.</li>
|
||||
<li><strong>사용 데이터</strong> (앱 이용 통계, 화면·이벤트 상호작용): 앱 품질 개선과 분석을 위해 Firebase Analytics가 수집합니다.</li>
|
||||
<li><strong>기기 정보</strong> (기기 모델, 운영체제 버전, 대략적 지역 등): 광고·분석의 기본 진단 정보로 사용됩니다.</li>
|
||||
</ul>
|
||||
<p>본 앱 개발자는 위 정보를 통해 개인을 식별하지 않으며, 별도의 서버에 개인정보를 저장하지 않습니다. 게임 진행·설정은 기기 내부(로컬)에만 저장됩니다.</p>
|
||||
|
||||
<h2>2. 정보 이용 목적</h2>
|
||||
<ul>
|
||||
<li>광고 게재 및 수익 창출 (무료 제공을 위한 광고 기반 모델)</li>
|
||||
<li>앱 사용성 분석 및 기능·난이도 개선</li>
|
||||
<li>오류 진단 및 안정성 향상</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 제3자 제공 및 처리</h2>
|
||||
<p>본 앱은 다음 제3자 서비스를 사용하며, 해당 서비스의 정책에 따라 정보가 처리됩니다.</p>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (광고) — <a href="https://policies.google.com/privacy">Google 개인정보처리방침</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (분석) — <a href="https://firebase.google.com/support/privacy">Firebase 개인정보 보호</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. 추적 및 맞춤 광고 (iOS)</h2>
|
||||
<p>iOS에서는 앱 실행 시 <strong>추적 허용(App Tracking Transparency)</strong> 동의를 요청합니다. 동의하면 맞춤형 광고가 제공될 수 있고, 거부해도 본 앱의 모든 기능을 정상적으로 이용할 수 있으며 비맞춤형 광고가 표시됩니다.</p>
|
||||
|
||||
<h2>5. 인앱 구매</h2>
|
||||
<p>일회성 "광고 제거(Remove Ads)" 인앱 구매를 제공합니다. 결제는 Apple App Store 또는 Google Play를 통해 처리되며, 개발자는 결제 카드 등 결제 수단 정보를 수집하거나 보관하지 않습니다.</p>
|
||||
|
||||
<h2>6. 아동의 개인정보</h2>
|
||||
<p>본 앱은 만 13세 미만 아동을 주 대상으로 하지 않으며, 아동의 개인정보를 고의로 수집하지 않습니다.</p>
|
||||
|
||||
<h2>7. 데이터 보관 및 삭제</h2>
|
||||
<p>로컬 저장 데이터는 앱 삭제 시 함께 제거됩니다. 광고/분석 데이터의 처리·삭제는 위 제3자 정책을 따릅니다. 관련 문의는 아래 이메일로 연락 주십시오.</p>
|
||||
|
||||
<h2>8. 문의</h2>
|
||||
<p>개인정보 관련 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ========================= ENGLISH ========================= -->
|
||||
<h1>Block Seasons Privacy Policy</h1>
|
||||
<p class="meta">Last updated: June 14, 2026 · Contact: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>This policy describes how the mobile game <strong>Block Seasons</strong> ("the App") handles information. The App requires no account sign-up and does not collect directly identifying personal information such as your name or email.</p>
|
||||
|
||||
<h2>1. Information We Collect</h2>
|
||||
<ul>
|
||||
<li><strong>Advertising identifier</strong> (Android Advertising ID / Apple IDFA): used by our advertising partner (Google AdMob) to serve and measure ads.</li>
|
||||
<li><strong>Usage data</strong> (app interaction, screen and event analytics): collected by Firebase Analytics to improve app quality.</li>
|
||||
<li><strong>Device information</strong> (device model, OS version, coarse region): used for advertising and analytics diagnostics.</li>
|
||||
</ul>
|
||||
<p>The developer does not use this information to identify you personally and stores no personal data on its own servers. Game progress and settings are stored only locally on your device.</p>
|
||||
|
||||
<h2>2. How We Use Information</h2>
|
||||
<ul>
|
||||
<li>To serve ads and generate revenue (an ad-supported free model)</li>
|
||||
<li>To analyze usage and improve features and difficulty</li>
|
||||
<li>To diagnose errors and improve stability</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Third Parties</h2>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (advertising) — <a href="https://policies.google.com/privacy">Google Privacy Policy</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (analytics) — <a href="https://firebase.google.com/support/privacy">Firebase Privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Tracking & Personalized Ads (iOS)</h2>
|
||||
<p>On iOS the App requests <strong>App Tracking Transparency</strong> permission. If you allow it, personalized ads may be shown. If you decline, the App works fully and shows non-personalized ads.</p>
|
||||
|
||||
<h2>5. In-App Purchases</h2>
|
||||
<p>A one-time "Remove Ads" purchase is offered. Payment is handled by the Apple App Store or Google Play; the developer does not collect or store your payment details.</p>
|
||||
|
||||
<h2>6. Children's Privacy</h2>
|
||||
<p>The App is not primarily directed at children under 13 and does not knowingly collect personal information from children.</p>
|
||||
|
||||
<h2>7. Data Retention & Deletion</h2>
|
||||
<p>Locally stored data is removed when the App is uninstalled. Advertising and analytics data follow the third-party policies above. For requests, contact us below.</p>
|
||||
|
||||
<h2>8. Contact</h2>
|
||||
<p>Privacy inquiries: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
google.com, pub-5605900229781491, DIRECT, f08c47fec0942fa0
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — Privacy Policy / 개인정보처리방침</title>
|
||||
<style>
|
||||
body{max-width:760px;margin:0 auto;padding:24px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.6;color:#1a1a2e}
|
||||
h1{font-size:1.5rem} h2{font-size:1.15rem;margin-top:2rem;border-bottom:1px solid #ddd;padding-bottom:4px}
|
||||
h3{font-size:1rem;margin-top:1.4rem} code{background:#f0f0f5;padding:1px 5px;border-radius:4px}
|
||||
.meta{color:#666;font-size:.9rem} hr{margin:3rem 0;border:none;border-top:2px solid #eee}
|
||||
a{color:#3a5fcd}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ========================= KOREAN ========================= -->
|
||||
<h1>Block Seasons 개인정보처리방침</h1>
|
||||
<p class="meta">최종 업데이트: 2026년 6월 14일 · 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>본 방침은 모바일 게임 <strong>Block Seasons</strong>(이하 "본 앱")의 개인정보 처리에 관한 내용을 설명합니다. 본 앱은 계정 가입이 필요 없으며, 이름·이메일 등 직접적인 개인 식별 정보를 수집하지 않습니다.</p>
|
||||
|
||||
<h2>1. 수집하는 정보</h2>
|
||||
<ul>
|
||||
<li><strong>광고 식별자</strong> (Android 광고 ID / Apple IDFA): 광고 게재 및 측정을 위해 광고 파트너(Google AdMob)가 사용합니다.</li>
|
||||
<li><strong>사용 데이터</strong> (앱 이용 통계, 화면·이벤트 상호작용): 앱 품질 개선과 분석을 위해 Firebase Analytics가 수집합니다.</li>
|
||||
<li><strong>기기 정보</strong> (기기 모델, 운영체제 버전, 대략적 지역 등): 광고·분석의 기본 진단 정보로 사용됩니다.</li>
|
||||
</ul>
|
||||
<p>본 앱 개발자는 위 정보를 통해 개인을 식별하지 않으며, 별도의 서버에 개인정보를 저장하지 않습니다. 게임 진행·설정은 기기 내부(로컬)에만 저장됩니다.</p>
|
||||
|
||||
<h2>2. 정보 이용 목적</h2>
|
||||
<ul>
|
||||
<li>광고 게재 및 수익 창출 (무료 제공을 위한 광고 기반 모델)</li>
|
||||
<li>앱 사용성 분석 및 기능·난이도 개선</li>
|
||||
<li>오류 진단 및 안정성 향상</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 제3자 제공 및 처리</h2>
|
||||
<p>본 앱은 다음 제3자 서비스를 사용하며, 해당 서비스의 정책에 따라 정보가 처리됩니다.</p>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (광고) — <a href="https://policies.google.com/privacy">Google 개인정보처리방침</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (분석) — <a href="https://firebase.google.com/support/privacy">Firebase 개인정보 보호</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. 추적 및 맞춤 광고 (iOS)</h2>
|
||||
<p>iOS에서는 앱 실행 시 <strong>추적 허용(App Tracking Transparency)</strong> 동의를 요청합니다. 동의하면 맞춤형 광고가 제공될 수 있고, 거부해도 본 앱의 모든 기능을 정상적으로 이용할 수 있으며 비맞춤형 광고가 표시됩니다.</p>
|
||||
|
||||
<h2>5. 인앱 구매</h2>
|
||||
<p>일회성 "광고 제거(Remove Ads)" 인앱 구매를 제공합니다. 결제는 Apple App Store 또는 Google Play를 통해 처리되며, 개발자는 결제 카드 등 결제 수단 정보를 수집하거나 보관하지 않습니다.</p>
|
||||
|
||||
<h2>6. 아동의 개인정보</h2>
|
||||
<p>본 앱은 만 13세 미만 아동을 주 대상으로 하지 않으며, 아동의 개인정보를 고의로 수집하지 않습니다.</p>
|
||||
|
||||
<h2>7. 데이터 보관 및 삭제</h2>
|
||||
<p>로컬 저장 데이터는 앱 삭제 시 함께 제거됩니다. 광고/분석 데이터의 처리·삭제는 위 제3자 정책을 따릅니다. 관련 문의는 아래 이메일로 연락 주십시오.</p>
|
||||
|
||||
<h2>8. 문의</h2>
|
||||
<p>개인정보 관련 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ========================= ENGLISH ========================= -->
|
||||
<h1>Block Seasons Privacy Policy</h1>
|
||||
<p class="meta">Last updated: June 14, 2026 · Contact: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>This policy describes how the mobile game <strong>Block Seasons</strong> ("the App") handles information. The App requires no account sign-up and does not collect directly identifying personal information such as your name or email.</p>
|
||||
|
||||
<h2>1. Information We Collect</h2>
|
||||
<ul>
|
||||
<li><strong>Advertising identifier</strong> (Android Advertising ID / Apple IDFA): used by our advertising partner (Google AdMob) to serve and measure ads.</li>
|
||||
<li><strong>Usage data</strong> (app interaction, screen and event analytics): collected by Firebase Analytics to improve app quality.</li>
|
||||
<li><strong>Device information</strong> (device model, OS version, coarse region): used for advertising and analytics diagnostics.</li>
|
||||
</ul>
|
||||
<p>The developer does not use this information to identify you personally and stores no personal data on its own servers. Game progress and settings are stored only locally on your device.</p>
|
||||
|
||||
<h2>2. How We Use Information</h2>
|
||||
<ul>
|
||||
<li>To serve ads and generate revenue (an ad-supported free model)</li>
|
||||
<li>To analyze usage and improve features and difficulty</li>
|
||||
<li>To diagnose errors and improve stability</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Third Parties</h2>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (advertising) — <a href="https://policies.google.com/privacy">Google Privacy Policy</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (analytics) — <a href="https://firebase.google.com/support/privacy">Firebase Privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Tracking & Personalized Ads (iOS)</h2>
|
||||
<p>On iOS the App requests <strong>App Tracking Transparency</strong> permission. If you allow it, personalized ads may be shown. If you decline, the App works fully and shows non-personalized ads.</p>
|
||||
|
||||
<h2>5. In-App Purchases</h2>
|
||||
<p>A one-time "Remove Ads" purchase is offered. Payment is handled by the Apple App Store or Google Play; the developer does not collect or store your payment details.</p>
|
||||
|
||||
<h2>6. Children's Privacy</h2>
|
||||
<p>The App is not primarily directed at children under 13 and does not knowingly collect personal information from children.</p>
|
||||
|
||||
<h2>7. Data Retention & Deletion</h2>
|
||||
<p>Locally stored data is removed when the App is uninstalled. Advertising and analytics data follow the third-party policies above. For requests, contact us below.</p>
|
||||
|
||||
<h2>8. Contact</h2>
|
||||
<p>Privacy inquiries: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
# Firebase Hosting 시즌 배포 가이드 (오너용)
|
||||
|
||||
앱은 시작할 때마다 `CONTENT_BASE_URL/manifest.json`을 확인하고, 새 시즌 팩을
|
||||
SHA256 검증 후 내려받습니다. 호스팅은 정적 파일 서버이기만 하면 되며,
|
||||
Firebase Hosting 무료 플랜이면 충분합니다.
|
||||
|
||||
## 1회 설정 (약 15분)
|
||||
|
||||
1. https://console.firebase.google.com → **프로젝트 추가** → 이름 `block-seasons`
|
||||
→ Google Analytics **사용 설정** (이후 분석 연동에 사용).
|
||||
2. 터미널에서:
|
||||
```bash
|
||||
npm install -g firebase-tools
|
||||
firebase login
|
||||
cd "/Volumes/Macintosh 2nd/Project/My_Game_Project/BlockSeasons"
|
||||
firebase init hosting
|
||||
# → Use an existing project → block-seasons
|
||||
# → public 디렉터리: deploy
|
||||
# → single-page app: No / 자동 빌드: No
|
||||
```
|
||||
3. 생성된 호스팅 도메인(예: `https://block-seasons.web.app`)을 Claude에게
|
||||
알려주세요 — 앱의 `CONTENT_BASE_URL` 기본값(lib/main.dart)을 그 도메인으로
|
||||
맞추고, `flutterfire configure`를 함께 진행해 Firebase Analytics 백엔드도
|
||||
연결합니다.
|
||||
|
||||
## 시즌 배포 (매 시즌 약 1분)
|
||||
|
||||
새 시즌 팩을 생성한 뒤(`dart run tool/stage_generator/generate.dart ...`):
|
||||
|
||||
```bash
|
||||
cd "/Volumes/Macintosh 2nd/Project/My_Game_Project/BlockSeasons"
|
||||
dart run tool/make_manifest.dart
|
||||
|
||||
rm -rf deploy/content && mkdir -p deploy/content/seasons
|
||||
cp content/manifest.json deploy/content/
|
||||
for d in content/season_*/; do
|
||||
id=$(basename "$d")
|
||||
mkdir -p "deploy/content/seasons/$id"
|
||||
cp "$d/pack.json" "deploy/content/seasons/$id/"
|
||||
done
|
||||
|
||||
firebase deploy --only hosting
|
||||
```
|
||||
|
||||
배포 직후 모든 유저의 **다음 콜드 스타트**에서 새 시즌이 나타납니다.
|
||||
앱 업데이트·스토어 심사가 필요 없습니다.
|
||||
|
||||
## 동작 방식 요약 (참고)
|
||||
|
||||
- `manifest.json`: 시즌 목록 + 버전 + SHA256. `tool/make_manifest.dart`가 생성.
|
||||
- 클라이언트: 버전이 다른 팩만 다운로드 → SHA256 일치 시에만 원자적으로 캐시
|
||||
교체. 검증 실패·오프라인·서버 오류는 전부 조용히 무시되고 기존 캐시 또는
|
||||
번들 시즌 1로 동작.
|
||||
- 시즌 1은 앱에 번들되어 있어 인터넷이 한 번도 연결되지 않아도 게임이
|
||||
완전히 동작합니다 (E2E 검증 완료: docs/screenshots/sim_offline_fallback.png).
|
||||
- 원격 시즌 등장 검증: docs/screenshots/sim_remote_season2.png ("SEASON 2 ·
|
||||
여름 파도"가 로컬 서버 배포만으로 등장).
|
||||
|
||||
## 주의
|
||||
|
||||
- `pack.json`을 수정하면 반드시 `make_manifest.dart`를 다시 실행해 SHA256을
|
||||
갱신해야 합니다 (불일치 시 클라이언트가 팩을 거부).
|
||||
- 시즌 팩에 새 필수 필드를 도입하는 스키마 변경 시 `schemaVersion`을 올리면
|
||||
구버전 앱은 그 팩을 무시합니다 (크래시 없음).
|
||||
- `minAppBuild` 필드는 아직 클라이언트가 강제하지 않습니다 — 앱 버전 의존
|
||||
콘텐츠를 배포하기 전에 강제 로직을 추가해야 합니다 (Phase 7 체크리스트).
|
||||
@@ -0,0 +1,108 @@
|
||||
# AdMob + IAP 실제 ID 준비 가이드 (오너용)
|
||||
|
||||
코드(Phase 5)는 이미 완성돼 있고 **Google 테스트 광고**로 동작합니다. 이 문서대로
|
||||
각 콘솔에서 앱/광고단위/상품을 만들고, 마지막 "**Claude에게 보낼 값**" 목록을
|
||||
알려주시면 제가 실제 ID로 교체해 마무리합니다.
|
||||
|
||||
공통 정보:
|
||||
- 번들 ID(앱 식별자): **`com.airkjw.blockseasons`** (iOS·Android 동일)
|
||||
- 앱 이름: **Block Seasons**
|
||||
|
||||
---
|
||||
|
||||
## 1. AdMob — 앱 2개 + 광고단위 6개 (약 15분)
|
||||
|
||||
https://apps.admob.com → 좌측 **앱** → **앱 추가**.
|
||||
|
||||
### iOS 앱 등록
|
||||
1. 플랫폼: **iOS**.
|
||||
2. "앱이 앱 스토어에 등록되어 있나요?" → 아직이면 **아니요** 선택 후 수동 등록
|
||||
(앱 이름 `Block Seasons` 입력). 나중에 스토어 출시 후 연결 가능.
|
||||
3. 생성되면 그 앱의 **앱 ID**(`ca-app-pub-...~...` 형식)를 적어둡니다 → **iOS 앱 ID**.
|
||||
4. 그 앱 안에서 **광고 단위** 3개 생성:
|
||||
- **전면(Interstitial)** — 이름 예: `ios_interstitial`
|
||||
- **보상형(Rewarded)** — 이름 예: `ios_rewarded`
|
||||
- **배너(Banner)** — 이름 예: `ios_banner`
|
||||
각 광고 단위 ID(`ca-app-pub-.../...` 형식)를 적어둡니다.
|
||||
|
||||
### Android 앱 등록
|
||||
1. 플랫폼: **Android**, 같은 방식으로 등록 → **Android 앱 ID** 기록.
|
||||
2. 광고 단위 3개 생성: `android_interstitial`, `android_rewarded`, `android_banner` → 각 ID 기록.
|
||||
|
||||
> 광고 형식 설정은 기본값으로 둬도 됩니다. (보상형은 보상 금액/이름을 물어보면
|
||||
> 임의로 1 / "reward"로 두세요 — 우리 코드는 금액을 쓰지 않습니다.)
|
||||
|
||||
> ⚠️ **본인 광고를 직접 클릭하지 마세요** (AdMob 무효 트래픽 정책). 실기기
|
||||
> 테스트는 디버그 빌드(자동으로 테스트 광고)로 하거나, AdMob **설정 → 테스트
|
||||
> 기기**에 본인 기기 광고 ID를 등록하세요.
|
||||
|
||||
---
|
||||
|
||||
## 2. App Store Connect — 앱 + remove_ads IAP (약 10분)
|
||||
|
||||
https://appstoreconnect.apple.com
|
||||
|
||||
1. **앱 → +** → 새 앱 생성: 플랫폼 iOS, 이름 `Block Seasons`,
|
||||
번들 ID `com.airkjw.blockseasons`(Apple Developer에서 먼저 App ID 등록 필요),
|
||||
SKU 임의(예: `blockseasons01`).
|
||||
2. 좌측 **수익화 → 앱 내 구입 → +**:
|
||||
- 유형: **비소모성(Non-Consumable)**
|
||||
- **제품 ID: `remove_ads`** ← 코드가 이 값을 그대로 찾습니다. **반드시 동일하게.**
|
||||
- 참조 이름: `Remove Ads`, 가격: 원하는 등급(예: ₩1,500 / $0.99) 선택.
|
||||
- 현지화(영문/한글) 표시 이름·설명 입력 후 저장.
|
||||
3. **Sandbox 테스터** 계정을 만들어두면(사용자 및 액세스 → Sandbox) 결제 테스트 가능.
|
||||
|
||||
---
|
||||
|
||||
## 3. Google Play Console — 앱 + remove_ads 상품 (약 10분)
|
||||
|
||||
https://play.google.com/console
|
||||
|
||||
1. **앱 만들기**: 이름 `Block Seasons`, 게임/무료 선택.
|
||||
2. 먼저 **내부 테스트 트랙에 서명된 빌드(AAB) 1개를 업로드**해야 인앱 상품을 만들 수
|
||||
있습니다 (Play 제약). 이 빌드는 Phase 7에서 제가 만들어 드립니다 — 지금은
|
||||
1~2단계만 해두고, 상품 생성은 그 빌드 업로드 후 진행해도 됩니다.
|
||||
3. **수익 창출 → 제품 → 인앱 상품 → 상품 만들기**:
|
||||
- **제품 ID: `remove_ads`** ← iOS와 동일하게.
|
||||
- 이름 `Remove Ads`, 가격 설정, 활성화.
|
||||
4. **라이선스 테스터**(설정 → 라이선스 테스트)에 본인 Google 계정을 넣으면 샌드박스 결제 가능.
|
||||
|
||||
---
|
||||
|
||||
## 4. (Phase 7) app-ads.txt — 광고 수익 인증
|
||||
|
||||
AdMob이 광고 인벤토리 판매를 인증하려면, 스토어 리스팅의 "개발자 웹사이트"
|
||||
도메인 루트에 `app-ads.txt`가 있어야 합니다. 내용 한 줄:
|
||||
```
|
||||
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
|
||||
```
|
||||
(`pub-XXXX...`는 AdMob 게시자 ID — AdMob 설정에 표시됨.) GitHub Pages 무료 사이트면
|
||||
충분합니다. **이건 Phase 7에서 함께 처리**하니 지금은 게시자 ID만 같이 적어주세요.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Claude에게 보낼 값 (이것만 주시면 코드 교체 끝)
|
||||
|
||||
```
|
||||
AdMob 게시자 ID: pub-________________
|
||||
AdMob iOS 앱 ID: ca-app-pub-________~________
|
||||
AdMob Android 앱 ID: ca-app-pub-________~________
|
||||
|
||||
iOS 전면 광고단위: ca-app-pub-________/________
|
||||
iOS 보상형 광고단위: ca-app-pub-________/________
|
||||
iOS 배너 광고단위: ca-app-pub-________/________
|
||||
Android 전면 광고단위: ca-app-pub-________/________
|
||||
Android 보상형 광고단위: ca-app-pub-________/________
|
||||
Android 배너 광고단위: ca-app-pub-________/________
|
||||
|
||||
IAP 제품 ID: remove_ads (그대로면 OK / 다르게 만들었으면 알려주세요)
|
||||
```
|
||||
|
||||
이 값을 받으면 제가:
|
||||
1. `lib/services/ad_config.dart`의 `_real*` 6개 광고단위 ID 교체
|
||||
2. `ios/Runner/Info.plist`의 `GADApplicationIdentifier` → iOS 앱 ID로 교체
|
||||
3. `android/app/src/main/AndroidManifest.xml`의 `APPLICATION_ID` → Android 앱 ID로 교체
|
||||
4. (Phase 7) app-ads.txt 생성
|
||||
|
||||
까지 처리하고, 릴리스 빌드가 실제 광고를 띄우도록 마무리합니다.
|
||||
(IAP 제품 ID가 `remove_ads`면 IAP 코드는 변경 불필요.)
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 278 KiB |
@@ -0,0 +1 @@
|
||||
google.com, pub-5605900229781491, DIRECT, f08c47fec0942fa0
|
||||
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,167 @@
|
||||
# Phase 7 — 스토어 제출 가이드 (오너 셀프 진행용)
|
||||
|
||||
> 이 문서 하나만 위에서부터 따라가면 **Google Play + Apple App Store 제출**까지 끝납니다.
|
||||
> 코드/빌드/카피/개인정보처리방침은 모두 준비돼 있습니다. 여기 적힌 답변·문구를 **그대로 복붙**하세요.
|
||||
> 막히면 Claude에게 "이 단계 막혔어" 하고 화면을 보여주면 됩니다.
|
||||
|
||||
마지막 업데이트: 2026-06-14
|
||||
|
||||
---
|
||||
|
||||
## ✅ 지금까지 완료된 것
|
||||
**Google Play**
|
||||
- 앱 생성: **Block Seasons** (`com.airkjw.blockseasons`)
|
||||
- AAB 업로드: 내부 테스트 트랙에 `1 (1.0.0)` 게시됨
|
||||
- 테스터 목록 설정 (`0614 테스트`)
|
||||
- **판매자(결제) 프로필 설정 완료**
|
||||
- **`remove_ads` 인앱상품 생성 + 활성** (173개 지역, "이전 버전과의 호환성" ✔)
|
||||
|
||||
**Apple App Store**
|
||||
- App ID `com.airkjw.blockseasons` 등록, App Store Connect에 앱 생성
|
||||
- `remove_ads` 비소모성 IAP 생성 + 가격 설정 (상태: 메타데이터 누락 → 아래 5단계에서 마무리)
|
||||
|
||||
**공통 자산 (이미 만들어 둠)**
|
||||
- 앱 아이콘 512px: `docs/store/play_icon_512.png`
|
||||
- 피처 그래픽 1024×500: `docs/store/feature_graphic.png`
|
||||
- ✅ **개인정보처리방침 호스팅 완료**: `https://block-seasons.web.app/privacy-policy.html`
|
||||
- ✅ **app-ads.txt 호스팅 완료**: `https://block-seasons.web.app/app-ads.txt`
|
||||
- 스토어 카피 EN/KO: `docs/store/store-listing.md`
|
||||
- ✅ **스크린샷** (각 3장: 홈·플레이·점수전):
|
||||
- iOS 6.7"(1290×2796): `docs/store/screenshots/ios/`
|
||||
- Android 폰(1080×1920): `docs/store/screenshots/android/`
|
||||
- (시즌 여정 맵 컷은 추후 추가 예정 — 현재 3장으로 제출 충분)
|
||||
|
||||
---
|
||||
|
||||
# 0단계 — 먼저 해둘 3가지 (양 스토어 공통 전제)
|
||||
|
||||
### 0-1. 🔑 안드로이드 서명키 백업 (가장 중요, 분실 시 영구 업데이트 불가)
|
||||
- 파일 `BlockSeasons/android/app/upload-keystore.jks` 를 **2곳 이상**(클라우드+외장 등)에 백업
|
||||
- 비밀번호 `35f52bb88a79b4279d3acce7935c33c9` (alias `upload`)을 비밀번호 관리자에 저장
|
||||
- (Play 앱 서명을 쓰므로 이건 "업로드 키"지만 그래도 백업 필수)
|
||||
|
||||
### 0-2. ✅ 개인정보처리방침 + app-ads.txt 호스팅 — **완료** (Firebase Hosting)
|
||||
두 파일을 이미 살아있는 Firebase Hosting(`block-seasons.web.app`)에 배포 완료. **아래 URL을 스토어 폼에 그대로 복붙**하세요.
|
||||
|
||||
| 항목 | 스토어에 넣을 값 |
|
||||
|---|---|
|
||||
| **개인정보처리방침 URL** | `https://block-seasons.web.app/privacy-policy.html` |
|
||||
| **app-ads.txt** | `https://block-seasons.web.app/app-ads.txt` (live, text/plain ✔) |
|
||||
| **개발자 웹사이트 / 마케팅 URL** | `https://block-seasons.web.app` |
|
||||
|
||||
> ⚠️ AdMob이 app-ads.txt를 인식하려면 **스토어의 "웹사이트/마케팅 URL"을 반드시 `https://block-seasons.web.app`로** 적어야 합니다(여기 루트의 app-ads.txt를 크롤링). `web.app`은 그 자체가 독립 루트 도메인이라 인식에 문제없음.
|
||||
> 참고: `gru.farm` 루트는 NAS가 아니라 외부 사이트빌더(아임웹)라서 app-ads.txt를 못 올림 → Firebase로 호스팅함.
|
||||
> 재배포 방법(파일 수정 시): `docs/store/`의 원본을 `deploy/`로 복사 후 `firebase deploy --only hosting --project block-seasons`.
|
||||
|
||||
### 0-3. 결제 계약 확인
|
||||
- **Google**: 판매자 프로필 완료됨 ✔ (이미 했음)
|
||||
- **Apple**: 아래 App Store **5-④ 유료 앱 계약**을 완료해야 IAP 판매·심사 가능 (세금/뱅킹 — 본인이 입력)
|
||||
|
||||
---
|
||||
|
||||
# A. GOOGLE PLAY — 남은 순서
|
||||
|
||||
위치: Play Console → Block Seasons → **대시보드 → "앱 설정"** 항목을 위에서부터 채우면 됩니다.
|
||||
(내부 테스트 빌드는 이미 올라가 있으니, 아래는 **프로덕션 출시에 필요한 정책/리스팅** 작성입니다.)
|
||||
|
||||
### A-1. 앱 액세스 권한
|
||||
→ **"모든 기능을 제한 없이 사용할 수 있습니다"** 선택 (로그인 없음).
|
||||
|
||||
### A-2. 광고
|
||||
→ **"예, 앱에 광고가 있습니다."**
|
||||
|
||||
### A-3. 콘텐츠 등급 (설문)
|
||||
- 이메일 입력 → 카테고리 **"게임"**
|
||||
- 폭력/성적/언어/약물/도박 등 전부 **아니요** (단순 블록 퍼즐)
|
||||
- 결과: **전체이용가 / Everyone / PEGI 3** 예상
|
||||
|
||||
### A-4. 타겟층 및 콘텐츠
|
||||
- 대상 연령: **만 13세 이상**(13~15, 16~17, 18세 이상) 선택 권장 — **만 13세 미만은 선택하지 말 것**
|
||||
(아동 대상이 되면 'Designed for Families' 정책 + 아동 개인정보 의무가 생겨 광고 단가에 불리)
|
||||
- "앱이 아동의 관심을 끌도록 디자인되었나요?" → **아니요**
|
||||
|
||||
### A-5. 데이터 보안 (Data safety) — 아래 값 그대로
|
||||
- 데이터 수집·공유: **예**
|
||||
- **기기 또는 기타 ID** (광고 ID): 수집함 · 목적 **광고 또는 마케팅** · **Google과 공유함** · 사용자 연결 안 함
|
||||
- **앱 활동 / 앱 상호작용**: 수집함 · 목적 **분석** · 공유 안 함 · 사용자 연결 안 함
|
||||
- 전송 중 암호화: **예**
|
||||
- 사용자가 데이터 삭제 요청 가능: 개인정보처리방침 문의 이메일 기재로 충족
|
||||
- ⚠️ "아동 대상 앱" → **아니요**
|
||||
|
||||
### A-6. 기타 선언
|
||||
- 정부 앱 / 금융 기능 / 건강 앱 → 전부 **아니요**
|
||||
- 뉴스 앱 → 아니요. 콘텐츠 가이드라인·미국 수출법 → **동의**
|
||||
|
||||
### A-7. 메인 스토어 등록정보
|
||||
`docs/store/store-listing.md`에서 복붙:
|
||||
- **앱 이름**: `Block Seasons`
|
||||
- **간단한 설명** (≤80): KO `블록을 놓아 줄을 지우고, 몇 주마다 새 테마 시즌을 즐기세요. 광고 강요 없는 편안한 퍼즐.`
|
||||
- **자세한 설명** (≤4000): store-listing.md의 KO(또는 EN) 본문
|
||||
- **앱 아이콘** (512×512): `docs/store/play_icon_512.png`
|
||||
- **그래픽 이미지** (1024×500): `docs/store/feature_graphic.png`
|
||||
- **휴대전화 스크린샷** (최소 2장): `docs/store/screenshots/android/` (3장)
|
||||
- **카테고리**: 게임 → 퍼즐 · **태그**: 퍼즐/캐주얼
|
||||
- **개인정보처리방침 URL**: `https://block-seasons.web.app/privacy-policy.html`
|
||||
- **웹사이트(선택)**: `https://block-seasons.web.app` ← app-ads.txt 인식용으로 이 주소 권장
|
||||
- **연락처 이메일**: `airkjw@gmail.com`
|
||||
|
||||
### A-8. 프로덕션 출시
|
||||
1. **테스트 및 출시 → 프로덕션 → 새 버전 만들기**
|
||||
2. 내부 테스트의 빌드를 **"라이브러리에서 추가"**로 가져오거나 AAB 재업로드(같은 `app-release.aab`)
|
||||
3. 출시명/노트 입력 → 검토 → **출시 시작**(심사 제출)
|
||||
4. ⚠️ **신규 개인 개발자 계정**은 "비공개 테스트 20명 × 14일" 후에야 프로덕션 가능 규정이 있습니다.
|
||||
- 너는 이미 출시된 앱(복지앱)이 있어 **해당 없을 가능성이 높음**. 프로덕션에서 막히면 알려줘 — 비공개 테스트로 우회 안내할게.
|
||||
|
||||
### A-9. (출시 후) 샌드박스 결제 테스트
|
||||
- 설정 → **라이선스 테스트**에 본인 Google 계정 추가 → 내부 테스트 앱에서 `remove_ads` 구매가 **실제 청구 없이** 테스트됨.
|
||||
|
||||
---
|
||||
|
||||
# B. APPLE APP STORE — 남은 순서
|
||||
|
||||
위치: [App Store Connect](https://appstoreconnect.apple.com) → Block Seasons
|
||||
|
||||
### B-1. 빌드 업로드 (Xcode 권장 — CLI는 경로 공백 이슈 회피)
|
||||
1. Xcode로 `ios/Runner.xcworkspace` 열기
|
||||
2. 상단 기기 선택을 **"Any iOS Device (arm64)"**
|
||||
3. 메뉴 **Product ▸ Archive** (서명은 Xcode 자동 서명)
|
||||
4. Organizer 창 → **Distribute App ▸ App Store Connect ▸ Upload**
|
||||
5. 처리에 수십 분 → App Store Connect → **TestFlight**에 빌드가 나타남
|
||||
> Claude가 대신 IPA를 만들기 어려운 이유: iOS 서명은 너의 Apple 인증서가 필요해서 Xcode에서 해야 안전함. 막히면 단계별로 안내할게.
|
||||
|
||||
### B-2. 앱 정보 / 버전 정보
|
||||
- 카테고리: **게임 → 퍼즐**, 연령 등급 설문 → **4+**
|
||||
- 부제(≤30): KO `시즌마다 새로워지는 블록 퍼즐`
|
||||
- 프로모션 텍스트·설명·키워드: `docs/store/store-listing.md`에서 복붙
|
||||
- **개인정보처리방침 URL**: `https://block-seasons.web.app/privacy-policy.html`
|
||||
- **스크린샷**: `docs/store/screenshots/ios/` (1290×2796=6.7", 3장). 6.5"는 같은 컷 재업로드 또는 생략 가능
|
||||
|
||||
### B-3. 앱 개인정보 (App Privacy) — 아래 값 그대로
|
||||
| 데이터 | 수집 | 목적 | 사용자 연결 | 추적 |
|
||||
|---|---|---|---|---|
|
||||
| 기기 ID(광고 식별자) | 예 | 제3자 광고 | 아니요 | **예** |
|
||||
| 사용 데이터(제품 상호작용) | 예 | 분석, 앱 기능 | 아니요 | 아니요 |
|
||||
- 추적 사용 이유(ATT) 문구 예: "비맞춤형 대신 맞춤형 광고를 제공하기 위해 사용합니다."
|
||||
|
||||
### B-4. IAP 마무리 (`remove_ads`)
|
||||
- 표시 이름: EN `Remove Ads` / KO `광고 제거`
|
||||
- 설명: `배너·전면 광고를 영구히 제거합니다. 보상형 영상은 그대로 사용할 수 있어요.`
|
||||
- **리뷰용 스크린샷**: 앱 **설정 화면**(광고 제거 버튼 보이는 캡처) 1장 첨부
|
||||
- ⚠️ **첫 IAP는 앱 버전과 함께 제출**해야 함 (버전 화면에서 IAP 포함시켜 제출)
|
||||
|
||||
### B-5. 계약 / 제출
|
||||
- ④ **유료 앱 계약(Paid Apps)**: 비즈니스 → 계약/세금/뱅킹 완료 (본인 입력) — IAP 판매·심사 전제
|
||||
- 앱 심사 정보: 로그인 없음 → 데모 계정 불필요, 비고에 "No login required" 정도
|
||||
- **심사 제출**
|
||||
|
||||
---
|
||||
|
||||
# 권장 진행 순서 (최단 경로)
|
||||
1. **0-1 키스토어 백업** (5분, 지금)
|
||||
2. ✅ **0-2 개인정보처리방침 + app-ads.txt 호스팅 완료** (`block-seasons.web.app` — 스토어 폼에 URL 복붙만 하면 됨)
|
||||
3. **스크린샷 준비됨** ✅ (`docs/store/screenshots/`) — 리스팅에 바로 업로드
|
||||
4. **Apple B-1 빌드 업로드 시작** (처리에 시간 걸리니 먼저 걸어두기)
|
||||
5. Play A-1~A-7 + Apple B-2~B-4 문항 채우기 (대부분 위 표 복붙)
|
||||
6. Apple B-5 유료앱 계약 → 양쪽 **심사 제출**
|
||||
|
||||
문항별로 막히면 그 화면만 보여줘 — 그 부분만 같이 풀자.
|
||||
|
After Width: | Height: | Size: 152 KiB |
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — Privacy Policy / 개인정보처리방침</title>
|
||||
<style>
|
||||
body{max-width:760px;margin:0 auto;padding:24px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.6;color:#1a1a2e}
|
||||
h1{font-size:1.5rem} h2{font-size:1.15rem;margin-top:2rem;border-bottom:1px solid #ddd;padding-bottom:4px}
|
||||
h3{font-size:1rem;margin-top:1.4rem} code{background:#f0f0f5;padding:1px 5px;border-radius:4px}
|
||||
.meta{color:#666;font-size:.9rem} hr{margin:3rem 0;border:none;border-top:2px solid #eee}
|
||||
a{color:#3a5fcd}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ========================= KOREAN ========================= -->
|
||||
<h1>Block Seasons 개인정보처리방침</h1>
|
||||
<p class="meta">최종 업데이트: 2026년 6월 15일 · 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>본 방침은 모바일 게임 <strong>Block Seasons</strong>(이하 "본 앱")의 개인정보 처리에 관한 내용을 설명합니다. 본 앱은 계정 가입이 필요 없으며, 이름·이메일 등 직접적인 개인 식별 정보를 수집하지 않습니다.</p>
|
||||
|
||||
<h2>1. 수집하는 정보</h2>
|
||||
<ul>
|
||||
<li><strong>광고 식별자</strong> (Android 광고 ID / Apple IDFA): 광고 게재 및 측정을 위해 광고 파트너(Google AdMob)가 사용합니다.</li>
|
||||
<li><strong>사용 데이터</strong> (앱 이용 통계, 화면·이벤트 상호작용): 앱 품질 개선과 분석을 위해 Firebase Analytics가 수집합니다.</li>
|
||||
<li><strong>기기 정보</strong> (기기 모델, 운영체제 버전, 대략적 지역 등): 광고·분석의 기본 진단 정보로 사용됩니다.</li>
|
||||
</ul>
|
||||
<p>본 앱 개발자는 위 정보를 통해 개인을 식별하지 않으며, 별도의 서버에 개인정보를 저장하지 않습니다. 게임 진행·설정은 기기 내부(로컬)에만 저장됩니다.</p>
|
||||
|
||||
<h2>2. 정보 이용 목적</h2>
|
||||
<ul>
|
||||
<li>광고 게재 및 수익 창출 (무료 제공을 위한 광고 기반 모델)</li>
|
||||
<li>앱 사용성 분석 및 기능·난이도 개선</li>
|
||||
<li>오류 진단 및 안정성 향상</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. 제3자 제공 및 처리</h2>
|
||||
<p>본 앱은 다음 제3자 서비스를 사용하며, 해당 서비스의 정책에 따라 정보가 처리됩니다.</p>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (광고) — <a href="https://policies.google.com/privacy">Google 개인정보처리방침</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (분석) — <a href="https://firebase.google.com/support/privacy">Firebase 개인정보 보호</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. 추적 및 맞춤 광고 (iOS)</h2>
|
||||
<p>iOS에서는 앱 실행 시 <strong>추적 허용(App Tracking Transparency)</strong> 동의를 요청합니다. 동의하면 맞춤형 광고가 제공될 수 있고, 거부해도 본 앱의 모든 기능을 정상적으로 이용할 수 있으며 비맞춤형 광고가 표시됩니다.</p>
|
||||
|
||||
<h2>5. 아동의 개인정보</h2>
|
||||
<p>본 앱은 만 13세 미만 아동을 주 대상으로 하지 않으며, 아동의 개인정보를 고의로 수집하지 않습니다.</p>
|
||||
|
||||
<h2>6. 데이터 보관 및 삭제</h2>
|
||||
<p>로컬 저장 데이터는 앱 삭제 시 함께 제거됩니다. 광고/분석 데이터의 처리·삭제는 위 제3자 정책을 따릅니다. 관련 문의는 아래 이메일로 연락 주십시오.</p>
|
||||
|
||||
<h2>7. 문의</h2>
|
||||
<p>개인정보 관련 문의: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ========================= ENGLISH ========================= -->
|
||||
<h1>Block Seasons Privacy Policy</h1>
|
||||
<p class="meta">Last updated: June 15, 2026 · Contact: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
<p>This policy describes how the mobile game <strong>Block Seasons</strong> ("the App") handles information. The App requires no account sign-up and does not collect directly identifying personal information such as your name or email.</p>
|
||||
|
||||
<h2>1. Information We Collect</h2>
|
||||
<ul>
|
||||
<li><strong>Advertising identifier</strong> (Android Advertising ID / Apple IDFA): used by our advertising partner (Google AdMob) to serve and measure ads.</li>
|
||||
<li><strong>Usage data</strong> (app interaction, screen and event analytics): collected by Firebase Analytics to improve app quality.</li>
|
||||
<li><strong>Device information</strong> (device model, OS version, coarse region): used for advertising and analytics diagnostics.</li>
|
||||
</ul>
|
||||
<p>The developer does not use this information to identify you personally and stores no personal data on its own servers. Game progress and settings are stored only locally on your device.</p>
|
||||
|
||||
<h2>2. How We Use Information</h2>
|
||||
<ul>
|
||||
<li>To serve ads and generate revenue (an ad-supported free model)</li>
|
||||
<li>To analyze usage and improve features and difficulty</li>
|
||||
<li>To diagnose errors and improve stability</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Third Parties</h2>
|
||||
<ul>
|
||||
<li><strong>Google AdMob</strong> (advertising) — <a href="https://policies.google.com/privacy">Google Privacy Policy</a></li>
|
||||
<li><strong>Google Firebase / Analytics</strong> (analytics) — <a href="https://firebase.google.com/support/privacy">Firebase Privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Tracking & Personalized Ads (iOS)</h2>
|
||||
<p>On iOS the App requests <strong>App Tracking Transparency</strong> permission. If you allow it, personalized ads may be shown. If you decline, the App works fully and shows non-personalized ads.</p>
|
||||
|
||||
<h2>5. Children's Privacy</h2>
|
||||
<p>The App is not primarily directed at children under 13 and does not knowingly collect personal information from children.</p>
|
||||
|
||||
<h2>6. Data Retention & Deletion</h2>
|
||||
<p>Locally stored data is removed when the App is uninstalled. Advertising and analytics data follow the third-party policies above. For requests, contact us below.</p>
|
||||
|
||||
<h2>7. Contact</h2>
|
||||
<p>Privacy inquiries: <a href="mailto:airkjw@gmail.com">airkjw@gmail.com</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 308 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 614 KiB |
|
After Width: | Height: | Size: 693 KiB |
@@ -0,0 +1,86 @@
|
||||
# Store Listing Copy — Block Seasons
|
||||
|
||||
Paste these into App Store Connect (App Information / Version) and Google Play
|
||||
Console (Main store listing). Character limits noted; both EN and KO provided.
|
||||
|
||||
---
|
||||
|
||||
## App name
|
||||
- **EN:** `Block Seasons` (13)
|
||||
- **KO:** `블록 시즌즈` — or keep `Block Seasons` (영문 그대로도 무방)
|
||||
|
||||
## Subtitle (iOS, ≤30) / Short description (Play, ≤80)
|
||||
- **EN (iOS subtitle):** `Seasonal block puzzle bliss` (27)
|
||||
- **KO (iOS subtitle):** `시즌마다 새로워지는 블록 퍼즐` (16)
|
||||
- **EN (Play short, ≤80):** `Drop blocks, clear lines, and chase a fresh themed season every few weeks.` (73)
|
||||
- **KO (Play short, ≤80):** `블록을 놓아 줄을 지우고, 몇 주마다 새 테마 시즌을 즐기세요. 광고 강요 없는 편안한 퍼즐.` (47)
|
||||
|
||||
## Keywords (iOS, ≤100, comma-separated)
|
||||
- **EN:** `block,puzzle,blocks,brain,grid,tetris,blast,relax,season,line,casual,offline,jewel,combo`
|
||||
- **KO:** `블록,퍼즐,블럭,두뇌,그리드,테트리스,블라스트,힐링,시즌,라인,캐주얼,오프라인,콤보`
|
||||
|
||||
## Promotional text (iOS, ≤170)
|
||||
- **EN:** `New Season 2 "Summer Tide" is live — cool teal blocks and 30 fresh stages. No internet? No problem: Season 1 plays fully offline.` (131)
|
||||
- **KO:** `새 시즌 2 "여름 파도" 공개 — 시원한 청록 블록과 30개 새 스테이지. 인터넷이 없어도 시즌 1은 완전 오프라인으로 즐길 수 있어요.` (66)
|
||||
|
||||
## Description (long, ≤4000)
|
||||
|
||||
### EN
|
||||
```
|
||||
Block Seasons is a cozy, beautiful block puzzle you can actually relax with.
|
||||
|
||||
Drag three pieces at a time onto an 8×8 board, fill rows and columns, and watch
|
||||
them clear in a satisfying glossy burst. Easy to pick up, deep enough to chase
|
||||
"just one more" — chase combos, beat your best, and feel the board breathe.
|
||||
|
||||
WHAT MAKES IT DIFFERENT
|
||||
• Seasons — every few weeks a brand-new themed season arrives with fresh stages
|
||||
and its own look, no app update needed.
|
||||
• A journey, not a grid — wind your way up an illustrated map, one stage at a time.
|
||||
• Endless mode — no limits, no objectives, just you and your high score.
|
||||
• Glossy, hand-tuned visuals and a calm, drifting season backdrop.
|
||||
• Plays offline — Season 1 is built in, so you can play on a plane, a subway,
|
||||
anywhere.
|
||||
|
||||
FAIR BY DESIGN
|
||||
• No forced video before every move. Ads are spaced out, and rescue/continue is
|
||||
always your choice.
|
||||
|
||||
Whether you have two minutes or twenty, Block Seasons is the puzzle that's always
|
||||
in season. Download free and start your first season today.
|
||||
```
|
||||
|
||||
### KO
|
||||
```
|
||||
블록 시즌즈는 정말로 편안하게 즐길 수 있는, 예쁜 블록 퍼즐입니다.
|
||||
|
||||
한 번에 세 조각을 8×8 보드에 드래그해서 가로·세로 줄을 채우고, 반짝이는 글로시
|
||||
연출과 함께 줄이 사라지는 쾌감을 느껴보세요. 시작은 쉽지만 "한 판만 더"를 부르는
|
||||
깊이가 있습니다 — 콤보를 노리고, 최고 점수를 갱신하세요.
|
||||
|
||||
무엇이 다른가요
|
||||
• 시즌제 — 몇 주마다 새 테마의 시즌과 스테이지가 앱 업데이트 없이 도착합니다.
|
||||
• 그리드가 아닌 여정 — 일러스트 맵을 따라 한 스테이지씩 올라갑니다.
|
||||
• 엔드리스 모드 — 제한도 목표도 없이, 오직 최고 점수에 도전.
|
||||
• 손으로 다듬은 글로시 비주얼과 잔잔하게 흐르는 시즌 배경.
|
||||
• 오프라인 플레이 — 시즌 1이 내장돼 비행기·지하철 어디서든 즐길 수 있습니다.
|
||||
|
||||
설계부터 공정하게
|
||||
• 매 수마다 강제 영상 광고 없음. 광고는 충분히 띄엄띄엄, 구조/계속하기는 항상 선택.
|
||||
|
||||
2분이든 20분이든, 블록 시즌즈는 언제나 제철인 퍼즐입니다. 무료로 받고 첫 시즌을
|
||||
시작하세요.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category & age
|
||||
- Primary category: **Games → Puzzle** (iOS) / **Puzzle** (Play).
|
||||
- Age rating: **Everyone / 4+** — but answer the questionnaires honestly and DO
|
||||
NOT mark the app as "directed at children" (it is not — this protects ad rates
|
||||
and avoids child-privacy obligations).
|
||||
|
||||
## Required URLs (owner)
|
||||
- Support URL + Marketing URL: a simple page is fine (GitHub Pages works).
|
||||
- **Privacy Policy URL: required by both stores** — a hosted privacy policy page.
|
||||
See docs/store/phase7-submission-guide.md for what it must cover.
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Block Seasons — 시즌마다 새로워지는 블록 퍼즐</title>
|
||||
<meta name="description" content="Block Seasons는 8×8 보드에 블록을 놓아 줄을 지우고, 몇 주마다 새 테마 시즌을 즐기는 편안한 블록 퍼즐입니다.">
|
||||
<style>
|
||||
:root{ --navy:#0E1430; --navy2:#1B2350; --accent:#5B7FFF; --ink:#EAF0FF; --muted:#9DA9C7; }
|
||||
*{ box-sizing:border-box; }
|
||||
body{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans KR",sans-serif;
|
||||
background:linear-gradient(160deg,var(--navy),var(--navy2)); color:var(--ink); line-height:1.6; }
|
||||
.wrap{ max-width:680px; margin:0 auto; padding:56px 24px 72px; }
|
||||
.mark{ display:flex; gap:6px; margin-bottom:28px; }
|
||||
.mark span{ width:30px; height:30px; border-radius:8px; box-shadow:inset 0 -3px 0 rgba(0,0,0,.18), 0 2px 6px rgba(0,0,0,.3); }
|
||||
.b1{ background:#6E8BFF; } .b2{ background:#F4B6C2; } .b3{ background:#7FD4C0; } .b4{ background:#F6CF76; }
|
||||
h1{ font-size:2.2rem; margin:0 0 6px; letter-spacing:-.5px; }
|
||||
.tag{ color:var(--accent); font-weight:600; margin:0 0 28px; font-size:1.05rem; }
|
||||
p{ color:var(--ink); }
|
||||
.lead{ font-size:1.05rem; }
|
||||
ul{ padding-left:1.1rem; } li{ margin:.3rem 0; color:var(--ink); }
|
||||
.muted{ color:var(--muted); }
|
||||
h2{ font-size:1.1rem; margin:2.4rem 0 .6rem; color:#fff; }
|
||||
.links{ display:flex; flex-wrap:wrap; gap:12px; margin:30px 0 8px; }
|
||||
.links a{ display:inline-block; text-decoration:none; padding:12px 20px; border-radius:10px;
|
||||
background:var(--accent); color:#fff; font-weight:600; }
|
||||
.links a.alt{ background:transparent; border:1px solid rgba(255,255,255,.25); color:var(--ink); }
|
||||
hr{ border:none; border-top:1px solid rgba(255,255,255,.12); margin:40px 0 24px; }
|
||||
footer{ color:var(--muted); font-size:.86rem; }
|
||||
a.inline{ color:var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<div class="mark"><span class="b1"></span><span class="b2"></span><span class="b3"></span><span class="b4"></span></div>
|
||||
|
||||
<h1>Block Seasons</h1>
|
||||
<p class="tag">시즌마다 새로워지는 블록 퍼즐 · A seasonal block puzzle</p>
|
||||
|
||||
<p class="lead">8×8 보드에 세 조각을 드래그해 가로·세로 줄을 지우는, 편안하고 예쁜 블록 퍼즐입니다.
|
||||
몇 주마다 새 테마의 시즌과 스테이지가 앱 업데이트 없이 도착하고, 시즌 1은 오프라인으로도 즐길 수 있어요.</p>
|
||||
|
||||
<ul>
|
||||
<li>시즌제 — 몇 주마다 새 테마와 스테이지</li>
|
||||
<li>일러스트 여정 맵 + 엔드리스 모드</li>
|
||||
<li>광고 강요 없는 공정한 설계, 일회성 ‘광고 제거’ 지원</li>
|
||||
<li>오프라인 플레이 (시즌 1 내장)</li>
|
||||
</ul>
|
||||
|
||||
<p class="muted">A cozy 8×8 block puzzle. Drop three pieces, clear lines, and enjoy a fresh themed
|
||||
season every few weeks — no app update needed. Season 1 plays fully offline.</p>
|
||||
|
||||
<div class="links">
|
||||
<a href="mailto:airkjw@gmail.com">문의 / Contact</a>
|
||||
<a class="alt" href="/privacy-policy.html">개인정보처리방침 / Privacy</a>
|
||||
</div>
|
||||
|
||||
<h2>지원 / Support</h2>
|
||||
<p class="muted">문의 사항은 <a class="inline" href="mailto:airkjw@gmail.com">airkjw@gmail.com</a> 으로 보내주세요.
|
||||
보통 2~3일 내에 답변드립니다. · For support, email
|
||||
<a class="inline" href="mailto:airkjw@gmail.com">airkjw@gmail.com</a>.</p>
|
||||
|
||||
<hr>
|
||||
<footer>© 2026 Joungwook Kwon · Block Seasons</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,743 @@
|
||||
# Phase 6 — Localization Finalize + Icon + Juice + Store Assets Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Ship the pre-release polish — a real app icon, a Sound & vibration setting, light feel polish (button press, screen transitions, themed Settings), an EN/KO localization sweep, and store assets (feature graphic + screenshots).
|
||||
|
||||
**Architecture:** The app icon and feature graphic are drawn in Dart with `CustomPainter`/`Canvas` (reusing the in-game `paintGlossyTile`) and rasterized to PNG headlessly under `flutter test` — no external SVG/raster tooling needed. `flutter_launcher_icons` consumes those PNGs to generate every platform size. The Sound setting follows the established repo-backed Riverpod Notifier pattern (`adsRemovedProvider`/`endlessBestProvider`). Juice items are small reusable widgets/route helpers.
|
||||
|
||||
**Tech Stack:** Flutter, Riverpod 3 (plain Notifiers), `flutter_launcher_icons`, shared_preferences, `dart:ui` Picture→Image→PNG.
|
||||
|
||||
---
|
||||
|
||||
## Design constants
|
||||
|
||||
- Navy background gradient: `0xFF101736 → 0xFF192555 → 0xFF2C3168` (top-left → bottom-right).
|
||||
- Brand block colors: pink `0xFFFF7EB3`, yellow `0xFFFFD166`, cyan `0xFF6FCDF5`, green `0xFF7EDB9C`.
|
||||
- Icon layout: 2×2 block grid, group = 60% of canvas (master) / 52% (adaptive foreground), gap = 5% of canvas, block `radiusFactor` 0.24. Pink TL, yellow TR, cyan BL, green BR.
|
||||
|
||||
## File Structure
|
||||
|
||||
**New:**
|
||||
- `lib/ui/branding/app_icon_painter.dart` — paints the icon (navy bg + 2×2 glossy blocks). Reused by the generator.
|
||||
- `lib/ui/branding/feature_graphic_painter.dart` — paints the 1024×500 Play feature graphic.
|
||||
- `lib/ui/widgets/pressable_scale.dart` — tap-down scale feedback wrapper.
|
||||
- `lib/ui/widgets/fade_route.dart` — `fadeRoute<T>(Widget)` PageRoute helper.
|
||||
- `lib/state/sound_notifier.dart` — `SoundEnabledNotifier` (repo-backed bool).
|
||||
- `test/tool/generate_brand_assets_test.dart` — renders icon + feature-graphic PNGs to disk.
|
||||
- `test/data/save_repository_sound_test.dart`, `test/state/sound_notifier_test.dart`.
|
||||
- `assets/icon/icon.png`, `icon_foreground.png`, `icon_background.png` (generated, committed).
|
||||
- `docs/store/feature_graphic.png`, `docs/store/screenshots/{en,ko}/*.png` (committed).
|
||||
- `flutter_launcher_icons.yaml`.
|
||||
|
||||
**Modified:**
|
||||
- `lib/data/save_repository.dart` — additive `soundEnabled` flag.
|
||||
- `lib/state/providers.dart` — `soundEnabledProvider`; `audioServiceProvider` applies it.
|
||||
- `lib/ui/screens/game_screen.dart` — gate the 3 `HapticFeedback` calls by sound flag.
|
||||
- `lib/ui/screens/settings_screen.dart` — Sound switch + game theming.
|
||||
- `lib/ui/screens/home_screen.dart`, `season_map_screen.dart` — press feedback + fade routes.
|
||||
- `lib/l10n/app_en.arb`, `app_ko.arb` — `soundAndVibration` key (+ any sweep fixes).
|
||||
- `pubspec.yaml` — `flutter_launcher_icons` dev dep.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Sound setting persistence (SaveRepository) — TDD
|
||||
|
||||
**Files:** Modify `lib/data/save_repository.dart`; Test `test/data/save_repository_sound_test.dart`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
// test/data/save_repository_sound_test.dart
|
||||
import 'package:block_seasons/data/save_repository.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||
|
||||
test('soundEnabled defaults true and persists across reopen', () async {
|
||||
final repo = await SaveRepository.open();
|
||||
expect(repo.soundEnabled, isTrue);
|
||||
await repo.setSoundEnabled(false);
|
||||
expect(repo.soundEnabled, isFalse);
|
||||
final reopened = await SaveRepository.open();
|
||||
expect(reopened.soundEnabled, isFalse);
|
||||
});
|
||||
|
||||
test('legacy save without the sound flag reads as true', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'save_v1': '{"saveVersion":1,"progress":{},"flags":{"tutorialDone":true}}',
|
||||
});
|
||||
final repo = await SaveRepository.open();
|
||||
expect(repo.soundEnabled, isTrue);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it; expect FAIL** (`soundEnabled`/`setSoundEnabled` undefined): `flutter test test/data/save_repository_sound_test.dart`
|
||||
|
||||
- [ ] **Step 3: Implement (additive, default true)**
|
||||
|
||||
In `lib/data/save_repository.dart`:
|
||||
- Add field after `bool _adsRemoved = false;`:
|
||||
```dart
|
||||
bool _soundEnabled = true;
|
||||
```
|
||||
- In the constructor, after the `_adsRemoved = ...` block, add (default TRUE when missing):
|
||||
```dart
|
||||
_soundEnabled =
|
||||
(json['flags'] as Map<String, dynamic>?)?['soundEnabled'] as bool? ??
|
||||
true;
|
||||
```
|
||||
- Add getter near `bool get adsRemoved`:
|
||||
```dart
|
||||
bool get soundEnabled => _soundEnabled;
|
||||
```
|
||||
- Add setter near `setAdsRemoved`:
|
||||
```dart
|
||||
Future<void> setSoundEnabled(bool value) {
|
||||
_soundEnabled = value;
|
||||
return _flush();
|
||||
}
|
||||
```
|
||||
- In `_flush()`, extend the `'flags'` map:
|
||||
```dart
|
||||
'flags': {
|
||||
'tutorialDone': _tutorialDone,
|
||||
'adsRemoved': _adsRemoved,
|
||||
'soundEnabled': _soundEnabled,
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run it; expect PASS (2 tests).** Then `flutter test` (full suite) stays green.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add lib/data/save_repository.dart test/data/save_repository_sound_test.dart
|
||||
git commit -m "feat(settings): persist soundEnabled flag (additive, default true)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: soundEnabledProvider + audio/haptics wiring — TDD
|
||||
|
||||
**Files:** Create `lib/state/sound_notifier.dart`; Modify `lib/state/providers.dart`, `lib/ui/screens/game_screen.dart`; Test `test/state/sound_notifier_test.dart`
|
||||
|
||||
- [ ] **Step 1: Write the failing notifier test**
|
||||
|
||||
```dart
|
||||
// test/state/sound_notifier_test.dart
|
||||
import 'package:block_seasons/data/save_repository.dart';
|
||||
import 'package:block_seasons/state/providers.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
test('reads persisted sound flag and toggles + persists', () async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final repo = await SaveRepository.open();
|
||||
final c = ProviderContainer(
|
||||
overrides: [saveRepositoryProvider.overrideWithValue(repo)],
|
||||
);
|
||||
addTearDown(c.dispose);
|
||||
|
||||
expect(c.read(soundEnabledProvider), isTrue);
|
||||
await c.read(soundEnabledProvider.notifier).toggle();
|
||||
expect(c.read(soundEnabledProvider), isFalse);
|
||||
expect(repo.soundEnabled, isFalse);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it; expect FAIL** (`soundEnabledProvider` undefined).
|
||||
|
||||
- [ ] **Step 3: Create the notifier**
|
||||
|
||||
```dart
|
||||
// lib/state/sound_notifier.dart
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'providers.dart';
|
||||
|
||||
/// SFX + gameplay haptics on/off, seeded from the save repository.
|
||||
class SoundEnabledNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() => ref.read(saveRepositoryProvider).soundEnabled;
|
||||
|
||||
Future<void> toggle() => set(!state);
|
||||
|
||||
Future<void> set(bool value) async {
|
||||
if (state == value) return;
|
||||
await ref.read(saveRepositoryProvider).setSoundEnabled(value);
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Register provider + drive AudioService** in `lib/state/providers.dart`
|
||||
|
||||
Add import:
|
||||
```dart
|
||||
import 'sound_notifier.dart';
|
||||
```
|
||||
Add the provider (near `audioServiceProvider`):
|
||||
```dart
|
||||
final soundEnabledProvider =
|
||||
NotifierProvider<SoundEnabledNotifier, bool>(SoundEnabledNotifier.new);
|
||||
```
|
||||
Replace the `audioServiceProvider` body so it applies the flag live:
|
||||
```dart
|
||||
final audioServiceProvider = Provider<AudioService>((ref) {
|
||||
final service = AudioService(enabled: ref.read(soundEnabledProvider));
|
||||
ref.listen<bool>(soundEnabledProvider, (_, next) => service.enabled = next);
|
||||
ref.onDispose(service.dispose);
|
||||
return service;
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Gate gameplay haptics by the flag** in `lib/ui/screens/game_screen.dart`
|
||||
|
||||
In `_onSessionChange`, the placement block currently calls `HapticFeedback.mediumImpact()`, `HapticFeedback.heavyImpact()`, `HapticFeedback.lightImpact()`. Read the flag once at the top of that placement branch and guard each call. Concretely, where the code does `if (placement.linesCleared > 0) { audio.play(...); HapticFeedback.mediumImpact(); ... } else { audio.play(Sfx.place); HapticFeedback.lightImpact(); }`, capture:
|
||||
```dart
|
||||
final hapticsOn = ref.read(soundEnabledProvider);
|
||||
```
|
||||
just before that `if`, and wrap each of the three `HapticFeedback.*` calls as `if (hapticsOn) HapticFeedback.mediumImpact();` etc. (Audio is already gated inside `AudioService.play`.)
|
||||
|
||||
- [ ] **Step 6: Run the notifier test (PASS) + full suite + analyze**
|
||||
```bash
|
||||
flutter test test/state/sound_notifier_test.dart
|
||||
flutter analyze lib/state/providers.dart lib/state/sound_notifier.dart lib/ui/screens/game_screen.dart
|
||||
flutter test
|
||||
```
|
||||
Expected: notifier test passes, analyze clean, full suite green.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
```bash
|
||||
git add lib/state/sound_notifier.dart lib/state/providers.dart lib/ui/screens/game_screen.dart test/state/sound_notifier_test.dart
|
||||
git commit -m "feat(settings): soundEnabled provider gates SFX and haptics"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Settings screen — Sound switch + game theming + l10n
|
||||
|
||||
**Files:** Modify `lib/ui/screens/settings_screen.dart`, `lib/l10n/app_en.arb`, `lib/l10n/app_ko.arb`
|
||||
|
||||
- [ ] **Step 1: Add l10n key (en)** — in `app_en.arb`:
|
||||
```json
|
||||
"soundAndVibration": "Sound & vibration",
|
||||
```
|
||||
- [ ] **Step 2: Add l10n key (ko)** — in `app_ko.arb`:
|
||||
```json
|
||||
"soundAndVibration": "소리 및 진동",
|
||||
```
|
||||
- [ ] **Step 3: Regenerate** — `flutter gen-l10n` (adds `soundAndVibration` getter).
|
||||
|
||||
- [ ] **Step 4: Add the Sound switch + theme the screen**
|
||||
|
||||
In `settings_screen.dart`, (a) read sound state, (b) add a `SwitchListTile` at the top of the list, (c) wrap the body in the game's `SeasonBackground` with a transparent scaffold. Replace the `build` return with:
|
||||
|
||||
```dart
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final adsRemoved = ref.watch(adsRemovedProvider);
|
||||
final soundOn = ref.watch(soundEnabledProvider);
|
||||
final iap = ref.read(iapServiceProvider);
|
||||
|
||||
ref.listen<bool>(adsRemovedProvider, (prev, next) {
|
||||
if (next && !(prev ?? false)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.adsRemovedThanks)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const SeasonBackground(theme: SeasonTheme.fallback),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
title: Text(l10n.settings),
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text(l10n.soundAndVibration),
|
||||
value: soundOn,
|
||||
onChanged: (v) =>
|
||||
ref.read(soundEnabledProvider.notifier).set(v),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(l10n.removeAds),
|
||||
subtitle: Text(l10n.removeAdsDescription),
|
||||
trailing: adsRemoved
|
||||
? const Icon(Icons.check_circle, color: Colors.green)
|
||||
: Text(iap.product?.price ?? ''),
|
||||
onTap: adsRemoved
|
||||
? null
|
||||
: () async {
|
||||
if (!iap.available || iap.product == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.purchaseUnavailable)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await iap.buyRemoveAds();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.restore),
|
||||
title: Text(l10n.restorePurchases),
|
||||
onTap: () => iap.restorePurchases(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
```
|
||||
Add imports: `import '../../game/models/season.dart';` (for `SeasonTheme`) and `import '../widgets/season_background.dart';`.
|
||||
|
||||
- [ ] **Step 5: Analyze + full suite**
|
||||
```bash
|
||||
flutter analyze lib/ui/screens/settings_screen.dart
|
||||
flutter test
|
||||
```
|
||||
Expected: clean; green.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
```bash
|
||||
git add lib/ui/screens/settings_screen.dart lib/l10n/app_en.arb lib/l10n/app_ko.arb
|
||||
git commit -m "feat(settings): sound & vibration toggle; themed settings screen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: App icon painter + PNG generator
|
||||
|
||||
**Files:** Create `lib/ui/branding/app_icon_painter.dart`, `test/tool/generate_brand_assets_test.dart`; output `assets/icon/*.png`
|
||||
|
||||
- [ ] **Step 1: Write the icon painter**
|
||||
|
||||
```dart
|
||||
// lib/ui/branding/app_icon_painter.dart
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../widgets/tile_painter.dart';
|
||||
|
||||
/// Draws the Block Seasons brand mark: deep-navy field + a 2×2 grid of glossy
|
||||
/// brand-color blocks. Shared by the launcher-icon and feature-graphic
|
||||
/// generators so the brand stays identical everywhere.
|
||||
class AppIconMark {
|
||||
static const navy = [Color(0xFF101736), Color(0xFF192555), Color(0xFF2C3168)];
|
||||
static const pink = Color(0xFFFF7EB3);
|
||||
static const yellow = Color(0xFFFFD166);
|
||||
static const cyan = Color(0xFF6FCDF5);
|
||||
static const green = Color(0xFF7EDB9C);
|
||||
|
||||
/// Fills [rect] with the navy gradient.
|
||||
static void paintBackground(Canvas canvas, Rect rect) {
|
||||
final paint = Paint()
|
||||
..shader = const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: navy,
|
||||
).createShader(rect);
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
|
||||
/// Paints the 2×2 glossy blocks centered in a square of side [size], the
|
||||
/// block group occupying [groupFraction] of the side.
|
||||
static void paintBlocks(Canvas canvas, double size,
|
||||
{double groupFraction = 0.6}) {
|
||||
final group = size * groupFraction;
|
||||
final gap = size * 0.05;
|
||||
final block = (group - gap) / 2;
|
||||
final m = (size - group) / 2;
|
||||
final far = m + block + gap;
|
||||
void tile(double x, double y, Color c) => paintGlossyTile(
|
||||
canvas, Rect.fromLTWH(x, y, block, block), c, radiusFactor: 0.24);
|
||||
tile(m, m, pink);
|
||||
tile(far, m, yellow);
|
||||
tile(m, far, cyan);
|
||||
tile(far, far, green);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the generator (renders PNGs under flutter test)**
|
||||
|
||||
```dart
|
||||
// test/tool/generate_brand_assets_test.dart
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:block_seasons/ui/branding/app_icon_painter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Future<void> _writePng(String path, int size, void Function(Canvas) draw) async {
|
||||
final recorder = PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
draw(canvas);
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(size, size);
|
||||
final bytes = await image.toByteData(format: ImageByteFormat.png);
|
||||
File(path).parent.createSync(recursive: true);
|
||||
File(path).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('generate launcher icon PNGs', (tester) async {
|
||||
const s = 1024;
|
||||
final full = Rect.fromLTWH(0, 0, s.toDouble(), s.toDouble());
|
||||
|
||||
// Master (iOS + fallback): opaque navy + blocks at 60%.
|
||||
await _writePng('assets/icon/icon.png', s, (c) {
|
||||
AppIconMark.paintBackground(c, full);
|
||||
AppIconMark.paintBlocks(c, s.toDouble(), groupFraction: 0.6);
|
||||
});
|
||||
// Adaptive background: navy only.
|
||||
await _writePng('assets/icon/icon_background.png', s, (c) {
|
||||
AppIconMark.paintBackground(c, full);
|
||||
});
|
||||
// Adaptive foreground: blocks only (transparent), 52% for the safe zone.
|
||||
await _writePng('assets/icon/icon_foreground.png', s, (c) {
|
||||
AppIconMark.paintBlocks(c, s.toDouble(), groupFraction: 0.52);
|
||||
});
|
||||
|
||||
for (final f in ['icon.png', 'icon_background.png', 'icon_foreground.png']) {
|
||||
expect(File('assets/icon/$f').existsSync(), isTrue, reason: f);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Generate the PNGs**
|
||||
|
||||
Run: `flutter test test/tool/generate_brand_assets_test.dart`
|
||||
Expected: PASS; `assets/icon/icon.png`, `icon_background.png`, `icon_foreground.png` exist (1024×1024). Verify visually:
|
||||
```bash
|
||||
file assets/icon/icon.png # PNG image data, 1024 x 1024
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add lib/ui/branding/app_icon_painter.dart test/tool/generate_brand_assets_test.dart assets/icon/icon.png assets/icon/icon_background.png assets/icon/icon_foreground.png
|
||||
git commit -m "feat(brand): app icon painter + generated 1024px icon PNGs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: flutter_launcher_icons — generate platform icons
|
||||
|
||||
**Files:** Modify `pubspec.yaml`; Create `flutter_launcher_icons.yaml`
|
||||
|
||||
- [ ] **Step 1: Add the dev dependency**
|
||||
|
||||
Run: `flutter pub add --dev flutter_launcher_icons`
|
||||
Expected: `flutter_launcher_icons` under `dev_dependencies`.
|
||||
|
||||
- [ ] **Step 2: Write `flutter_launcher_icons.yaml`**
|
||||
|
||||
```yaml
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/icon/icon.png"
|
||||
remove_alpha_ios: true
|
||||
min_sdk_android: 21
|
||||
adaptive_icon_background: "assets/icon/icon_background.png"
|
||||
adaptive_icon_foreground: "assets/icon/icon_foreground.png"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Generate**
|
||||
|
||||
Run: `dart run flutter_launcher_icons`
|
||||
Expected: "Successfully generated launcher icons". It overwrites
|
||||
`ios/Runner/Assets.xcassets/AppIcon.appiconset/*` and
|
||||
`android/app/src/main/res/mipmap-*/*`.
|
||||
|
||||
- [ ] **Step 4: Sanity-check outputs**
|
||||
```bash
|
||||
ls ios/Runner/Assets.xcassets/AppIcon.appiconset/ | head
|
||||
ls android/app/src/main/res/mipmap-hdpi/
|
||||
```
|
||||
Expected: regenerated icon PNGs present; an `mipmap-anydpi-v26/ic_launcher.xml` (adaptive) on Android.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
```bash
|
||||
git add pubspec.yaml pubspec.lock flutter_launcher_icons.yaml ios/Runner/Assets.xcassets/AppIcon.appiconset android/app/src/main/res
|
||||
git commit -m "build(brand): generate iOS/Android launcher icons from brand mark"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Juice — press feedback + fade transitions
|
||||
|
||||
**Files:** Create `lib/ui/widgets/pressable_scale.dart`, `lib/ui/widgets/fade_route.dart`; Modify `lib/ui/screens/home_screen.dart`, `lib/ui/screens/season_map_screen.dart`
|
||||
|
||||
- [ ] **Step 1: Press-scale wrapper**
|
||||
|
||||
```dart
|
||||
// lib/ui/widgets/pressable_scale.dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Wraps a tappable child with a quick scale-down on press for tactile feel.
|
||||
/// Delegates the actual tap to [onTap]; pass the child WITHOUT its own
|
||||
/// onPressed (or keep it — this only adds the visual squish).
|
||||
class PressableScale extends StatefulWidget {
|
||||
const PressableScale({super.key, required this.child, this.onTap});
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
State<PressableScale> createState() => _PressableScaleState();
|
||||
}
|
||||
|
||||
class _PressableScaleState extends State<PressableScale> {
|
||||
bool _down = false;
|
||||
|
||||
void _set(bool v) => setState(() => _down = v);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: (_) => _set(true),
|
||||
onTapUp: (_) => _set(false),
|
||||
onTapCancel: () => _set(false),
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedScale(
|
||||
scale: _down ? 0.94 : 1.0,
|
||||
duration: const Duration(milliseconds: 90),
|
||||
curve: Curves.easeOut,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Fade route helper**
|
||||
|
||||
```dart
|
||||
// lib/ui/widgets/fade_route.dart
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A gentle fade(+slight scale) page transition for in-app navigation.
|
||||
Route<T> fadeRoute<T>(Widget page) {
|
||||
return PageRouteBuilder<T>(
|
||||
transitionDuration: const Duration(milliseconds: 320),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 240),
|
||||
pageBuilder: (_, __, ___) => page,
|
||||
transitionsBuilder: (_, animation, __, child) {
|
||||
final curved =
|
||||
CurvedAnimation(parent: animation, curve: Curves.easeOutCubic);
|
||||
return FadeTransition(
|
||||
opacity: curved,
|
||||
child: ScaleTransition(
|
||||
scale: Tween(begin: 0.98, end: 1.0).animate(curved),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Use fade routes + press feedback on Home**
|
||||
|
||||
In `home_screen.dart`:
|
||||
- Add imports `import '../widgets/fade_route.dart';` and `import '../widgets/pressable_scale.dart';`.
|
||||
- Replace the two `Navigator.of(context).push(MaterialPageRoute(builder: (_) => const SeasonMapScreen()))` and `... const GameScreen()` calls with `Navigator.of(context).push(fadeRoute(const SeasonMapScreen()))` / `fadeRoute(const GameScreen())`. (Keep the settings-gear navigation on the default route, or switch it too — optional.)
|
||||
- Wrap the Adventure `FilledButton` and Classic `OutlinedButton` each in `PressableScale(child: ...)`. The buttons keep their own `onPressed` (the PressableScale `onTap` is left null — it only adds the squish; the button handles the tap). Do NOT double-fire navigation.
|
||||
|
||||
- [ ] **Step 4: Use press feedback on map nodes**
|
||||
|
||||
In `season_map_screen.dart`, the stage node widgets (the tappable `Key('stage_node_$i')` elements that start a stage) — wrap each node's existing tappable widget in `PressableScale`, preserving its current onTap. Also change the node's stage-start navigation to `fadeRoute(const GameScreen())` if it pushes GameScreen. Add the imports.
|
||||
|
||||
- [ ] **Step 5: Analyze + full suite + a transitions smoke widget test**
|
||||
|
||||
Run: `flutter analyze lib/ui/widgets/pressable_scale.dart lib/ui/widgets/fade_route.dart lib/ui/screens/home_screen.dart lib/ui/screens/season_map_screen.dart`
|
||||
Expected: clean.
|
||||
Run: `flutter test`
|
||||
Expected: green. The existing home/map widget tests must still find and tap the same buttons/nodes — `PressableScale` keeps them tappable via `onTap`/the inner button. If a test taps by widget type (e.g. `FilledButton`) it still works; if any test breaks because the tappable moved, fix the finder minimally and report.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
```bash
|
||||
git add lib/ui/widgets/pressable_scale.dart lib/ui/widgets/fade_route.dart lib/ui/screens/home_screen.dart lib/ui/screens/season_map_screen.dart
|
||||
git commit -m "feat(juice): button press feedback + fade screen transitions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Localization sweep + KO integrity (controller-run)
|
||||
|
||||
**Files:** Possibly `lib/l10n/app_en.arb`, `app_ko.arb`, and any screen with a hardcoded string. Verification is manual on the simulator — the controller runs this, not a subagent.
|
||||
|
||||
- [ ] **Step 1: Hardcoded-string sweep**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
grep -rnE "Text\((['\"])" lib/ui | grep -vE "l10n\.|AppLocalizations|style:|\\\$|Text\(''\)"
|
||||
```
|
||||
For each hit, decide: is it user-facing copy? If yes, move it to an ARB key (en + ko) and reference `l10n.<key>`. The known dynamic ones (`Text('$e')` error states, `Text('${view.score}')`) are NOT copy — leave them.
|
||||
|
||||
- [ ] **Step 2: EN/KO key parity check**
|
||||
```bash
|
||||
diff <(grep -oE '"[a-zA-Z0-9_]+":' lib/l10n/app_en.arb | grep -v '^"@' | sort -u) \
|
||||
<(grep -oE '"[a-zA-Z0-9_]+":' lib/l10n/app_ko.arb | grep -v '^"@' | sort -u)
|
||||
```
|
||||
Resolve any message key present in one ARB but not the other. (`@`-prefixed metadata lives only in the en template — that's expected.)
|
||||
|
||||
- [ ] **Step 3: KO overflow pass on the simulator**
|
||||
|
||||
Build & run under the Korean locale and walk every screen (splash, season title, home, map, game HUD + all result overlays — clear/fail/stuck/out-of-moves/endless game-over, settings, tutorial, streak snackbar):
|
||||
```bash
|
||||
flutter run -d <ios-sim-id> --dart-define=... # then switch device language to Korean, or
|
||||
xcrun simctl spawn booted defaults write -g AppleLanguages '("ko")' # before launch
|
||||
```
|
||||
Capture screenshots of each screen in KO. Fix any overflow/truncation (wrap text, `maxLines`, `FittedBox`, reduce font, or shorten the KO string). Re-verify.
|
||||
|
||||
- [ ] **Step 4: Commit any fixes**
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "i18n: localize remaining strings; fix KO overflow on <screens>"
|
||||
```
|
||||
(If no fixes were needed, record that in the Phase report instead of an empty commit.)
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Store assets — feature graphic + screenshots (controller-run)
|
||||
|
||||
**Files:** Create `lib/ui/branding/feature_graphic_painter.dart`; extend `test/tool/generate_brand_assets_test.dart`; output `docs/store/feature_graphic.png`, `docs/store/screenshots/{en,ko}/*.png`
|
||||
|
||||
- [ ] **Step 1: Feature graphic painter**
|
||||
|
||||
```dart
|
||||
// lib/ui/branding/feature_graphic_painter.dart
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_icon_painter.dart';
|
||||
|
||||
/// Paints the Play feature graphic (1024×500): navy field, the brand blocks on
|
||||
/// the left, wordmark + tagline on the right.
|
||||
class FeatureGraphic {
|
||||
static void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
AppIconMark.paintBackground(canvas, rect);
|
||||
|
||||
// Blocks on the left, vertically centered.
|
||||
canvas.save();
|
||||
final blockArea = size.height * 0.74;
|
||||
canvas.translate(size.height * 0.16, (size.height - blockArea) / 2);
|
||||
AppIconMark.paintBlocks(canvas, blockArea, groupFraction: 0.92);
|
||||
canvas.restore();
|
||||
|
||||
void text(String s, double dy, double fontSize, FontWeight w, Color c) {
|
||||
final tp = TextPainter(
|
||||
text: TextSpan(
|
||||
text: s,
|
||||
style: TextStyle(
|
||||
color: c, fontSize: fontSize, fontWeight: w, letterSpacing: 0.5),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
tp.paint(canvas, Offset(size.height * 1.02, dy));
|
||||
}
|
||||
|
||||
text('Block Seasons', size.height * 0.34, 76, FontWeight.w900, Colors.white);
|
||||
text('A new season of blocks every few weeks.', size.height * 0.50, 30,
|
||||
FontWeight.w500, const Color(0xFFB9C4E6));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend the generator** — add to `test/tool/generate_brand_assets_test.dart` a second `testWidgets` that renders the feature graphic. Add a non-square writer:
|
||||
|
||||
```dart
|
||||
import 'package:block_seasons/ui/branding/feature_graphic_painter.dart';
|
||||
|
||||
// ... inside main():
|
||||
testWidgets('generate feature graphic', (tester) async {
|
||||
const w = 1024, h = 500;
|
||||
final recorder = PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
FeatureGraphic.paint(canvas, const Size(1024, 500));
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(w, h);
|
||||
final bytes = await image.toByteData(format: ImageByteFormat.png);
|
||||
File('docs/store/feature_graphic.png').parent.createSync(recursive: true);
|
||||
File('docs/store/feature_graphic.png')
|
||||
.writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
expect(File('docs/store/feature_graphic.png').existsSync(), isTrue);
|
||||
});
|
||||
```
|
||||
|
||||
Run: `flutter test test/tool/generate_brand_assets_test.dart` → `docs/store/feature_graphic.png` (1024×500) exists.
|
||||
|
||||
- [ ] **Step 3: Capture EN + KO screenshots (simulator, controller-run)**
|
||||
|
||||
For each locale in {en, ko}: set the simulator language, launch the app, and capture: home, season map, gameplay (mid-combo), stage win (stars), endless game-over. Save under `docs/store/screenshots/<locale>/<screen>.png`. (Use the burst/file-size technique from prior phases to grab transient frames; tap navigation via the booted sim or computer-use if available.)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
```bash
|
||||
git add lib/ui/branding/feature_graphic_painter.dart test/tool/generate_brand_assets_test.dart docs/store
|
||||
git commit -m "feat(store): feature graphic + EN/KO screenshot set"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Final verification
|
||||
|
||||
**Files:** none (verification only)
|
||||
|
||||
- [ ] **Step 1: Static + tests**
|
||||
```bash
|
||||
flutter analyze # No issues
|
||||
flutter test # all green (≥171 with the new sound/icon tests)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build + icon on device**
|
||||
```bash
|
||||
flutter build ios --debug --simulator
|
||||
xcrun simctl install booted build/ios/iphonesimulator/Runner.app
|
||||
```
|
||||
Confirm the **home-screen app icon** is the navy 2×2 block mark (not the default Flutter icon). Screenshot it to `docs/screenshots/sim_app_icon.png`.
|
||||
|
||||
- [ ] **Step 3: Sound toggle smoke**
|
||||
|
||||
Launch, open Settings, toggle Sound off → play a stage and confirm no SFX/haptics; toggle on → SFX return. Relaunch → setting persisted.
|
||||
|
||||
- [ ] **Step 4: Commit evidence**
|
||||
```bash
|
||||
git add docs/screenshots/sim_app_icon.png
|
||||
git commit -m "docs: Phase 6 verified — real app icon, sound toggle, KO clean"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage:** icon (Tasks 4–5, 9) ✓; l10n finalize + KO pass (Tasks 3, 7) ✓; sound/haptics toggle (Tasks 1–3) ✓; themed settings (Task 3) ✓; button press + transitions (Task 6) ✓; feature graphic + screenshots (Task 8) ✓. All spec success criteria map to Task 9 / 7 / 3.
|
||||
|
||||
**Placeholder scan:** Task 7 and Task 8 Step 3 are controller-run manual/visual steps (KO overflow, screenshots) — they describe concrete actions and outputs, not deferred work. The only "decide per hit" is the hardcoded-string sweep, which is inherent to an audit; the grep + rule are explicit.
|
||||
|
||||
**Type/name consistency:** `soundEnabled`/`setSoundEnabled` (repo), `soundEnabledProvider`/`SoundEnabledNotifier.toggle/set`, `AppIconMark.paintBackground/paintBlocks(groupFraction:)`, `FeatureGraphic.paint`, `fadeRoute<T>`, `PressableScale` — used identically across tasks. `paintGlossyTile(canvas, rect, color, radiusFactor:)` matches the real signature in `tile_painter.dart`.
|
||||
|
||||
**Note on execution:** Tasks 1–6 are subagent-friendly (deterministic, testable). Tasks 7–9 are controller-run (simulator-visual: KO overflow, screenshots, icon-on-device, sound smoke) — the controller executes these directly rather than dispatching, mirroring Phase 5's Task 14.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Phase 6 — Localization Finalize + Icon + Juice + Store Assets (Design)
|
||||
|
||||
**Date:** 2026-06-13
|
||||
**Status:** Approved (owner approved design 2026-06-13)
|
||||
|
||||
## Context
|
||||
|
||||
Phases 0–5 are complete (gameplay, seasons, endless, commercial polish, remote
|
||||
content, Firebase analytics, AdMob + IAP). Phase 6 is the pre-release polish
|
||||
round. The master plan assumed AI-generated background/tilesheet **images**, but
|
||||
Phase 3.5 implemented all in-game visuals **procedurally** (CustomPainter glossy
|
||||
tiles, painted season backgrounds). So image assets are NOT needed for gameplay;
|
||||
the only image deliverables are the **app icon** and **store graphics**, both of
|
||||
which are produced as vectors in the game's existing visual language — no
|
||||
external AI image generation required.
|
||||
|
||||
Owner is a non-developer; Claude produces all code and all vector art.
|
||||
|
||||
## Visual identity (established, reused here)
|
||||
|
||||
- Deep navy background gradient `#101736 → #192555 → #2C3168` (season-1 palette).
|
||||
- Glossy rounded blocks in four brand colors: pink `#FF7EB3`, yellow `#FFD166`,
|
||||
cyan `#6FCDF5`, green `#7EDB9C` (each rendered light→base→dark vertical
|
||||
gradient + a soft white highlight ellipse near the top — same look as the
|
||||
in-game `paintGlossyTile`).
|
||||
|
||||
## Workstreams
|
||||
|
||||
### 1. App icon (DECIDED: "Clean 2×2 block mark")
|
||||
|
||||
- Composition: rounded-square, deep-navy background, a centered 2×2 grid of the
|
||||
four glossy brand-color blocks (pink top-left, yellow top-right, cyan
|
||||
bottom-left, green bottom-right). No cherry-blossom accent (icon must be a
|
||||
permanent, season-independent brand asset). Mirrors the existing in-app
|
||||
`_logoMark`.
|
||||
- Blocks kept within the central safe zone so Android's circular/rounded mask
|
||||
never clips them.
|
||||
- Deliverables:
|
||||
- A master **SVG** committed to the repo (source of truth).
|
||||
- A **1024×1024 PNG**, opaque (no alpha) for the iOS App Store.
|
||||
- **Android adaptive icon**: foreground = the 2×2 blocks (transparent
|
||||
surround), background = the navy gradient (flat or gradient drawable).
|
||||
- All platform sizes generated via the `flutter_launcher_icons` package
|
||||
(config in `pubspec.yaml` / `flutter_launcher_icons.yaml`), replacing the
|
||||
default Flutter icon on both platforms.
|
||||
- Tooling: rasterize SVG→PNG with a deterministic, scriptable converter
|
||||
(`rsvg-convert`, or a tiny Dart/`image` script, or `flutter_svg` render). The
|
||||
conversion command is checked in so the icon can be regenerated.
|
||||
|
||||
### 2. Localization finalize (EN + KO)
|
||||
|
||||
- Audit every user-facing screen for hardcoded (non-l10n) strings; move any to
|
||||
ARB. Screens to sweep: splash, season title, home, season map, game (HUD +
|
||||
result overlays), settings, tutorial, snackbars.
|
||||
- Verify EN copy is store-quality and KO copy reads natively (no machine-y
|
||||
phrasing).
|
||||
- **KO integrity pass:** run the app under the `ko` locale on the simulator and
|
||||
confirm no text overflow/truncation/clipping on any screen (Korean strings are
|
||||
often longer/shorter than English — buttons, chips, result cards, settings
|
||||
rows). Fix overflows (wrap, ellipsis, sizing) where found.
|
||||
- Add any new keys introduced by workstream 3 (sound/haptics toggle labels).
|
||||
|
||||
### 3. Juice / polish pass (light, on top of Phase 3.5; BGM stays on hold)
|
||||
|
||||
- **Sound on/off setting** (real gap — commercial games need it): a single
|
||||
toggle in Settings that enables/disables SFX, persisted in `SaveRepository`
|
||||
(additive flag, saveVersion stays 1) and applied through `AudioService`. The
|
||||
same flag also gates gameplay haptics (no separate haptics toggle — one
|
||||
"Sound & vibration" control keeps Settings simple).
|
||||
- **Settings screen theming:** the just-added Settings screen is a plain
|
||||
`ListView`; give it the game's look (transparent scaffold over
|
||||
`SeasonBackground`, themed text/cards) so it matches the rest of the app.
|
||||
- **Button press feedback:** subtle scale-on-press for the primary buttons
|
||||
(home Adventure/Classic, map nodes) for a more tactile "game" feel.
|
||||
- **Screen transitions:** a gentle shared fade(-through) transition for
|
||||
home → map → game navigation instead of the default platform slide.
|
||||
|
||||
### 4. Store assets
|
||||
|
||||
- **Feature graphic (Play, 1024×500):** navy + glossy blocks + "Block Seasons"
|
||||
wordmark + a short tagline, in the icon's visual language. Produced as vector
|
||||
→ PNG and committed under `docs/store/`.
|
||||
- **Screenshots:** captured from the running app on the simulator — home,
|
||||
season map, gameplay (mid-combo), stage win (stars), endless game-over — in
|
||||
**both EN and KO**. Stored under `docs/store/screenshots/<locale>/`.
|
||||
- Exact per-store framing/sizes (iOS 6.7"/6.5", Play phone) are finalized at
|
||||
submission time in **Phase 7**; Phase 6 captures the raw, clean source shots.
|
||||
|
||||
## Out of scope (explicit)
|
||||
|
||||
- BGM / music (on hold by prior decision).
|
||||
- AI-generated raster backgrounds/tilesheets (visuals are procedural).
|
||||
- Real AdMob unit IDs and store IAP product creation (owner prerequisites,
|
||||
tracked for the Phase 5 finalize step between Phase 6 and Phase 7).
|
||||
- Final per-store screenshot framing (Phase 7).
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Real app icon shows on both iOS and Android (no default Flutter icon).
|
||||
- KO full play-through has zero text overflow/clipping; no hardcoded strings.
|
||||
- Sound toggle works and persists across relaunch.
|
||||
- Feature graphic + EN/KO screenshot set committed under `docs/store/`.
|
||||
- `flutter analyze` clean; full test suite green (≥169, plus any new tests for
|
||||
the sound-setting persistence).
|
||||
@@ -0,0 +1,141 @@
|
||||
# 부스터 & 데일리 보상 — 설계 (Boosters & Daily Reward)
|
||||
|
||||
작성일: 2026-06-18 · 상태: 승인됨(오너) → 구현 계획 단계로
|
||||
|
||||
## 목표 / 맥락
|
||||
|
||||
광고 수익형 퍼즐게임 Block Seasons의 **리텐션(복귀) + 광고 노출**을 동시에 키우는
|
||||
키스톤 기능. 현재 게임엔 플레이어에게 줄 "보상 대상"이 없어 데일리 보상·보상형
|
||||
광고를 붙일 곳이 없었음 → **가벼운 부스터(파워업) 경제**를 도입해 둘 다 해금한다.
|
||||
|
||||
사업자등록이 없어 IAP는 막혀 있으므로 **재화/상점 없이** 부스터를 직접 주고받는다.
|
||||
|
||||
## 확정된 결정 (브레인스토밍)
|
||||
|
||||
1. **부스터 3종**: 🔨해머 / 🔀셔플 / 💥줄폭탄
|
||||
2. **획득**: 재화 없음 — 데일리 보상 + 보상형 광고로 부스터를 직접 받아 **비축**
|
||||
3. **데일리**: 7일 출석 캘린더 (점증, Day7 잭팟) + "광고 보고 2배"
|
||||
4. **사용**: 게임 중 **부스터 바 → 대상 지정**
|
||||
|
||||
## 부스터 규칙 (밸런스, 오너 승인)
|
||||
|
||||
- **이동 수 미소모** — 부스터는 보조라 무브 카운터를 깎지 않는다.
|
||||
- **점수·콤보 미부여, 목표 미반영** — 그리드만 직접 바꾸고 점수/목표 이벤트 파이프라인을
|
||||
타지 않는다. 줄폭탄으로 "줄 N개" 목표를 깨거나 해머로 보석을 제거해 "보석 N개"
|
||||
목표를 달성할 수 없다(스테이지 난이도 보존). 콤보 상태도 건드리지 않는다(전진·리셋 둘 다 없음).
|
||||
- **막힌 보드 되살리기 허용** — 부스터 사용 후 phase를 재평가(`_checkStuck`)해, 죽은 판
|
||||
(boardDead)을 다시 playing으로 되돌릴 수 있다 → 보상형 '컨티뉴'의 대안.
|
||||
- **사용 가능 시점** — phase가 `playing` 또는 `stuck`일 때만. `won`/`lost` 후엔 불가.
|
||||
- **사용 한도** — 별도 횟수 제한 없음. 보유량이 곧 한도(가진 만큼만 사용).
|
||||
- **엔드리스 모드 포함** — 엔드리스에서도 사용 가능.
|
||||
|
||||
### 부스터별 동작
|
||||
| 부스터 | 동작 | 대상 |
|
||||
|---|---|---|
|
||||
| 🔨 해머 | 채워진 칸 1개를 비움 | 칸 1개 탭 |
|
||||
| 🔀 셔플 | 현재 트레이(3조각)를 새로 교체 | 즉시(대상 없음) |
|
||||
| 💥 줄폭탄 | 가로 또는 세로 한 줄 전체를 비움 | 줄(행/열) 선택 |
|
||||
|
||||
## 아키텍처 (기존 레이어 준수: ui → state → game|data|services)
|
||||
|
||||
### 1) 엔진 (순수 Dart, `lib/game/engine/game_engine.dart`)
|
||||
세 메서드 추가. 성공 시 `true`, 잘못된 대상/시점이면 `false`(보유량 차감 안 하도록).
|
||||
|
||||
```
|
||||
bool useHammer(int x, int y) // (x,y)가 채워진 칸이면 비움
|
||||
bool useShuffle() // 트레이 재추첨
|
||||
bool useLineBomb({int? row, int? col}) // row 또는 col 중 하나의 줄을 비움
|
||||
```
|
||||
|
||||
공통: 이동/점수/콤보/목표 불변, 마지막에 `_checkStuck()` 호출. `playing`/`stuck`에서만 허용.
|
||||
줄폭탄은 `row`와 `col` 중 정확히 하나만 지정(둘 다/둘 다 없음이면 `false`).
|
||||
|
||||
### 2) 모델 (순수 Dart, `lib/game/models/booster.dart`)
|
||||
```
|
||||
enum BoosterType { hammer, shuffle, lineBomb }
|
||||
```
|
||||
|
||||
### 3) 저장 (`lib/data/save_repository.dart`, JSON blob 확장)
|
||||
- 보유량: `int boosterCount(BoosterType)`, `Future<void> grantBooster(BoosterType, [int n = 1])`,
|
||||
`Future<bool> consumeBooster(BoosterType)`(0이면 false).
|
||||
- 데일리: `String? dailyLastClaimedYmd`, `int dailyCalendarDay`(1~7),
|
||||
`Future<void> recordDailyClaim(String ymd, int day)`.
|
||||
- JSON에 `boosters: {hammer, shuffle, lineBomb}` 와 `daily: {lastYmd, day}` 추가
|
||||
(기존 streak의 ymd 유틸 재사용).
|
||||
|
||||
### 4) 상태 (Riverpod, `lib/state/`)
|
||||
- `BoosterInventoryNotifier` — 보유량 노출 + `grant/consume`.
|
||||
- `DailyRewardNotifier` — 오늘 받을 수 있는지 + 캘린더 day 계산 + claim.
|
||||
- `GameSessionNotifier`에 사용 메서드 추가: `useHammer(x,y)` / `useShuffle()` /
|
||||
`useLineBomb(...)`. 흐름: **엔진 먼저 호출 → 성공 시에만 인벤토리 차감 → 뷰 갱신**.
|
||||
보유 0이면 호출하지 않음(UI가 광고 제안).
|
||||
- `GameViewState`에 grid가 이미 있어 부스터 후 UI 재렌더 가능.
|
||||
|
||||
### 5) 데일리 캘린더 로직 (순수, 테스트 가능)
|
||||
- 오늘 ymd 계산. `lastClaimedYmd == 오늘` → 이미 받음(비활성).
|
||||
- 받을 수 있는 경우의 day:
|
||||
- `lastClaimedYmd == 어제` → `day = (이전 day % 7) + 1`(연속, 7→1 순환).
|
||||
- 그 외(하루 이상 빠짐/최초) → `day = 1`(리셋).
|
||||
- claim 시 해당 day 보상 지급 + `lastClaimedYmd = 오늘`, `calendarDay = day` 저장.
|
||||
- **보상표(제안, 튜닝 가능)**:
|
||||
| Day | 보상 |
|
||||
|---|---|
|
||||
| 1 | 🔨×1 |
|
||||
| 2 | 🔀×1 |
|
||||
| 3 | 💥×1 |
|
||||
| 4 | 🔨×1 🔀×1 |
|
||||
| 5 | 🔀×1 💥×1 |
|
||||
| 6 | 🔨×1 💥×1 |
|
||||
| 7 | 🔨×2 🔀×2 💥×2 (잭팟) |
|
||||
- 시작 보유량: 각 1개(최초 1회).
|
||||
|
||||
### 6) UI (`lib/ui/`)
|
||||
- **부스터 바 위젯** — 보드 아래 3버튼(아이콘+개수). 탭 시 타겟팅 모드 진입.
|
||||
- 해머: 채워진 칸 탭 → 제거.
|
||||
- 줄폭탄: 보드 가장자리에 행/열 핸들 표시 → 핸들 탭으로 줄 선택(가장 명확한 UX,
|
||||
구현 중 세부 조정 가능).
|
||||
- 셔플: 즉시 적용.
|
||||
- 개수 0인 버튼 탭 → "광고 보고 1개 받기" 다이얼로그.
|
||||
- **데일리 팝업** — 홈 화면 진입 시 받을 수 있으면 표시. 7칸(과거=체크/딤, 오늘=하이라이트,
|
||||
미래=잠금) + [받기] + [광고 보고 2배].
|
||||
|
||||
### 7) 광고 (`lib/services/ad_service.dart` 재사용)
|
||||
- 부스터 0개 → 보상형 광고 → 성공 시 해당 부스터 +1.
|
||||
- 데일리 2배 → 보상형 광고 → 성공 시 보상 2배 지급.
|
||||
- `showRewarded()`는 광고 미로드 시에도 true(기존 플레이어 친화 폴백) → 부스터는 지급됨.
|
||||
|
||||
### 8) 분석 (`lib/services/analytics_service.dart`)
|
||||
- `booster_used {type}`
|
||||
- `booster_granted {type, count, source: start|daily|ad}`
|
||||
- `daily_reward_claimed {day, doubled}`
|
||||
|
||||
### 9) l10n (`lib/l10n/`)
|
||||
- 부스터 이름·설명, 데일리 보상 UI, "광고 보고 받기/2배" CTA — EN/KO.
|
||||
|
||||
## 테스트 전략 (TDD)
|
||||
- **엔진**: 해머(칸 제거·이동/점수/목표 불변·빈 칸이면 false·죽은 판 되살림),
|
||||
셔플(새 트레이·불변·재stuck), 줄폭탄(행/열 제거·불변·재stuck·row^col 검증),
|
||||
won/lost 후 사용 차단.
|
||||
- **저장**: grant/consume/영속, 0에서 consume=false, 데일리 ymd/day 영속.
|
||||
- **데일리 로직**: 연속 시 day+1, 빠짐 시 리셋, 같은 날 재수령 불가, 보상표 지급, 2배.
|
||||
- **세션 노티파이어**: 성공 시 차감, 잘못된 대상/0개면 미차감.
|
||||
- **위젯**: 부스터 바 개수 렌더·타겟팅, 데일리 팝업 상태.
|
||||
|
||||
## 구현 단계 (계획에서 상세화)
|
||||
1. 엔진 부스터 3종 (TDD, 순수)
|
||||
2. `BoosterType` + SaveRepository 인벤토리 (TDD)
|
||||
3. 인벤토리/세션 노티파이어 + 사용 흐름 (TDD)
|
||||
4. 부스터 바 UI + 타겟팅 (위젯 테스트)
|
||||
5. 데일리 캘린더 로직 + 노티파이어 (TDD)
|
||||
6. 데일리 팝업 UI
|
||||
7. 보상형 광고 지급(0-상태 + 데일리 2배)
|
||||
8. 분석 + l10n
|
||||
9. 통합 + 전체 테스트 그린
|
||||
|
||||
## 비목표 (YAGNI)
|
||||
- 코인/재화/상점, 부스터 회전·되돌리기, IAP 부스터 판매, 부스터 합성/업그레이드.
|
||||
|
||||
## 리스크 / 메모
|
||||
- 부스터가 점수·목표에 반영되지 않으므로 기존 스테이지 밸런스는 그대로 유효.
|
||||
- 보상형 광고 미로드 시에도 부스터를 지급(폴백) → 신규 앱 no-fill 기간에도 게임 흐름 유지.
|
||||
- 새 빌드 필요(부스터·데일리는 다음 릴리스부터). 빌드는 오너 명령 시에만.
|
||||
@@ -0,0 +1,28 @@
|
||||
zardu.plum@gmail.com
|
||||
zardu.sqa.101@gmail.com
|
||||
zardu.sqa.102@gmail.com
|
||||
zardu.sqa.103@gmail.com
|
||||
zardu.sqa.104@gmail.com
|
||||
zardu.sqa.105@gmail.com
|
||||
zardu.sqa.106@gmail.com
|
||||
zardu.sqa.107@gmail.com
|
||||
zardu.sqa.108@gmail.com
|
||||
zardu.sqa.109@gmail.com
|
||||
zardu.sqa.110@gmail.com
|
||||
zardu.sqa.111@gmail.com
|
||||
zardu.sqa.112@gmail.com
|
||||
zardu.sqa.113@gmail.com
|
||||
zardu.sqa.114@gmail.com
|
||||
zardu.sqa.115@gmail.com
|
||||
zardu.sqa.116@gmail.com
|
||||
zardu.sqa.117@gmail.com
|
||||
zardu.sqa.118@gmail.com
|
||||
zardu.sqa.119@gmail.com
|
||||
zardu.sqa.120@gmail.com
|
||||
zardu.sqa.121@gmail.com
|
||||
axiom.kor@gmail.com
|
||||
boson.seoul@gmail.com
|
||||
cepheid.space@gmail.com
|
||||
diffraction.rayman@gmail.com
|
||||
duality.frame@gmail.com
|
||||
quantum.tteokshop@gmail.com
|
||||
|
@@ -0,0 +1 @@
|
||||
{"hosting":{"public":"deploy","ignore":["firebase.json","**/.*","**/node_modules/**"]},"flutter":{"platforms":{"android":{"default":{"projectId":"block-seasons","appId":"1:190209969950:android:e08dc30877b1821b44c30f","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"block-seasons","appId":"1:190209969950:ios:f9d4578ec86f92c844c30f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"block-seasons","configurations":{"android":"1:190209969950:android:e08dc30877b1821b44c30f","ios":"1:190209969950:ios:f9d4578ec86f92c844c30f"}}}}}}
|
||||
@@ -0,0 +1,8 @@
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/icon/icon.png"
|
||||
remove_alpha_ios: true
|
||||
min_sdk_android: 21
|
||||
adaptive_icon_background: "assets/icon/icon_background.png"
|
||||
adaptive_icon_foreground: "assets/icon/icon_foreground.png"
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
# Firebase iOS SDK (firebase_core 4.x) requires a minimum of iOS 15.0.
|
||||
platform :ios, '15.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -1,37 +1,262 @@
|
||||
PODS:
|
||||
- app_tracking_transparency (0.0.1):
|
||||
- Flutter
|
||||
- audioplayers_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Firebase/CoreOnly (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- Firebase/Crashlytics (12.15.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseCrashlytics (~> 12.15.0)
|
||||
- firebase_analytics (12.4.2):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.15.0)
|
||||
- Flutter
|
||||
- firebase_core (4.11.0):
|
||||
- Firebase/CoreOnly (= 12.15.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (5.2.4):
|
||||
- Firebase/Crashlytics (= 12.15.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (12.15.0):
|
||||
- FirebaseAnalytics/Default (= 12.15.0)
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- FirebaseInstallations (~> 12.15.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/Default (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- FirebaseInstallations (~> 12.15.0)
|
||||
- GoogleAppMeasurement/Default (= 12.15.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (12.15.0):
|
||||
- FirebaseCoreInternal (~> 12.15.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreExtension (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- FirebaseCoreInternal (12.15.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseCrashlytics (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- FirebaseInstallations (~> 12.15.0)
|
||||
- FirebaseRemoteConfigInterop (~> 12.15.0)
|
||||
- FirebaseSessions (~> 12.15.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseInstallations (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseRemoteConfigInterop (12.15.0)
|
||||
- FirebaseSessions (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- FirebaseCoreExtension (~> 12.15.0)
|
||||
- FirebaseInstallations (~> 12.15.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesSwift (~> 2.1)
|
||||
- Flutter (1.0.0)
|
||||
- Google-Mobile-Ads-SDK (12.14.0):
|
||||
- GoogleUserMessagingPlatform (>= 1.1)
|
||||
- google_mobile_ads (7.0.0):
|
||||
- Flutter
|
||||
- Google-Mobile-Ads-SDK (~> 12.14.0)
|
||||
- webview_flutter_wkwebview
|
||||
- GoogleAdsOnDeviceConversion (3.6.0):
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Core (12.15.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Default (12.15.0):
|
||||
- GoogleAdsOnDeviceConversion (~> 3.6.0)
|
||||
- GoogleAppMeasurement/Core (= 12.15.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.15.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.15.0):
|
||||
- GoogleAppMeasurement/Core (= 12.15.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleUserMessagingPlatform (3.1.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (8.1.1):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Environment (8.1.1):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.1):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/MethodSwizzler (8.1.1):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Network (8.1.1):
|
||||
- GoogleUtilities/Logger
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (8.1.1)":
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.1)
|
||||
- GoogleUtilities/Reachability (8.1.1):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/UserDefaults (8.1.1):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- in_app_purchase_storekit (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- in_app_review (2.0.0):
|
||||
- Flutter
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.1)
|
||||
- PromisesSwift (2.4.1):
|
||||
- PromisesObjC (= 2.4.1)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_tracking_transparency (from `.symlinks/plugins/app_tracking_transparency/ios`)
|
||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`)
|
||||
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Firebase
|
||||
- FirebaseAnalytics
|
||||
- FirebaseCore
|
||||
- FirebaseCoreExtension
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseCrashlytics
|
||||
- FirebaseInstallations
|
||||
- FirebaseRemoteConfigInterop
|
||||
- FirebaseSessions
|
||||
- Google-Mobile-Ads-SDK
|
||||
- GoogleAdsOnDeviceConversion
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleUserMessagingPlatform
|
||||
- GoogleUtilities
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_tracking_transparency:
|
||||
:path: ".symlinks/plugins/app_tracking_transparency/ios"
|
||||
audioplayers_darwin:
|
||||
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
|
||||
firebase_analytics:
|
||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||
firebase_core:
|
||||
:path: ".symlinks/plugins/firebase_core/ios"
|
||||
firebase_crashlytics:
|
||||
:path: ".symlinks/plugins/firebase_crashlytics/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
google_mobile_ads:
|
||||
:path: ".symlinks/plugins/google_mobile_ads/ios"
|
||||
in_app_purchase_storekit:
|
||||
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_tracking_transparency: 3d84f147f67ca82d3c15355c36b1fa6b66ca7c92
|
||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
||||
Firebase: a8539b633d474fbeb654c7043f9c1649e274045b
|
||||
firebase_analytics: e0a17f792099472235f9ec7f31d1d3a0730d4891
|
||||
firebase_core: fc23178af8ea070194d09031ae4198a9608a3d22
|
||||
firebase_crashlytics: 344bb168f55aee1086c6cdd0b105a9db018cd344
|
||||
FirebaseAnalytics: 9c9fa7915fc52ea03077000d5a7b6a8947b2d76e
|
||||
FirebaseCore: 2e86a4ea1684d4381707069e4a6d89ac808e901e
|
||||
FirebaseCoreExtension: 10d2a627977b39418759ad88ada80fbbd34f1c4f
|
||||
FirebaseCoreInternal: 6ab6a02c94446c026d2cf35cf5383842ebaa4992
|
||||
FirebaseCrashlytics: 87e76cc33259b076dd1f96cd829db76849338e08
|
||||
FirebaseInstallations: eb29ccbf64eaedf86fd5b2ccc7fabde567660b52
|
||||
FirebaseRemoteConfigInterop: 7e3d57ce4b1e958bb1d15403faa7178f46bbb5b7
|
||||
FirebaseSessions: acfe7eadca47cda94ac86592737204581bb1abf6
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
Google-Mobile-Ads-SDK: 4534fd2dfcd3f705c5485a6633c5188d03d4eed2
|
||||
google_mobile_ads: df3008bafbe1f2ad6862f87334e560d2f047f902
|
||||
GoogleAdsOnDeviceConversion: 80ce443fa1b4b5750913d53a04ecda644ff57744
|
||||
GoogleAppMeasurement: a6d37949071d456e9147dac6789c4342e0e7a8c5
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUserMessagingPlatform: befe603da6501006420c206222acd449bba45a9c
|
||||
GoogleUtilities: 4f2618a4a1e762a1ee134a1e2323bba9843e06da
|
||||
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
|
||||
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
|
||||
PromisesSwift: 217dea0fd5d2ad65222a109c48698add13cc1c5b
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
PODFILE CHECKSUM: 7a0c05f8aeb53a8c858ca08a4666afaa242f0eb1
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
A1624C49AABB61D3BB6EBA00 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F021B835BC4E346AE82B4C9 /* Pods_RunnerTests.framework */; };
|
||||
BC732790904D77939BB8C135 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FE18481AC23043B44AB64814 /* PrivacyInfo.xcprivacy */; };
|
||||
D444497F007A61A9102D174D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92F5ACA56D636C056F52DDE6 /* Pods_Runner.framework */; };
|
||||
E746073DDE80B82D8D3C9659 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -47,6 +49,7 @@
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
55914DA7E8E89CB02E73C3F5 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
6BD9A45428DD4E519FC38754 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
@@ -65,6 +68,7 @@
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B4B2233E92790E4E03907BD2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B9983A741CFB90A0857F31CD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FE18481AC23043B44AB64814 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -129,6 +133,7 @@
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
2EECBA43D42E2853F949CCFC /* Pods */,
|
||||
9CFCC4FE458D4EC11DAF9E88 /* Frameworks */,
|
||||
43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -152,6 +157,7 @@
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
FE18481AC23043B44AB64814 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
@@ -199,6 +205,7 @@
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
B8E60F64310B9A81A7741264 /* [CP] Embed Pods Frameworks */,
|
||||
2838ED76467446CF49AD274C /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -264,12 +271,31 @@
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
E746073DDE80B82D8D3C9659 /* GoogleService-Info.plist in Resources */,
|
||||
BC732790904D77939BB8C135 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
2838ED76467446CF49AD274C /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
@@ -455,7 +481,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -540,7 +566,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -585,7 +611,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -597,7 +623,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -636,7 +662,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
||||
@@ -1,122 +1 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 601 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 960 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>API_KEY</key>
|
||||
<string>AIzaSyAIl2LM2fDrr7OH70K7uJnAwKwXMzPCBMI</string>
|
||||
<key>GCM_SENDER_ID</key>
|
||||
<string>190209969950</string>
|
||||
<key>PLIST_VERSION</key>
|
||||
<string>1</string>
|
||||
<key>BUNDLE_ID</key>
|
||||
<string>com.airkjw.blockseasons</string>
|
||||
<key>PROJECT_ID</key>
|
||||
<string>block-seasons</string>
|
||||
<key>STORAGE_BUCKET</key>
|
||||
<string>block-seasons.firebasestorage.app</string>
|
||||
<key>IS_ADS_ENABLED</key>
|
||||
<false></false>
|
||||
<key>IS_ANALYTICS_ENABLED</key>
|
||||
<false></false>
|
||||
<key>IS_APPINVITE_ENABLED</key>
|
||||
<true></true>
|
||||
<key>IS_GCM_ENABLED</key>
|
||||
<true></true>
|
||||
<key>IS_SIGNIN_ENABLED</key>
|
||||
<true></true>
|
||||
<key>GOOGLE_APP_ID</key>
|
||||
<string>1:190209969950:ios:f9d4578ec86f92c844c30f</string>
|
||||
</dict>
|
||||
</plist>
|
||||