---
id: wiki-2026-0508-skybound-low-level-first-upgrade-offer-balance
title: Skybound Low Level First Upgrade Offer Balance
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [First Upgrade Offer, Low Level Upgrade Pool, Skybound Onboarding Balance]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [skybound, balance, upgrade, onboarding, game-design]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: typescript
  framework: skybound-engine
---

# Skybound Low Level First Upgrade Offer Balance

## 매 한 줄
> **"매 first upgrade offer at low level 은 player retention 의 critical pivot"**. New player 의 첫 choice 가 dopamine arc 를 set — pool composition, rarity weight, anti-streak guard 의 tuning 의 결합이 30-day retention curve 의 ±15% delta 를 만든다. 매 modern roguelite 의 onboarding axiom.

## 매 핵심

### 매 문제 (incident 2026-04-26)
- Low-level first upgrade offer pool 이 너무 wide → new player 가 useless filler 만 받음.
- 결과: D1 retention drop, "didn't understand the game" 의 churn.

### 매 fix axes
- **Curated low-level pool**: level ≤ 3 일 때 power-fantasy 만.
- **Anti-streak guard**: 직전 offered card repeat 방지.
- **Rarity bias**: first offer 의 common-uncommon mix, no rare/legendary (saved for milestone).
- **Telemetry**: per-card pick rate per level bracket.

### 매 응용
1. Skybound first-run experience.
2. Roguelite tutorial pacing.
3. F2P gacha banner curation.

## 💻 패턴

### Curated low-level pool
```typescript
const LOW_LEVEL_POOL: UpgradeId[] = [
  "damage_up", "fire_rate_up", "projectile_count", "magnet_radius", "move_speed",
];
function rollFirstOffer(level: number, history: UpgradeId[]): UpgradeId[] {
  const pool = level <= 3
    ? LOW_LEVEL_POOL
    : ALL_UPGRADES.filter((u) => u.minLevel <= level);
  return sample3WithoutReplacement(pool, history);
}
```

### Anti-streak guard
```typescript
function sample3WithoutReplacement(pool: UpgradeId[], recent: UpgradeId[]): UpgradeId[] {
  const recentSet = new Set(recent.slice(-6));
  const filtered = pool.filter((u) => !recentSet.has(u));
  const candidates = filtered.length >= 3 ? filtered : pool;
  return shuffleAndTake(candidates, 3);
}
```

### Rarity bias by level
```typescript
type Rarity = "common" | "uncommon" | "rare" | "legendary";
const RARITY_WEIGHTS: Record<number, Record<Rarity, number>> = {
  1: { common: 0.85, uncommon: 0.15, rare: 0, legendary: 0 },
  2: { common: 0.70, uncommon: 0.28, rare: 0.02, legendary: 0 },
  5: { common: 0.50, uncommon: 0.35, rare: 0.13, legendary: 0.02 },
};
function rollRarity(level: number, rng: RNG): Rarity {
  const weights = RARITY_WEIGHTS[Math.min(level, 5)] ?? RARITY_WEIGHTS[5];
  return weightedSample(weights, rng);
}
```

### Telemetry hook
```typescript
function recordOfferShown(level: number, options: UpgradeId[], picked: UpgradeId) {
  metrics.increment(`upgrade.offer.shown.lv${level}.${picked}`);
  options.filter((o) => o !== picked).forEach((skipped) =>
    metrics.increment(`upgrade.offer.skipped.lv${level}.${skipped}`),
  );
}
```

### Pity timer (no rare in 10 offers → guarantee)
```typescript
function applyPity(state: OfferState, rolled: Upgrade): Upgrade {
  if (rolled.rarity === "common" || rolled.rarity === "uncommon") {
    state.commonStreak += 1;
    if (state.commonStreak >= 10) {
      state.commonStreak = 0;
      return pickRandom(POOL.filter((u) => u.rarity === "rare"));
    }
  } else {
    state.commonStreak = 0;
  }
  return rolled;
}
```

### Reroll currency mechanic
```typescript
function rerollOffer(state: PlayerState, currentOffers: UpgradeId[]): UpgradeId[] {
  if (state.rerollTokens <= 0) throw new Error("no reroll tokens");
  state.rerollTokens -= 1;
  return rollFirstOffer(state.level, [...state.history, ...currentOffers]);
}
```

## 매 결정 기준
| 상황 | Approach |
|---|---|
| Brand new player (D0) | curated pool + no rare/legendary |
| Returning player (level ≥ 5) | full pool + rarity bias |
| Skill-tree heavy game | gate by class/build |
| Short run (< 5 min) | tighter pool, no pity |

**기본값**: level-gated pool + anti-streak + rarity bias by level + per-card telemetry.

## 🔗 Graph
- 부모: [[게임 밸런싱|Game Balance]]
- 응용: [[Skybound Knowledge Hub]]

## 🤖 LLM 활용
**언제**: roguelite / vampire-survivors-like upgrade offer pool tuning 시.
**언제 X**: deterministic build games (Slay the Spire 의 path is curated differently).

## ❌ 안티패턴
- **Uniform pool from level 1**: new player 가 filler 받고 confused.
- **No anti-streak**: 같은 card 3번 연속 → "RNG broken" perception.
- **Legendary at level 1**: peak experience 가 too early → arc collapse.
- **No telemetry**: pick rate 없으면 tuning blind.

## 🧪 검증 / 중복
- Verified (Skybound balance pass 2026-04-26 + Vampire Survivors / Brotato design analysis).
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-04-26 | Original balance note |
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — FULL spec rewrite with patterns + telemetry |
