Initial scaffold: Nakama backend, Godot client, content schema

Sets up the Step 1 (Project Foundation) and Step 2 (Content Schemas) scaffold
from the implementation sequence. Includes Docker Compose stack, Nakama
TypeScript module stubs, PostgreSQL migration, Godot 4 project shell with
scene stubs and NakamaClient singleton, YAML content schemas for all entity
types, and example data for 3 characters, 2 jobs, and all 7 core resources.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoimhinr 2026-06-02 23:33:06 +02:00
commit fb00954954
37 changed files with 2730 additions and 0 deletions

121
content/schema/banner.yaml Normal file
View file

@ -0,0 +1,121 @@
# banner.yaml — schema definition for gacha banner definitions
#
# All rate tables are content-driven. No hardcoded pull rates in server code.
# Banner end dates and pity rules must be visible to players per design spec.
$schema: "xyvera/content-schema/banner/v1"
type: object
required:
- id
- name
- type
- pull_cost
- pool
- pity
properties:
id:
type: string
pattern: "^[a-z][a-z0-9_]*$"
name:
type: string
type:
type: string
enum: [standard, featured, event]
description: >
standard = permanent evergreen with wishlist;
featured = region or character banner with up/down rate;
event = limited mega-event with spark currency.
pull_cost:
type: object
required: [currency, single, multi]
properties:
currency:
type: string
description: "Resource ID used to pull. Usually 'diamond' or a banner-specific ticket."
single:
type: integer
minimum: 1
multi:
type: integer
minimum: 1
description: "Cost for a 10-pull. Typically single * 10 minus one free pull."
pool:
type: array
description: "All characters eligible from this banner."
items:
type: object
required: [character_id, rarity, base_weight]
properties:
character_id:
type: string
rarity:
type: string
enum: [R, SR, SSR, UR]
base_weight:
type: number
minimum: 0.0
description: "Relative weight before pity modifiers. Higher = more common."
is_featured:
type: boolean
default: false
pity:
type: object
required: [soft_pity_start, hard_pity, guaranteed_rarity]
properties:
soft_pity_start:
type: integer
minimum: 1
description: "Pull count at which SSR/UR rate starts increasing toward hard pity."
hard_pity:
type: integer
minimum: 1
description: "Pull count that guarantees at least guaranteed_rarity."
guaranteed_rarity:
type: string
enum: [SR, SSR, UR]
featured_guarantee:
type: object
description: "50/50 featured character guarantee rules."
properties:
rate:
type: number
minimum: 0.0
maximum: 1.0
description: "Chance featured character is selected when SSR hits."
guaranteed_after_loss:
type: boolean
default: true
description: "If false on the 50/50, next SSR is guaranteed featured."
spark_currency:
type: object
description: "Only applies to event-type banners."
properties:
currency_id:
type: string
spark_cost:
type: integer
description: "Amount of spark currency required to select a featured character."
active_from:
type: string
format: date-time
description: "ISO 8601 UTC datetime. Null means active from server start."
active_until:
type: string
format: date-time
description: "ISO 8601 UTC datetime. Null means permanent."
rerun_policy:
type: string
enum: [permanent, will_rerun, no_rerun_planned, collaboration_locked]
default: permanent
description: "Displayed to players per the fair-visibility design pillar."

View file

