@musashishao/agent-kit 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/agents/ai-asset-factory.md +700 -0
- package/.agent/agents/ai-audio-factory.md +503 -0
- package/.agent/agents/game-developer.md +4 -4
- package/.agent/agents/orchestrator.md +113 -3
- package/.agent/agents/project-planner.md +67 -0
- package/.agent/agents/unity-mobile-master.md +949 -0
- package/.agent/mcp/config/registry.json +65 -51
- package/.agent/mcp/servers/notebooklm/README.md +114 -0
- package/.agent/mcp/servers/notebooklm/package.json +35 -0
- package/.agent/mcp/servers/notebooklm/src/auth/chrome.ts +225 -0
- package/.agent/mcp/servers/notebooklm/src/auth/index.ts +1 -0
- package/.agent/mcp/servers/notebooklm/src/index.ts +516 -0
- package/.agent/mcp/servers/notebooklm/src/services/index.ts +3 -0
- package/.agent/mcp/servers/notebooklm/src/services/library.ts +217 -0
- package/.agent/mcp/servers/notebooklm/src/services/notebooklm.ts +380 -0
- package/.agent/mcp/servers/notebooklm/tsconfig.json +15 -0
- package/.agent/mcp-gateway/README.md +169 -20
- package/.agent/mcp-gateway/package.json +22 -7
- package/.agent/mcp-gateway/src/auth/index.ts +55 -0
- package/.agent/mcp-gateway/src/auth/middleware.ts +242 -0
- package/.agent/mcp-gateway/src/auth/oauth.ts +462 -0
- package/.agent/mcp-gateway/src/auth/scopes.ts +227 -0
- package/.agent/mcp-gateway/src/index.ts +252 -105
- package/.agent/mcp-gateway/src/observability/index.ts +5 -0
- package/.agent/mcp-gateway/src/observability/otel.ts +405 -0
- package/.agent/mcp-gateway/src/transports/index.ts +5 -0
- package/.agent/mcp-gateway/src/transports/streamableHttp.ts +235 -0
- package/.agent/rules/CODEX.md +89 -0
- package/.agent/rules/CODE_RULES.md +73 -0
- package/.agent/rules/GEMINI.md +25 -0
- package/.agent/rules/MEMORY_STATE.md +110 -0
- package/.agent/rules/REFERENCE.md +33 -141
- package/.agent/rules/REF_SKILLS.md +116 -0
- package/.agent/rules/REF_WORKFLOWS.md +81 -0
- package/.agent/scripts/ak_cli.py +106 -5
- package/.agent/scripts/memory_manager.py +48 -9
- package/.agent/skills/anti-hallucination/SKILL.md +295 -0
- package/.agent/skills/anti-hallucination/scripts/check_hallucination.py +299 -0
- package/.agent/skills/bifurcation-analysis/SKILL.md +56 -0
- package/.agent/skills/brainstorming/SKILL.md +80 -6
- package/.agent/skills/decision-memory/SKILL.md +317 -0
- package/.agent/skills/emergence-detector/SKILL.md +230 -0
- package/.agent/skills/emergence-detector/scripts/check_emergence.py +265 -0
- package/.agent/skills/explained-qa/SKILL.md +142 -0
- package/.agent/skills/explained-qa/game-terminology.md +214 -0
- package/.agent/skills/game-development/ai-dialogue-engine/SKILL.md +442 -0
- package/.agent/skills/game-development/ai-graphics-generator/SKILL.md +463 -0
- package/.agent/skills/game-development/ai-playtest-framework/SKILL.md +570 -0
- package/.agent/skills/game-development/camera-systems/SKILL.md +607 -0
- package/.agent/skills/game-development/card-battle-engine/SKILL.md +618 -0
- package/.agent/skills/game-development/character-controller-3d/SKILL.md +908 -0
- package/.agent/skills/game-development/cloud-save-sync/SKILL.md +527 -0
- package/.agent/skills/game-development/combat-system/SKILL.md +748 -0
- package/.agent/skills/game-development/compliance-rating/SKILL.md +277 -0
- package/.agent/skills/game-development/crossplatform-build/SKILL.md +386 -0
- package/.agent/skills/game-development/cultivation-progression/SKILL.md +520 -0
- package/.agent/skills/game-development/data-driven-balance/SKILL.md +535 -0
- package/.agent/skills/game-development/game-analytics-integrator/SKILL.md +410 -0
- package/.agent/skills/game-development/game-audio-advanced/SKILL.md +646 -0
- package/.agent/skills/game-development/game-economy-designer/SKILL.md +375 -0
- package/.agent/skills/game-development/game-marketing/SKILL.md +85 -0
- package/.agent/skills/game-development/game-state-manager/SKILL.md +883 -0
- package/.agent/skills/game-development/hybrid-game-spec/SKILL.md +220 -0
- package/.agent/skills/game-development/inventory-quest/SKILL.md +747 -0
- package/.agent/skills/game-development/liveops/SKILL.md +308 -0
- package/.agent/skills/game-development/localization/SKILL.md +286 -0
- package/.agent/skills/game-development/mobile-input-patterns/SKILL.md +343 -0
- package/.agent/skills/game-development/monetization-strategy/SKILL.md +94 -0
- package/.agent/skills/game-development/multiplayer-master/SKILL.md +727 -0
- package/.agent/skills/game-development/narrative-branching/SKILL.md +593 -0
- package/.agent/skills/game-development/procedural-level-ai/SKILL.md +367 -0
- package/.agent/skills/game-development/prototyping-rapid/SKILL.md +205 -0
- package/.agent/skills/game-development/spec-ecosystem/SKILL.md +155 -0
- package/.agent/skills/game-development/spec-ecosystem/decision-log-format.md +129 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/PLAN-template.md +178 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/SPEC-template.md +110 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/TASKS-template.md +156 -0
- package/.agent/skills/game-development/survival-systems/SKILL.md +493 -0
- package/.agent/skills/game-development/testing-qa/SKILL.md +270 -0
- package/.agent/skills/game-development/unity-mobile-optimization/SKILL.md +271 -0
- package/.agent/skills/intent-capture/SKILL.md +65 -0
- package/.agent/skills/mcp-composition/SKILL.md +362 -0
- package/.agent/skills/mcp-observability/SKILL.md +323 -0
- package/.agent/skills/mcp-security/SKILL.md +314 -0
- package/.agent/skills/trust-spectrum/SKILL.md +291 -0
- package/.agent/skills/vibe-coding-guard/SKILL.md +328 -0
- package/.agent/templates/AGENTS.game.md +63 -0
- package/.agent/templates/docs/WORKFLOW_GUIDE.en.md +100 -0
- package/.agent/templates/docs/WORKFLOW_GUIDE.vi.md +100 -0
- package/.agent/workflows/ai-agent.md +2 -0
- package/.agent/workflows/autofix.md +1 -0
- package/.agent/workflows/brainstorm.md +1 -0
- package/.agent/workflows/context.md +1 -0
- package/.agent/workflows/create.md +39 -8
- package/.agent/workflows/dashboard.md +1 -0
- package/.agent/workflows/debug.md +14 -0
- package/.agent/workflows/deploy.md +14 -0
- package/.agent/workflows/enhance.md +44 -0
- package/.agent/workflows/gamekit-init.md +177 -0
- package/.agent/workflows/gamekit-launch.md +338 -0
- package/.agent/workflows/gamekit-plan.md +204 -0
- package/.agent/workflows/gamekit-qa.md +153 -0
- package/.agent/workflows/gamekit-spec.md +243 -0
- package/.agent/workflows/gamekit-tasks.md +208 -0
- package/.agent/workflows/marketing.md +2 -0
- package/.agent/workflows/next.md +1 -0
- package/.agent/workflows/orchestrate.md +12 -0
- package/.agent/workflows/pentest.md +2 -0
- package/.agent/workflows/plan.md +42 -0
- package/.agent/workflows/preview.md +1 -0
- package/.agent/workflows/quality.md +1 -0
- package/.agent/workflows/saas.md +2 -0
- package/.agent/workflows/spec.md +42 -0
- package/.agent/workflows/status.md +1 -0
- package/.agent/workflows/test.md +14 -0
- package/.agent/workflows/ui-ux-pro-max.md +1 -0
- package/bin/cli.js +411 -111
- package/package.json +1 -2
- package/.agent/agents/game-asset-curator.md +0 -317
- package/.agent/agents/game-narrative-designer.md +0 -310
- package/.agent/agents/game-qa-agent.md +0 -441
- package/.agent/workflows/game-prototype.md +0 -154
- package/docs/AI_DATA_INFRASTRUCTURE.md +0 -288
- package/docs/CHANGELOG_AI_INFRA.md +0 -141
- package/docs/MIGRATION_GUIDE_V1.9.md +0 -55
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-driven-balance
|
|
3
|
+
description: Data-driven game balancing systems. External configuration files, hot-patching values, A/B testing for game balance, spreadsheet imports, and live tuning for Unity.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Data-Driven Balance Skill
|
|
7
|
+
|
|
8
|
+
> **Purpose**: Enable rapid iteration and live balancing without app updates.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
- Setting up external config files
|
|
12
|
+
- Implementing hot-patching for balance
|
|
13
|
+
- Running A/B tests for game values
|
|
14
|
+
- Importing from spreadsheets
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. External Configuration System
|
|
19
|
+
|
|
20
|
+
### JSON Config Pattern
|
|
21
|
+
```csharp
|
|
22
|
+
[System.Serializable]
|
|
23
|
+
public class GameBalanceConfig
|
|
24
|
+
{
|
|
25
|
+
public PlayerConfig player;
|
|
26
|
+
public EnemyConfig[] enemies;
|
|
27
|
+
public ItemConfig[] items;
|
|
28
|
+
public EconomyConfig economy;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[System.Serializable]
|
|
32
|
+
public class PlayerConfig
|
|
33
|
+
{
|
|
34
|
+
public float baseHealth = 100f;
|
|
35
|
+
public float baseAttack = 10f;
|
|
36
|
+
public float baseMoveSpeed = 5f;
|
|
37
|
+
public float healthPerLevel = 10f;
|
|
38
|
+
public float attackPerLevel = 2f;
|
|
39
|
+
|
|
40
|
+
// Cultivation specific
|
|
41
|
+
public float[] qiRequirementsPerRealm;
|
|
42
|
+
public float breakthroughBaseChance = 0.5f;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[System.Serializable]
|
|
46
|
+
public class EnemyConfig
|
|
47
|
+
{
|
|
48
|
+
public string enemyId;
|
|
49
|
+
public float health;
|
|
50
|
+
public float attack;
|
|
51
|
+
public float expReward;
|
|
52
|
+
public float goldDrop;
|
|
53
|
+
public float[] dropRates;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[System.Serializable]
|
|
57
|
+
public class EconomyConfig
|
|
58
|
+
{
|
|
59
|
+
public int startingGold = 100;
|
|
60
|
+
public float goldMultiplier = 1f;
|
|
61
|
+
public float expMultiplier = 1f;
|
|
62
|
+
public int dailyRewardBase = 50;
|
|
63
|
+
public float[] dailyRewardStreak; // Day 1, 2, 3... bonuses
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Config Loader
|
|
68
|
+
```csharp
|
|
69
|
+
public class ConfigManager : MonoBehaviour
|
|
70
|
+
{
|
|
71
|
+
public static ConfigManager Instance { get; private set; }
|
|
72
|
+
|
|
73
|
+
private GameBalanceConfig _config;
|
|
74
|
+
private string _configVersion;
|
|
75
|
+
|
|
76
|
+
public GameBalanceConfig Config => _config;
|
|
77
|
+
|
|
78
|
+
public event Action<GameBalanceConfig> OnConfigLoaded;
|
|
79
|
+
public event Action<GameBalanceConfig> OnConfigUpdated;
|
|
80
|
+
|
|
81
|
+
private void Awake()
|
|
82
|
+
{
|
|
83
|
+
Instance = this;
|
|
84
|
+
LoadLocalConfig();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private void Start()
|
|
88
|
+
{
|
|
89
|
+
// Fetch remote on start
|
|
90
|
+
StartCoroutine(FetchRemoteConfig());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private void LoadLocalConfig()
|
|
94
|
+
{
|
|
95
|
+
// Load bundled default
|
|
96
|
+
TextAsset defaultConfig = Resources.Load<TextAsset>("default_config");
|
|
97
|
+
_config = JsonUtility.FromJson<GameBalanceConfig>(defaultConfig.text);
|
|
98
|
+
|
|
99
|
+
// Check for cached remote
|
|
100
|
+
string cached = PlayerPrefs.GetString("cached_config", "");
|
|
101
|
+
if (!string.IsNullOrEmpty(cached))
|
|
102
|
+
{
|
|
103
|
+
_config = JsonUtility.FromJson<GameBalanceConfig>(cached);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
OnConfigLoaded?.Invoke(_config);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private IEnumerator FetchRemoteConfig()
|
|
110
|
+
{
|
|
111
|
+
string url = "https://your-cdn.com/config/balance.json";
|
|
112
|
+
|
|
113
|
+
using UnityWebRequest request = UnityWebRequest.Get(url);
|
|
114
|
+
yield return request.SendWebRequest();
|
|
115
|
+
|
|
116
|
+
if (request.result == UnityWebRequest.Result.Success)
|
|
117
|
+
{
|
|
118
|
+
string json = request.downloadHandler.text;
|
|
119
|
+
var newConfig = JsonUtility.FromJson<GameBalanceConfig>(json);
|
|
120
|
+
|
|
121
|
+
// Cache locally
|
|
122
|
+
PlayerPrefs.SetString("cached_config", json);
|
|
123
|
+
|
|
124
|
+
// Apply if changed
|
|
125
|
+
if (HasConfigChanged(newConfig))
|
|
126
|
+
{
|
|
127
|
+
_config = newConfig;
|
|
128
|
+
OnConfigUpdated?.Invoke(_config);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public T GetValue<T>(string path)
|
|
134
|
+
{
|
|
135
|
+
// Access nested values via path like "player.baseHealth"
|
|
136
|
+
return ReflectionHelper.GetNestedValue<T>(_config, path);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 2. Firebase Remote Config
|
|
144
|
+
|
|
145
|
+
### Firebase Integration
|
|
146
|
+
```csharp
|
|
147
|
+
public class FirebaseConfigManager : MonoBehaviour
|
|
148
|
+
{
|
|
149
|
+
private FirebaseRemoteConfig _remoteConfig;
|
|
150
|
+
|
|
151
|
+
public async Task Initialize()
|
|
152
|
+
{
|
|
153
|
+
await FirebaseApp.CheckAndFixDependenciesAsync();
|
|
154
|
+
|
|
155
|
+
_remoteConfig = FirebaseRemoteConfig.DefaultInstance;
|
|
156
|
+
|
|
157
|
+
// Set defaults
|
|
158
|
+
var defaults = new Dictionary<string, object>
|
|
159
|
+
{
|
|
160
|
+
{ "player_base_health", 100 },
|
|
161
|
+
{ "player_base_attack", 10 },
|
|
162
|
+
{ "exp_multiplier", 1.0 },
|
|
163
|
+
{ "gold_multiplier", 1.0 },
|
|
164
|
+
{ "daily_reward_base", 50 },
|
|
165
|
+
{ "feature_flag_new_mode", false }
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await _remoteConfig.SetDefaultsAsync(defaults);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public async Task FetchAndActivate()
|
|
172
|
+
{
|
|
173
|
+
// Fetch with cache expiration
|
|
174
|
+
await _remoteConfig.FetchAsync(TimeSpan.FromHours(1));
|
|
175
|
+
await _remoteConfig.ActivateAsync();
|
|
176
|
+
|
|
177
|
+
GameEvents.OnRemoteConfigUpdated?.Invoke();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public float GetFloat(string key)
|
|
181
|
+
{
|
|
182
|
+
return (float)_remoteConfig.GetValue(key).DoubleValue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public int GetInt(string key)
|
|
186
|
+
{
|
|
187
|
+
return (int)_remoteConfig.GetValue(key).LongValue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public bool GetBool(string key)
|
|
191
|
+
{
|
|
192
|
+
return _remoteConfig.GetValue(key).BooleanValue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public string GetString(string key)
|
|
196
|
+
{
|
|
197
|
+
return _remoteConfig.GetValue(key).StringValue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public T GetJson<T>(string key)
|
|
201
|
+
{
|
|
202
|
+
string json = GetString(key);
|
|
203
|
+
return JsonUtility.FromJson<T>(json);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 3. A/B Testing for Balance
|
|
211
|
+
|
|
212
|
+
### Experiment System
|
|
213
|
+
```csharp
|
|
214
|
+
[System.Serializable]
|
|
215
|
+
public class BalanceExperiment
|
|
216
|
+
{
|
|
217
|
+
public string experimentId;
|
|
218
|
+
public string experimentName;
|
|
219
|
+
public bool isActive;
|
|
220
|
+
|
|
221
|
+
public ExperimentVariant[] variants;
|
|
222
|
+
public string[] targetUserGroups; // "new_users", "paying_users"
|
|
223
|
+
|
|
224
|
+
public float[] variantWeights; // Distribution
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
[System.Serializable]
|
|
228
|
+
public class ExperimentVariant
|
|
229
|
+
{
|
|
230
|
+
public string variantId;
|
|
231
|
+
public string variantName;
|
|
232
|
+
|
|
233
|
+
public BalanceOverride[] overrides;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
[System.Serializable]
|
|
237
|
+
public class BalanceOverride
|
|
238
|
+
{
|
|
239
|
+
public string configPath;
|
|
240
|
+
public string valueJson;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public class ABTestManager : MonoBehaviour
|
|
244
|
+
{
|
|
245
|
+
private Dictionary<string, string> _userAssignments = new();
|
|
246
|
+
|
|
247
|
+
public event Action<string, string> OnExperimentAssigned;
|
|
248
|
+
|
|
249
|
+
public void AssignToExperiment(BalanceExperiment experiment)
|
|
250
|
+
{
|
|
251
|
+
if (_userAssignments.ContainsKey(experiment.experimentId))
|
|
252
|
+
return; // Already assigned
|
|
253
|
+
|
|
254
|
+
// Consistent assignment based on user ID
|
|
255
|
+
int hash = GetUserHash(experiment.experimentId);
|
|
256
|
+
float roll = (hash % 1000) / 1000f;
|
|
257
|
+
|
|
258
|
+
float cumulative = 0f;
|
|
259
|
+
for (int i = 0; i < experiment.variants.Length; i++)
|
|
260
|
+
{
|
|
261
|
+
cumulative += experiment.variantWeights[i];
|
|
262
|
+
if (roll < cumulative)
|
|
263
|
+
{
|
|
264
|
+
string variantId = experiment.variants[i].variantId;
|
|
265
|
+
_userAssignments[experiment.experimentId] = variantId;
|
|
266
|
+
|
|
267
|
+
ApplyVariant(experiment.variants[i]);
|
|
268
|
+
|
|
269
|
+
// Log for analytics
|
|
270
|
+
Analytics.LogEvent("experiment_assigned", new Dictionary<string, object>
|
|
271
|
+
{
|
|
272
|
+
{ "experiment_id", experiment.experimentId },
|
|
273
|
+
{ "variant_id", variantId }
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
OnExperimentAssigned?.Invoke(experiment.experimentId, variantId);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private void ApplyVariant(ExperimentVariant variant)
|
|
283
|
+
{
|
|
284
|
+
foreach (var override_ in variant.overrides)
|
|
285
|
+
{
|
|
286
|
+
ConfigManager.Instance.SetOverride(override_.configPath, override_.valueJson);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public string GetAssignedVariant(string experimentId)
|
|
291
|
+
{
|
|
292
|
+
return _userAssignments.TryGetValue(experimentId, out var variant) ? variant : null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 4. Spreadsheet Import
|
|
300
|
+
|
|
301
|
+
### Google Sheets Integration
|
|
302
|
+
```csharp
|
|
303
|
+
public class SpreadsheetImporter
|
|
304
|
+
{
|
|
305
|
+
private const string SHEET_ID = "your-google-sheet-id";
|
|
306
|
+
private const string API_KEY = "your-api-key";
|
|
307
|
+
|
|
308
|
+
public async Task<List<EnemyConfig>> ImportEnemies(string sheetName)
|
|
309
|
+
{
|
|
310
|
+
string url = $"https://sheets.googleapis.com/v4/spreadsheets/{SHEET_ID}/values/{sheetName}?key={API_KEY}";
|
|
311
|
+
|
|
312
|
+
using var client = new HttpClient();
|
|
313
|
+
string json = await client.GetStringAsync(url);
|
|
314
|
+
|
|
315
|
+
var response = JsonConvert.DeserializeObject<SheetsResponse>(json);
|
|
316
|
+
|
|
317
|
+
var enemies = new List<EnemyConfig>();
|
|
318
|
+
var headers = response.values[0]; // First row = headers
|
|
319
|
+
|
|
320
|
+
for (int i = 1; i < response.values.Count; i++)
|
|
321
|
+
{
|
|
322
|
+
var row = response.values[i];
|
|
323
|
+
|
|
324
|
+
enemies.Add(new EnemyConfig
|
|
325
|
+
{
|
|
326
|
+
enemyId = row[0],
|
|
327
|
+
health = float.Parse(row[1]),
|
|
328
|
+
attack = float.Parse(row[2]),
|
|
329
|
+
expReward = float.Parse(row[3]),
|
|
330
|
+
goldDrop = float.Parse(row[4])
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return enemies;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#if UNITY_EDITOR
|
|
339
|
+
public class SpreadsheetImportEditor : EditorWindow
|
|
340
|
+
{
|
|
341
|
+
[MenuItem("Tools/Import Balance from Sheets")]
|
|
342
|
+
public static async void ImportFromSheets()
|
|
343
|
+
{
|
|
344
|
+
var importer = new SpreadsheetImporter();
|
|
345
|
+
|
|
346
|
+
// Import enemies
|
|
347
|
+
var enemies = await importer.ImportEnemies("Enemies");
|
|
348
|
+
SaveToScriptableObjects(enemies);
|
|
349
|
+
|
|
350
|
+
Debug.Log($"Imported {enemies.Count} enemies from spreadsheet");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
#endif
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### CSV Import for Offline Use
|
|
357
|
+
```csharp
|
|
358
|
+
public class CSVImporter
|
|
359
|
+
{
|
|
360
|
+
public static List<T> Import<T>(string csvPath) where T : new()
|
|
361
|
+
{
|
|
362
|
+
var results = new List<T>();
|
|
363
|
+
var lines = File.ReadAllLines(csvPath);
|
|
364
|
+
|
|
365
|
+
if (lines.Length < 2) return results;
|
|
366
|
+
|
|
367
|
+
var headers = ParseCSVLine(lines[0]);
|
|
368
|
+
var type = typeof(T);
|
|
369
|
+
|
|
370
|
+
for (int i = 1; i < lines.Length; i++)
|
|
371
|
+
{
|
|
372
|
+
var values = ParseCSVLine(lines[i]);
|
|
373
|
+
var item = new T();
|
|
374
|
+
|
|
375
|
+
for (int j = 0; j < headers.Length && j < values.Length; j++)
|
|
376
|
+
{
|
|
377
|
+
var field = type.GetField(headers[j]);
|
|
378
|
+
if (field != null)
|
|
379
|
+
{
|
|
380
|
+
object value = Convert.ChangeType(values[j], field.FieldType);
|
|
381
|
+
field.SetValue(item, value);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
results.Add(item);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return results;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private static string[] ParseCSVLine(string line)
|
|
392
|
+
{
|
|
393
|
+
// Handle quoted values with commas
|
|
394
|
+
var regex = new Regex(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
|
|
395
|
+
return regex.Split(line).Select(s => s.Trim('"')).ToArray();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 5. Live Tuning Dashboard
|
|
403
|
+
|
|
404
|
+
### In-Game Debug Panel
|
|
405
|
+
```csharp
|
|
406
|
+
public class LiveTuningPanel : MonoBehaviour
|
|
407
|
+
{
|
|
408
|
+
[SerializeField] private GameObject _panelRoot;
|
|
409
|
+
[SerializeField] private Transform _sliderContainer;
|
|
410
|
+
[SerializeField] private GameObject _sliderPrefab;
|
|
411
|
+
|
|
412
|
+
private Dictionary<string, Slider> _sliders = new();
|
|
413
|
+
|
|
414
|
+
private void Start()
|
|
415
|
+
{
|
|
416
|
+
#if DEVELOPMENT_BUILD || UNITY_EDITOR
|
|
417
|
+
CreateTuningSliders();
|
|
418
|
+
#else
|
|
419
|
+
_panelRoot.SetActive(false);
|
|
420
|
+
#endif
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private void CreateTuningSliders()
|
|
424
|
+
{
|
|
425
|
+
CreateSlider("player.baseHealth", 50, 200, 100);
|
|
426
|
+
CreateSlider("player.baseAttack", 5, 50, 10);
|
|
427
|
+
CreateSlider("economy.goldMultiplier", 0.5f, 3f, 1f);
|
|
428
|
+
CreateSlider("economy.expMultiplier", 0.5f, 3f, 1f);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private void CreateSlider(string configPath, float min, float max, float defaultValue)
|
|
432
|
+
{
|
|
433
|
+
var go = Instantiate(_sliderPrefab, _sliderContainer);
|
|
434
|
+
var slider = go.GetComponentInChildren<Slider>();
|
|
435
|
+
var label = go.GetComponentInChildren<TMP_Text>();
|
|
436
|
+
|
|
437
|
+
slider.minValue = min;
|
|
438
|
+
slider.maxValue = max;
|
|
439
|
+
slider.value = ConfigManager.Instance.GetValue<float>(configPath);
|
|
440
|
+
|
|
441
|
+
label.text = $"{configPath}: {slider.value:F1}";
|
|
442
|
+
|
|
443
|
+
slider.onValueChanged.AddListener(value =>
|
|
444
|
+
{
|
|
445
|
+
ConfigManager.Instance.SetOverride(configPath, value);
|
|
446
|
+
label.text = $"{configPath}: {value:F1}";
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
_sliders[configPath] = slider;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
public void ExportCurrentValues()
|
|
453
|
+
{
|
|
454
|
+
var overrides = new Dictionary<string, float>();
|
|
455
|
+
|
|
456
|
+
foreach (var kvp in _sliders)
|
|
457
|
+
{
|
|
458
|
+
overrides[kvp.Key] = kvp.Value.value;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
string json = JsonConvert.SerializeObject(overrides, Formatting.Indented);
|
|
462
|
+
GUIUtility.systemCopyBuffer = json;
|
|
463
|
+
|
|
464
|
+
Debug.Log($"Copied to clipboard:\n{json}");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## 6. Balance Analytics
|
|
472
|
+
|
|
473
|
+
### Track Balance Metrics
|
|
474
|
+
```csharp
|
|
475
|
+
public class BalanceAnalytics : MonoBehaviour
|
|
476
|
+
{
|
|
477
|
+
public void LogCombatResult(CombatResult result)
|
|
478
|
+
{
|
|
479
|
+
Analytics.LogEvent("combat_end", new Dictionary<string, object>
|
|
480
|
+
{
|
|
481
|
+
{ "player_level", result.playerLevel },
|
|
482
|
+
{ "enemy_id", result.enemyId },
|
|
483
|
+
{ "victory", result.playerWon },
|
|
484
|
+
{ "duration_seconds", result.durationSeconds },
|
|
485
|
+
{ "damage_dealt", result.playerDamageDealt },
|
|
486
|
+
{ "damage_taken", result.playerDamageTaken },
|
|
487
|
+
{ "skills_used", result.skillsUsed },
|
|
488
|
+
{ "items_consumed", result.itemsConsumed },
|
|
489
|
+
{ "player_health_percent", result.playerHealthPercent }
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
public void LogEconomyAction(string action, int amount, string source)
|
|
494
|
+
{
|
|
495
|
+
Analytics.LogEvent("economy", new Dictionary<string, object>
|
|
496
|
+
{
|
|
497
|
+
{ "action", action }, // "gain", "spend"
|
|
498
|
+
{ "currency", "gold" },
|
|
499
|
+
{ "amount", amount },
|
|
500
|
+
{ "source", source }, // "quest", "shop", "loot"
|
|
501
|
+
{ "balance_after", PlayerStats.Gold }
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
public void LogProgressionMilestone(string milestone, int attempts, float timeSpentMinutes)
|
|
506
|
+
{
|
|
507
|
+
Analytics.LogEvent("milestone", new Dictionary<string, object>
|
|
508
|
+
{
|
|
509
|
+
{ "milestone", milestone },
|
|
510
|
+
{ "attempts", attempts },
|
|
511
|
+
{ "time_minutes", timeSpentMinutes },
|
|
512
|
+
{ "player_level", PlayerStats.Level }
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Anti-Patterns
|
|
521
|
+
|
|
522
|
+
| ❌ Don't | ✅ Do |
|
|
523
|
+
|----------|-------|
|
|
524
|
+
| Hardcode balance values | External configs |
|
|
525
|
+
| Require app update to balance | Hot-patch via Remote Config |
|
|
526
|
+
| Guess what to tune | Track metrics, A/B test |
|
|
527
|
+
| Manual data entry | Import from spreadsheets |
|
|
528
|
+
| No version control for configs | Git-track config files |
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Related Skills
|
|
533
|
+
- `game-development/liveops` - Live operations
|
|
534
|
+
- `game-development/game-economy-designer` - Economy design
|
|
535
|
+
- `game-development/game-analytics-integrator` - Analytics
|