{
  "_comment": "PGS (PlayCraft Gameplay Schema) 参考骨架 - 狙击射击玩法。Agent 以此为起点编写或 fork 玩法规则。",
  "version": "1.0",
  "gameplayType": "sniper_shot",
  "entities": {
    "scope": {
      "type": "aim-overlay",
      "description": "瞄准镜，覆盖在场景上的瞄准视图",
      "fields": {
        "position": { "type": "vec2", "description": "瞄准镜中心坐标（世界空间）" },
        "driftOffset": { "type": "vec2", "description": "呼吸抖动叠加偏移量，周期性变化" },
        "zoom": { "type": "number", "default": 4, "description": "当前放大倍数（2/4/8）" }
      }
    },
    "targets": {
      "type": "entity-list",
      "description": "场景中的所有目标",
      "item": {
        "id": { "type": "string" },
        "targetType": { "type": "enum", "values": ["enemy", "civilian"], "description": "敌人可击杀，平民误伤扣分" },
        "position": { "type": "vec2" },
        "radius": { "type": "number", "description": "命中判定圆半径（px）" },
        "movementPattern": {
          "type": "enum",
          "values": ["static", "linear", "patrol", "random"],
          "description": "运动模式"
        },
        "movementSpeed": { "type": "number", "default": 0 },
        "patrolPoints": { "type": "vec2[]", "description": "巡逻路径点（patrol 模式）" },
        "state": { "type": "enum", "values": ["alive", "dead"], "default": "alive" }
      }
    },
    "wind": {
      "type": "environmental",
      "description": "风力，影响弹道偏移",
      "fields": {
        "direction": { "type": "number", "description": "风向角度（度，0=右，90=下）" },
        "strength": { "type": "number", "description": "风力强度（0~1），影响弹道偏移量" }
      }
    },
    "ammoCount": {
      "type": "counter",
      "initial": 10,
      "description": "剩余子弹数（由 config 覆盖）"
    },
    "score": {
      "type": "counter",
      "initial": 0,
      "description": "当前得分"
    },
    "friendlyFireCount": {
      "type": "counter",
      "initial": 0,
      "description": "误伤平民次数"
    }
  },
  "rules": {
    "aim": {
      "description": "玩家拖动屏幕，移动瞄准镜中心位置",
      "input": "drag (dx, dy)",
      "effect": "scope.position += (dx, dy)，受边界钳制"
    },
    "breathe_cycle": {
      "description": "瞄准镜随呼吸周期自动漂移，形成正弦曲线抖动",
      "period": "breathCycleMs（默认 3000ms）",
      "amplitude": { "base": 8, "zoomScale": "amplitude / zoom", "description": "放大倍数越高抖动越小" },
      "formula": "driftOffset.x = sin(t/period * 2π) * amplitude; driftOffset.y = cos(t/period * 2π) * amplitude * 0.5"
    },
    "wind_correction": {
      "description": "玩家需手动调整瞄准偏移来补偿风力",
      "trajectoryOffset": "wind.strength * wind.direction_vector * 20px（基础偏移量）",
      "note": "实际命中检测使用 scope.position + wind_trajectory_offset，玩家须预判"
    },
    "shoot": {
      "description": "玩家点击屏幕发射子弹",
      "steps": [
        "1. ammoCount - 1",
        "2. 计算有效瞄准位置 = scope.position + driftOffset + wind_trajectory_offset",
        "3. 检测命中：遍历所有 alive 目标，判断有效瞄准位置是否在目标圆内",
        "4a. 命中 enemy：target.state = dead，score += calculateScore(target)",
        "4b. 命中 civilian：friendlyFireCount++，score -= 200，若 friendlyFireCount >= max 则 gameOver",
        "4c. 未命中：无效果"
      ]
    },
    "target_move": {
      "description": "目标按运动模式移动",
      "linear": "以固定速度向初始方向运动，碰壁反弹",
      "patrol": "在 patrolPoints 之间来回巡逻",
      "random": "每隔 1~3 秒随机改变方向"
    },
    "ammo_depletion": {
      "description": "子弹耗尽时检查胜负条件"
    }
  },
  "algorithm": {
    "description": "主循环：aim → drift → shoot → check",
    "steps": [
      "1. 每帧：applyBreathDrift(deltaMs) 更新 scope.driftOffset",
      "2. 每帧：updateTargets(deltaMs) 按运动模式移动目标",
      "3. 玩家拖动：aimScope(dx, dy) 移动 scope.position",
      "4. 玩家点击射击：shoot()",
      "   4a. ammoCount - 1",
      "   4b. hitDetection(effectiveScopePos)",
      "   4c. 根据命中类型更新 score / friendlyFireCount / target.state",
      "5. 检查胜负：",
      "   - Win: 所有 enemy targets 均为 dead",
      "   - Lose: ammoCount == 0 且仍有 alive enemy，或 friendlyFireCount >= maxFriendlyFireHits"
    ]
  },
  "winCondition": {
    "type": "all_enemies_eliminated",
    "description": "在子弹耗尽前，将所有 enemy 类型目标射杀"
  },
  "loseCondition": {
    "type": "ammo_exhausted_or_friendly_fire",
    "description": "ammoCount == 0 且有 alive enemy，或累计误伤平民超过 maxFriendlyFireHits"
  },
  "testCases": [
    {
      "id": "tc_direct_hit_win",
      "description": "直接命中：瞄准位置正好在唯一敌人圆内，射击后胜利",
      "initialState": {
        "scope": { "position": { "x": 200, "y": 150 }, "driftOffset": { "x": 0, "y": 0 }, "zoom": 4 },
        "targets": [
          { "id": "enemy_1", "targetType": "enemy", "position": { "x": 200, "y": 150 }, "radius": 20, "movementPattern": "static", "state": "alive" }
        ],
        "wind": { "direction": 0, "strength": 0 },
        "ammoCount": 3,
        "score": 0,
        "friendlyFireCount": 0
      },
      "actions": [
        { "type": "shoot" }
      ],
      "expectedState": {
        "targets[0].state": "dead",
        "ammoCount": 2,
        "score": { "min": 100 },
        "gameOver": false,
        "win": true
      }
    },
    {
      "id": "tc_miss_ammo_depleted",
      "description": "连续射失：子弹耗尽仍有存活敌人，游戏失败",
      "initialState": {
        "scope": { "position": { "x": 0, "y": 0 }, "driftOffset": { "x": 0, "y": 0 }, "zoom": 4 },
        "targets": [
          { "id": "enemy_1", "targetType": "enemy", "position": { "x": 300, "y": 300 }, "radius": 20, "movementPattern": "static", "state": "alive" }
        ],
        "wind": { "direction": 0, "strength": 0 },
        "ammoCount": 1,
        "score": 0,
        "friendlyFireCount": 0
      },
      "actions": [
        { "type": "shoot" }
      ],
      "expectedState": {
        "targets[0].state": "alive",
        "ammoCount": 0,
        "win": false,
        "gameOver": true
      }
    },
    {
      "id": "tc_civilian_hit_penalty",
      "description": "误伤平民：命中平民扣分，friendlyFireCount 达上限后游戏失败",
      "initialState": {
        "scope": { "position": { "x": 150, "y": 150 }, "driftOffset": { "x": 0, "y": 0 }, "zoom": 4 },
        "targets": [
          { "id": "civilian_1", "targetType": "civilian", "position": { "x": 150, "y": 150 }, "radius": 20, "movementPattern": "static", "state": "alive" },
          { "id": "enemy_1", "targetType": "enemy", "position": { "x": 400, "y": 400 }, "radius": 20, "movementPattern": "static", "state": "alive" }
        ],
        "wind": { "direction": 0, "strength": 0 },
        "ammoCount": 5,
        "score": 500,
        "friendlyFireCount": 0,
        "config": { "maxFriendlyFireHits": 1 }
      },
      "actions": [
        { "type": "shoot" }
      ],
      "expectedState": {
        "friendlyFireCount": 1,
        "score": 300,
        "win": false,
        "gameOver": true
      }
    },
    {
      "id": "tc_breath_drift_applied",
      "description": "呼吸抖动：经过半个呼吸周期后 driftOffset 不为零",
      "initialState": {
        "scope": { "position": { "x": 200, "y": 200 }, "driftOffset": { "x": 0, "y": 0 }, "zoom": 4 },
        "targets": [],
        "wind": { "direction": 0, "strength": 0 },
        "ammoCount": 5,
        "score": 0,
        "friendlyFireCount": 0,
        "breathTimer": 0
      },
      "actions": [
        { "type": "tick", "deltaMs": 1500 }
      ],
      "expectedState": {
        "scope.driftOffset.x": { "notEqual": 0 }
      }
    }
  ]
}
