Skip to main content

What This Guide Covers

This guide shows a persistent multiplayer game loop:
  • Load player save data on join
  • Keep inventory, quest progress, and level in persistent storage
  • Auto-save while players are connected
  • Award and spend credits with Crafty.economy

Input Actions Used

Configure actions in your project/manifest:
  • quest_complete
  • buy_potion

Complete Game Script (quest_arena.gd)

extends CraftyGame

const MOVE_SPEED := 7.0
const GRAVITY := 20.0
const AUTO_SAVE_INTERVAL := 15.0
const QUEST_XP_REWARD := 25
const QUEST_CREDIT_REWARD := 10
const POTION_COST := 8

var _auto_save_accum := 0.0

func get_prediction_params() -> Dictionary:
    return {"move_speed": MOVE_SPEED, "gravity": GRAVITY}

func _game_init() -> void:
    var spawn_root := get_node_or_null("SpawnPoints")
    if spawn_root:
        for child in spawn_root.get_children():
            if child is Node3D:
                spawn_points.append(child.global_position)

    if spawn_points.is_empty():
        spawn_points = [
            Vector3(-6, 1, -6),
            Vector3(6, 1, -6),
            Vector3(-6, 1, 6),
            Vector3(6, 1, 6)
        ]

func _game_start() -> void:
    Crafty.set_time_limit(1800.0)
    Crafty.send_announcement("Quest Arena started")

func _game_end() -> void:
    for p in get_players():
        _save_player_snapshot(p)
    Crafty.send_announcement("Quest Arena session saved")

func _player_joined(player: CraftyPlayer) -> void:
    player.respawn(get_random_spawn_point())
    _load_player_snapshot(player)
    _sync_player_hud_fields(player)

func _player_left(player: CraftyPlayer) -> void:
    _save_player_snapshot(player)

func _process(delta: float) -> void:
    super._process(delta)
    if not Crafty.is_server():
        return

    for p in get_players():
        apply_default_movement(p, delta, MOVE_SPEED, GRAVITY)
        _handle_player_actions(p)

    _auto_save_accum += delta
    if _auto_save_accum >= AUTO_SAVE_INTERVAL:
        _auto_save_accum = 0.0
        for p in get_players():
            _save_player_snapshot(p)

func _handle_player_actions(player: CraftyPlayer) -> void:
    if player.input.is_action_just_pressed("quest_complete"):
        _complete_quest(player)

    if player.input.is_action_just_pressed("buy_potion"):
        _buy_potion(player)

func _complete_quest(player: CraftyPlayer) -> void:
    var progress := Crafty.data.load(player, "quest_progress")
    if progress == null:
        progress = {"main": 0}

    progress["main"] = int(progress.get("main", 0)) + 1
    Crafty.data.save(player, "quest_progress", progress)

    var xp := int(Crafty.data.load(player, "xp") if Crafty.data.load(player, "xp") != null else 0)
    xp += QUEST_XP_REWARD
    Crafty.data.save(player, "xp", xp)

    var level := 1 + int(xp / 100)
    Crafty.data.save(player, "level", level)

    var awarded := await Crafty.economy.award(player, QUEST_CREDIT_REWARD, "quest_complete")
    if awarded:
        Crafty.send_announcement("%s completed a quest (+%d credits)" % [player.display_name, QUEST_CREDIT_REWARD])

    _sync_player_hud_fields(player)

func _buy_potion(player: CraftyPlayer) -> void:
    var purchased := await Crafty.economy.spend(player, POTION_COST, "potion_purchase")
    if not purchased:
        Crafty.send_announcement("%s cannot afford a potion" % player.display_name)
        return

    var inventory := Crafty.data.load(player, "inventory")
    if inventory == null:
        inventory = {"potions": 0}
    inventory["potions"] = int(inventory.get("potions", 0)) + 1
    Crafty.data.save(player, "inventory", inventory)
    Crafty.send_announcement("%s bought a potion" % player.display_name)
    _sync_player_hud_fields(player)

func _load_player_snapshot(player: CraftyPlayer) -> void:
    var level = Crafty.data.load(player, "level")
    if level == null:
        level = 1

    var xp = Crafty.data.load(player, "xp")
    if xp == null:
        xp = 0

    var inventory = Crafty.data.load(player, "inventory")
    if inventory == null:
        inventory = {"potions": 0}

    var quest_progress = Crafty.data.load(player, "quest_progress")
    if quest_progress == null:
        quest_progress = {"main": 0}

    Crafty.data.save(player, "level", level)
    Crafty.data.save(player, "xp", xp)
    Crafty.data.save(player, "inventory", inventory)
    Crafty.data.save(player, "quest_progress", quest_progress)

func _save_player_snapshot(player: CraftyPlayer) -> void:
    # Data API is write-through to in-memory cache; backend flush is handled by SDK.
    var level = Crafty.data.load(player, "level")
    var xp = Crafty.data.load(player, "xp")
    var inventory = Crafty.data.load(player, "inventory")
    var quest_progress = Crafty.data.load(player, "quest_progress")

    Crafty.data.save(player, "level", 1 if level == null else level)
    Crafty.data.save(player, "xp", 0 if xp == null else xp)
    Crafty.data.save(player, "inventory", {"potions": 0} if inventory == null else inventory)
    Crafty.data.save(player, "quest_progress", {"main": 0} if quest_progress == null else quest_progress)

func _sync_player_hud_fields(player: CraftyPlayer) -> void:
    var level := int(Crafty.data.load(player, "level"))
    var xp := int(Crafty.data.load(player, "xp"))
    var inventory := Crafty.data.load(player, "inventory")
    var quest_progress := Crafty.data.load(player, "quest_progress")
    var balance := await Crafty.economy.get_balance(player)

    player.set_synced("level", level)
    player.set_synced("xp", xp)
    player.set_synced("potions", int(inventory.get("potions", 0)))
    player.set_synced("quest_main", int(quest_progress.get("main", 0)))
    player.set_synced("credits", balance)

Suggested manifest.json

{
  "id": "quest-arena",
  "name": "Quest Arena",
  "version": "1.0.0",
  "crafty_sdk": "1.0",
  "entry_scene": "quest_arena.tscn",
  "player_scene": "player.tscn",
  "min_players": 1,
  "max_players": 12,
  "tick_rate": 60,
  "description": "Persistent quest progression with economy rewards.",
  "tags": ["rpg", "quest", "persistence"],
  "input_actions": {
    "quest_complete": {"keyboard": "Q", "gamepad": "Y"},
    "buy_potion": {"keyboard": "B", "gamepad": "X"}
  }
}