# Arcforge

Arcforge is a mobile-first personal rules engine with an RPG interface. Positive actions forge Gold, negative actions and missed recurring tasks cost HP, and Gold can be exchanged for real-world rewards. Every balance change is deterministic, explained, and persisted in an append-only ledger.

## Requirements

- PHP 8.2 or newer with `pdo_mysql`
- MySQL 8.0 or newer
- Apache with `mod_rewrite`, Nginx, or PHP's development server
- A browser with modern JavaScript support

No Composer or JavaScript package installation is required.

## Installation

1. Create an empty MySQL database and a least-privilege database user.
2. Copy `.env.example` to `.env` and set the database connection values.
3. Point the web server document root at `public/`. Never expose the project root.
4. Open the application. The central migrator automatically creates or upgrades the schema.

You can also migrate explicitly:

```sh
php database/migrate.php
```

For local development:

```sh
php -S 127.0.0.1:8080 -t public
```

Then open `http://127.0.0.1:8080`.

### Optional password protection

Create a password hash and put it in `APP_PASSWORD_HASH`:

```sh
php -r "echo password_hash('choose-a-strong-password', PASSWORD_DEFAULT);"
```

When no hash is configured, Arcforge runs as a local single-user application without a login prompt. Configure the password, HTTPS, and normal web-server protections before exposing it to a network.

### Demo data

Set `DEMO_MODE=true`, then run:

```sh
php database/seed.php
```

The seed is explicit, repeat-safe, and separate from migrations. Disable demo mode afterward.

## Configuration

`config/game.php` is the only source of game-balance values. It defines maximum HP, Gold and HP values by difficulty, item-type multipliers, death behavior, the default Daily damage policy, precision, and the affordability averaging window. Change balance there rather than in controllers, templates, or JavaScript.

`config/app.php` owns the application name, environment, timezone, optional password, and demo mode. `config/database.php` owns PDO configuration. The default timezone is `Europe/Vienna`; recurrence calculations use `DateTimeImmutable` in that timezone, including daylight-saving boundaries.

## Mechanics

### Gold

The formula is:

```text
base Gold × difficulty Gold value × item-type multiplier
```

With the default configuration, a Hard Daily is `1 × 7 × 1 = 7 GP`. There is no random factor. Habit, Daily, and To-Do awards use `GoldCalculator`; the browser never supplies an award amount.

Every change updates the locked character row and inserts a `gold_transactions` record in the same database transaction. The transaction contains a UUID, signed amount, resulting balance, source, reason, and calculation. Purchases and death losses use the same path.

### HP and death

Difficulty maps directly to deterministic HP damage through `HpCalculator`. Restoration is capped at maximum HP, while the full configured reward price is still charged. At 0 HP the active life becomes dead, the configured Gold loss is entered in the Gold ledger, and a death event is recorded. Starting again creates the next life number without deleting tasks, occurrences, or history.

### Habits

Habits can be positive, negative, or both. Positive actions award Gold; negative actions remove HP. `expected_per_day` tells the Perfect Day engine how many positive uses are expected. Set it to zero for an earnable action that should not be required for a Perfect Day.

### Dailies and recurrence

Dailies are recurring objectives, despite their name. The supported schedules are:

- `daily`: every date on or after `start_date`
- `weekly`: `{"weekdays":[1,3,5]}` where Monday is 1 and Sunday is 7
- `monthly`: `{"day":15}`
- `yearly`: `{"month":9,"day":11}`
- `interval`: `{"every":2,"unit":"weeks"}`; units are days, weeks, months, or years
- `custom`: `{"dates":["2026-10-01","2026-10-17"]}`

Checkpoints are progress indicators and never award Gold. The Daily's main completion control is authoritative. If “require all checkpoints” is enabled, all active checkpoints must be complete first.

The first request after a local day boundary runs `DailyPenaltyService`. It creates one occurrence per due Daily and date, then marks it missed and applies exactly one penalty, or marks it excused when damage was paused. The unique `(daily_id, occurrence_date)` constraint prevents repeat penalties. For installations that must process exactly at midnight even when nobody opens the app, schedule a daily request or a small CLI wrapper around this service.

