FigureOut is a Flutter mobile puzzle/action game where the player defeats different shapes with gesture-specific attacks before the mission timer runs out.
Stage and mission data come from the configured Google Sheet. See architecture and conventions, Z movement, and parabolic movement for maintained implementation notes.
- Flutter / Dart
- Flame
- GoRouter
- Google Sheet based stage, mission, translation, and log data
- PNG sprite gameplay shapes with Canvas paths for gestures and attack indicators
- SVG and image assets for menu and UI, including a lossless WebP background
flutter pub get
flutter runLocal web preview (mobile portrait frame):
flutter run --no-pub -d web-server --web-hostname 127.0.0.1 --web-port 5317Lint and automated tests after dependency resolution:
flutter analyze --no-pub
flutter test --no-pubThe lint configuration includes package:flutter_lints/flutter.yaml. The
repository tracks selected Android and iOS configuration files. The remaining
native scaffolding, web scaffolding, and signing inputs are local.
The repository tracks the rewarded ad lifecycle and gameplay regression tests.
The test/ ignore rule applies to new, untracked test files.
The rewarded ad test uses mocked native channels to check consent and connectivity eligibility, the show lock, duplicate callbacks, and preparation of the next ad without automatic playback.
The app expects Google Sheet environment values in assets/.env.
Required keys:
GOOGLESHEETAPIKEYGOOGLESHEETID
- Main menu
- Stage select
- Mission select
- Game play
- Pause screen
- Mission clear popup
- Mission failed popup
- Endless mode and leaderboard
- Settings, including audio and language
Each shape has its own attack gesture.
- Circle: tap
- Rectangle: swipe slash, similar to Fruit Ninja
- Pentagon: long press for the health duration
- Triangle: draw a circle around it without touching another shape
- Hexagon: pinch zoom; each health point requires 20% scale change
Multiple matching shapes can be attacked at the same time when the gesture supports it. If a visible forbidden shape passes over the touch point while a pentagon is held, it applies the usual touch penalty once and releases that hold. Other pentagons can remain held and keep the hold sound playing. The pentagon touch outline is scaled to 110% of the painted shape.
Enemy syntax follows the current parser and Google Sheet data.
The current Google Sheet uses the shape column for both enemies and wait rows.
There is no separate e command column.
In stage sheets, the parser expects the enemy/wait value in the Shape column
detected from the sheet layout; keep the shape, attack, movement, and position
fields aligned. See SheetService for the accepted layouts.
Basic format:
ShapeSize_Order (Health)
Examples:
Circle3 (5)Triangle4 (10)Rectangle30:100 (15)Pentagon3 (10)Hexagon5 (20)Circle5_01 (10)for ordered enemiesTriangle4_URD(1,9) (1)for a triangle with a randomly assigned orderCircle5 (-1)for dark enemies that should not be attacked
Ordered triangles display their order on the right. An enclosure that includes a numbered triangle out of sequence applies the penalty before removing any enclosed triangle.
Shape size numbers map to percentage scale:
1= 25%2= 50%3= 75%4= 100% default5= 125%6= 150%7= 175%8= 200%9= 250%10= 300%
Size change replaces the size number with S(start, end, seconds):
Circle S(4, 7, 9) (1)
Pentagon S(6, 2, 4) (2)
The shape spawns at size start and grows or shrinks to size end over
seconds seconds (same size scale as above). An order suffix still follows
it: Circle S(4, 7, 9)_01 (1).
Rectangle supports explicit width, height, and an optional angle:
Rectangle35:250/45 (60)
This means width 35, height 250 in sheet coordinate units, angle 45 degrees, and health 60. These are dimensions, not a ratio. Rectangle dimensions do not use the numbered size table above.
Attack data uses:
(attackTime, damage)
- Health
-1enemies do not attack automatically. - If the player attacks a dark enemy, the mission timer is reduced by the damage value.
- Normal enemies explode after
attackTimeif not defeated, then reduce the mission timer bydamage. - An initial movement
Waitdelays the start of the attack timer. A successful attack before expiry must not apply the expiry penalty.
Sheet Y coordinates align the centers of shapes at the same height, regardless of shape type or size. This applies to initial positions and movement targets. Positive Y is up.
Supported movement commands:
Z(x, y, speed): move to a point, then stop or continue to the next path commandZ(x, y, targetSpeed, accelerationSeconds): accelerate linearly from zero to the target speed, then continue at that speed; stop on arrival even if the acceleration period has not ended. Each leg, includingBack, restarts acceleration.B(x, y, speed): rebound movement toward a direction point, reflecting off play-area wallsC(radius, degreesPerSecond): clockwise circular movementCC(radius, degreesPerSecond): counterclockwise circular movementP(vx, vy, gravity): parabolic launch; see P commandL(x1, y1, x2, y2, speed): move repeatedly between two pointsM(angle, speed, stopY): legacy movement toward the specified YD(visible, hidden): repeat visible and hidden timing with smooth fade-in/outDR(visible, hidden): repeat visible and hidden timing, then reappear at a random positionRepeat: loop the path from the startBack: move back through the path in reverse order, then repeat from the initial wait; the return journey does not add another endpoint waitWait n: before the movement, delays its start bynseconds; between twoZlegs, holds the shape in place fornseconds.Wait RND(a,b)samples a new delay on each repeated visitDisappear(n): fades the shape out and removes itnseconds after itsZpath ends (forB/C/D/DR/L/MandRepeat/Backpaths,nseconds after the movement starts); no penalty or reward, and it no longer blocksWait 0
Timing lines share the movement cell, one command per line:
Wait 2
Z(200, 0, 100)
Z(160, 0, 250)
Disappear(3)
RND(a, b)(aliasRD(a, b)): random value betweenaandbURD(a, b): unique random value betweenaandbwithin the sameWait 0group
Wait n: waitnseconds before spawning the next enemy groupWait 0: wait until all current non-dark enemies and their removal effects have finished, including sliced Rectangle piecesWait 0waits for newly spawned enemies to mount before checking clear state.- Already removed or removing enemies must not block the next wave.
Waitrows are written in the shape column.- Dark enemies with health
-1do not blockWait 0
- Portrait artwork reference:
1179 x 2556 - Web preview logical frame:
393 x 852, fitted inside the available panel - Mobile orientation is locked to portrait
- Keep mobile touch targets at least
44 x 44 - Avoid system font scaling breaking the UI
- Preserve smooth FPS and avoid unnecessary memory churn
- Mission result popups use a parchment panel, shared blue ribbon, and blue action pill buttons.
- The failed Continue button includes an AD badge; badge/icon spacing must not overlap button text.
- Keep gameplay values in constants instead of magic numbers.
- Preserve existing game logic unless a bug fix requires changing it.
- Use Google Sheet data as the source of truth for stage and mission behavior.
- Gameplay shapes use PNG sprites and Canvas geometry; preserve referenced UI/menu SVG assets.
- Do not delete unused code without explicit approval.
game_modes/one_second_game.dartowns mission progression, game state, and gesture input.behaviors/movement_command_parser.dartconverts movement cells into behaviors shared by stage and endless mode. The individual commands live alongside it.functions/play_coordinates.dartconverts coordinates.functions/gesture_geometry.darthandles path intersections and slice calculations.services/sheet_service.dartfetches and parses stage data.services/random_context.darttracks unique random values within a wave.
The packaged background is assets/menu/common/noise_texture.webp. Its
original PNG is archived at assets/source/noise_texture.png outside the
Flutter asset declarations.
The background swap saves about 4.92 MiB at the file level with identical decoded pixels. The asset cleanup also removes retired BGM samples, an unused slice sound, and a duplicate result ribbon SVG. Gameplay cleanup removes unused helpers and overlap calculations whose result was never read.
Before delivery, the workspace passed Flutter analysis and eight automated tests covering gestures, alignment, movement, and the rewarded ad lifecycle. APK size and on-device behavior were outside that verification.
- Normally only modify code.
- Run
flutter/react build webonly when explicitly requested. - Run builds only when explicitly requested.
- Avoid running
flutter/react cleanwhile the server is running. - Use normal privileges. Request scoped elevation only when an authorized command needs access outside the workspace, such as the shared Flutter SDK.
- Do not run compilation, linting, Git commit/push, diff checks, tests, or deployment unless the user explicitly requests them.
The delivery includes the earlier uncommitted work from this conversation as well as the cleanup and lint follow-up. Earlier fixes already in the baseline history remain in place; they are not reapplied or merged from another branch.
-
Web preview: fit a
393 x 852portrait frame inside the side panel, avoid blocking the initial web screen on browser audio playback, and bundle the locally suppliedassets/.env. Credentials themselves remain untracked. -
Stage selection: enable mouse dragging as well as touch swipes; one clickable indicator per loaded stage navigates to the matching page. The source sheet's stage ordering is preserved.
-
Settings/audio: add persisted All Sounds, Sound Effects, and BGM controls in the existing seven UI languages; synchronize the app-bar BGM control, stop muted effects, guard duplicate playback starts, and repair Settings Back navigation and its touch area. Preference key:
figureout.audio-preferences.v1. -
Gameplay: completely hidden D/DR shapes do not block enclosure/path checks or circle taps. D preserves the authored position; only DR randomizes it. Preserve slice endpoints through resampling/smoothing, avoid applying a drag penalty after a tap, and wait for sliced Rectangle pieces before
Wait 0starts the next wave. -
Timing/movement: keep initial and intermediate fixed/random waits in the Z sequence, repeat the leading wait after
Back, and start attack timers only after the initial movement wait. Match D/DR as complete command lines so theDinRND(...)cannot create a blinking behavior accidentally. -
Commands: smooth D/DR fade-in and fade-out;
Cclockwise andCCcounterclockwise;P(vx, vy, gravity)parabolic launches; optional fourth Z argument for linear acceleration. See P and Z for parameters and behavior. -
The rejected falling-defense and Simon memory modes are absent from the current Dart implementation. No such mode is restored by this delivery.
-
Stage/mission rows (including Stage 5 M6 Fruit Ninja and Stage 6 M3/M4 variants/layouts) live in the Google Sheet. Sheet-only edits, row heights, and Google Cloud account/credential settings are external state, not Git changes. Their live contents were not rechecked or modified during this handoff.
-
Include the user-approved full
pubspec.lockchanges, not only the three dependency removals: characters 1.4.1, matcher 0.12.20, material_color_utilities 0.13.0, meta 1.19.0, test_api 0.7.12, and vector_math 2.4.4; locked Dart requirement>=3.11.0-0 <4.0.0. This is an explicitly approved exception to the usual lockfile delivery restriction in the architecture document. No SDK upgrade was run for delivery. -
Include the user-approved
analysis_options.yamlexclusions forbuild/**,android/**, andios/**;lib/remains subject to Flutter lint rules. No further analysis is run during this handoff.
Only main and its single worktree are present. Review found no additional
local branches/worktrees requiring a separate commit or push. No merge is part
of this handoff. The lint result below belongs to the preceding authorized lint
request; this documentation/Git delivery does not rerun builds, lint, or tests.
No release build, store upload, deployment, or device/gameplay verification is
claimed by the source delivery.
- Baseline:
mainat11f4f2e218f1da503502657fece20861b347e267, with existing uncommitted gameplay, settings, and data-loading changes preserved. - Removed 117 unreferenced assets (41,060,423 source bytes, approximately 39.2 MiB),
four unreachable Dart files, unused helper APIs/state, and the unused direct
dependencies
google_fonts,carousel_slider, andcupertino_icons. - Asset review included constructed paths for stars, stage cards, audio tracks, result panels, the no-hearts overlay, and the Flame pause button. Local Dart imports and retained asset paths resolve. These are static checks, not device QA.
flutter pub get --offlinecompleted.analysis_options.yamlnow enables the declared Flutter lint rules and is no longer ignored by Git.flutter analyze --no-pub: 0 errors, 0 warnings, 93 informational findings; exit code 1, so the lint run is not clean. Remaining findings include naming, override annotations, logging, and asyncBuildContextuse. They remain visible; no rules were suppressed. The earlier default analyzer reported 28 findings before the missing lint configuration was restored; the totals are not comparable.- No build, gameplay/device QA, commit, push, or deployment was performed for this cleanup. Source bytes removed are not a measured package-size reduction.
- Baseline: the cleanup above, with 93 informational findings and existing uncommitted changes preserved; branch and HEAD remain unchanged.
- Renamed 46 Dart files to
snake_caseand updated imports and documentation. Normalized app/type/constant names, added explicit field types and override annotations, matched callback parameter names, and added required braces. - Preserved gameplay conditions and values. Removed pass-through overrides and the duplicate endless-mode timer field (using the same initialized base field). Local function declarations retain the same arguments and calculations.
- Added mounted checks after async initialization/navigation waits, including the endless-mode load continuation. Closed screens no longer access a stale context or start a game after disposal. Reward callbacks and persistence keys remain unchanged.
- Diagnostic messages now use the existing debug-level
LoggerService; this level stays local and does not append to the logging sheet. Analytics calls and payloads were not changed. flutter analyze --no-pub: No issues found (0 errors, warnings, or infos). Lint rules remain enabled; no new suppressions or exclusions were added. Local imports were checked with exact path casing. No build or device/gameplay QA was performed.
공통 지침의 해당 분야를 적용합니다. 새 사용자용 문구의 사전 승인, 기존 작업·서명 보존, 품질을 유지하는 자산 최적화, 언어·시트·광고·IAP 규칙과 출시 전 점검은 프로젝트의 실제 기능 범위 및 승인 예외와 함께 판단합니다. APK/AAB 공동 작업·캐시 입력·QA/출시 ABI 분리·TestFlight 제출 전용 재시도는 기존 빌드 경로에서 구현 여부를 확인해야 합니다.
이번 반영은 공통 지침 연결과 확인된 일부 소스/설정 차이의 보완이며, 전체 기능의 정책 준수나 출시 준비 완료를 뜻하지 않습니다. 기존 미커밋 변경을 보존했으며 빌드·린트·테스트·스토어 업로드는 실행하지 않았습니다. 호환성이 확인되지 않은 SDK/AGP 일괄 업그레이드, 신규 문구, 자격 증명 변경은 수행하지 않습니다.
analyze: 실패 또는 차단(원본 로그 확인); test: 실패 또는 차단(원본 로그 확인). 앞 절의 미실행 기록은 소스 반영 당시 상태입니다. Android APK/AAB 및 기기 검증 완료를 뜻하지 않습니다. 이번 커밋에는 정책 적용 변경만 포함하며 기존 다른 작업과 생성 산출물은 보존·제외합니다.
Reward presentation now reserves one immutable ad instance, ignores duplicate terminal/reward callbacks, rejects overlapping shows, and discards cached ads after one hour (also checked before reuse after suspension). A late earned callback is handled at most once. Heart credit is persisted on the earned callback instead of depending on a still-open dialog; checkpoint continuation waits for both earned and closed signals and a mounted route. Per the user's explicit decision on 2026-11-03, a failed/unavailable ad no longer bypasses the checkpoint reward requirement; the existing continue overlay remains available for a user retry. No new app-facing text was added.
flutter analyze --no-pub on the three affected source files and the regression test passed after correcting one braces-style finding. flutter test --no-pub test/ad_manager_lifecycle_test.dart passed against the real manager and SDK Dart objects with a mocked native channel: duplicate/late earned callbacks, duplicate dismissal, overlapping presentation, one-hour cache expiry, and next-load-without-auto-show. This is mock-boundary evidence, not device verification. This app uses consumable hearts/continuation rather than a timed ad-free entitlement, so benefit-expiry testing does not apply in the same way. Android device QA and iOS remain unverified. At the time of this review, demo IDs and the missing UMP integration were separate release-readiness limitations. The current manager now checks UMP consent before requesting ads and still uses demo rewarded IDs.
lib/src/services/ad_network.dart가 connectivity_plus 6.1.5의 연결 상태를 확인하고, lib/src/services/ad_manager.dart가 보상 광고 준비와 표시에 반영합니다.
최초 연결 확인 전·확인된 오프라인 중에는 새 보상 광고 요청과 표시를 막고,
기존 재시도 횟수·예정 시각 및 진행 중 SDK 요청의 콜백 소유권을 보존합니다.
연결 복구는 남아 있는 준비만 재개하며 실패 횟수 소진을 초기화하거나 자동 재생하지 않습니다.
팝업 진입 시 만료 광고 교체 준비를 요청하며, 오프라인이면 연결 복구 후 이어갑니다.
준비 완료 후 기존 Watch Ad 버튼을 새로 눌러야 표시됩니다.
상세 기준은 공통 광고 정책을 따릅니다. 이번 변경은 소스 검토와 필요한 의존성·잠금 파일 갱신 범위입니다. 새 앱 문구·번역, 보상 기간, 광고 ID, UMP 설정은 변경하지 않았습니다. 빌드·린트·테스트·기기 설치·커밋·푸시는 실행하지 않았습니다. 기존 APK·AAB·TestFlight에 포함됐다는 의미가 아닙니다. 다음 승인된 검증에서 오프라인 시작, 재시도 대기 중 단절, 늦은 로드 콜백, 백그라운드 복귀, 오프라인 중 만료 팝업 열기, 복구 후 새 Watch Ad 탭을 확인해야 합니다.