@ -0,0 +1,165 @@
# character.yaml — schema definition for Xyvera character content files
#
# Every file under content/data/characters/ must conform to this schema.
# Run `tools/validate_content.py` to check all data files against schemas.
#
# Design notes from spec:
# - Rarity: R / SR / SSR / UR (four-tier PoC model; full ladder naming deferred)
# - Combat roles: recon, sabotage, healer, support, tank, glass_cannon
# - Profession roles: miner, lumberjack, researcher, construction, farmer, trainer
# - Male and female presentation variants are art/naming only; mechanics identical
# - No duplicate-dependency model: progression uses universal materials, bonds,
# and assignment mastery
$schema: "xyvera/content-schema/character/v1"
type: object
required:
- id
- name
- rarity
- combat_role
- profession
- base_stats
- skills
- suitability_bonuses
properties:
id:
type: string
description: "Unique character identifier. Snake_case. Shared between male/female variants."
pattern: "^[a-z][a-z0-9_]*$"
example: "lyra_stoneheart"
name:
type: object
description: "Display names for each presentation variant."
required: [female]
properties:
female:
type: string
example: "Lyra Stoneheart"
male:
type: string
example: "Leon Stoneheart"
additionalProperties: false
rarity:
type: string
enum: [R, SR, SSR, UR]
description: "R = common filler, SR = reliable specialist, SSR = premium, UR = rare/prestige"
combat_role:
type: string
enum: [recon, sabotage, healer, support, tank, glass_cannon]
profession:
type: string
enum: [miner, lumberjack, researcher, construction, farmer, trainer]
description: "Primary village assignment profession. Used to determine suitability bonuses."
region_origin:
type: string
description: "World region this character is associated with. Affects banner pools and event tags."
example: "heartwood_vale"
element:
type: string
enum: [fire, water, earth, wind, light, shadow, null]
description: "Optional elemental archetype. 'null' means no element."
default: "null"
personality:
type: string
description: "Short personality descriptor used for relationship flavor and dialogue tags."
example: "stoic_protector"
base_stats:
type: object
description: "Stats at level 1 / ascension 0. Scaling is applied by the server formula."
required: [hp, attack, defense, speed]
properties:
hp:
type: integer
minimum: 1
attack:
type: integer
minimum: 1
defense:
type: integer
minimum: 1
speed:
type: integer
minimum: 1
crit_rate:
type: number
minimum: 0.0
maximum: 1.0
default: 0.05
crit_damage:
type: number
minimum: 1.0
default: 1.5
additionalProperties: false
skills:
type: array
description: "Character skills. First entry is the combat passive; second is the signature active."
minItems: 1
maxItems: 4
items:
type: object
required: [id, name, description, type]
properties:
id:
type: string
pattern: "^[a-z][a-z0-9_]*$"
name:
type: string
description:
type: string
type:
type: string
enum: [passive, active, ultimate]
max_level:
type: integer
minimum: 1
maximum: 10
default: 5
upgrade_cost_per_level:
type: object
description: "Resource costs to advance one skill level. Keys are resource IDs."
additionalProperties:
type: integer
minimum: 0
suitability_bonuses:
type: object
description: >
Multiplier applied to passive output when this character is assigned to a matching
job slot. A character with their primary profession gets 1.5x; secondary gets 1.2x;
unmatched gets 1.0x (no bonus). These values override the defaults.
properties:
primary_profession_multiplier:
type: number
minimum: 1.0
default: 1.5
secondary_professions:
type: array
items:
type: string
enum: [miner, lumberjack, researcher, construction, farmer, trainer]
description: "Additional professions where this character earns the secondary bonus."
additionalProperties: false
bond_hooks:
type: array
description: "Relationship flavor tags used to unlock bond dialogue and minor stat bonuses."
items:
type: string
example: ["loyalty", "mentor", "rival"]
lore_summary:
type: string
description: "One or two sentence background. Minimal story as per spec."

91
content/schema/event.yaml Normal file
View file

@ -0,0 +1,91 @@
# event.yaml — schema definition for live-ops event entries
#
# Events are content-defined, not code-defined. Adding a new event of an
# existing template type requires only a new YAML file, no code changes.
#
# PoC event templates (from spec):
# - resource_push: multiplied resource gain window
# - combat_score: score-attack combat ranking
# - profession_spotlight: boosted output for one profession
$schema: "xyvera/content-schema/event/v1"
type: object
required:
- id
- name
- template
- schedule
- rewards
properties:
id:
type: string
pattern: "^[a-z][a-z0-9_]*$"
name:
type: string
description:
type: string
template:
type: string
enum: [resource_push, combat_score, profession_spotlight]
description: >
Determines which server-side event handler processes this event.
New templates require code; new instances of existing templates do not.
schedule:
type: object
required: [start, end]
properties:
start:
type: string
format: date-time
end:
type: string
format: date-time
timezone_note:
type: string
description: "Optional human-readable timezone note for players."
template_params:
type: object
description: >
Template-specific configuration. Contents depend on the template value.
resource_push: { resource_id, multiplier }
combat_score: { stage_id, score_formula }
profession_spotlight: { profession, bonus_multiplier }
additionalProperties: true
rewards:
type: array
description: "Milestone reward tiers. Players claim each tier once."
items:
type: object
required: [milestone, reward]
properties:
milestone:
type: integer
minimum: 1
description: "Cumulative event score or points required to unlock this reward tier."
reward:
type: object
description: "Map of resource_id to amount granted at this milestone."
additionalProperties:
type: integer
minimum: 1
banner_id:
type: string
description: "Optional linked banner that runs alongside this event."
calendar_visible_from:
type: string
format: date-time
description: >
When this event appears on the player's calendar preview.
Must be earlier than schedule.start. Ensures low-FOMO scheduling
visibility per design spec.

99
content/schema/job.yaml Normal file
View file