The included wrapper can be scheduled just after local midnight:

```sh
php database/daily-reset.php
```

### Damage pause

Damage can be paused indefinitely or through a date, then resumed manually. Dailies remain completable and still award Gold. Each pause and resume is included in history; missed occurrences covered by a pause are stored as excused.

### To-Dos and rewards

To-Dos are one-time objectives with optional due dates and checkpoints. Their database completion timestamp prevents repeat awards. Rewards have a fixed GP price, an optional HP restoration amount, and a spendable factor between 0 and 1.

Reward affordability is:

```text
effective daily savings = average Perfect Day GP × spendable factor
estimated perfect days = ceiling(price / effective daily savings)
```

The Perfect Day engine averages all scheduled Dailies over the configured 365-day window and adds expected positive Habit income. One-time To-Dos are excluded by default. Estimates recalculate when the setup changes.

### Perfect Days and streaks

A day becomes perfect when every due Daily is completed and each positive Habit reaches its configured expected count. The date is stored once. Daily streaks count consecutive resolved scheduled occurrences ending in completion; Habit counters show recorded positive completions.

## Directory structure

```text
config/                 app, database, and centralized balance configuration
database/migrations/    ordered schema migrations only
database/migrate.php    CLI migration entry point
database/seed.php       optional demo fixtures
public/                 sole web root; router, API, CSS, and JavaScript
src/Http/               response concerns
src/Repositories/       persistence and read models
src/Services/           authoritative game mechanics
src/Support/            CSRF, authentication, and UUID helpers
tests/                  dependency-free mechanics tests
views/                  presentation templates without balance logic
```

## API and retry safety

The web UI posts JSON to `public/api.php`; Apache also accepts paths below `/api/`. Actions include Habit positive/negative, Daily completion, To-Do completion, checkpoint toggles, reward purchase, pause/resume, item management, and starting a new life.

Economy-changing requests require a 12–100 character idempotency key. A unique database key claims the request before any mutation, concurrent retries lock that record, and completed responses are replayed. Dailies and To-Dos have additional state constraints, so a new request key cannot award the same completion twice. All game actions lock the relevant item and character inside a transaction.

## Security

- PDO native prepared statements and explicit table allowlists prevent SQL injection.
- Output is HTML-escaped at the template boundary.
- Mutations require a session-bound CSRF token and POST.
- Session cookies are HTTP-only and SameSite; they are Secure under HTTPS.
- An optional password gate regenerates the session ID after authentication.
- Content Security Policy, anti-framing, MIME-sniffing, and referrer headers are set centrally.
- Server-side services calculate all Gold and HP; client values are never trusted.
- Destructive actions require confirmation, and referenced items are archived rather than erased.

For public hosting, also use HTTPS, database backups, web-server rate limiting, a private database network, and a strong `APP_PASSWORD_HASH`.

## Tests

Run the dependency-free unit suite:

```sh
php tests/run.php
```

The unit layer covers deterministic Gold and HP, weekly/monthly/yearly recurrence, and affordability math. Database invariants provide the final line of defense for duplicate occurrences, ledger UUIDs, life numbers, and idempotency keys.

The suite also contains MySQL integration coverage for retry replay, duplicate Daily/To-Do completion, insufficient Gold, capped potion restoration, damage pause, death, and Gold loss. It runs only with both `APP_ENV=test` and a database name ending in `_test`, so it cannot accidentally mutate a production-named database.

## Extending Arcforge

Keep new balance values in `config/game.php`. Put date rules in `DailyScheduler`, economy calculations in calculator classes, and state changes in transactional services. Add schema changes as a new numbered migration; never add `CREATE TABLE` statements to endpoints. Read-oriented aggregates belong in `AppRepository`, while templates should only format already-calculated values.

When adding a new Gold or HP source, always route it through `LedgerService`, provide a human-readable reason and calculation, add an activity event, and require an idempotency key if a retry could repeat the mutation.
