mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Compare commits
6 Commits
ef69c54949
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 63df7d7055 | |||
| c961fde8ac | |||
| 72b2bd1ec5 | |||
| dc7c1df4fd | |||
| 2d0c0d8ef0 | |||
| 810adcf990 |
@@ -0,0 +1,117 @@
|
|||||||
|
name: Build and Publish Release Binaries
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.goos }}/${{ matrix.goarch }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { goos: linux, goarch: amd64, archive: tar.gz }
|
||||||
|
- { goos: linux, goarch: arm64, archive: tar.gz }
|
||||||
|
- { goos: darwin, goarch: amd64, archive: tar.gz }
|
||||||
|
- { goos: darwin, goarch: arm64, archive: tar.gz }
|
||||||
|
- { goos: windows, goarch: amd64, archive: zip }
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
cache-dependency-path: server/go.sum
|
||||||
|
|
||||||
|
- name: Install protoc
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
|
||||||
|
|
||||||
|
- name: Install protoc-gen-go plugins
|
||||||
|
run: |
|
||||||
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
|
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Generate proto stubs
|
||||||
|
working-directory: server
|
||||||
|
run: make proto
|
||||||
|
|
||||||
|
- name: Cross-compile binaries
|
||||||
|
working-directory: server
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
stage="staging"
|
||||||
|
mkdir -p "$stage/bin"
|
||||||
|
|
||||||
|
ext=""
|
||||||
|
if [ "$GOOS" = "windows" ]; then ext=".exe"; fi
|
||||||
|
|
||||||
|
build() {
|
||||||
|
local name="$1" dest="$2"
|
||||||
|
go build -trimpath -ldflags="-s -w" -o "$dest/${name}${ext}" "./cmd/${name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wizard sits at the root so end-users see it immediately.
|
||||||
|
build wizard "$stage"
|
||||||
|
|
||||||
|
# Sub-services and admin tools go in bin/ to match the runtime layout.
|
||||||
|
for name in dev lunar-tear octo-cdn auth-server import-snapshot claim-account register-account wizard-restore; do
|
||||||
|
build "$name" "$stage/bin"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Stage docs
|
||||||
|
working-directory: server
|
||||||
|
run: |
|
||||||
|
cp ../README.md staging/README.md
|
||||||
|
cp ../LICENSE staging/LICENSE
|
||||||
|
|
||||||
|
- name: Archive
|
||||||
|
working-directory: server
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
name="lunar-tear-server-${{ github.ref_name }}-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
mv staging "$name"
|
||||||
|
if [ "${{ matrix.archive }}" = "zip" ]; then
|
||||||
|
zip -r "${name}.zip" "$name"
|
||||||
|
else
|
||||||
|
tar -czf "${name}.tar.gz" "$name"
|
||||||
|
fi
|
||||||
|
ls -lh "${name}".*
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lunar-tear-server-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: server/lunar-tear-server-*.${{ matrix.archive }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Attach archives to GitHub Release
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: List downloaded artifacts
|
||||||
|
run: ls -lh artifacts/
|
||||||
|
|
||||||
|
- uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: artifacts/*
|
||||||
|
generate_release_notes: true
|
||||||
|
draft: true
|
||||||
@@ -5,7 +5,14 @@ Discord server: https://discord.gg/MZAf5aVkJG
|
|||||||
|
|
||||||
## How To Launch The Server
|
## How To Launch The Server
|
||||||
|
|
||||||
### Prerequisites
|
### Download & Run (no setup)
|
||||||
|
|
||||||
|
Prebuilt binaries are published for Linux, macOS, and Windows on the [Releases page](https://github.com/Walter-Sparrow/lunar-tear/releases).
|
||||||
|
|
||||||
|
1. Download the archive for your OS/arch (`lunar-tear-server-<version>-<os>-<arch>.{tar.gz,zip}`).
|
||||||
|
2. Run `./wizard` (macOS/Linux) or double-click `wizard.exe` (Windows).
|
||||||
|
|
||||||
|
### Prerequisites (build from source)
|
||||||
|
|
||||||
- Go 1.25+
|
- Go 1.25+
|
||||||
- [goose](https://github.com/pressly/goose) migration tool
|
- [goose](https://github.com/pressly/goose) migration tool
|
||||||
@@ -40,12 +47,12 @@ By default the wizard uses ports 8003 (gRPC), 8080 (CDN), and 3000 (auth). Overr
|
|||||||
go run ./cmd/wizard --grpc-port 9003 --cdn-port 9080
|
go run ./cmd/wizard --grpc-port 9003 --cdn-port 9080
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ---------------- | ------- | ---------------------------------- |
|
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `--prefer-saved` | `false` | Reuse saved config without prompting |
|
| `--prefer-saved` | `false` | Reuse saved config without prompting |
|
||||||
| `--grpc-port` | `8003` | gRPC server port |
|
| `--grpc-port` | `8003` | gRPC server port |
|
||||||
| `--cdn-port` | `8080` | CDN server port |
|
| `--cdn-port` | `8080` | CDN server port |
|
||||||
| `--auth-port` | `3000` | Auth server port |
|
| `--auth-port` | `3000` | Auth server port |
|
||||||
| `--admin-port` | `0` | Admin webhook port (`0` = disabled). Bound on `127.0.0.1`; only takes effect when `LUNAR_ADMIN_TOKEN` is set in the env. |
|
| `--admin-port` | `0` | Admin webhook port (`0` = disabled). Bound on `127.0.0.1`; only takes effect when `LUNAR_ADMIN_TOKEN` is set in the env. |
|
||||||
|
|
||||||
Custom ports are saved to `.wizard.json` alongside your other settings. On the next run the saved ports are reused automatically — no need to pass the flags again. If you later pass different port flags, the wizard warns you that the ports changed and asks for confirmation before continuing.
|
Custom ports are saved to `.wizard.json` alongside your other settings. On the next run the saved ports are reused automatically — no need to pass the flags again. If you later pass different port flags, the wizard warns you that the ports changed and asks for confirmation before continuing.
|
||||||
@@ -105,9 +112,9 @@ go run ./cmd/import-snapshot \
|
|||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ------------ | ------------ | --------------------------------------------- |
|
| ------------ | ------------ | --------------------------------------------- |
|
||||||
| `--snapshot` | *(required)* | Path to JSON snapshot file |
|
| `--snapshot` | _(required)_ | Path to JSON snapshot file |
|
||||||
| `--uuid` | *(required)* | UUID to assign (must match the client's UUID) |
|
| `--uuid` | _(required)_ | UUID to assign (must match the client's UUID) |
|
||||||
| `--db` | `db/game.db` | SQLite database path |
|
| `--db` | `db/game.db` | SQLite database path |
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
|
||||||
@@ -174,40 +181,40 @@ Or via `make`:
|
|||||||
make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000"
|
make dev ARGS="--grpc.listen 0.0.0.0:9000 --grpc.public-addr 10.0.2.2:9000"
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| --------------------- | ------------------ | ---------------------------------------- |
|
| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `--auth.listen` | `0.0.0.0:3000` | auth-server listen address |
|
| `--auth.listen` | `0.0.0.0:3000` | auth-server listen address |
|
||||||
| `--auth.db` | `db/auth.db` | auth-server SQLite database path |
|
| `--auth.db` | `db/auth.db` | auth-server SQLite database path |
|
||||||
| `--cdn.listen` | `0.0.0.0:8080` | octo-cdn local bind address |
|
| `--cdn.listen` | `0.0.0.0:8080` | octo-cdn local bind address |
|
||||||
| `--cdn.public-addr` | `10.0.2.2:8080` | octo-cdn externally-reachable addr |
|
| `--cdn.public-addr` | `10.0.2.2:8080` | octo-cdn externally-reachable addr |
|
||||||
| `--grpc.listen` | `0.0.0.0:8003` | lunar-tear gRPC listen address |
|
| `--grpc.listen` | `0.0.0.0:8003` | lunar-tear gRPC listen address |
|
||||||
| `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr |
|
| `--grpc.public-addr` | `10.0.2.2:8003` | lunar-tear externally-reachable addr |
|
||||||
| `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear |
|
| `--grpc.octo-url` | `http://10.0.2.2:8080` | Octo CDN base URL passed to lunar-tear |
|
||||||
| `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear |
|
| `--grpc.auth-url` | `http://localhost:3000` | auth server base URL passed to lunar-tear |
|
||||||
| `--no-register` | `false` | disable new user registrations (only already registered users can connect). |
|
| `--no-register` | `false` | disable new user registrations (only already registered users can connect). |
|
||||||
| `--admin.listen` | *(empty)* | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. |
|
| `--admin.listen` | _(empty)_ | lunar-tear admin webhook bind. Empty = leave default; webhook only binds when `LUNAR_ADMIN_TOKEN` is set in the env. |
|
||||||
| `--no-color` | `false` | disable colored output |
|
| `--no-color` | `false` | disable colored output |
|
||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
|
|
||||||
| Protocol | Port | Binary | Notes |
|
| Protocol | Port | Binary | Notes |
|
||||||
| -------- | ---- | ------------- | ----------------------------------------------------------- |
|
| -------- | ---- | ------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||||
| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) |
|
| gRPC | 443 | `lunar-tear` | default; configurable with `--listen` (requires patched client) |
|
||||||
| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages |
|
| HTTP | 8080 | `octo-cdn` | Octo asset API + game web pages |
|
||||||
| HTTP | 8082 | `lunar-tear` | admin webhook (`/api/admin/master-data/reload`); loopback by default, only binds when `LUNAR_ADMIN_TOKEN` is set |
|
| HTTP | 8082 | `lunar-tear` | admin webhook (`/api/admin/master-data/reload`); loopback by default, only binds when `LUNAR_ADMIN_TOKEN` is set |
|
||||||
| HTTP | 3000 | `auth-server` | account registration and login |
|
| HTTP | 3000 | `auth-server` | account registration and login |
|
||||||
|
|
||||||
### Game Server Flags (`lunar-tear`)
|
### Game Server Flags (`lunar-tear`)
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ---------------- | ----------------- | ---------------------------------------------------- |
|
| ---------------- | ---------------- | --------------------------------------------------------------------------- |
|
||||||
| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) |
|
| `--listen` | `0.0.0.0:443` | gRPC listen address (host:port) |
|
||||||
| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients |
|
| `--public-addr` | `127.0.0.1:443` | externally-reachable host:port advertised to clients |
|
||||||
| `--octo-url` | *(required)* | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) |
|
| `--octo-url` | _(required)_ | CDN base URL the client uses for assets (e.g. `http://10.0.2.2:8080`) |
|
||||||
| `--db` | `db/game.db` | SQLite database path |
|
| `--db` | `db/game.db` | SQLite database path |
|
||||||
| `--auth-url` | *(empty)* | Auth server base URL (e.g. `http://localhost:3000`) |
|
| `--auth-url` | _(empty)_ | Auth server base URL (e.g. `http://localhost:3000`) |
|
||||||
| `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. |
|
| `--admin-listen` | `127.0.0.1:8082` | Admin webhook listen address. Only binds when `LUNAR_ADMIN_TOKEN` is set. |
|
||||||
| `--no-register` | `false` | Disable new user registrations (only already registered users can connect). |
|
| `--no-register` | `false` | Disable new user registrations (only already registered users can connect). |
|
||||||
|
|
||||||
### Live Master Data Reload
|
### Live Master Data Reload
|
||||||
|
|
||||||
@@ -231,11 +238,11 @@ Security defaults are fail-closed:
|
|||||||
|
|
||||||
### CDN Flags (`octo-cdn`)
|
### CDN Flags (`octo-cdn`)
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| --------------- | ----------------- | -------------------------------------------------------- |
|
| --------------- | ---------------- | --------------------------------------------------------- |
|
||||||
| `--listen` | `0.0.0.0:8080` | local bind address |
|
| `--listen` | `0.0.0.0:8080` | local bind address |
|
||||||
| `--public-addr` | `127.0.0.1:8080` | externally-reachable address (used in list.bin rewriting) |
|
| `--public-addr` | `127.0.0.1:8080` | externally-reachable address (used in list.bin rewriting) |
|
||||||
| `--assets-dir` | `.` | root directory containing the `assets/` tree |
|
| `--assets-dir` | `.` | root directory containing the `assets/` tree |
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
@@ -250,22 +257,22 @@ The `db/` directory is mounted as a volume so both `game.db` and `auth.db` persi
|
|||||||
|
|
||||||
Each service has its own image and can be deployed independently:
|
Each service has its own image and can be deployed independently:
|
||||||
|
|
||||||
| Service | Image | Default Port | Notes |
|
| Service | Image | Default Port | Notes |
|
||||||
| -------- | --------------------------- | ------------ | ------------------------------ |
|
| -------- | --------------------------- | ------------ | -------------------------------- |
|
||||||
| `server` | `kretts/lunar-tear:latest` | 8003, 8082 | gRPC game server + admin webhook |
|
| `server` | `kretts/lunar-tear:latest` | 8003, 8082 | gRPC game server + admin webhook |
|
||||||
| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN |
|
| `cdn` | `kretts/octo-cdn:latest` | 8080 | HTTP asset CDN |
|
||||||
| `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login |
|
| `auth` | `kretts/auth-server:latest` | 3000 | Account registration and login |
|
||||||
|
|
||||||
The game server is configured via environment variables in the compose file:
|
The game server is configured via environment variables in the compose file:
|
||||||
|
|
||||||
| Env var | Description |
|
| Env var | Description |
|
||||||
| --------------------- | -------------------------------------------------------------------------------------------- |
|
| -------------------- | ------------------------------------------------------------------------------------- |
|
||||||
| `LUNAR_LISTEN` | gRPC bind address |
|
| `LUNAR_LISTEN` | gRPC bind address |
|
||||||
| `LUNAR_PUBLIC_ADDR` | Client-facing address advertised to the game |
|
| `LUNAR_PUBLIC_ADDR` | Client-facing address advertised to the game |
|
||||||
| `LUNAR_OCTO_URL` | CDN base URL the client uses for assets |
|
| `LUNAR_OCTO_URL` | CDN base URL the client uses for assets |
|
||||||
| `LUNAR_AUTH_URL` | Auth server base URL (optional) |
|
| `LUNAR_AUTH_URL` | Auth server base URL (optional) |
|
||||||
| `LUNAR_ADMIN_LISTEN` | Admin webhook bind address inside the container (compose default: `0.0.0.0:8082`) |
|
| `LUNAR_ADMIN_LISTEN` | Admin webhook bind address inside the container (compose default: `0.0.0.0:8082`) |
|
||||||
| `LUNAR_ADMIN_TOKEN` | Bearer token for the admin webhook. **The webhook does not bind unless this is set.** |
|
| `LUNAR_ADMIN_TOKEN` | Bearer token for the admin webhook. **The webhook does not bind unless this is set.** |
|
||||||
|
|
||||||
Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. The admin webhook is published to `127.0.0.1:8082` on the host so the master-data reload endpoint stays loopback-only by default; set `LUNAR_ADMIN_TOKEN` (e.g. via a `.env` file) before bringing the stack up.
|
Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without it. The admin webhook is published to `127.0.0.1:8082` on the host so the master-data reload endpoint stays loopback-only by default; set `LUNAR_ADMIN_TOKEN` (e.g. via a `.env` file) before bringing the stack up.
|
||||||
|
|
||||||
@@ -273,22 +280,22 @@ Auth is optional — if `LUNAR_AUTH_URL` is unset the game server starts without
|
|||||||
|
|
||||||
All targets run from the `server/` directory.
|
All targets run from the `server/` directory.
|
||||||
|
|
||||||
| Target | Description |
|
| Target | Description |
|
||||||
| -------------- | ------------------------------------------------------- |
|
| ----------------------------- | ------------------------------------------------------ |
|
||||||
| `make proto` | Regenerate protobuf stubs |
|
| `make proto` | Regenerate protobuf stubs |
|
||||||
| `make build` | Build the game server binary |
|
| `make build` | Build the game server binary |
|
||||||
| `make build-cdn` | Build the CDN binary |
|
| `make build-cdn` | Build the CDN binary |
|
||||||
| `make build-auth` | Build the auth server binary |
|
| `make build-auth` | Build the auth server binary |
|
||||||
| `make build-dev` | Build the dev runner binary to `bin/` |
|
| `make build-dev` | Build the dev runner binary to `bin/` |
|
||||||
| `make build-all` | Build all service binaries to `bin/` |
|
| `make build-all` | Build all service binaries to `bin/` |
|
||||||
| `make build-import` | Build the import-snapshot tool |
|
| `make build-import` | Build the import-snapshot tool |
|
||||||
| `make build-claim-account` | Build the claim-account tool |
|
| `make build-claim-account` | Build the claim-account tool |
|
||||||
| `make build-register-account` | Build the register-account tool |
|
| `make build-register-account` | Build the register-account tool |
|
||||||
| `make clean` | Remove the `bin/` directory |
|
| `make clean` | Remove the `bin/` directory |
|
||||||
| `make dev` | Run all three services with one command |
|
| `make dev` | Run all three services with one command |
|
||||||
| `make migrate` | Run goose migrations on `db/game.db` |
|
| `make migrate` | Run goose migrations on `db/game.db` |
|
||||||
| `make restore` | Interactive restore of `db/game.db` from `db/backups/` |
|
| `make restore` | Interactive restore of `db/game.db` from `db/backups/` |
|
||||||
| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) |
|
| `make import` | Import a snapshot (`SNAPSHOT=... UUID=...` required) |
|
||||||
|
|
||||||
## Claim Account
|
## Claim Account
|
||||||
|
|
||||||
@@ -301,10 +308,10 @@ cd server
|
|||||||
go run ./cmd/claim-account --name "PlayerName" --db db/game.db
|
go run ./cmd/claim-account --name "PlayerName" --db db/game.db
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| -------- | ------------ | ---------------------------------------------------- |
|
| -------- | ------------ | ---------------------------- |
|
||||||
| `--name` | *(required)* | In-game player name to claim |
|
| `--name` | _(required)_ | In-game player name to claim |
|
||||||
| `--db` | `db/game.db` | SQLite database path |
|
| `--db` | `db/game.db` | SQLite database path |
|
||||||
|
|
||||||
## Auth Server
|
## Auth Server
|
||||||
|
|
||||||
@@ -323,12 +330,12 @@ The `--secret` flag accepts a hex-encoded HMAC key. If omitted, a random key is
|
|||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ---------------- | --------------- | -------------------------------------------- |
|
| --------------- | -------------- | -------------------------------------------------------------------------- |
|
||||||
| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) |
|
| `--listen` | `0.0.0.0:3000` | HTTP listen address (host:port) |
|
||||||
| `--db` | `db/auth.db` | SQLite database path for auth users |
|
| `--db` | `db/auth.db` | SQLite database path for auth users |
|
||||||
| `--secret` | *(generated)* | Hex-encoded HMAC secret for token signing |
|
| `--secret` | _(generated)_ | Hex-encoded HMAC secret for token signing |
|
||||||
| `--no-register` | `false` | Disable new user registrations (only already registered users can log in). |
|
| `--no-register` | `false` | Disable new user registrations (only already registered users can log in). |
|
||||||
|
|
||||||
## Create account
|
## Create account
|
||||||
|
|
||||||
@@ -339,13 +346,13 @@ A primary mean of registering new accounts when `--no-register` flag is passed t
|
|||||||
go run ./cmd/register-account --name "AccountName" --password "AccountPassword" --platform "android"
|
go run ./cmd/register-account --name "AccountName" --password "AccountPassword" --platform "android"
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| ------------ | ------------ | ------------------------------------------------------------ |
|
| ------------ | ------------ | ------------------------------------------------- |
|
||||||
| `--name` | *(required)* | Auth Server account nickname to be registered |
|
| `--name` | _(required)_ | Auth Server account nickname to be registered |
|
||||||
| `--password` | *(required)* | Auth Server account password to be registered |
|
| `--password` | _(required)_ | Auth Server account password to be registered |
|
||||||
| `--platform` | `android` | Platform of new user account (`android` or `ios`) |
|
| `--platform` | `android` | Platform of new user account (`android` or `ios`) |
|
||||||
| `--db` | `db/game.db` | SQLite main database path |
|
| `--db` | `db/game.db` | SQLite main database path |
|
||||||
| `--auth-db` | `db/auth.db` | SQLite Auth Server database path |
|
| `--auth-db` | `db/auth.db` | SQLite Auth Server database path |
|
||||||
|
|
||||||
This only sets the nickname of Auth Server account, a player can choose their in-game nickname upon first login!
|
This only sets the nickname of Auth Server account, a player can choose their in-game nickname upon first login!
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ var childTables = []string{
|
|||||||
"user_big_hunt_max_scores",
|
"user_big_hunt_max_scores",
|
||||||
"user_quest_limit_content_status",
|
"user_quest_limit_content_status",
|
||||||
"user_side_story_quests",
|
"user_side_story_quests",
|
||||||
|
"user_main_quest_season_routes",
|
||||||
"user_missions",
|
"user_missions",
|
||||||
"user_quest_missions",
|
"user_quest_missions",
|
||||||
"user_quests",
|
"user_quests",
|
||||||
|
|||||||
@@ -120,8 +120,12 @@ func main() {
|
|||||||
colorCyan = ""
|
colorCyan = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("building services...")
|
if _, err := os.Stat("go.mod"); err == nil {
|
||||||
buildAll()
|
log.Println("building services...")
|
||||||
|
buildAll()
|
||||||
|
} else {
|
||||||
|
log.Println("prebuilt mode: skipping build, using bin/ from archive")
|
||||||
|
}
|
||||||
|
|
||||||
ext := binExt()
|
ext := binExt()
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
|||||||
+29
-21
@@ -27,31 +27,39 @@ func backupGameDB() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = spinner.New().Title(" Backing up db/game.db...").Action(func() {
|
if !sourceMode {
|
||||||
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
fmt.Println(" Backing up db/game.db...")
|
||||||
fmt.Fprintf(os.Stderr, " failed to create %s: %v\n", backupDir, err)
|
doBackupGameDB()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := time.Now().UTC().Format("20060102T150405Z")
|
_ = spinner.New().Title(" Backing up db/game.db...").Action(doBackupGameDB).Run()
|
||||||
dest := filepath.Join(backupDir, fmt.Sprintf("game.db.%s%s", ts, backupSuffix))
|
}
|
||||||
|
|
||||||
db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)")
|
func doBackupGameDB() {
|
||||||
if err != nil {
|
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, " failed to open %s: %v\n", gameDBPath, err)
|
fmt.Fprintf(os.Stderr, " failed to create %s: %v\n", backupDir, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
escaped := strings.ReplaceAll(dest, "'", "''")
|
ts := time.Now().UTC().Format("20060102T150405Z")
|
||||||
if _, err := db.Exec(fmt.Sprintf("VACUUM INTO '%s'", escaped)); err != nil {
|
dest := filepath.Join(backupDir, fmt.Sprintf("game.db.%s%s", ts, backupSuffix))
|
||||||
fmt.Fprintf(os.Stderr, " VACUUM INTO failed: %v\n", err)
|
|
||||||
_ = os.Remove(dest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pruneOldBackups()
|
db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)")
|
||||||
}).Run()
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " failed to open %s: %v\n", gameDBPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
escaped := strings.ReplaceAll(dest, "'", "''")
|
||||||
|
if _, err := db.Exec(fmt.Sprintf("VACUUM INTO '%s'", escaped)); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " VACUUM INTO failed: %v\n", err)
|
||||||
|
_ = os.Remove(dest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pruneOldBackups()
|
||||||
}
|
}
|
||||||
|
|
||||||
func pruneOldBackups() {
|
func pruneOldBackups() {
|
||||||
|
|||||||
+22
-13
@@ -74,14 +74,21 @@ func main() {
|
|||||||
|
|
||||||
fmt.Print(banner)
|
fmt.Print(banner)
|
||||||
|
|
||||||
|
sourceMode = isSourceCheckout()
|
||||||
|
|
||||||
if !*setupOnly {
|
if !*setupOnly {
|
||||||
validateAssets()
|
validateAssets()
|
||||||
validateTools()
|
if sourceMode {
|
||||||
validateProtocIncludes()
|
validateTools()
|
||||||
runProtoc()
|
validateProtocIncludes()
|
||||||
backupGameDB()
|
runProtoc()
|
||||||
runMigrate()
|
backupGameDB()
|
||||||
downloadDeps()
|
runMigrate()
|
||||||
|
downloadDeps()
|
||||||
|
} else {
|
||||||
|
backupGameDB()
|
||||||
|
runMigrateEmbedded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, cfg, firstRun := resolveIP(*preferSaved)
|
ip, cfg, firstRun := resolveIP(*preferSaved)
|
||||||
@@ -901,13 +908,15 @@ func launchDev(ip string, p ports) {
|
|||||||
}
|
}
|
||||||
devBin := filepath.Join("bin", "dev"+ext)
|
devBin := filepath.Join("bin", "dev"+ext)
|
||||||
|
|
||||||
_ = spinner.New().Title(" Building services...").Action(func() {
|
if sourceMode {
|
||||||
if err := os.MkdirAll("bin", 0755); err != nil {
|
_ = spinner.New().Title(" Building services...").Action(func() {
|
||||||
fmt.Fprintf(os.Stderr, " Failed to create bin/: %v\n", err)
|
if err := os.MkdirAll("bin", 0755); err != nil {
|
||||||
os.Exit(1)
|
fmt.Fprintf(os.Stderr, " Failed to create bin/: %v\n", err)
|
||||||
}
|
os.Exit(1)
|
||||||
runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev")
|
}
|
||||||
}).Run()
|
runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev")
|
||||||
|
}).Run()
|
||||||
|
}
|
||||||
|
|
||||||
devArgs := []string{
|
devArgs := []string{
|
||||||
"--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC),
|
"--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC),
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"lunar-tear/server/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sourceMode bool
|
||||||
|
|
||||||
|
func isSourceCheckout() bool {
|
||||||
|
if _, err := os.Stat("go.mod"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("proto"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrateEmbedded() {
|
||||||
|
fmt.Println(" Running migrations...")
|
||||||
|
if err := os.MkdirAll("db", 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " Failed to create db/: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite", gameDBPath+"?_pragma=busy_timeout(5000)")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " open db: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
if err := migrations.Up(context.Background(), db); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, " migration failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-7
@@ -7,12 +7,12 @@ require (
|
|||||||
github.com/pierrec/lz4/v4 v4.1.26
|
github.com/pierrec/lz4/v4 v4.1.26
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||||
golang.org/x/crypto v0.50.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/net v0.52.0
|
golang.org/x/net v0.53.0
|
||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.43.0
|
||||||
golang.org/x/term v0.42.0
|
golang.org/x/term v0.42.0
|
||||||
google.golang.org/grpc v1.79.1
|
google.golang.org/grpc v1.80.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
modernc.org/sqlite v1.48.2
|
modernc.org/sqlite v1.49.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -34,21 +34,25 @@ require (
|
|||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pressly/goose/v3 v3.27.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/text v0.36.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.72.1 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,8 +54,12 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
|
|||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||||
|
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
@@ -66,10 +70,14 @@ github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY
|
|||||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
|
||||||
|
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
@@ -82,20 +90,28 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
|||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -109,18 +125,25 @@ golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
|||||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
||||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/cc/v4 v4.28.1 h1:XpLbkYVQ24E8tX5u8+yWGvaxerxkR/S4zqxI8ZoSBuc=
|
||||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||||
|
modernc.org/ccgo/v4 v4.33.0 h1:dspBCm75jsj8Y/ufwAMVfe375L2iYdMyQ2QG/v3hL54=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
@@ -131,16 +154,21 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
|
modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0=
|
||||||
|
modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
|
||||||
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
|
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||||
|
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package campaign
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Catalog struct {
|
||||||
|
enhance []enhanceRow
|
||||||
|
quest []questRow
|
||||||
|
}
|
||||||
|
|
||||||
|
type enhanceRow struct {
|
||||||
|
effectType EnhanceCampaignEffectType
|
||||||
|
effectValue int32
|
||||||
|
targets []enhanceMatch
|
||||||
|
startMillis int64
|
||||||
|
endMillis int64
|
||||||
|
userStatus TargetUserStatusType
|
||||||
|
}
|
||||||
|
|
||||||
|
type enhanceMatch struct {
|
||||||
|
t EnhanceCampaignTargetType
|
||||||
|
v int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type questRow struct {
|
||||||
|
effectType QuestCampaignEffectType
|
||||||
|
effectValue int32
|
||||||
|
bonusItems []BonusDrop
|
||||||
|
targets []questMatch
|
||||||
|
startMillis int64
|
||||||
|
endMillis int64
|
||||||
|
userStatus TargetUserStatusType
|
||||||
|
}
|
||||||
|
|
||||||
|
type questMatch struct {
|
||||||
|
t QuestCampaignTargetType
|
||||||
|
v int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Catalog, error) {
|
||||||
|
enhance, err := loadEnhanceRows()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load enhance campaigns: %w", err)
|
||||||
|
}
|
||||||
|
quest, err := loadQuestRows()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load quest campaigns: %w", err)
|
||||||
|
}
|
||||||
|
return &Catalog{enhance: enhance, quest: quest}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Catalog) EnhanceCount() int { return len(c.enhance) }
|
||||||
|
func (c *Catalog) QuestCount() int { return len(c.quest) }
|
||||||
|
|
||||||
|
func loadEnhanceRows() ([]enhanceRow, error) {
|
||||||
|
campaigns, err := utils.ReadTable[masterdata.EntityMEnhanceCampaign]("m_enhance_campaign")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targets, err := utils.ReadTable[masterdata.EntityMEnhanceCampaignTargetGroup]("m_enhance_campaign_target_group")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
byGroup := make(map[int32][]enhanceMatch, len(targets))
|
||||||
|
for _, t := range targets {
|
||||||
|
byGroup[t.EnhanceCampaignTargetGroupId] = append(byGroup[t.EnhanceCampaignTargetGroupId], enhanceMatch{
|
||||||
|
t: EnhanceCampaignTargetType(t.EnhanceCampaignTargetType),
|
||||||
|
v: t.EnhanceCampaignTargetValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]enhanceRow, 0, len(campaigns))
|
||||||
|
for _, c := range campaigns {
|
||||||
|
grp := byGroup[c.EnhanceCampaignTargetGroupId]
|
||||||
|
if len(grp) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows = append(rows, enhanceRow{
|
||||||
|
effectType: EnhanceCampaignEffectType(c.EnhanceCampaignEffectType),
|
||||||
|
effectValue: c.EnhanceCampaignEffectValue / 10,
|
||||||
|
targets: grp,
|
||||||
|
startMillis: c.StartDatetime,
|
||||||
|
endMillis: c.EndDatetime,
|
||||||
|
userStatus: TargetUserStatusType(c.TargetUserStatusType),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadQuestRows() ([]questRow, error) {
|
||||||
|
campaigns, err := utils.ReadTable[masterdata.EntityMQuestCampaign]("m_quest_campaign")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targets, err := utils.ReadTable[masterdata.EntityMQuestCampaignTargetGroup]("m_quest_campaign_target_group")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
effects, err := utils.ReadTable[masterdata.EntityMQuestCampaignEffectGroup]("m_quest_campaign_effect_group")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
itemGroups, err := utils.ReadTable[masterdata.EntityMQuestCampaignTargetItemGroup]("m_quest_campaign_target_item_group")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetsByGroup := make(map[int32][]questMatch, len(targets))
|
||||||
|
for _, t := range targets {
|
||||||
|
targetsByGroup[t.QuestCampaignTargetGroupId] = append(targetsByGroup[t.QuestCampaignTargetGroupId], questMatch{
|
||||||
|
t: QuestCampaignTargetType(t.QuestCampaignTargetType),
|
||||||
|
v: t.QuestCampaignTargetValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
bonusByGroup := make(map[int32][]BonusDrop, len(itemGroups))
|
||||||
|
for _, ig := range itemGroups {
|
||||||
|
bonusByGroup[ig.QuestCampaignTargetItemGroupId] = append(bonusByGroup[ig.QuestCampaignTargetItemGroupId], BonusDrop{
|
||||||
|
PossessionType: ig.PossessionType,
|
||||||
|
PossessionId: ig.PossessionId,
|
||||||
|
Count: ig.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
effectByGroup := make(map[int32]masterdata.EntityMQuestCampaignEffectGroup, len(effects))
|
||||||
|
for _, e := range effects {
|
||||||
|
effectByGroup[e.QuestCampaignEffectGroupId] = e
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]questRow, 0, len(campaigns))
|
||||||
|
for _, c := range campaigns {
|
||||||
|
grp := targetsByGroup[c.QuestCampaignTargetGroupId]
|
||||||
|
if len(grp) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eff, ok := effectByGroup[c.QuestCampaignEffectGroupId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rows = append(rows, questRow{
|
||||||
|
effectType: QuestCampaignEffectType(eff.QuestCampaignEffectType),
|
||||||
|
effectValue: eff.QuestCampaignEffectValue,
|
||||||
|
bonusItems: bonusByGroup[eff.QuestCampaignTargetItemGroupId],
|
||||||
|
targets: grp,
|
||||||
|
startMillis: c.StartDatetime,
|
||||||
|
endMillis: c.EndDatetime,
|
||||||
|
userStatus: TargetUserStatusType(c.TargetUserStatusType),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r enhanceRow) isActive(f Filter) bool {
|
||||||
|
if f.NowMillis < r.startMillis || f.NowMillis > r.endMillis {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return r.userStatus == TargetUserStatusAll || r.userStatus == f.UserStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r questRow) isActive(f Filter) bool {
|
||||||
|
if f.NowMillis < r.startMillis || f.NowMillis > r.endMillis {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return r.userStatus == TargetUserStatusAll || r.userStatus == f.UserStatus
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package campaign
|
||||||
|
|
||||||
|
func (c *Catalog) PartsRateBonus(t PartsTarget, f Filter) RateBonus {
|
||||||
|
var out RateBonus
|
||||||
|
for _, r := range c.enhance {
|
||||||
|
if !r.isActive(f) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchesParts(r.targets, t) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = applyEnhanceEffect(out, r)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Catalog) CostumeExpBonus(t CostumeTarget, f Filter) ExpBonus {
|
||||||
|
var sum int32
|
||||||
|
for _, r := range c.enhance {
|
||||||
|
if !r.isActive(f) || r.effectType != EnhanceEffectAdditionalPerm {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchesCostume(r.targets, t) {
|
||||||
|
sum += r.effectValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ExpBonus{bonusPermil: sum}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Catalog) WeaponExpBonus(t WeaponTarget, f Filter) ExpBonus {
|
||||||
|
var sum int32
|
||||||
|
for _, r := range c.enhance {
|
||||||
|
if !r.isActive(f) || r.effectType != EnhanceEffectAdditionalPerm {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchesWeapon(r.targets, t) {
|
||||||
|
sum += r.effectValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ExpBonus{bonusPermil: sum}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyEnhanceEffect(b RateBonus, r enhanceRow) RateBonus {
|
||||||
|
switch r.effectType {
|
||||||
|
case EnhanceEffectProbability:
|
||||||
|
b.override = r.effectValue
|
||||||
|
case EnhanceEffectAdditionalPerm:
|
||||||
|
b.bonusPermil += r.effectValue
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesParts(targets []enhanceMatch, t PartsTarget) bool {
|
||||||
|
for _, m := range targets {
|
||||||
|
switch m.t {
|
||||||
|
case EnhanceTargetPartsAll:
|
||||||
|
return true
|
||||||
|
case EnhanceTargetPartsSeriesId:
|
||||||
|
if m.v == t.PartsGroupId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case EnhanceTargetPartsId:
|
||||||
|
if m.v == t.PartsId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesCostume(targets []enhanceMatch, t CostumeTarget) bool {
|
||||||
|
for _, m := range targets {
|
||||||
|
switch m.t {
|
||||||
|
case EnhanceTargetCostumeAll:
|
||||||
|
return true
|
||||||
|
case EnhanceTargetCostumeCharacterId:
|
||||||
|
if m.v == t.CharacterId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case EnhanceTargetCostumeSkillfulWeapon:
|
||||||
|
if m.v == t.SkillfulWeaponType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case EnhanceTargetCostumeId:
|
||||||
|
if m.v == t.CostumeId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesWeapon(targets []enhanceMatch, t WeaponTarget) bool {
|
||||||
|
for _, m := range targets {
|
||||||
|
switch m.t {
|
||||||
|
case EnhanceTargetWeaponAll:
|
||||||
|
return true
|
||||||
|
case EnhanceTargetWeaponTypeId:
|
||||||
|
if m.v == t.WeaponType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case EnhanceTargetWeaponAttributeTypeId:
|
||||||
|
if m.v == t.AttributeType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case EnhanceTargetWeaponId:
|
||||||
|
if m.v == t.WeaponId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package campaign
|
||||||
|
|
||||||
|
type RateBonus struct {
|
||||||
|
override int32
|
||||||
|
bonusPermil int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b RateBonus) Apply(basePermil int32) int32 {
|
||||||
|
base := basePermil
|
||||||
|
if b.override > 0 {
|
||||||
|
base = b.override
|
||||||
|
}
|
||||||
|
return clampPermil(int32(int64(base) + int64(b.bonusPermil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpBonus struct {
|
||||||
|
bonusPermil int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b ExpBonus) Apply(base int32) int32 {
|
||||||
|
return int32(int64(base) * int64(1000+b.bonusPermil) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaminaMul struct {
|
||||||
|
permil int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StaminaMul) Apply(base int32) int32 {
|
||||||
|
if m.permil == 1000 {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return int32(int64(base) * int64(m.permil) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropRateMul struct {
|
||||||
|
bonusPermil int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DropRateMul) Apply(base int32) int32 {
|
||||||
|
return int32((int64(base)*int64(1000+m.bonusPermil) + 999) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BonusDrop struct {
|
||||||
|
PossessionType int32
|
||||||
|
PossessionId int32
|
||||||
|
Count int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampPermil(v int32) int32 {
|
||||||
|
if v < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v > 1000 {
|
||||||
|
return 1000
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package campaign
|
||||||
|
|
||||||
|
func (c *Catalog) QuestStamina(t QuestTarget, f Filter) StaminaMul {
|
||||||
|
return questPermilMin(c.quest, QuestEffectStaminaConsume, t, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Catalog) QuestDropRate(t QuestTarget, f Filter) DropRateMul {
|
||||||
|
var best int32
|
||||||
|
for _, r := range c.quest {
|
||||||
|
if !r.isActive(f) || r.effectType != QuestEffectDropRate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchesQuest(r.targets, t) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.effectValue > best {
|
||||||
|
best = r.effectValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DropRateMul{bonusPermil: best}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Catalog) QuestBonusDrops(t QuestTarget, f Filter) []BonusDrop {
|
||||||
|
var out []BonusDrop
|
||||||
|
for _, r := range c.quest {
|
||||||
|
if !r.isActive(f) || r.effectType != QuestEffectDropItemAdd {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchesQuest(r.targets, t) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, r.bonusItems...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func questPermilMin(rows []questRow, want QuestCampaignEffectType, t QuestTarget, f Filter) StaminaMul {
|
||||||
|
min := int32(1000)
|
||||||
|
for _, r := range rows {
|
||||||
|
if !r.isActive(f) || r.effectType != want {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !matchesQuest(r.targets, t) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.effectValue < min {
|
||||||
|
min = r.effectValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StaminaMul{permil: min}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesQuest(targets []questMatch, t QuestTarget) bool {
|
||||||
|
for _, m := range targets {
|
||||||
|
switch m.t {
|
||||||
|
case QuestTargetWholeQuest:
|
||||||
|
return true
|
||||||
|
case QuestTargetQuestType:
|
||||||
|
if int32(t.QuestType) == m.v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case QuestTargetEventQuestType:
|
||||||
|
if t.QuestType == QuestTypeEventQuest && t.EventQuestType == m.v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case QuestTargetMainQuestChapterId:
|
||||||
|
if t.QuestType == QuestTypeMainQuest && t.ChapterId == m.v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case QuestTargetMainQuestQuestId:
|
||||||
|
if t.QuestType == QuestTypeMainQuest && t.QuestId == m.v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case QuestTargetSubQuestChapterId:
|
||||||
|
if t.QuestType == QuestTypeEventQuest && t.ChapterId == m.v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case QuestTargetSubQuestQuestId:
|
||||||
|
if t.QuestType == QuestTypeEventQuest && t.QuestId == m.v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package campaign
|
||||||
|
|
||||||
|
import "lunar-tear/server/internal/model"
|
||||||
|
|
||||||
|
type EnhanceCampaignEffectType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnhanceEffectUnknown EnhanceCampaignEffectType = 0
|
||||||
|
EnhanceEffectProbability EnhanceCampaignEffectType = 1
|
||||||
|
EnhanceEffectAdditionalPerm EnhanceCampaignEffectType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type EnhanceCampaignTargetType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnhanceTargetUnknown EnhanceCampaignTargetType = 0
|
||||||
|
EnhanceTargetCostumeAll EnhanceCampaignTargetType = 1
|
||||||
|
EnhanceTargetWeaponAll EnhanceCampaignTargetType = 2
|
||||||
|
EnhanceTargetPartsAll EnhanceCampaignTargetType = 3
|
||||||
|
EnhanceTargetCostumeCharacterId EnhanceCampaignTargetType = 11
|
||||||
|
EnhanceTargetCostumeSkillfulWeapon EnhanceCampaignTargetType = 12
|
||||||
|
EnhanceTargetCostumeId EnhanceCampaignTargetType = 13
|
||||||
|
EnhanceTargetWeaponTypeId EnhanceCampaignTargetType = 21
|
||||||
|
EnhanceTargetWeaponAttributeTypeId EnhanceCampaignTargetType = 22
|
||||||
|
EnhanceTargetWeaponId EnhanceCampaignTargetType = 23
|
||||||
|
EnhanceTargetPartsSeriesId EnhanceCampaignTargetType = 31
|
||||||
|
EnhanceTargetPartsId EnhanceCampaignTargetType = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuestCampaignEffectType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuestEffectUnknown QuestCampaignEffectType = 0
|
||||||
|
QuestEffectDropRate QuestCampaignEffectType = 1
|
||||||
|
QuestEffectDropCount QuestCampaignEffectType = 2
|
||||||
|
QuestEffectStaminaConsume QuestCampaignEffectType = 3
|
||||||
|
QuestEffectClearRewardGold QuestCampaignEffectType = 4
|
||||||
|
QuestEffectDropItemAdd QuestCampaignEffectType = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuestCampaignTargetType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuestTargetUnknown QuestCampaignTargetType = 0
|
||||||
|
QuestTargetWholeQuest QuestCampaignTargetType = 1
|
||||||
|
QuestTargetQuestType QuestCampaignTargetType = 2
|
||||||
|
QuestTargetEventQuestType QuestCampaignTargetType = 3
|
||||||
|
QuestTargetMainQuestChapterId QuestCampaignTargetType = 4
|
||||||
|
QuestTargetMainQuestQuestId QuestCampaignTargetType = 5
|
||||||
|
QuestTargetSubQuestChapterId QuestCampaignTargetType = 6
|
||||||
|
QuestTargetSubQuestQuestId QuestCampaignTargetType = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuestType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuestTypeUnknown QuestType = 0
|
||||||
|
QuestTypeMainQuest QuestType = 1
|
||||||
|
QuestTypeEventQuest QuestType = 2
|
||||||
|
QuestTypeExtraQuest QuestType = 3
|
||||||
|
QuestTypeBigHunt QuestType = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type TargetUserStatusType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
TargetUserStatusUnknown TargetUserStatusType = 0
|
||||||
|
TargetUserStatusAll TargetUserStatusType = 1
|
||||||
|
TargetUserStatusComeback TargetUserStatusType = 2
|
||||||
|
TargetUserStatusBeginner TargetUserStatusType = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
NowMillis int64
|
||||||
|
UserStatus TargetUserStatusType
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartsTarget struct {
|
||||||
|
PartsId int32
|
||||||
|
PartsGroupId int32
|
||||||
|
Rarity model.RarityType
|
||||||
|
}
|
||||||
|
|
||||||
|
type CostumeTarget struct {
|
||||||
|
CostumeId int32
|
||||||
|
CharacterId int32
|
||||||
|
SkillfulWeaponType int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeaponTarget struct {
|
||||||
|
WeaponId int32
|
||||||
|
WeaponType int32
|
||||||
|
AttributeType int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestTarget struct {
|
||||||
|
QuestId int32
|
||||||
|
QuestType QuestType
|
||||||
|
EventQuestType int32
|
||||||
|
ChapterId int32
|
||||||
|
}
|
||||||
@@ -17,3 +17,16 @@ func LevelAndCap(exp int32, thresholds []int32) (level, capped int32) {
|
|||||||
}
|
}
|
||||||
return level, exp
|
return level, exp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplyExpWithMaxLevel runs LevelAndCap and then clamps the resulting
|
||||||
|
// level to the per-instance maxLevel (e.g. limit break + awaken for
|
||||||
|
// weapons, limit break + rebirth for costumes). A maxLevel <= 0 means
|
||||||
|
// "no per-instance cap" and the result is identical to LevelAndCap.
|
||||||
|
func ApplyExpWithMaxLevel(exp int32, thresholds []int32, maxLevel int32) (level, capped int32) {
|
||||||
|
level, capped = LevelAndCap(exp, thresholds)
|
||||||
|
if maxLevel > 0 && level > maxLevel && int(maxLevel) < len(thresholds) {
|
||||||
|
level = maxLevel
|
||||||
|
capped = thresholds[maxLevel]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,25 @@ type CharacterRebirthCatalog struct {
|
|||||||
MaterialsByGroupId map[int32][]EntityMCharacterRebirthMaterialGroup
|
MaterialsByGroupId map[int32][]EntityMCharacterRebirthMaterialGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CharacterRebirthCatalog) CostumeLevelLimitUp(characterId, rebirthCount int32) int32 {
|
||||||
|
if c == nil || rebirthCount <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
stepGroupId, ok := c.StepGroupByCharacterId[characterId]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var total int32
|
||||||
|
for i := range rebirthCount {
|
||||||
|
step, ok := c.StepByGroupAndCount[StepKey{GroupId: stepGroupId, BeforeRebirthCount: i}]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += step.CostumeLevelLimitUp
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
func LoadCharacterRebirthCatalog() (*CharacterRebirthCatalog, error) {
|
func LoadCharacterRebirthCatalog() (*CharacterRebirthCatalog, error) {
|
||||||
rebirthRows, err := utils.ReadTable[EntityMCharacterRebirth]("m_character_rebirth")
|
rebirthRows, err := utils.ReadTable[EntityMCharacterRebirth]("m_character_rebirth")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -158,5 +158,9 @@ func buildPartsStatusMain() (map[int32]PartsStatusMainDef, map[int32][]int32) {
|
|||||||
id++
|
id++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Newer parts groups (PartsGroupId 401-490) use PartsStatusSubLotteryGroupId
|
||||||
|
// 11/12 for rarities 10/20 instead of 1/2. Same stat pools — alias them.
|
||||||
|
pool[11] = pool[1]
|
||||||
|
pool[12] = pool[2]
|
||||||
return defs, pool
|
return defs, pool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ type QuestCatalog struct {
|
|||||||
TutorialUnlockConditions []EntityMTutorialUnlockCondition
|
TutorialUnlockConditions []EntityMTutorialUnlockCondition
|
||||||
ChapterLastSceneByQuestId map[int32]int32
|
ChapterLastSceneByQuestId map[int32]int32
|
||||||
SeasonIdByRouteId map[int32]int32
|
SeasonIdByRouteId map[int32]int32
|
||||||
|
RoutesBySeason map[int32][]int32
|
||||||
|
RouteCompletionQuestId map[int32]int32
|
||||||
BattleOnlyTargetSceneByQuestId map[int32]int32
|
BattleOnlyTargetSceneByQuestId map[int32]int32
|
||||||
|
MainQuestChapterIdByQuestId map[int32]int32
|
||||||
|
EventQuestTypeByChapterId map[int32]int32
|
||||||
|
|
||||||
UserExpThresholds []int32
|
UserExpThresholds []int32
|
||||||
CharacterExpThresholds []int32
|
CharacterExpThresholds []int32
|
||||||
@@ -114,8 +118,53 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
return nil, fmt.Errorf("load main quest route table: %w", err)
|
return nil, fmt.Errorf("load main quest route table: %w", err)
|
||||||
}
|
}
|
||||||
seasonIdByRouteId := make(map[int32]int32, len(routes))
|
seasonIdByRouteId := make(map[int32]int32, len(routes))
|
||||||
|
routesBySeason := make(map[int32][]int32, len(routes))
|
||||||
|
sortOrderByRoute := make(map[int32]int32, len(routes))
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
|
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
|
||||||
|
routesBySeason[r.MainQuestSeasonId] = append(routesBySeason[r.MainQuestSeasonId], r.MainQuestRouteId)
|
||||||
|
sortOrderByRoute[r.MainQuestRouteId] = r.SortOrder
|
||||||
|
}
|
||||||
|
for seasonId, ids := range routesBySeason {
|
||||||
|
s := ids
|
||||||
|
sort.Slice(s, func(i, j int) bool { return sortOrderByRoute[s[i]] > sortOrderByRoute[s[j]] })
|
||||||
|
routesBySeason[seasonId] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
anotherReplayConds, err := utils.ReadTable[EntityMMainQuestRouteAnotherReplayFlowUnlockCondition]("m_main_quest_route_another_replay_flow_unlock_condition")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load main quest route another replay flow unlock condition table: %w", err)
|
||||||
|
}
|
||||||
|
evaluateConds, err := utils.ReadTable[EntityMEvaluateCondition]("m_evaluate_condition")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load evaluate condition table: %w", err)
|
||||||
|
}
|
||||||
|
valueGroupByConditionId := make(map[int32]int32, len(evaluateConds))
|
||||||
|
for _, c := range evaluateConds {
|
||||||
|
valueGroupByConditionId[c.EvaluateConditionId] = c.EvaluateConditionValueGroupId
|
||||||
|
}
|
||||||
|
evaluateValueGroups, err := utils.ReadTable[EntityMEvaluateConditionValueGroup]("m_evaluate_condition_value_group")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load evaluate condition value group table: %w", err)
|
||||||
|
}
|
||||||
|
valueByGroupId := make(map[int32]int32, len(evaluateValueGroups))
|
||||||
|
for _, vg := range evaluateValueGroups {
|
||||||
|
if _, exists := valueByGroupId[vg.EvaluateConditionValueGroupId]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
valueByGroupId[vg.EvaluateConditionValueGroupId] = int32(vg.Value)
|
||||||
|
}
|
||||||
|
routeCompletionQuestId := make(map[int32]int32, len(anotherReplayConds))
|
||||||
|
for _, c := range anotherReplayConds {
|
||||||
|
valueGroupId, ok := valueGroupByConditionId[c.UnlockEvaluateConditionId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
questId, ok := valueByGroupId[valueGroupId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
routeCompletionQuestId[c.MainQuestRouteId] = questId
|
||||||
}
|
}
|
||||||
|
|
||||||
firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch")
|
firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch")
|
||||||
@@ -335,12 +384,23 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter
|
chapterBySequenceId[chapter.MainQuestSequenceGroupId] = chapter
|
||||||
}
|
}
|
||||||
routeIdByQuestId := make(map[int32]int32)
|
routeIdByQuestId := make(map[int32]int32)
|
||||||
|
mainQuestChapterIdByQuestId := make(map[int32]int32)
|
||||||
for _, sequence := range sequences {
|
for _, sequence := range sequences {
|
||||||
if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok {
|
if chapter, ok := chapterBySequenceId[sequence.MainQuestSequenceId]; ok {
|
||||||
routeIdByQuestId[sequence.QuestId] = chapter.MainQuestRouteId
|
routeIdByQuestId[sequence.QuestId] = chapter.MainQuestRouteId
|
||||||
|
mainQuestChapterIdByQuestId[sequence.QuestId] = chapter.MainQuestChapterId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventChapters, err := utils.ReadTable[EntityMEventQuestChapter]("m_event_quest_chapter")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load event quest chapter table: %w", err)
|
||||||
|
}
|
||||||
|
eventQuestTypeByChapterId := make(map[int32]int32, len(eventChapters))
|
||||||
|
for _, ec := range eventChapters {
|
||||||
|
eventQuestTypeByChapterId[ec.EventQuestChapterId] = ec.EventQuestType
|
||||||
|
}
|
||||||
|
|
||||||
sortedChapters := make([]EntityMMainQuestChapter, len(chapters))
|
sortedChapters := make([]EntityMMainQuestChapter, len(chapters))
|
||||||
copy(sortedChapters, chapters)
|
copy(sortedChapters, chapters)
|
||||||
sort.Slice(sortedChapters, func(i, j int) bool {
|
sort.Slice(sortedChapters, func(i, j int) bool {
|
||||||
@@ -539,7 +599,11 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
TutorialUnlockConditions: tutorialUnlockConds,
|
TutorialUnlockConditions: tutorialUnlockConds,
|
||||||
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
||||||
SeasonIdByRouteId: seasonIdByRouteId,
|
SeasonIdByRouteId: seasonIdByRouteId,
|
||||||
|
RoutesBySeason: routesBySeason,
|
||||||
|
RouteCompletionQuestId: routeCompletionQuestId,
|
||||||
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
||||||
|
MainQuestChapterIdByQuestId: mainQuestChapterIdByQuestId,
|
||||||
|
EventQuestTypeByChapterId: eventQuestTypeByChapterId,
|
||||||
|
|
||||||
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
||||||
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ package model
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
|
type QuestType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuestTypeUnknown QuestType = 0
|
||||||
|
QuestTypeMain QuestType = 1
|
||||||
|
QuestTypeEvent QuestType = 2
|
||||||
|
QuestTypeExtra QuestType = 3
|
||||||
|
QuestTypeBigHunt QuestType = 4
|
||||||
|
)
|
||||||
|
|
||||||
type QuestFlowType int32
|
type QuestFlowType int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ func (h *QuestHandler) HandleBigHuntQuestStart(user *store.UserState, questId, u
|
|||||||
|
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForBigHunt(questId), nowMillis)
|
||||||
|
store.ConsumeStamina(user, stamina, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
@@ -33,13 +34,16 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleBigHuntQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
target := h.targetForBigHunt(questId)
|
||||||
if !isRetired {
|
var outcome FinishOutcome
|
||||||
|
if !isRetired && !isAnnihilated {
|
||||||
|
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package questflow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *QuestHandler) targetForMain(questId int32) campaign.QuestTarget {
|
||||||
|
return campaign.QuestTarget{
|
||||||
|
QuestId: questId,
|
||||||
|
QuestType: campaign.QuestTypeMainQuest,
|
||||||
|
ChapterId: h.MainQuestChapterIdByQuestId[questId],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) targetForEvent(eventChapterId, questId int32) campaign.QuestTarget {
|
||||||
|
return campaign.QuestTarget{
|
||||||
|
QuestId: questId,
|
||||||
|
QuestType: campaign.QuestTypeEventQuest,
|
||||||
|
EventQuestType: h.EventQuestTypeByChapterId[eventChapterId],
|
||||||
|
ChapterId: eventChapterId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) targetForExtra(questId int32) campaign.QuestTarget {
|
||||||
|
return campaign.QuestTarget{QuestId: questId, QuestType: campaign.QuestTypeExtraQuest}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) targetForBigHunt(questId int32) campaign.QuestTarget {
|
||||||
|
return campaign.QuestTarget{QuestId: questId, QuestType: campaign.QuestTypeBigHunt}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) campaignFilter(nowMillis int64) campaign.Filter {
|
||||||
|
return campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) staminaWithCampaign(baseStamina int32, t campaign.QuestTarget, nowMillis int64) int32 {
|
||||||
|
if h.Campaigns == nil {
|
||||||
|
return baseStamina
|
||||||
|
}
|
||||||
|
return h.Campaigns.QuestStamina(t, h.campaignFilter(nowMillis)).Apply(baseStamina)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) appendBonusDrops(drops []RewardGrant, t campaign.QuestTarget, nowMillis int64) []RewardGrant {
|
||||||
|
if h.Campaigns == nil {
|
||||||
|
return drops
|
||||||
|
}
|
||||||
|
for _, bd := range h.Campaigns.QuestBonusDrops(t, h.campaignFilter(nowMillis)) {
|
||||||
|
drops = append(drops, RewardGrant{
|
||||||
|
PossessionType: model.PossessionType(bd.PossessionType),
|
||||||
|
PossessionId: bd.PossessionId,
|
||||||
|
Count: bd.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return drops
|
||||||
|
}
|
||||||
@@ -18,7 +18,8 @@ func (h *QuestHandler) HandleEventQuestStart(user *store.UserState, eventQuestCh
|
|||||||
|
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForEvent(eventQuestChapterId, questId), nowMillis)
|
||||||
|
store.ConsumeStamina(user, stamina, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
@@ -42,14 +43,17 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleEventQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
target := h.targetForEvent(eventQuestChapterId, questId)
|
||||||
if !isRetired {
|
var outcome FinishOutcome
|
||||||
|
if !isRetired && !isAnnihilated {
|
||||||
|
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
|
h.recordSideStoryLimitContentStatus(user, questId, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ func (h *QuestHandler) HandleExtraQuestStart(user *store.UserState, questId, use
|
|||||||
|
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForExtra(questId), nowMillis)
|
||||||
|
store.ConsumeStamina(user, stamina, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
@@ -40,13 +41,16 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleExtraQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
target := h.targetForExtra(questId)
|
||||||
if !isRetired {
|
var outcome FinishOutcome
|
||||||
|
if !isRetired && !isAnnihilated {
|
||||||
|
outcome = h.evaluateFinishOutcome(user, questId, target, nowMillis)
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, target, nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package questflow
|
package questflow
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
@@ -10,6 +13,7 @@ type RewardGrant struct {
|
|||||||
PossessionType model.PossessionType
|
PossessionType model.PossessionType
|
||||||
PossessionId int32
|
PossessionId int32
|
||||||
Count int32
|
Count int32
|
||||||
|
IsAutoSale bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type FinishOutcome struct {
|
type FinishOutcome struct {
|
||||||
@@ -28,10 +32,12 @@ type QuestHandler struct {
|
|||||||
Config *masterdata.GameConfig
|
Config *masterdata.GameConfig
|
||||||
Granter *store.PossessionGranter
|
Granter *store.PossessionGranter
|
||||||
SideStoryChapterByEventQuestId map[int32]int32
|
SideStoryChapterByEventQuestId map[int32]int32
|
||||||
|
Campaigns *campaign.Catalog
|
||||||
|
CharacterRebirth *masterdata.CharacterRebirthCatalog
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog) *QuestHandler {
|
func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig, sideStory *masterdata.SideStoryCatalog, campaigns *campaign.Catalog, characterRebirth *masterdata.CharacterRebirthCatalog) *QuestHandler {
|
||||||
granter := BuildGranter(catalog)
|
granter := BuildGranter(catalog, config)
|
||||||
var sideStoryChapters map[int32]int32
|
var sideStoryChapters map[int32]int32
|
||||||
if sideStory != nil {
|
if sideStory != nil {
|
||||||
sideStoryChapters = sideStory.ChapterByEventQuestId
|
sideStoryChapters = sideStory.ChapterByEventQuestId
|
||||||
@@ -41,10 +47,12 @@ func NewQuestHandler(catalog *masterdata.QuestCatalog, config *masterdata.GameCo
|
|||||||
Config: config,
|
Config: config,
|
||||||
Granter: granter,
|
Granter: granter,
|
||||||
SideStoryChapterByEventQuestId: sideStoryChapters,
|
SideStoryChapterByEventQuestId: sideStoryChapters,
|
||||||
|
Campaigns: campaigns,
|
||||||
|
CharacterRebirth: characterRebirth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
func BuildGranter(catalog *masterdata.QuestCatalog, config *masterdata.GameConfig) *store.PossessionGranter {
|
||||||
costumeById := make(map[int32]store.CostumeRef, len(catalog.CostumeById))
|
costumeById := make(map[int32]store.CostumeRef, len(catalog.CostumeById))
|
||||||
for id, cm := range catalog.CostumeById {
|
for id, cm := range catalog.CostumeById {
|
||||||
costumeById[id] = store.CostumeRef{CharacterId: cm.CharacterId}
|
costumeById[id] = store.CostumeRef{CharacterId: cm.CharacterId}
|
||||||
@@ -70,12 +78,49 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
|||||||
releaseConditions[groupId] = conds
|
releaseConditions[groupId] = conds
|
||||||
}
|
}
|
||||||
partsById := make(map[int32]store.PartsRef, len(catalog.PartsById))
|
partsById := make(map[int32]store.PartsRef, len(catalog.PartsById))
|
||||||
|
partsVariants := make(map[int32]map[int32][]int32)
|
||||||
for id, p := range catalog.PartsById {
|
for id, p := range catalog.PartsById {
|
||||||
partsById[id] = store.PartsRef{
|
partsById[id] = store.PartsRef{
|
||||||
PartsGroupId: p.PartsGroupId,
|
PartsGroupId: p.PartsGroupId,
|
||||||
|
RarityType: p.RarityType,
|
||||||
|
PartsInitialLotteryId: p.PartsInitialLotteryId,
|
||||||
PartsStatusMainLotteryGroupId: p.PartsStatusMainLotteryGroupId,
|
PartsStatusMainLotteryGroupId: p.PartsStatusMainLotteryGroupId,
|
||||||
|
PartsStatusSubLotteryGroupId: p.PartsStatusSubLotteryGroupId,
|
||||||
|
}
|
||||||
|
if partsVariants[p.PartsGroupId] == nil {
|
||||||
|
partsVariants[p.PartsGroupId] = map[int32][]int32{}
|
||||||
|
}
|
||||||
|
partsVariants[p.PartsGroupId][p.RarityType] = append(partsVariants[p.PartsGroupId][p.RarityType], p.PartsId)
|
||||||
|
}
|
||||||
|
for _, byRarity := range partsVariants {
|
||||||
|
for _, ids := range byRarity {
|
||||||
|
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partsSubDefs := make(map[int32]store.PartsStatusSubDef, len(catalog.PartsStatusMainById))
|
||||||
|
for id, d := range catalog.PartsStatusMainById {
|
||||||
|
var fn func(int32) int32
|
||||||
|
if f, ok := catalog.FuncResolver.Resolve(d.StatusNumericalFunctionId); ok {
|
||||||
|
fn = f.Evaluate
|
||||||
|
}
|
||||||
|
partsSubDefs[id] = store.PartsStatusSubDef{
|
||||||
|
StatusKindType: d.StatusKindType,
|
||||||
|
StatusCalculationType: d.StatusCalculationType,
|
||||||
|
StatusChangeInitialValue: d.StatusChangeInitialValue,
|
||||||
|
StatusFunc: fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partsSellPriceL1 := make(map[int32]int32, len(catalog.SellPriceByRarity))
|
||||||
|
for rarity, fn := range catalog.SellPriceByRarity {
|
||||||
|
partsSellPriceL1[int32(rarity)] = fn.Evaluate(1)
|
||||||
|
}
|
||||||
|
var goldItemId int32
|
||||||
|
if config != nil {
|
||||||
|
goldItemId = config.ConsumableItemIdForGold
|
||||||
|
}
|
||||||
|
|
||||||
return &store.PossessionGranter{
|
return &store.PossessionGranter{
|
||||||
CostumeById: costumeById,
|
CostumeById: costumeById,
|
||||||
WeaponById: weaponById,
|
WeaponById: weaponById,
|
||||||
@@ -84,5 +129,10 @@ func BuildGranter(catalog *masterdata.QuestCatalog) *store.PossessionGranter {
|
|||||||
ReleaseConditions: releaseConditions,
|
ReleaseConditions: releaseConditions,
|
||||||
PartsById: partsById,
|
PartsById: partsById,
|
||||||
DefaultPartsStatusMainByLotteryGroup: catalog.DefaultPartsStatusMainByLotteryGroup,
|
DefaultPartsStatusMainByLotteryGroup: catalog.DefaultPartsStatusMainByLotteryGroup,
|
||||||
|
PartsVariantsByGroupRarity: partsVariants,
|
||||||
|
PartsSubStatusPool: catalog.SubStatusPool,
|
||||||
|
PartsSubStatusDefs: partsSubDefs,
|
||||||
|
PartsSellPriceL1ByRarity: partsSellPriceL1,
|
||||||
|
GoldConsumableItemId: goldItemId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
|
|||||||
|
|
||||||
h.initQuestState(user, questId)
|
h.initQuestState(user, questId)
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
store.ConsumeStamina(user, quest.Stamina, h.MaxStaminaByLevel[user.Status.Level]*1000, nowMillis)
|
stamina := h.staminaWithCampaign(quest.Stamina, h.targetForMain(questId), nowMillis)
|
||||||
|
store.ConsumeStamina(user, stamina, h.MaxStaminaByLevel[user.Status.Level]*1000, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
@@ -203,9 +204,8 @@ func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, o
|
|||||||
}
|
}
|
||||||
questState.IsRewardGranted = true
|
questState.IsRewardGranted = true
|
||||||
}
|
}
|
||||||
for _, drop := range outcome.DropRewards {
|
raritySet, rankSet := parseAutoSaleRules(user.AutoSaleSettings)
|
||||||
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
h.grantDropRewards(user, outcome.DropRewards, raritySet, rankSet, nowMillis)
|
||||||
}
|
|
||||||
for _, reward := range outcome.ReplayFlowFirstClearRewards {
|
for _, reward := range outcome.ReplayFlowFirstClearRewards {
|
||||||
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
|
h.applyRewardPossession(user, reward.PossessionType, reward.PossessionId, reward.Count, nowMillis)
|
||||||
}
|
}
|
||||||
@@ -259,11 +259,12 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
|
|
||||||
h.initQuestState(user, questId)
|
h.initQuestState(user, questId)
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
|
||||||
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||||
wasMenuReplay := user.MainQuest.SavedContext.Active
|
wasMenuReplay := user.MainQuest.SavedContext.Active
|
||||||
|
|
||||||
if !isRetired {
|
var outcome FinishOutcome
|
||||||
|
if !isRetired && !isAnnihilated {
|
||||||
|
outcome = h.evaluateFinishOutcome(user, questId, h.targetForMain(questId), nowMillis)
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, wasReplay)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, wasReplay)
|
||||||
|
|
||||||
// A replay-flow finish must NOT move the MainFlow scene pointer: the
|
// A replay-flow finish must NOT move the MainFlow scene pointer: the
|
||||||
@@ -277,8 +278,9 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
consumed := h.staminaWithCampaign(quest.Stamina, h.targetForMain(questId), nowMillis)
|
||||||
refund := quest.Stamina - 1
|
if isRetired && !isAnnihilated && consumed > 1 {
|
||||||
|
refund := consumed - 1
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
@@ -322,21 +324,21 @@ func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestSkip", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleQuestSkip", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target := h.targetForMain(questId)
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
||||||
store.ConsumeStamina(user, skipCount, maxMillis, nowMillis)
|
perSkipStamina := h.staminaWithCampaign(questDef.Stamina, target, nowMillis)
|
||||||
|
store.ConsumeStamina(user, perSkipStamina*skipCount, maxMillis, nowMillis)
|
||||||
|
|
||||||
skipTicketId := h.Config.ConsumableItemIdForQuestSkipTicket
|
skipTicketId := h.Config.ConsumableItemIdForQuestSkipTicket
|
||||||
user.ConsumableItems[skipTicketId] -= skipCount
|
user.ConsumableItems[skipTicketId] -= skipCount
|
||||||
if user.ConsumableItems[skipTicketId] < 0 {
|
if user.ConsumableItems[skipTicketId] < 0 {
|
||||||
user.ConsumableItems[skipTicketId] = 0
|
user.ConsumableItems[skipTicketId] = 0
|
||||||
}
|
}
|
||||||
|
raritySet, rankSet := parseAutoSaleRules(user.AutoSaleSettings)
|
||||||
var allDrops []RewardGrant
|
var allDrops []RewardGrant
|
||||||
for range skipCount {
|
for range skipCount {
|
||||||
drops := h.computeDropRewards(questDef)
|
drops := h.computeDropRewards(questDef, target, nowMillis)
|
||||||
for _, drop := range drops {
|
h.grantDropRewards(user, drops, raritySet, rankSet, nowMillis)
|
||||||
h.applyRewardPossession(user, drop.PossessionType, drop.PossessionId, drop.Count, nowMillis)
|
|
||||||
}
|
|
||||||
allDrops = append(allDrops, drops...)
|
allDrops = append(allDrops, drops...)
|
||||||
|
|
||||||
if questDef.Gold != 0 {
|
if questDef.Gold != 0 {
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package questflow
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
@@ -40,7 +43,7 @@ func (h *QuestHandler) firstClearRewardGroupId(user *store.UserState, questDef m
|
|||||||
return rewardGroupId
|
return rewardGroupId
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int32) FinishOutcome {
|
func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int32, target campaign.QuestTarget, nowMillis int64) FinishOutcome {
|
||||||
outcome := FinishOutcome{}
|
outcome := FinishOutcome{}
|
||||||
questState, ok := user.Quests[questId]
|
questState, ok := user.Quests[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -123,25 +126,76 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome.DropRewards = h.computeDropRewards(questDef)
|
outcome.DropRewards = h.computeDropRewards(questDef, target, nowMillis)
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest) []RewardGrant {
|
var autoSaleRarityTiers = map[int32]bool{10: true, 20: true, 30: true, 40: true, 50: true}
|
||||||
if questDef.QuestPickupRewardGroupId == 0 {
|
|
||||||
return nil
|
// Rarity tiers (10..50) and ranks (1..5) are disjoint, so the delimited values
|
||||||
}
|
// are classified by range — independent of the client's map key or delimiter.
|
||||||
var drops []RewardGrant
|
func parseAutoSaleRules(settings map[int32]store.AutoSaleSettingState) (raritySet, rankSet map[int32]bool) {
|
||||||
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
|
raritySet = map[int32]bool{}
|
||||||
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
|
rankSet = map[int32]bool{}
|
||||||
drops = append(drops, RewardGrant{
|
for _, s := range settings {
|
||||||
PossessionType: model.PossessionType(bdr.PossessionType),
|
for _, n := range extractInts(s.PossessionAutoSaleItemValue) {
|
||||||
PossessionId: bdr.PossessionId,
|
switch {
|
||||||
Count: bdr.Count,
|
case autoSaleRarityTiers[n]:
|
||||||
})
|
raritySet[n] = true
|
||||||
|
case n >= 1 && n <= 5:
|
||||||
|
rankSet[n] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return drops
|
return raritySet, rankSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractInts(s string) []int32 {
|
||||||
|
fields := strings.FieldsFunc(s, func(r rune) bool { return r < '0' || r > '9' })
|
||||||
|
out := make([]int32, 0, len(fields))
|
||||||
|
for _, f := range fields {
|
||||||
|
if v, err := strconv.Atoi(f); err == nil {
|
||||||
|
out = append(out, int32(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) grantDropRewards(user *store.UserState, drops []RewardGrant, raritySet, rankSet map[int32]bool, nowMillis int64) {
|
||||||
|
for i := range drops {
|
||||||
|
d := drops[i]
|
||||||
|
if d.PossessionType == model.PossessionTypeParts || d.PossessionType == model.PossessionTypePartsEnhanced {
|
||||||
|
chosenId, sold := h.Granter.GrantOrSellPartsDrop(user, d.PossessionId, raritySet, rankSet, nowMillis)
|
||||||
|
if sold {
|
||||||
|
// Sold parts have no inventory row, so the popup needs the rolled
|
||||||
|
// variant id; kept parts read theirs from the parts table diff.
|
||||||
|
drops[i].PossessionId = chosenId
|
||||||
|
drops[i].IsAutoSale = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.applyRewardPossession(user, d.PossessionType, d.PossessionId, d.Count, nowMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) computeDropRewards(questDef masterdata.EntityMQuest, target campaign.QuestTarget, nowMillis int64) []RewardGrant {
|
||||||
|
var drops []RewardGrant
|
||||||
|
var dropRate campaign.DropRateMul
|
||||||
|
if h.Campaigns != nil {
|
||||||
|
dropRate = h.Campaigns.QuestDropRate(target, h.campaignFilter(nowMillis))
|
||||||
|
}
|
||||||
|
if questDef.QuestPickupRewardGroupId != 0 {
|
||||||
|
for _, dropId := range h.PickupRewardIdsByGroupId[questDef.QuestPickupRewardGroupId] {
|
||||||
|
if bdr, ok := h.BattleDropRewardById[dropId]; ok {
|
||||||
|
drops = append(drops, RewardGrant{
|
||||||
|
PossessionType: model.PossessionType(bdr.PossessionType),
|
||||||
|
PossessionId: bdr.PossessionId,
|
||||||
|
Count: dropRate.Apply(bdr.Count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h.appendBonusDrops(drops, target, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {
|
func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
@@ -193,8 +247,10 @@ func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, now
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
var maxLevel int32
|
||||||
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
|
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
|
||||||
maxLevel := maxLevelFunc.Evaluate(row.LimitBreakCount)
|
maxLevel = maxLevelFunc.Evaluate(row.LimitBreakCount) +
|
||||||
|
h.CharacterRebirth.CostumeLevelLimitUp(cm.CharacterId, user.CharacterRebirths[cm.CharacterId].RebirthCount)
|
||||||
if row.Level >= maxLevel {
|
if row.Level >= maxLevel {
|
||||||
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): at max level %d, skipping", questId, row.CostumeId, key, row.Level)
|
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): at max level %d, skipping", questId, row.CostumeId, key, row.Level)
|
||||||
continue
|
continue
|
||||||
@@ -202,14 +258,7 @@ func (h *QuestHandler) applyExpRewards(user *store.UserState, questId int32, now
|
|||||||
}
|
}
|
||||||
row.Exp += questDef.CostumeExp
|
row.Exp += questDef.CostumeExp
|
||||||
if thresholds, ok := h.CostumeExpByRarity[cm.RarityType]; ok {
|
if thresholds, ok := h.CostumeExpByRarity[cm.RarityType]; ok {
|
||||||
row.Level, row.Exp = gameutil.LevelAndCap(row.Exp, thresholds)
|
row.Level, row.Exp = gameutil.ApplyExpWithMaxLevel(row.Exp, thresholds, maxLevel)
|
||||||
if maxLevelFunc, hasMax := h.CostumeMaxLevelByRarity[cm.RarityType]; hasMax {
|
|
||||||
maxLevel := maxLevelFunc.Evaluate(row.LimitBreakCount)
|
|
||||||
if row.Level > maxLevel && int(maxLevel) < len(thresholds) {
|
|
||||||
row.Level = maxLevel
|
|
||||||
row.Exp = thresholds[maxLevel]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
user.Costumes[key] = row
|
user.Costumes[key] = row
|
||||||
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): +%d exp -> total=%d level=%d", questId, row.CostumeId, key, questDef.CostumeExp, row.Exp, row.Level)
|
log.Printf("[applyExpRewards] questId=%d costume=%d (key=%s): +%d exp -> total=%d level=%d", questId, row.CostumeId, key, questDef.CostumeExp, row.Exp, row.Level)
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, scen
|
|||||||
user.MainQuest.CurrentMainQuestRouteId = routeId
|
user.MainQuest.CurrentMainQuestRouteId = routeId
|
||||||
if seasonId, ok := h.SeasonIdByRouteId[routeId]; ok {
|
if seasonId, ok := h.SeasonIdByRouteId[routeId]; ok {
|
||||||
user.MainQuest.MainQuestSeasonId = seasonId
|
user.MainQuest.MainQuestSeasonId = seasonId
|
||||||
RecordSeasonRoute(user, seasonId, routeId, gametime.NowMillis())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,22 +58,27 @@ func (h *QuestHandler) advanceReplayFlowScene(user *store.UserState, sceneId int
|
|||||||
user.MainQuest.ReplayFlowHeadQuestSceneId = sceneId
|
user.MainQuest.ReplayFlowHeadQuestSceneId = sceneId
|
||||||
}
|
}
|
||||||
|
|
||||||
func RecordSeasonRoute(user *store.UserState, seasonId, routeId int32, nowMillis int64) {
|
func (h *QuestHandler) SeasonRoutesFor(user *store.UserState) map[int32]int32 {
|
||||||
if seasonId <= 0 || routeId <= 0 {
|
out := make(map[int32]int32)
|
||||||
return
|
for seasonId, routes := range h.RoutesBySeason {
|
||||||
|
if seasonId <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, routeId := range routes {
|
||||||
|
finalQuestId, ok := h.RouteCompletionQuestId[routeId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if q, ok := user.Quests[finalQuestId]; ok && q.ClearCount > 0 {
|
||||||
|
out[seasonId] = routeId
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if user.MainQuestSeasonRoutes == nil {
|
if cur := user.MainQuest.MainQuestSeasonId; cur >= 2 && user.MainQuest.CurrentMainQuestRouteId > 0 {
|
||||||
user.MainQuestSeasonRoutes = make(map[store.SeasonRouteKey]store.SeasonRouteEntry)
|
out[cur] = user.MainQuest.CurrentMainQuestRouteId
|
||||||
}
|
|
||||||
key := store.SeasonRouteKey{MainQuestSeasonId: seasonId, MainQuestRouteId: routeId}
|
|
||||||
if _, exists := user.MainQuestSeasonRoutes[key]; exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user.MainQuestSeasonRoutes[key] = store.SeasonRouteEntry{
|
|
||||||
MainQuestSeasonId: seasonId,
|
|
||||||
MainQuestRouteId: routeId,
|
|
||||||
LatestVersion: nowMillis,
|
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
@@ -177,15 +181,8 @@ func (h *QuestHandler) replayFlowTypeForRoute(user *store.UserState, routeId int
|
|||||||
if !ok {
|
if !ok {
|
||||||
return model.QuestFlowTypeReplayFlow
|
return model.QuestFlowTypeReplayFlow
|
||||||
}
|
}
|
||||||
for key, entry := range user.MainQuestSeasonRoutes {
|
pairs := h.SeasonRoutesFor(user)
|
||||||
if key.MainQuestSeasonId == seasonId && entry.MainQuestRouteId != routeId {
|
if recorded, ok := pairs[seasonId]; ok && recorded != routeId {
|
||||||
return model.QuestFlowTypeAnotherRouteReplayFlow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(user.MainQuestSeasonRoutes) == 0 &&
|
|
||||||
user.MainQuest.MainQuestSeasonId == seasonId &&
|
|
||||||
user.MainQuest.CurrentMainQuestRouteId != 0 &&
|
|
||||||
user.MainQuest.CurrentMainQuestRouteId != routeId {
|
|
||||||
return model.QuestFlowTypeAnotherRouteReplayFlow
|
return model.QuestFlowTypeAnotherRouteReplayFlow
|
||||||
}
|
}
|
||||||
return model.QuestFlowTypeReplayFlow
|
return model.QuestFlowTypeReplayFlow
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gacha"
|
"lunar-tear/server/internal/gacha"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/masterdata/memorydb"
|
"lunar-tear/server/internal/masterdata/memorydb"
|
||||||
"lunar-tear/server/internal/questflow"
|
"lunar-tear/server/internal/questflow"
|
||||||
|
"lunar-tear/server/internal/userdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever
|
// buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever
|
||||||
@@ -34,7 +36,18 @@ func buildCatalogs() (*Catalogs, error) {
|
|||||||
return nil, fmt.Errorf("load quest catalog: %w", err)
|
return nil, fmt.Errorf("load quest catalog: %w", err)
|
||||||
}
|
}
|
||||||
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
|
||||||
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog)
|
campaignCatalog, err := campaign.Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load campaign catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("campaign catalog loaded: %d enhance, %d quest", campaignCatalog.EnhanceCount(), campaignCatalog.QuestCount())
|
||||||
|
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load character rebirth catalog: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
|
||||||
|
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog, campaignCatalog, characterRebirthCatalog)
|
||||||
|
userdata.SetQuestHandler(questHandler)
|
||||||
|
|
||||||
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,12 +138,6 @@ func buildCatalogs() (*Catalogs, error) {
|
|||||||
}
|
}
|
||||||
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
|
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
|
||||||
|
|
||||||
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("load character rebirth catalog: %w", err)
|
|
||||||
}
|
|
||||||
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
|
|
||||||
|
|
||||||
companionCatalog, err := masterdata.LoadCompanionCatalog()
|
companionCatalog, err := masterdata.LoadCompanionCatalog()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load companion catalog: %w", err)
|
return nil, fmt.Errorf("load companion catalog: %w", err)
|
||||||
@@ -170,6 +177,7 @@ func buildCatalogs() (*Catalogs, error) {
|
|||||||
BigHunt: bigHuntCatalog,
|
BigHunt: bigHuntCatalog,
|
||||||
Tower: towerCatalog,
|
Tower: towerCatalog,
|
||||||
Labyrinth: labyrinthCatalog,
|
Labyrinth: labyrinthCatalog,
|
||||||
|
Campaign: campaignCatalog,
|
||||||
QuestHandler: questHandler,
|
QuestHandler: questHandler,
|
||||||
GachaHandler: gachaHandler,
|
GachaHandler: gachaHandler,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gacha"
|
"lunar-tear/server/internal/gacha"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
"lunar-tear/server/internal/masterdata/memorydb"
|
"lunar-tear/server/internal/masterdata/memorydb"
|
||||||
@@ -52,23 +53,17 @@ type Catalogs struct {
|
|||||||
BigHunt *masterdata.BigHuntCatalog
|
BigHunt *masterdata.BigHuntCatalog
|
||||||
Tower *masterdata.TowerCatalog
|
Tower *masterdata.TowerCatalog
|
||||||
Labyrinth *masterdata.LabyrinthCatalog
|
Labyrinth *masterdata.LabyrinthCatalog
|
||||||
|
Campaign *campaign.Catalog
|
||||||
|
|
||||||
// Catalog-derived handlers must rebuild on every reload because they
|
|
||||||
// embed/cache pointers to specific catalog instances.
|
|
||||||
QuestHandler *questflow.QuestHandler
|
QuestHandler *questflow.QuestHandler
|
||||||
GachaHandler *gacha.GachaHandler
|
GachaHandler *gacha.GachaHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holder owns the current *Catalogs and the bin.e path. Concurrent readers
|
|
||||||
// call Get(); the single-writer Reload() rebuilds and atomically publishes.
|
|
||||||
type Holder struct {
|
type Holder struct {
|
||||||
binPath string
|
binPath string
|
||||||
cur atomic.Pointer[Catalogs]
|
cur atomic.Pointer[Catalogs]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHolder reads the binary at binPath, builds the initial catalogs, and
|
|
||||||
// returns a ready-to-use Holder. Subsequent calls to Reload() re-read the
|
|
||||||
// same path.
|
|
||||||
func NewHolder(binPath string) (*Holder, error) {
|
func NewHolder(binPath string) (*Holder, error) {
|
||||||
h := &Holder{binPath: binPath}
|
h := &Holder{binPath: binPath}
|
||||||
if err := h.Reload(); err != nil {
|
if err := h.Reload(); err != nil {
|
||||||
@@ -77,9 +72,6 @@ func NewHolder(binPath string) (*Holder, error) {
|
|||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload re-reads the bin.e from disk, rebuilds every catalog and handler,
|
|
||||||
// atomically publishes the new snapshot, and bumps the bin.e mtime so client
|
|
||||||
// caches invalidate (see service/data.go GetLatestMasterDataVersion).
|
|
||||||
func (h *Holder) Reload() error {
|
func (h *Holder) Reload() error {
|
||||||
if err := memorydb.Init(h.binPath); err != nil {
|
if err := memorydb.Init(h.binPath); err != nil {
|
||||||
return fmt.Errorf("memorydb.Init: %w", err)
|
return fmt.Errorf("memorydb.Init: %w", err)
|
||||||
@@ -91,16 +83,11 @@ func (h *Holder) Reload() error {
|
|||||||
h.cur.Store(c)
|
h.cur.Store(c)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if err := os.Chtimes(h.binPath, now, now); err != nil {
|
if err := os.Chtimes(h.binPath, now, now); err != nil {
|
||||||
// Non-fatal: the catalogs swapped fine in-memory; clients may take
|
|
||||||
// longer to invalidate their cached download but server-side state is
|
|
||||||
// already coherent.
|
|
||||||
log.Printf("[runtime] os.Chtimes(%s) failed (clients may not invalidate cache): %v", h.binPath, err)
|
log.Printf("[runtime] os.Chtimes(%s) failed (clients may not invalidate cache): %v", h.binPath, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the current snapshot. Safe for concurrent callers; the returned
|
|
||||||
// pointer is stable for the duration of the caller's use.
|
|
||||||
func (h *Holder) Get() *Catalogs {
|
func (h *Holder) Get() *Catalogs {
|
||||||
return h.cur.Load()
|
return h.cur.Load()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
@@ -50,6 +51,12 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expBonus := cat.Campaign.CostumeExpBonus(campaign.CostumeTarget{
|
||||||
|
CostumeId: costume.CostumeId,
|
||||||
|
CharacterId: cm.CharacterId,
|
||||||
|
SkillfulWeaponType: cm.SkillfulWeaponType,
|
||||||
|
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll})
|
||||||
|
|
||||||
totalExp := int32(0)
|
totalExp := int32(0)
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
for materialId, count := range req.Materials {
|
for materialId, count := range req.Materials {
|
||||||
@@ -71,7 +78,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
|
if mat.WeaponType != 0 && mat.WeaponType == cm.SkillfulWeaponType {
|
||||||
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += expPerUnit * count
|
totalExp += expBonus.Apply(expPerUnit * count)
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.EnhanceCostByRarity[cm.RarityType]; ok && totalMaterialCount > 0 {
|
||||||
@@ -83,7 +90,12 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
|
|||||||
costume.Exp += totalExp
|
costume.Exp += totalExp
|
||||||
|
|
||||||
if thresholds, ok := catalog.ExpByRarity[cm.RarityType]; ok {
|
if thresholds, ok := catalog.ExpByRarity[cm.RarityType]; ok {
|
||||||
costume.Level, costume.Exp = gameutil.LevelAndCap(costume.Exp, thresholds)
|
var maxLevel int32
|
||||||
|
if maxLevelFunc, hasMax := catalog.MaxLevelByRarity[cm.RarityType]; hasMax {
|
||||||
|
maxLevel = maxLevelFunc.Evaluate(costume.LimitBreakCount) +
|
||||||
|
cat.CharacterRebirth.CostumeLevelLimitUp(cm.CharacterId, user.CharacterRebirths[cm.CharacterId].RebirthCount)
|
||||||
|
}
|
||||||
|
costume.Level, costume.Exp = gameutil.ApplyExpWithMaxLevel(costume.Exp, thresholds, maxLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
costume.LatestVersion = nowMillis
|
costume.LatestVersion = nowMillis
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/runtime"
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
@@ -180,17 +182,23 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
|
|||||||
successRate = r
|
successRate = r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
baseRate := successRate
|
||||||
|
successRate = cat.Campaign.PartsRateBonus(campaign.PartsTarget{
|
||||||
|
PartsId: part.PartsId,
|
||||||
|
PartsGroupId: partDef.PartsGroupId,
|
||||||
|
Rarity: model.RarityType(partDef.RarityType),
|
||||||
|
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll}).Apply(baseRate)
|
||||||
|
|
||||||
if rand.Intn(1000) < int(successRate) {
|
if rand.Intn(1000) < int(successRate) {
|
||||||
part.Level++
|
part.Level++
|
||||||
isSuccess = true
|
isSuccess = true
|
||||||
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰, cost=%d gold)",
|
log.Printf("[PartsService] Enhance: SUCCESS partsId=%d level %d -> %d (rate=%d‰ base=%d‰, cost=%d gold)",
|
||||||
part.PartsId, part.Level-1, part.Level, successRate, goldCost)
|
part.PartsId, part.Level-1, part.Level, successRate, baseRate, goldCost)
|
||||||
|
|
||||||
grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis)
|
grantPartsSubStatuses(catalog, user, req.UserPartsUuid, part, partDef, nowMillis)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰, cost=%d gold)",
|
log.Printf("[PartsService] Enhance: FAIL partsId=%d stays level %d (rate=%d‰ base=%d‰, cost=%d gold)",
|
||||||
part.PartsId, part.Level, successRate, goldCost)
|
part.PartsId, part.Level, successRate, baseRate, goldCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
part.LatestVersion = nowMillis
|
part.LatestVersion = nowMillis
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/questflow"
|
||||||
|
"lunar-tear/server/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startAutoOrbit(user *store.UserState, questType model.QuestType, chapterId, questId, maxCount int32, nowMillis int64) {
|
||||||
|
if maxCount <= 0 {
|
||||||
|
if user.QuestAutoOrbit.MaxAutoOrbitCount > 0 {
|
||||||
|
log.Printf("[autoOrbit] clear (start without max): prev questType=%d chapter=%d quest=%d cleared=%d/%d",
|
||||||
|
user.QuestAutoOrbit.QuestType, user.QuestAutoOrbit.ChapterId, user.QuestAutoOrbit.QuestId,
|
||||||
|
user.QuestAutoOrbit.ClearedAutoOrbitCount, user.QuestAutoOrbit.MaxAutoOrbitCount)
|
||||||
|
}
|
||||||
|
user.QuestAutoOrbit = store.QuestAutoOrbitState{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s := user.QuestAutoOrbit
|
||||||
|
if s.MaxAutoOrbitCount > 0 &&
|
||||||
|
s.QuestType == int32(questType) && s.ChapterId == chapterId &&
|
||||||
|
s.QuestId == questId && s.MaxAutoOrbitCount == maxCount {
|
||||||
|
s.LatestVersion = nowMillis
|
||||||
|
user.QuestAutoOrbit = s
|
||||||
|
log.Printf("[autoOrbit] continue cleared=%d/%d", s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[autoOrbit] start questType=%d chapter=%d quest=%d max=%d", questType, chapterId, questId, maxCount)
|
||||||
|
user.QuestAutoOrbit = store.QuestAutoOrbitState{
|
||||||
|
QuestType: int32(questType),
|
||||||
|
ChapterId: chapterId,
|
||||||
|
QuestId: questId,
|
||||||
|
MaxAutoOrbitCount: maxCount,
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func finishAutoOrbit(user *store.UserState, isAutoOrbit, isRetired, isAnnihilated bool, questType model.QuestType, chapterId, questId int32, nowMillis int64, drops []questflow.RewardGrant) (endedDrops []store.AutoOrbitDropEntry, loopEnded bool) {
|
||||||
|
s := user.QuestAutoOrbit
|
||||||
|
if s.MaxAutoOrbitCount <= 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if s.QuestType != int32(questType) || s.ChapterId != chapterId || s.QuestId != questId {
|
||||||
|
log.Printf("[autoOrbit] finish for other quest, ignored: tracked={qt=%d ch=%d q=%d} got={qt=%d ch=%d q=%d}",
|
||||||
|
s.QuestType, s.ChapterId, s.QuestId, int32(questType), chapterId, questId)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if !isRetired && !isAnnihilated {
|
||||||
|
added := 0
|
||||||
|
for _, d := range drops {
|
||||||
|
s.AccumulatedDrops = append(s.AccumulatedDrops, store.AutoOrbitDropEntry{
|
||||||
|
PossessionType: int32(d.PossessionType),
|
||||||
|
PossessionId: d.PossessionId,
|
||||||
|
Count: d.Count,
|
||||||
|
IsAutoSale: d.IsAutoSale,
|
||||||
|
})
|
||||||
|
added++
|
||||||
|
}
|
||||||
|
s.ClearedAutoOrbitCount++
|
||||||
|
log.Printf("[autoOrbit] iter cleared=%d/%d +%d drops (total=%d)",
|
||||||
|
s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount, added, len(s.AccumulatedDrops))
|
||||||
|
}
|
||||||
|
s.LastClearDatetime = nowMillis
|
||||||
|
s.LatestVersion = nowMillis
|
||||||
|
if !isAutoOrbit || isRetired || isAnnihilated || s.ClearedAutoOrbitCount >= s.MaxAutoOrbitCount {
|
||||||
|
log.Printf("[autoOrbit] loop end: cleared=%d/%d total drops=%d (returned in response, accumulator kept)",
|
||||||
|
s.ClearedAutoOrbitCount, s.MaxAutoOrbitCount, len(s.AccumulatedDrops))
|
||||||
|
user.QuestAutoOrbit = store.QuestAutoOrbitState{AccumulatedDrops: s.AccumulatedDrops}
|
||||||
|
return s.AccumulatedDrops, true
|
||||||
|
}
|
||||||
|
user.QuestAutoOrbit = s
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeAutoOrbitRewards(user *store.UserState) []store.AutoOrbitDropEntry {
|
||||||
|
drops := user.QuestAutoOrbit.AccumulatedDrops
|
||||||
|
log.Printf("[autoOrbit] consume on FinishAutoOrbit: returning %d drops (loop status max=%d cleared=%d)",
|
||||||
|
len(drops), user.QuestAutoOrbit.MaxAutoOrbitCount, user.QuestAutoOrbit.ClearedAutoOrbitCount)
|
||||||
|
user.QuestAutoOrbit = store.QuestAutoOrbitState{}
|
||||||
|
return drops
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/questflow"
|
"lunar-tear/server/internal/questflow"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
|
|
||||||
@@ -13,13 +14,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) {
|
func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartEventQuestRequest) (*pb.StartEventQuestResponse, error) {
|
||||||
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v", req.EventQuestChapterId, req.QuestId, req.IsBattleOnly)
|
log.Printf("[QuestService] StartEventQuest: chapterId=%d questId=%d isBattleOnly=%v maxAutoOrbitCount=%d",
|
||||||
|
req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.MaxAutoOrbitCount)
|
||||||
|
|
||||||
engine := s.holder.Get().QuestHandler
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
engine.HandleEventQuestStart(user, req.EventQuestChapterId, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
||||||
|
startAutoOrbit(user, model.QuestTypeEvent, req.EventQuestChapterId, req.QuestId, req.MaxAutoOrbitCount, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
drops := engine.BattleDropRewards(req.QuestId)
|
drops := engine.BattleDropRewards(req.QuestId)
|
||||||
@@ -38,16 +41,25 @@ func (s *QuestServiceServer) StartEventQuest(ctx context.Context, req *pb.StartE
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.FinishEventQuestRequest) (*pb.FinishEventQuestResponse, error) {
|
func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.FinishEventQuestRequest) (*pb.FinishEventQuestResponse, error) {
|
||||||
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v", req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated)
|
log.Printf("[QuestService] FinishEventQuest: chapterId=%d questId=%d isRetired=%v isAnnihilated=%v isAutoOrbit=%v",
|
||||||
|
req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, req.IsAutoOrbit)
|
||||||
|
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
engine := s.holder.Get().QuestHandler
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var outcome questflow.FinishOutcome
|
var outcome questflow.FinishOutcome
|
||||||
|
var endedDrops []store.AutoOrbitDropEntry
|
||||||
|
var loopEnded bool
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
outcome = engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
outcome = engine.HandleEventQuestFinish(user, req.EventQuestChapterId, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||||
|
endedDrops, loopEnded = finishAutoOrbit(user, req.IsAutoOrbit, req.IsRetired, req.IsAnnihilated, model.QuestTypeEvent, req.EventQuestChapterId, req.QuestId, nowMillis, outcome.DropRewards)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
autoOrbitReward := emptyAutoOrbitReward()
|
||||||
|
if loopEnded {
|
||||||
|
autoOrbitReward.DropReward = autoOrbitDropsToProto(endedDrops)
|
||||||
|
}
|
||||||
|
|
||||||
return &pb.FinishEventQuestResponse{
|
return &pb.FinishEventQuestResponse{
|
||||||
DropReward: toProtoRewards(outcome.DropRewards),
|
DropReward: toProtoRewards(outcome.DropRewards),
|
||||||
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
|
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
|
||||||
@@ -57,6 +69,7 @@ func (s *QuestServiceServer) FinishEventQuest(ctx context.Context, req *pb.Finis
|
|||||||
IsBigWin: outcome.IsBigWin,
|
IsBigWin: outcome.IsBigWin,
|
||||||
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
|
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
|
||||||
UserStatusCampaignReward: []*pb.QuestReward{},
|
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||||
|
AutoOrbitReward: autoOrbitReward,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ func (s *QuestServiceServer) UpdateMainQuestSceneProgress(ctx context.Context, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) {
|
func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMainQuestRequest) (*pb.StartMainQuestResponse, error) {
|
||||||
log.Printf("[QuestService] StartMainQuest: %+v", req)
|
log.Printf("[QuestService] StartMainQuest: questId=%d isMainFlow=%v isReplayFlow=%v isBattleOnly=%v maxAutoOrbitCount=%d",
|
||||||
|
req.QuestId, req.IsMainFlow, req.IsReplayFlow, req.IsBattleOnly, req.MaxAutoOrbitCount)
|
||||||
|
|
||||||
engine := s.holder.Get().QuestHandler
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
@@ -76,6 +77,7 @@ func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMa
|
|||||||
} else {
|
} else {
|
||||||
engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.IsMainFlow, req.UserDeckNumber, nowMillis)
|
engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.IsMainFlow, req.UserDeckNumber, nowMillis)
|
||||||
}
|
}
|
||||||
|
startAutoOrbit(user, model.QuestTypeMain, 0, req.QuestId, req.MaxAutoOrbitCount, nowMillis)
|
||||||
})
|
})
|
||||||
|
|
||||||
drops := engine.BattleDropRewards(req.QuestId)
|
drops := engine.BattleDropRewards(req.QuestId)
|
||||||
@@ -93,6 +95,26 @@ func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMa
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func emptyAutoOrbitReward() *pb.QuestAutoOrbitResult {
|
||||||
|
return &pb.QuestAutoOrbitResult{
|
||||||
|
DropReward: []*pb.QuestReward{},
|
||||||
|
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoOrbitDropsToProto(drops []store.AutoOrbitDropEntry) []*pb.QuestReward {
|
||||||
|
out := make([]*pb.QuestReward, len(drops))
|
||||||
|
for i, d := range drops {
|
||||||
|
out[i] = &pb.QuestReward{
|
||||||
|
PossessionType: d.PossessionType,
|
||||||
|
PossessionId: d.PossessionId,
|
||||||
|
Count: d.Count,
|
||||||
|
IsAutoSale: d.IsAutoSale,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func toProtoRewards(grants []questflow.RewardGrant) []*pb.QuestReward {
|
func toProtoRewards(grants []questflow.RewardGrant) []*pb.QuestReward {
|
||||||
if len(grants) == 0 {
|
if len(grants) == 0 {
|
||||||
return []*pb.QuestReward{}
|
return []*pb.QuestReward{}
|
||||||
@@ -103,23 +125,32 @@ func toProtoRewards(grants []questflow.RewardGrant) []*pb.QuestReward {
|
|||||||
PossessionType: int32(g.PossessionType),
|
PossessionType: int32(g.PossessionType),
|
||||||
PossessionId: g.PossessionId,
|
PossessionId: g.PossessionId,
|
||||||
Count: g.Count,
|
Count: g.Count,
|
||||||
|
IsAutoSale: g.IsAutoSale,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.FinishMainQuestRequest) (*pb.FinishMainQuestResponse, error) {
|
func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.FinishMainQuestRequest) (*pb.FinishMainQuestResponse, error) {
|
||||||
log.Printf("[QuestService] FinishMainQuest: questId=%d isMainFlow=%v isRetired=%v isAnnihilated=%v storySkipType=%d",
|
log.Printf("[QuestService] FinishMainQuest: questId=%d isMainFlow=%v isRetired=%v isAnnihilated=%v isAutoOrbit=%v storySkipType=%d",
|
||||||
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.StorySkipType)
|
req.QuestId, req.IsMainFlow, req.IsRetired, req.IsAnnihilated, req.IsAutoOrbit, req.StorySkipType)
|
||||||
|
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
engine := s.holder.Get().QuestHandler
|
engine := s.holder.Get().QuestHandler
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
var outcome questflow.FinishOutcome
|
var outcome questflow.FinishOutcome
|
||||||
|
var endedDrops []store.AutoOrbitDropEntry
|
||||||
|
var loopEnded bool
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
outcome = engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
outcome = engine.HandleQuestFinish(user, req.QuestId, req.IsRetired, req.IsAnnihilated, nowMillis)
|
||||||
|
endedDrops, loopEnded = finishAutoOrbit(user, req.IsAutoOrbit, req.IsRetired, req.IsAnnihilated, model.QuestTypeMain, 0, req.QuestId, nowMillis, outcome.DropRewards)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
autoOrbitReward := emptyAutoOrbitReward()
|
||||||
|
if loopEnded {
|
||||||
|
autoOrbitReward.DropReward = autoOrbitDropsToProto(endedDrops)
|
||||||
|
}
|
||||||
|
|
||||||
return &pb.FinishMainQuestResponse{
|
return &pb.FinishMainQuestResponse{
|
||||||
DropReward: toProtoRewards(outcome.DropRewards),
|
DropReward: toProtoRewards(outcome.DropRewards),
|
||||||
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
|
FirstClearReward: toProtoRewards(outcome.FirstClearRewards),
|
||||||
@@ -130,6 +161,7 @@ func (s *QuestServiceServer) FinishMainQuest(ctx context.Context, req *pb.Finish
|
|||||||
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
|
BigWinClearedQuestMissionIdList: outcome.BigWinClearedQuestMissionIds,
|
||||||
ReplayFlowFirstClearReward: toProtoRewards(outcome.ReplayFlowFirstClearRewards),
|
ReplayFlowFirstClearReward: toProtoRewards(outcome.ReplayFlowFirstClearRewards),
|
||||||
UserStatusCampaignReward: []*pb.QuestReward{},
|
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||||
|
AutoOrbitReward: autoOrbitReward,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +194,26 @@ func (s *QuestServiceServer) RestartMainQuest(ctx context.Context, req *pb.Resta
|
|||||||
|
|
||||||
func (s *QuestServiceServer) FinishAutoOrbit(ctx context.Context, req *emptypb.Empty) (*pb.FinishAutoOrbitResponse, error) {
|
func (s *QuestServiceServer) FinishAutoOrbit(ctx context.Context, req *emptypb.Empty) (*pb.FinishAutoOrbitResponse, error) {
|
||||||
log.Printf("[QuestService] FinishAutoOrbit")
|
log.Printf("[QuestService] FinishAutoOrbit")
|
||||||
return &pb.FinishAutoOrbitResponse{}, nil
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
|
var drops []store.AutoOrbitDropEntry
|
||||||
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
|
drops = consumeAutoOrbitRewards(user)
|
||||||
|
})
|
||||||
|
pbDrops := make([]*pb.QuestReward, len(drops))
|
||||||
|
for i, d := range drops {
|
||||||
|
pbDrops[i] = &pb.QuestReward{
|
||||||
|
PossessionType: d.PossessionType,
|
||||||
|
PossessionId: d.PossessionId,
|
||||||
|
Count: d.Count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &pb.FinishAutoOrbitResponse{
|
||||||
|
AutoOrbitResult: []*pb.QuestReward{},
|
||||||
|
AutoOrbitReward: &pb.QuestAutoOrbitResult{
|
||||||
|
DropReward: pbDrops,
|
||||||
|
UserStatusCampaignReward: []*pb.QuestReward{},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestRequest) (*pb.SkipQuestResponse, error) {
|
func (s *QuestServiceServer) SkipQuest(ctx context.Context, req *pb.SkipQuestRequest) (*pb.SkipQuestResponse, error) {
|
||||||
@@ -198,7 +249,6 @@ func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteReque
|
|||||||
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
|
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
|
||||||
if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
|
if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
|
||||||
user.MainQuest.MainQuestSeasonId = seasonId
|
user.MainQuest.MainQuestSeasonId = seasonId
|
||||||
questflow.RecordSeasonRoute(user, seasonId, req.MainQuestRouteId, gametime.NowMillis())
|
|
||||||
}
|
}
|
||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
user.PortalCageStatus.IsCurrentProgress = false
|
user.PortalCageStatus.IsCurrentProgress = false
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
|
"lunar-tear/server/internal/campaign"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/gameutil"
|
"lunar-tear/server/internal/gameutil"
|
||||||
"lunar-tear/server/internal/masterdata"
|
"lunar-tear/server/internal/masterdata"
|
||||||
@@ -91,6 +92,12 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expBonus := cat.Campaign.WeaponExpBonus(campaign.WeaponTarget{
|
||||||
|
WeaponId: weapon.WeaponId,
|
||||||
|
WeaponType: wm.WeaponType,
|
||||||
|
AttributeType: wm.AttributeType,
|
||||||
|
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll})
|
||||||
|
|
||||||
totalExp := int32(0)
|
totalExp := int32(0)
|
||||||
totalMaterialCount := int32(0)
|
totalMaterialCount := int32(0)
|
||||||
for materialId, count := range req.Materials {
|
for materialId, count := range req.Materials {
|
||||||
@@ -112,7 +119,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
|
if mat.WeaponType != 0 && mat.WeaponType == wm.WeaponType {
|
||||||
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
expPerUnit = expPerUnit * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += expPerUnit * count
|
totalExp += expBonus.Apply(expPerUnit * count)
|
||||||
}
|
}
|
||||||
|
|
||||||
if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
if costFunc, ok := catalog.GoldCostByEnhanceId[wm.WeaponSpecificEnhanceId]; ok && totalMaterialCount > 0 {
|
||||||
@@ -124,16 +131,11 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
weapon.Exp += totalExp
|
weapon.Exp += totalExp
|
||||||
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
||||||
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
var maxLevel int32
|
||||||
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
maxLevel = awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||||
if weapon.Level > cap {
|
|
||||||
weapon.Level = cap
|
|
||||||
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
|
||||||
weapon.Exp = thresholds[cap]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
weapon.Level, weapon.Exp = gameutil.ApplyExpWithMaxLevel(weapon.Exp, thresholds, maxLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
note := user.WeaponNotes[weapon.WeaponId]
|
note := user.WeaponNotes[weapon.WeaponId]
|
||||||
@@ -702,6 +704,12 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expBonus := cat.Campaign.WeaponExpBonus(campaign.WeaponTarget{
|
||||||
|
WeaponId: weapon.WeaponId,
|
||||||
|
WeaponType: wm.WeaponType,
|
||||||
|
AttributeType: wm.AttributeType,
|
||||||
|
}, campaign.Filter{NowMillis: nowMillis, UserStatus: campaign.TargetUserStatusAll})
|
||||||
|
|
||||||
totalExp := int32(0)
|
totalExp := int32(0)
|
||||||
consumedCount := int32(0)
|
consumedCount := int32(0)
|
||||||
for _, uuid := range req.MaterialUserWeaponUuids {
|
for _, uuid := range req.MaterialUserWeaponUuids {
|
||||||
@@ -722,7 +730,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
if matMaster.WeaponType != 0 && matMaster.WeaponType == wm.WeaponType {
|
||||||
baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
baseExp = baseExp * config.MaterialSameWeaponExpCoefficientPermil / 1000
|
||||||
}
|
}
|
||||||
totalExp += baseExp
|
totalExp += expBonus.Apply(baseExp)
|
||||||
|
|
||||||
if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
if medals, ok := catalog.MedalsByWeaponId[matWeapon.WeaponId]; ok {
|
||||||
for itemId, count := range medals {
|
for itemId, count := range medals {
|
||||||
@@ -746,16 +754,11 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
weapon.Exp += totalExp
|
weapon.Exp += totalExp
|
||||||
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
levelingEnhanceId := catalog.LevelingEnhanceIdByWeaponId[weapon.WeaponId]
|
||||||
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
var maxLevel int32
|
||||||
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
maxLevel = awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||||
if weapon.Level > cap {
|
|
||||||
weapon.Level = cap
|
|
||||||
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
|
||||||
weapon.Exp = thresholds[cap]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
weapon.Level, weapon.Exp = gameutil.ApplyExpWithMaxLevel(weapon.Exp, thresholds, maxLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
note := user.WeaponNotes[weapon.WeaponId]
|
note := user.WeaponNotes[weapon.WeaponId]
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ func CloneUserState(u UserState) UserState {
|
|||||||
out.CostumeLotteryEffectPending = maps.Clone(u.CostumeLotteryEffectPending)
|
out.CostumeLotteryEffectPending = maps.Clone(u.CostumeLotteryEffectPending)
|
||||||
out.AutoSaleSettings = maps.Clone(u.AutoSaleSettings)
|
out.AutoSaleSettings = maps.Clone(u.AutoSaleSettings)
|
||||||
out.CharacterRebirths = maps.Clone(u.CharacterRebirths)
|
out.CharacterRebirths = maps.Clone(u.CharacterRebirths)
|
||||||
|
out.QuestAutoOrbit.AccumulatedDrops = append([]AutoOrbitDropEntry(nil), u.QuestAutoOrbit.AccumulatedDrops...)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package store
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -102,7 +103,19 @@ type WeaponStoryReleaseCond struct {
|
|||||||
|
|
||||||
type PartsRef struct {
|
type PartsRef struct {
|
||||||
PartsGroupId int32
|
PartsGroupId int32
|
||||||
|
RarityType int32
|
||||||
|
PartsInitialLotteryId int32
|
||||||
PartsStatusMainLotteryGroupId int32
|
PartsStatusMainLotteryGroupId int32
|
||||||
|
PartsStatusSubLotteryGroupId int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// PartsStatusSubDef carries the per-lottery-id sub-status shape needed at
|
||||||
|
// grant time. Held here so the store package does not import masterdata.
|
||||||
|
type PartsStatusSubDef struct {
|
||||||
|
StatusKindType int32
|
||||||
|
StatusCalculationType int32
|
||||||
|
StatusChangeInitialValue int32
|
||||||
|
StatusFunc func(level int32) int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type PossessionGranter struct {
|
type PossessionGranter struct {
|
||||||
@@ -114,6 +127,12 @@ type PossessionGranter struct {
|
|||||||
|
|
||||||
PartsById map[int32]PartsRef
|
PartsById map[int32]PartsRef
|
||||||
DefaultPartsStatusMainByLotteryGroup map[int32]int32
|
DefaultPartsStatusMainByLotteryGroup map[int32]int32
|
||||||
|
PartsVariantsByGroupRarity map[int32]map[int32][]int32
|
||||||
|
PartsSubStatusPool map[int32][]int32
|
||||||
|
PartsSubStatusDefs map[int32]PartsStatusSubDef
|
||||||
|
|
||||||
|
PartsSellPriceL1ByRarity map[int32]int32
|
||||||
|
GoldConsumableItemId int32
|
||||||
|
|
||||||
LastChangedStoryWeaponIds []int32
|
LastChangedStoryWeaponIds []int32
|
||||||
}
|
}
|
||||||
@@ -184,26 +203,108 @@ func (g *PossessionGranter) GrantCompanion(user *UserState, companionId int32, n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *PossessionGranter) GrantParts(user *UserState, partsId int32, nowMillis int64) {
|
func (g *PossessionGranter) GrantParts(user *UserState, requestedPartsId int32, nowMillis int64) {
|
||||||
var mainStatId int32
|
chosenPartsId, chosenRef, ok := g.rollPartsVariant(requestedPartsId)
|
||||||
if ref, ok := g.PartsById[partsId]; ok {
|
if !ok {
|
||||||
mainStatId = g.DefaultPartsStatusMainByLotteryGroup[ref.PartsStatusMainLotteryGroupId]
|
g.grantBareParts(user, requestedPartsId, nowMillis)
|
||||||
if _, exists := user.PartsGroupNotes[ref.PartsGroupId]; !exists {
|
return
|
||||||
user.PartsGroupNotes[ref.PartsGroupId] = PartsGroupNoteState{
|
|
||||||
PartsGroupId: ref.PartsGroupId,
|
|
||||||
FirstAcquisitionDatetime: nowMillis,
|
|
||||||
LatestVersion: nowMillis,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
g.createParts(user, chosenPartsId, chosenRef, nowMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rolled variant sets both rarity and rank, so the auto-sale decision can
|
||||||
|
// only happen after the roll. Returns the rolled variant id and whether it sold.
|
||||||
|
func (g *PossessionGranter) GrantOrSellPartsDrop(user *UserState, requestedPartsId int32, raritySet, rankSet map[int32]bool, nowMillis int64) (int32, bool) {
|
||||||
|
chosenPartsId, chosenRef, ok := g.rollPartsVariant(requestedPartsId)
|
||||||
|
if !ok {
|
||||||
|
g.grantBareParts(user, requestedPartsId, nowMillis)
|
||||||
|
return requestedPartsId, false
|
||||||
|
}
|
||||||
|
rarity := chosenRef.RarityType
|
||||||
|
rank := chosenRef.PartsInitialLotteryId
|
||||||
|
if price, ok := g.PartsSellPriceL1ByRarity[rarity]; ok && raritySet[rarity] && rankSet[rank] {
|
||||||
|
user.ConsumableItems[g.GoldConsumableItemId] += price
|
||||||
|
log.Printf("[GrantParts] auto-sold chosen=%d rarity=%d rank=%d -> %d gold", chosenPartsId, rarity, rank, price)
|
||||||
|
return chosenPartsId, true
|
||||||
|
}
|
||||||
|
g.createParts(user, chosenPartsId, chosenRef, nowMillis)
|
||||||
|
return chosenPartsId, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *PossessionGranter) grantBareParts(user *UserState, partsId int32, nowMillis int64) {
|
||||||
key := uuid.New().String()
|
key := uuid.New().String()
|
||||||
user.Parts[key] = PartsState{
|
user.Parts[key] = PartsState{
|
||||||
UserPartsUuid: key,
|
UserPartsUuid: key,
|
||||||
PartsId: partsId,
|
PartsId: partsId,
|
||||||
Level: 1,
|
Level: 1,
|
||||||
|
AcquisitionDatetime: nowMillis,
|
||||||
|
}
|
||||||
|
log.Printf("[GrantParts] unknown partsId=%d, granted as-is with no variant roll", partsId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rollPartsVariant picks one of a parts group's 5 variants at random; the five
|
||||||
|
// carry distinct PartsInitialLotteryId 1..5, which is the part's rank.
|
||||||
|
func (g *PossessionGranter) rollPartsVariant(requestedPartsId int32) (int32, PartsRef, bool) {
|
||||||
|
ref, refOk := g.PartsById[requestedPartsId]
|
||||||
|
if !refOk {
|
||||||
|
return requestedPartsId, PartsRef{}, false
|
||||||
|
}
|
||||||
|
chosenPartsId := requestedPartsId
|
||||||
|
chosenRef := ref
|
||||||
|
if variants := g.PartsVariantsByGroupRarity[ref.PartsGroupId][ref.RarityType]; len(variants) == 5 {
|
||||||
|
chosenPartsId = variants[rand.Intn(len(variants))]
|
||||||
|
chosenRef = g.PartsById[chosenPartsId]
|
||||||
|
} else {
|
||||||
|
log.Printf("[GrantParts] no 5-variant set for group=%d rarity=%d (have %d), granting requested=%d", ref.PartsGroupId, ref.RarityType, len(variants), requestedPartsId)
|
||||||
|
}
|
||||||
|
return chosenPartsId, chosenRef, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *PossessionGranter) createParts(user *UserState, chosenPartsId int32, chosenRef PartsRef, nowMillis int64) {
|
||||||
|
mainStatId := g.DefaultPartsStatusMainByLotteryGroup[chosenRef.PartsStatusMainLotteryGroupId]
|
||||||
|
if _, exists := user.PartsGroupNotes[chosenRef.PartsGroupId]; !exists {
|
||||||
|
user.PartsGroupNotes[chosenRef.PartsGroupId] = PartsGroupNoteState{
|
||||||
|
PartsGroupId: chosenRef.PartsGroupId,
|
||||||
|
FirstAcquisitionDatetime: nowMillis,
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := uuid.New().String()
|
||||||
|
user.Parts[key] = PartsState{
|
||||||
|
UserPartsUuid: key,
|
||||||
|
PartsId: chosenPartsId,
|
||||||
|
Level: 1,
|
||||||
PartsStatusMainId: mainStatId,
|
PartsStatusMainId: mainStatId,
|
||||||
AcquisitionDatetime: nowMillis,
|
AcquisitionDatetime: nowMillis,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialCount := chosenRef.PartsInitialLotteryId
|
||||||
|
pool := g.PartsSubStatusPool[chosenRef.PartsStatusSubLotteryGroupId]
|
||||||
|
if initialCount > 1 && len(pool) > 0 {
|
||||||
|
for i := int32(0); i < initialCount-1; i++ {
|
||||||
|
pickId := pool[rand.Intn(len(pool))]
|
||||||
|
def, ok := g.PartsSubStatusDefs[pickId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := def.StatusChangeInitialValue
|
||||||
|
if def.StatusFunc != nil {
|
||||||
|
val = def.StatusFunc(1)
|
||||||
|
}
|
||||||
|
user.PartsStatusSubs[PartsStatusSubKey{UserPartsUuid: key, StatusIndex: i + 1}] = PartsStatusSubState{
|
||||||
|
UserPartsUuid: key,
|
||||||
|
StatusIndex: i + 1,
|
||||||
|
PartsStatusSubLotteryId: pickId,
|
||||||
|
Level: 1,
|
||||||
|
StatusKindType: def.StatusKindType,
|
||||||
|
StatusCalculationType: def.StatusCalculationType,
|
||||||
|
StatusChangeValue: val,
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[GrantParts] chosen=%d group=%d rarity=%d preUnlockedSubs=%d", chosenPartsId, chosenRef.PartsGroupId, chosenRef.RarityType, initialCount-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
|
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package sqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
@@ -86,7 +87,6 @@ func initMaps(u *store.UserState) {
|
|||||||
u.CharacterRebirths = make(map[int32]store.CharacterRebirthState)
|
u.CharacterRebirths = make(map[int32]store.CharacterRebirthState)
|
||||||
u.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState)
|
u.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState)
|
||||||
u.SideStoryQuests = make(map[int32]store.SideStoryQuestProgress)
|
u.SideStoryQuests = make(map[int32]store.SideStoryQuestProgress)
|
||||||
u.MainQuestSeasonRoutes = make(map[store.SeasonRouteKey]store.SeasonRouteEntry)
|
|
||||||
u.QuestLimitContentStatus = make(map[int32]store.QuestLimitContentStatus)
|
u.QuestLimitContentStatus = make(map[int32]store.QuestLimitContentStatus)
|
||||||
u.BigHuntMaxScores = make(map[int32]store.BigHuntMaxScore)
|
u.BigHuntMaxScores = make(map[int32]store.BigHuntMaxScore)
|
||||||
u.BigHuntStatuses = make(map[int32]store.BigHuntStatus)
|
u.BigHuntStatuses = make(map[int32]store.BigHuntStatus)
|
||||||
@@ -211,6 +211,17 @@ func load1to1(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
Scan(&u.GuerrillaFreeOpen.StartDatetime, &u.GuerrillaFreeOpen.OpenMinutes,
|
Scan(&u.GuerrillaFreeOpen.StartDatetime, &u.GuerrillaFreeOpen.OpenMinutes,
|
||||||
&u.GuerrillaFreeOpen.DailyOpenedCount, &u.GuerrillaFreeOpen.LatestVersion)
|
&u.GuerrillaFreeOpen.DailyOpenedCount, &u.GuerrillaFreeOpen.LatestVersion)
|
||||||
|
|
||||||
|
var accumulatedDropsJSON string
|
||||||
|
_ = db.QueryRow(`SELECT quest_type, chapter_id, quest_id, max_auto_orbit_count, cleared_auto_orbit_count, last_clear_datetime, latest_version, accumulated_drops_json
|
||||||
|
FROM user_quest_auto_orbit WHERE user_id=?`, uid).
|
||||||
|
Scan(&u.QuestAutoOrbit.QuestType, &u.QuestAutoOrbit.ChapterId, &u.QuestAutoOrbit.QuestId,
|
||||||
|
&u.QuestAutoOrbit.MaxAutoOrbitCount, &u.QuestAutoOrbit.ClearedAutoOrbitCount,
|
||||||
|
&u.QuestAutoOrbit.LastClearDatetime, &u.QuestAutoOrbit.LatestVersion,
|
||||||
|
&accumulatedDropsJSON)
|
||||||
|
if accumulatedDropsJSON != "" && accumulatedDropsJSON != "[]" {
|
||||||
|
_ = json.Unmarshal([]byte(accumulatedDropsJSON), &u.QuestAutoOrbit.AccumulatedDrops)
|
||||||
|
}
|
||||||
|
|
||||||
var isTicket int
|
var isTicket int
|
||||||
_ = db.QueryRow(`SELECT is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version
|
_ = db.QueryRow(`SELECT is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version
|
||||||
FROM user_explore WHERE user_id=?`, uid).
|
FROM user_explore WHERE user_id=?`, uid).
|
||||||
@@ -378,16 +389,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
queryRows(db, `SELECT main_quest_season_id, main_quest_route_id, latest_version
|
|
||||||
FROM user_main_quest_season_routes WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
|
||||||
var seasonId, routeId int32
|
|
||||||
var lv int64
|
|
||||||
rows.Scan(&seasonId, &routeId, &lv)
|
|
||||||
u.MainQuestSeasonRoutes[store.SeasonRouteKey{MainQuestSeasonId: seasonId, MainQuestRouteId: routeId}] = store.SeasonRouteEntry{
|
|
||||||
MainQuestSeasonId: seasonId, MainQuestRouteId: routeId, LatestVersion: lv,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
queryRows(db, `SELECT limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version
|
queryRows(db, `SELECT limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version
|
||||||
FROM user_quest_limit_content_status WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
FROM user_quest_limit_content_status WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var id int32
|
var id int32
|
||||||
|
|||||||
@@ -2,12 +2,24 @@ package sqlite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func marshalAutoOrbitDrops(drops []store.AutoOrbitDropEntry) string {
|
||||||
|
if len(drops) == 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(drops)
|
||||||
|
if err != nil {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
func boolToInt(b bool) int {
|
func boolToInt(b bool) int {
|
||||||
if b {
|
if b {
|
||||||
return 1
|
return 1
|
||||||
@@ -109,6 +121,13 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
uid, u.GuerrillaFreeOpen.StartDatetime, u.GuerrillaFreeOpen.OpenMinutes, u.GuerrillaFreeOpen.DailyOpenedCount, u.GuerrillaFreeOpen.LatestVersion); err != nil {
|
uid, u.GuerrillaFreeOpen.StartDatetime, u.GuerrillaFreeOpen.OpenMinutes, u.GuerrillaFreeOpen.DailyOpenedCount, u.GuerrillaFreeOpen.LatestVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := exec(`INSERT INTO user_quest_auto_orbit (user_id, quest_type, chapter_id, quest_id, max_auto_orbit_count, cleared_auto_orbit_count, last_clear_datetime, latest_version, accumulated_drops_json) VALUES (?,?,?,?,?,?,?,?,?)`,
|
||||||
|
uid, u.QuestAutoOrbit.QuestType, u.QuestAutoOrbit.ChapterId, u.QuestAutoOrbit.QuestId,
|
||||||
|
u.QuestAutoOrbit.MaxAutoOrbitCount, u.QuestAutoOrbit.ClearedAutoOrbitCount,
|
||||||
|
u.QuestAutoOrbit.LastClearDatetime, u.QuestAutoOrbit.LatestVersion,
|
||||||
|
marshalAutoOrbitDrops(u.QuestAutoOrbit.AccumulatedDrops)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := exec(`INSERT INTO user_explore (user_id, is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version) VALUES (?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_explore (user_id, is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version) VALUES (?,?,?,?,?)`,
|
||||||
uid, boolToInt(u.Explore.IsUseExploreTicket), u.Explore.PlayingExploreId, u.Explore.LatestPlayDatetime, u.Explore.LatestVersion); err != nil {
|
uid, boolToInt(u.Explore.IsUseExploreTicket), u.Explore.PlayingExploreId, u.Explore.LatestPlayDatetime, u.Explore.LatestVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -224,12 +243,6 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, v := range u.MainQuestSeasonRoutes {
|
|
||||||
if err := exec(`INSERT INTO user_main_quest_season_routes (user_id, main_quest_season_id, main_quest_route_id, latest_version) VALUES (?,?,?,?)`,
|
|
||||||
uid, k.MainQuestSeasonId, k.MainQuestRouteId, v.LatestVersion); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for id, v := range u.QuestLimitContentStatus {
|
for id, v := range u.QuestLimitContentStatus {
|
||||||
if err := exec(`INSERT INTO user_quest_limit_content_status (user_id, limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version) VALUES (?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_quest_limit_content_status (user_id, limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version) VALUES (?,?,?,?,?)`,
|
||||||
uid, id, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion); err != nil {
|
uid, id, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion); err != nil {
|
||||||
@@ -680,6 +693,15 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !before.QuestAutoOrbit.Equal(after.QuestAutoOrbit) {
|
||||||
|
if err := exec(`UPDATE user_quest_auto_orbit SET quest_type=?, chapter_id=?, quest_id=?, max_auto_orbit_count=?, cleared_auto_orbit_count=?, last_clear_datetime=?, latest_version=?, accumulated_drops_json=? WHERE user_id=?`,
|
||||||
|
after.QuestAutoOrbit.QuestType, after.QuestAutoOrbit.ChapterId, after.QuestAutoOrbit.QuestId,
|
||||||
|
after.QuestAutoOrbit.MaxAutoOrbitCount, after.QuestAutoOrbit.ClearedAutoOrbitCount,
|
||||||
|
after.QuestAutoOrbit.LastClearDatetime, after.QuestAutoOrbit.LatestVersion,
|
||||||
|
marshalAutoOrbitDrops(after.QuestAutoOrbit.AccumulatedDrops), uid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if before.Explore != after.Explore {
|
if before.Explore != after.Explore {
|
||||||
if err := exec(`UPDATE user_explore SET is_use_explore_ticket=?, playing_explore_id=?, latest_play_datetime=?, latest_version=? WHERE user_id=?`,
|
if err := exec(`UPDATE user_explore SET is_use_explore_ticket=?, playing_explore_id=?, latest_play_datetime=?, latest_version=? WHERE user_id=?`,
|
||||||
boolToInt(after.Explore.IsUseExploreTicket), after.Explore.PlayingExploreId, after.Explore.LatestPlayDatetime, after.Explore.LatestVersion, uid); err != nil {
|
boolToInt(after.Explore.IsUseExploreTicket), after.Explore.PlayingExploreId, after.Explore.LatestPlayDatetime, after.Explore.LatestVersion, uid); err != nil {
|
||||||
@@ -798,17 +820,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
return []any{0, v.HeadSideStoryQuestSceneId, int32(v.SideStoryQuestStateType), v.LatestVersion}
|
return []any{0, v.HeadSideStoryQuestSceneId, int32(v.SideStoryQuestStateType), v.LatestVersion}
|
||||||
}, "side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version")
|
}, "side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version")
|
||||||
|
|
||||||
for k, v := range after.MainQuestSeasonRoutes {
|
|
||||||
if old, ok := before.MainQuestSeasonRoutes[k]; !ok || old != v {
|
|
||||||
exec(`INSERT OR REPLACE INTO user_main_quest_season_routes (user_id, main_quest_season_id, main_quest_route_id, latest_version) VALUES (?,?,?,?)`,
|
|
||||||
uid, k.MainQuestSeasonId, k.MainQuestRouteId, v.LatestVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k := range before.MainQuestSeasonRoutes {
|
|
||||||
if _, ok := after.MainQuestSeasonRoutes[k]; !ok {
|
|
||||||
exec(`DELETE FROM user_main_quest_season_routes WHERE user_id=? AND main_quest_season_id=? AND main_quest_route_id=?`, uid, k.MainQuestSeasonId, k.MainQuestRouteId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diffMapInt32(tx, uid, before.QuestLimitContentStatus, after.QuestLimitContentStatus, "user_quest_limit_content_status", "limit_content_id",
|
diffMapInt32(tx, uid, before.QuestLimitContentStatus, after.QuestLimitContentStatus, "user_quest_limit_content_status", "limit_content_id",
|
||||||
func(v store.QuestLimitContentStatus) []any {
|
func(v store.QuestLimitContentStatus) []any {
|
||||||
return []any{0, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion}
|
return []any{0, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
|
|||||||
"user_viewed_movies",
|
"user_viewed_movies",
|
||||||
"user_navi_cutin_played",
|
"user_navi_cutin_played",
|
||||||
"user_auto_sale_settings",
|
"user_auto_sale_settings",
|
||||||
|
"user_quest_auto_orbit",
|
||||||
"user_explore_scores",
|
"user_explore_scores",
|
||||||
"user_tutorials",
|
"user_tutorials",
|
||||||
"user_premium_items",
|
"user_premium_items",
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type UserState struct {
|
|||||||
LoginBonus UserLoginBonusState
|
LoginBonus UserLoginBonusState
|
||||||
Tutorials map[int32]TutorialProgressState
|
Tutorials map[int32]TutorialProgressState
|
||||||
MainQuest MainQuestState
|
MainQuest MainQuestState
|
||||||
MainQuestSeasonRoutes map[SeasonRouteKey]SeasonRouteEntry
|
|
||||||
EventQuest EventQuestState
|
EventQuest EventQuestState
|
||||||
ExtraQuest ExtraQuestState
|
ExtraQuest ExtraQuestState
|
||||||
SideStoryQuests map[int32]SideStoryQuestProgress
|
SideStoryQuests map[int32]SideStoryQuestProgress
|
||||||
@@ -120,6 +119,7 @@ type UserState struct {
|
|||||||
CostumeLotteryEffectPending map[string]CostumeLotteryEffectPendingState // key: userCostumeUuid
|
CostumeLotteryEffectPending map[string]CostumeLotteryEffectPendingState // key: userCostumeUuid
|
||||||
AutoSaleSettings map[int32]AutoSaleSettingState
|
AutoSaleSettings map[int32]AutoSaleSettingState
|
||||||
CharacterRebirths map[int32]CharacterRebirthState
|
CharacterRebirths map[int32]CharacterRebirthState
|
||||||
|
QuestAutoOrbit QuestAutoOrbitState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserState) EnsureMaps() {
|
func (u *UserState) EnsureMaps() {
|
||||||
@@ -162,9 +162,6 @@ func (u *UserState) EnsureMaps() {
|
|||||||
if u.SideStoryQuests == nil {
|
if u.SideStoryQuests == nil {
|
||||||
u.SideStoryQuests = make(map[int32]SideStoryQuestProgress)
|
u.SideStoryQuests = make(map[int32]SideStoryQuestProgress)
|
||||||
}
|
}
|
||||||
if u.MainQuestSeasonRoutes == nil {
|
|
||||||
u.MainQuestSeasonRoutes = make(map[SeasonRouteKey]SeasonRouteEntry)
|
|
||||||
}
|
|
||||||
if u.QuestLimitContentStatus == nil {
|
if u.QuestLimitContentStatus == nil {
|
||||||
u.QuestLimitContentStatus = make(map[int32]QuestLimitContentStatus)
|
u.QuestLimitContentStatus = make(map[int32]QuestLimitContentStatus)
|
||||||
}
|
}
|
||||||
@@ -335,6 +332,45 @@ type GuerrillaFreeOpenState struct {
|
|||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AutoOrbitDropEntry struct {
|
||||||
|
PossessionType int32
|
||||||
|
PossessionId int32
|
||||||
|
Count int32
|
||||||
|
IsAutoSale bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestAutoOrbitState struct {
|
||||||
|
QuestType int32
|
||||||
|
ChapterId int32
|
||||||
|
QuestId int32
|
||||||
|
MaxAutoOrbitCount int32
|
||||||
|
ClearedAutoOrbitCount int32
|
||||||
|
LastClearDatetime int64
|
||||||
|
LatestVersion int64
|
||||||
|
AccumulatedDrops []AutoOrbitDropEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s QuestAutoOrbitState) Equal(other QuestAutoOrbitState) bool {
|
||||||
|
if s.QuestType != other.QuestType ||
|
||||||
|
s.ChapterId != other.ChapterId ||
|
||||||
|
s.QuestId != other.QuestId ||
|
||||||
|
s.MaxAutoOrbitCount != other.MaxAutoOrbitCount ||
|
||||||
|
s.ClearedAutoOrbitCount != other.ClearedAutoOrbitCount ||
|
||||||
|
s.LastClearDatetime != other.LastClearDatetime ||
|
||||||
|
s.LatestVersion != other.LatestVersion {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(s.AccumulatedDrops) != len(other.AccumulatedDrops) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range s.AccumulatedDrops {
|
||||||
|
if s.AccumulatedDrops[i] != other.AccumulatedDrops[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type PortalCageStatusState struct {
|
type PortalCageStatusState struct {
|
||||||
IsCurrentProgress bool
|
IsCurrentProgress bool
|
||||||
DropItemStartDatetime int64
|
DropItemStartDatetime int64
|
||||||
@@ -590,17 +626,6 @@ type SideStoryActiveProgress struct {
|
|||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type SeasonRouteKey struct {
|
|
||||||
MainQuestSeasonId int32
|
|
||||||
MainQuestRouteId int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type SeasonRouteEntry struct {
|
|
||||||
MainQuestSeasonId int32
|
|
||||||
MainQuestRouteId int32
|
|
||||||
LatestVersion int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type QuestLimitContentStatus struct {
|
type QuestLimitContentStatus struct {
|
||||||
LimitContentQuestStatusType int32
|
LimitContentQuestStatusType int32
|
||||||
EventQuestChapterId int32
|
EventQuestChapterId int32
|
||||||
|
|||||||
@@ -101,8 +101,9 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
add("IUserMainQuestMainFlowStatus")
|
add("IUserMainQuestMainFlowStatus")
|
||||||
add("IUserMainQuestProgressStatus")
|
add("IUserMainQuestProgressStatus")
|
||||||
add("IUserMainQuestReplayFlowStatus")
|
add("IUserMainQuestReplayFlowStatus")
|
||||||
}
|
// IUserMainQuestSeasonRoute is derived from MainQuest + Quests at projection
|
||||||
if !mapsEqualStruct(before.MainQuestSeasonRoutes, after.MainQuestSeasonRoutes) {
|
// time (see proj_quest.go / questflow.QuestHandler.SeasonRoutesFor) — flag it
|
||||||
|
// whenever either of those upstream inputs changes.
|
||||||
add("IUserMainQuestSeasonRoute")
|
add("IUserMainQuestSeasonRoute")
|
||||||
}
|
}
|
||||||
if before.EventQuest != after.EventQuest {
|
if before.EventQuest != after.EventQuest {
|
||||||
@@ -202,6 +203,7 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.Quests, after.Quests) {
|
if !mapsEqualStruct(before.Quests, after.Quests) {
|
||||||
add("IUserQuest")
|
add("IUserQuest")
|
||||||
|
add("IUserMainQuestSeasonRoute")
|
||||||
}
|
}
|
||||||
if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) {
|
if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) {
|
||||||
add("IUserQuestMission")
|
add("IUserQuestMission")
|
||||||
@@ -266,6 +268,9 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
if !mapsEqualStruct(before.TowerAccumulationRewards, after.TowerAccumulationRewards) {
|
if !mapsEqualStruct(before.TowerAccumulationRewards, after.TowerAccumulationRewards) {
|
||||||
add("IUserEventQuestTowerAccumulationReward")
|
add("IUserEventQuestTowerAccumulationReward")
|
||||||
}
|
}
|
||||||
|
if !before.QuestAutoOrbit.Equal(after.QuestAutoOrbit) {
|
||||||
|
add("IUserQuestAutoOrbit")
|
||||||
|
}
|
||||||
if !mapsEqualStruct(before.LabyrinthStages, after.LabyrinthStages) {
|
if !mapsEqualStruct(before.LabyrinthStages, after.LabyrinthStages) {
|
||||||
add("IUserEventQuestLabyrinthStage")
|
add("IUserEventQuestLabyrinthStage")
|
||||||
}
|
}
|
||||||
@@ -474,6 +479,8 @@ func keyFieldsForTable(table string) []string {
|
|||||||
return []string{"userId", "bigHuntWeeklyVersion"}
|
return []string{"userId", "bigHuntWeeklyVersion"}
|
||||||
case "IUserDeckTypeNote":
|
case "IUserDeckTypeNote":
|
||||||
return []string{"userId", "deckType"}
|
return []string{"userId", "deckType"}
|
||||||
|
case "IUserQuestAutoOrbit":
|
||||||
|
return []string{"userId"}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package userdata
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/model"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
"lunar-tear/server/internal/utils"
|
"lunar-tear/server/internal/utils"
|
||||||
)
|
)
|
||||||
@@ -13,13 +14,30 @@ func sortedQuestRecords(user store.UserState) []map[string]any {
|
|||||||
ids = append(ids, int(id))
|
ids = append(ids, int(id))
|
||||||
}
|
}
|
||||||
sort.Ints(ids)
|
sort.Ints(ids)
|
||||||
|
|
||||||
|
var replayQuestId int32
|
||||||
|
if user.MainQuest.SavedContext.Active && questHandler != nil {
|
||||||
|
if scene, ok := questHandler.SceneById[user.MainQuest.ProgressQuestSceneId]; ok {
|
||||||
|
replayQuestId = scene.QuestId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
records := make([]map[string]any, 0, len(ids))
|
records := make([]map[string]any, 0, len(ids))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
row := user.Quests[int32(id)]
|
row := user.Quests[int32(id)]
|
||||||
|
stateType := row.QuestStateType
|
||||||
|
if replayQuestId != 0 {
|
||||||
|
switch {
|
||||||
|
case int32(id) == replayQuestId:
|
||||||
|
stateType = model.UserQuestStateTypeActive
|
||||||
|
case stateType == model.UserQuestStateTypeActive:
|
||||||
|
stateType = model.UserQuestStateTypeCleared
|
||||||
|
}
|
||||||
|
}
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"questId": row.QuestId,
|
"questId": row.QuestId,
|
||||||
"questStateType": row.QuestStateType,
|
"questStateType": stateType,
|
||||||
"isBattleOnly": row.IsBattleOnly,
|
"isBattleOnly": row.IsBattleOnly,
|
||||||
"latestStartDatetime": row.LatestStartDatetime,
|
"latestStartDatetime": row.LatestStartDatetime,
|
||||||
"clearCount": row.ClearCount,
|
"clearCount": row.ClearCount,
|
||||||
@@ -116,38 +134,29 @@ func init() {
|
|||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
register("IUserMainQuestSeasonRoute", func(user store.UserState) string {
|
register("IUserMainQuestSeasonRoute", func(user store.UserState) string {
|
||||||
if len(user.MainQuestSeasonRoutes) == 0 {
|
if questHandler == nil {
|
||||||
// Fallback to current (season, route) for legacy saves with no history.
|
return "[]"
|
||||||
s, _ := utils.EncodeJSONMaps(map[string]any{
|
|
||||||
"userId": user.UserId,
|
|
||||||
"mainQuestSeasonId": user.MainQuest.MainQuestSeasonId,
|
|
||||||
"mainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId,
|
|
||||||
"latestVersion": user.MainQuest.LatestVersion,
|
|
||||||
})
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
keys := make([]store.SeasonRouteKey, 0, len(user.MainQuestSeasonRoutes))
|
pairs := questHandler.SeasonRoutesFor(&user)
|
||||||
for k := range user.MainQuestSeasonRoutes {
|
if len(pairs) == 0 {
|
||||||
keys = append(keys, k)
|
return "[]"
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
seasons := make([]int32, 0, len(pairs))
|
||||||
if keys[i].MainQuestSeasonId != keys[j].MainQuestSeasonId {
|
for s := range pairs {
|
||||||
return keys[i].MainQuestSeasonId < keys[j].MainQuestSeasonId
|
seasons = append(seasons, s)
|
||||||
}
|
}
|
||||||
return keys[i].MainQuestRouteId < keys[j].MainQuestRouteId
|
sort.Slice(seasons, func(i, j int) bool { return seasons[i] < seasons[j] })
|
||||||
})
|
records := make([]map[string]any, 0, len(seasons))
|
||||||
records := make([]map[string]any, 0, len(keys))
|
for _, s := range seasons {
|
||||||
for _, k := range keys {
|
|
||||||
e := user.MainQuestSeasonRoutes[k]
|
|
||||||
records = append(records, map[string]any{
|
records = append(records, map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"mainQuestSeasonId": e.MainQuestSeasonId,
|
"mainQuestSeasonId": s,
|
||||||
"mainQuestRouteId": e.MainQuestRouteId,
|
"mainQuestRouteId": pairs[s],
|
||||||
"latestVersion": e.LatestVersion,
|
"latestVersion": user.MainQuest.LatestVersion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
s, _ := utils.EncodeJSONMaps(records...)
|
out, _ := utils.EncodeJSONMaps(records...)
|
||||||
return s
|
return out
|
||||||
})
|
})
|
||||||
register("IUserEventQuestProgressStatus", func(user store.UserState) string {
|
register("IUserEventQuestProgressStatus", func(user store.UserState) string {
|
||||||
s, _ := utils.EncodeJSONMaps(map[string]any{
|
s, _ := utils.EncodeJSONMaps(map[string]any{
|
||||||
@@ -259,10 +268,26 @@ func init() {
|
|||||||
s, _ := utils.EncodeJSONMaps(records...)
|
s, _ := utils.EncodeJSONMaps(records...)
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
register("IUserQuestAutoOrbit", func(user store.UserState) string {
|
||||||
|
s := user.QuestAutoOrbit
|
||||||
|
if s.MaxAutoOrbitCount <= 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
out, _ := utils.EncodeJSONMaps(map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"questType": s.QuestType,
|
||||||
|
"chapterId": s.ChapterId,
|
||||||
|
"questId": s.QuestId,
|
||||||
|
"maxAutoOrbitCount": s.MaxAutoOrbitCount,
|
||||||
|
"clearedAutoOrbitCount": s.ClearedAutoOrbitCount,
|
||||||
|
"lastClearDatetime": s.LastClearDatetime,
|
||||||
|
"latestVersion": s.LatestVersion,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
})
|
||||||
registerStatic(
|
registerStatic(
|
||||||
"IUserEventQuestDailyGroupCompleteReward",
|
"IUserEventQuestDailyGroupCompleteReward",
|
||||||
"IUserQuestReplayFlowRewardGroup",
|
"IUserQuestReplayFlowRewardGroup",
|
||||||
"IUserQuestAutoOrbit",
|
|
||||||
"IUserQuestSceneChoice",
|
"IUserQuestSceneChoice",
|
||||||
"IUserQuestSceneChoiceHistory",
|
"IUserQuestSceneChoiceHistory",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package userdata
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"lunar-tear/server/internal/questflow"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,6 +11,12 @@ type Projector func(user store.UserState) string
|
|||||||
|
|
||||||
var projectors = make(map[string]Projector)
|
var projectors = make(map[string]Projector)
|
||||||
|
|
||||||
|
var questHandler *questflow.QuestHandler
|
||||||
|
|
||||||
|
func SetQuestHandler(h *questflow.QuestHandler) {
|
||||||
|
questHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
func register(tableName string, fn Projector) {
|
func register(tableName string, fn Projector) {
|
||||||
projectors[tableName] = fn
|
projectors[tableName] = fn
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE user_quest_auto_orbit (
|
||||||
|
user_id INTEGER NOT NULL PRIMARY KEY REFERENCES users(user_id),
|
||||||
|
quest_type INTEGER NOT NULL DEFAULT 0,
|
||||||
|
chapter_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
quest_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
max_auto_orbit_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
cleared_auto_orbit_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_clear_datetime INTEGER NOT NULL DEFAULT 0,
|
||||||
|
latest_version INTEGER NOT NULL DEFAULT 0,
|
||||||
|
accumulated_drops_json TEXT NOT NULL DEFAULT '[]'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO user_quest_auto_orbit (user_id) SELECT user_id FROM users;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE IF EXISTS user_quest_auto_orbit;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var FS embed.FS
|
||||||
|
|
||||||
|
func Up(ctx context.Context, db *sql.DB) error {
|
||||||
|
goose.SetBaseFS(FS)
|
||||||
|
goose.SetLogger(goose.NopLogger())
|
||||||
|
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return goose.UpContext(ctx, db, ".", goose.WithAllowMissing())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user