@ -0,0 +1,99 @@
# job.yaml — schema definition for village job/assignment slot definitions
#
# Jobs define what characters can be assigned to and what passive resources
# they generate. All tuning values should be content-driven so the economy
# can be rebalanced without code changes.
#
# PoC jobs (from spec): mining, woodcutting, research, construction, farming
$schema: "xyvera/content-schema/job/v1"
type: object
required:
- id
- name
- profession
- output_resource
- base_output_per_hour
- max_slots
properties:
id:
type: string
pattern: "^[a-z][a-z0-9_]*$"
example: "lumber_camp"
name:
type: string
example: "Lumber Camp"
description:
type: string
example: "Workers harvest wood from the forest edge. Essential for construction and upgrades."
profession:
type: string
enum: [miner, lumberjack, researcher, construction, farmer, trainer]
description: "Determines which characters receive the suitability bonus when assigned here."
output_resource:
type: string
description: "Resource ID produced passively by characters in this job slot."
example: "wood"
secondary_output:
type: object
description: "Optional secondary resource produced at a lower rate."
properties:
resource:
type: string
rate_fraction:
type: number
minimum: 0.0
maximum: 1.0
description: "Fraction of base_output_per_hour for the secondary resource."
additionalProperties: false
base_output_per_hour:
type: number
minimum: 0.0
description: >
Base resource units generated per assigned character per hour.
Actual output = base * suitability_multiplier * character_level_factor.
Level factor formula: (level / max_level) ^ 0.5 — tunable in constants.yaml.
max_slots:
type: integer
minimum: 1
maximum: 10
description: "Maximum number of characters that can be simultaneously assigned to this job."
unlock_condition:
type: object
description: "What the player must have done before this job slot is available."
properties:
building_level:
type: integer
minimum: 0
description: "Village building level required to unlock this slot."
prerequisite_job_id:
type: string
description: "Another job that must be unlocked first."
additionalProperties: false
manual_work_eligible:
type: boolean
default: false
description: >
If true, this job can be targeted by the manual micro-management action.
Only one or two jobs should have this enabled in PoC to keep the system focused.
passive_cap_hours:
type: number
minimum: 1.0
default: 8.0
description: >
Hours after which passive accumulation stops until the player collects.
Prevents long absences from breaking the economy. Default is 8 hours
matching the server-side MAX_PASSIVE_HOURS constant.

View file

@ -0,0 +1,96 @@
# resource.yaml — schema definition for resource/currency type definitions
#
# Defines what each resource is, how it behaves, and display metadata.
# The economy starts with seven core resources as specified in the concept doc.
$schema: "xyvera/content-schema/resource/v1"
type: object
required:
- id
- name
- category
- description
properties:
id:
type: string
pattern: "^[a-z][a-z0-9_]*$"
description: "Matches the wallet key and all internal references."
example: "wood"
name:
type: string
example: "Wood"
description:
type: string
example: "Basic construction material harvested from forested areas."
category:
type: string
enum:
- soft_currency # gold, wood, stone — general economy resources
- knowledge # research and skill upgrade material
- stamina # action budget (not in core 7, reserved for future)
- premium # diamond — sparse, monetisation-adjacent
- ascension # character ascension materials
- event # event-specific temporary currencies
- guild # guild and territory tokens
description: "Resource family used to group items in UI and apply economy rules."
is_spendable:
type: boolean
default: true
description: "Can this resource be directly spent by the player? False for passive trackers."
is_capped:
type: boolean
default: false
description: "Whether this resource has a maximum carry limit."
carry_cap:
type: integer
minimum: 1
description: "Maximum held quantity. Only meaningful when is_capped is true."
decay_rate:
type: number
minimum: 0.0
default: 0.0
description: "Fraction lost per hour (0 = no decay). Reserved for future scarcity mechanics."
icon:
type: string
description: "Path to the icon asset relative to godot/assets/."
example: "icons/resources/wood.png"
color_hex:
type: string
pattern: "^#[0-9a-fA-F]{6}$"
description: "UI accent color for this resource."
example: "#8B5E3C"
sources:
type: array
description: >
Informational list of where this resource comes from.
Used to power the inventory help tooltip so players always know
how to get more of it. (Economy Rule 4: inventory confusion is an economy bug.)
items:
type: string
example:
- "Lumber Camp passive output"
- "Manual wood-chopping action"
- "Event shop purchase"
sinks:
type: array
description: "Informational list of what this resource is spent on."
items:
type: string
example:
- "Building upgrades"
- "Character ascension materials"
- "Construction job slot unlocks"