oettaM123 commited on
Commit
8a35e57
·
1 Parent(s): 48b25eb

Add pet enhancer upgrade planning

Browse files
Files changed (3) hide show
  1. app.py +180 -1
  2. static/index.html +75 -10
  3. tests/test_market_optimizer_logic.py +29 -0
app.py CHANGED
@@ -2123,6 +2123,9 @@ class UpgradeLabInput(BaseModel):
2123
  strategy: str = "auto"
2124
  elf_route: Optional[str] = None
2125
  elf_level5_count: int = 0
 
 
 
2126
 
2127
 
2128
  # ─────────────────────────────────────────────────────────────────
@@ -2874,6 +2877,94 @@ def _elf_result_distribution(level5_dist: dict, success_chance=0.8):
2874
  result[successes] = result.get(successes, 0.0) + level5_prob * prob
2875
  return dict(sorted(result.items()))
2876
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2877
  _KNOWN_ELF_ROUTES = {
2878
  ("sword", "Dragon's Poison"): "Dragon's Devil",
2879
  ("shield", "Dragon's Anger"): "Dragon's Soul",
@@ -2955,6 +3046,15 @@ def _upgrade_lab_meta():
2955
  "world": item.get("world", ""),
2956
  "source": item.get("source", ""),
2957
  })
 
 
 
 
 
 
 
 
 
2958
  return {
2959
  "items": items,
2960
  "swords": [
@@ -2965,6 +3065,8 @@ def _upgrade_lab_meta():
2965
  {"name": item.get("name"), "rarity": item.get("rarity"), "world": item.get("world", "")}
2966
  for item in (_GAME_CATALOG.get("shields") or [])
2967
  ],
 
 
2968
  "elf_routes": _elf_routes(),
2969
  "dwarf_luck": {
2970
  rarity: [
@@ -2980,13 +3082,90 @@ def _upgrade_lab_meta():
2980
  },
2981
  }
2982
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2983
  def _upgrade_lab_calculate(data: UpgradeLabInput):
2984
  item_name = str(data.item_name or "").strip()
 
 
 
2985
  singular, category, item = _upgrade_item_lookup(item_name, data.item_type)
2986
  rarity = str(item.get("rarity") or "Common")
2987
  target_level = max(1, min(10, int(data.target_level or 1)))
2988
  target_count = max(1, int(data.target_count or 1))
2989
- target_kind = str(data.target_kind or "level").strip().lower()
2990
  strategy = str(data.strategy or "auto").strip().lower()
2991
  if strategy not in ("auto", "dwarf", "dwarft", "dwarfting", "merge", "merging"):
2992
  strategy = "auto"
 
2123
  strategy: str = "auto"
2124
  elf_route: Optional[str] = None
2125
  elf_level5_count: int = 0
2126
+ player_level: Optional[int] = None
2127
+ pet_use_gold: bool = False
2128
+ pet_helper_odds: bool = False
2129
 
2130
 
2131
  # ─────────────────────────────────────────────────────────────────
 
2877
  result[successes] = result.get(successes, 0.0) + level5_prob * prob
2878
  return dict(sorted(result.items()))
2879
 
2880
+ _PET_ENHANCER_ROUTES = [
2881
+ {"source": "Peppy", "result": "XPeppy"},
2882
+ {"source": "Blue Axolotl", "result": "XAxolotl"},
2883
+ {"source": "Pengoo", "result": "XPengoo"},
2884
+ {"source": "Leo", "result": "XLeo"},
2885
+ {"source": "Phoenix", "result": "XPhoenix"},
2886
+ {"source": "Octoo", "result": "XOctoo"},
2887
+ {"source": "Xen", "result": "XXen"},
2888
+ {"source": "Inferno", "result": "XInferno"},
2889
+ ]
2890
+
2891
+ _PET_ENHANCER_CHANCES = [
2892
+ {"min": 0, "max": 2000, "free": [1.0, 0.5, 0.33, 0.25, 0.20, 0.17, 0.14, 0.13], "gold": [3.0, 1.5, 1.0, 0.75, 0.60, 0.50, 0.43, 0.38]},
2893
+ {"min": 2001, "max": 3000, "free": [3.0, 1.5, 1.0, 0.75, 0.60, 0.50, 0.43, 0.38], "gold": [9.0, 4.5, 3.0, 2.25, 1.80, 1.50, 1.29, 1.13]},
2894
+ {"min": 3001, "max": 4000, "free": [5.0, 2.5, 1.67, 1.25, 1.0, 0.83, 0.71, 0.63], "gold": [15.0, 7.5, 5.0, 3.75, 3.0, 2.50, 2.14, 1.88]},
2895
+ {"min": 4001, "max": 5000, "free": [7.0, 3.5, 2.33, 1.75, 1.40, 1.17, 1.0, 0.88], "gold": [21.0, 10.5, 7.0, 5.25, 4.20, 3.50, 3.0, 2.63]},
2896
+ {"min": 5001, "max": 6000, "free": [10.0, 5.0, 3.33, 2.50, 2.0, 1.67, 1.43, 1.25], "gold": [30.0, 15.0, 10.0, 7.50, 6.0, 5.0, 4.29, 3.75]},
2897
+ {"min": 6001, "max": 7000, "free": [15.0, 7.5, 5.0, 3.75, 3.0, 2.50, 2.14, 1.88], "gold": [45.0, 22.5, 15.0, 11.25, 9.0, 7.50, 6.43, 5.63]},
2898
+ {"min": 7001, "max": 8000, "free": [20.0, 10.0, 6.67, 5.0, 4.0, 3.33, 2.86, 2.50], "gold": [60.0, 30.0, 20.0, 15.0, 12.0, 10.0, 8.57, 7.50]},
2899
+ {"min": 8001, "max": 9000, "free": [25.0, 12.5, 8.33, 6.25, 5.0, 4.17, 3.57, 3.13], "gold": [75.0, 37.5, 25.0, 18.75, 15.0, 12.50, 10.71, 9.38]},
2900
+ {"min": 9001, "max": 10000, "free": [30.0, 15.0, 10.0, 7.50, 6.0, 5.0, 4.29, 3.75], "gold": [90.0, 45.0, 30.0, 22.50, 18.0, 15.0, 12.86, 11.25]},
2901
+ {"min": 10001, "max": None, "free": [33.0, 16.5, 11.0, 8.25, 6.60, 5.50, 4.71, 4.13], "gold": [99.0, 49.5, 33.0, 24.75, 19.80, 16.50, 14.14, 12.38]},
2902
+ ]
2903
+
2904
+ def _pet_enhancer_routes():
2905
+ routes = []
2906
+ for index, route in enumerate(_PET_ENHANCER_ROUTES):
2907
+ source = route["source"]
2908
+ result = route["result"]
2909
+ if source not in PETS or result not in PETS:
2910
+ continue
2911
+ routes.append({
2912
+ "key": f"pet|{source}|{result}",
2913
+ "item_type": "pet",
2914
+ "source": source,
2915
+ "result": result,
2916
+ "index": index,
2917
+ })
2918
+ return routes
2919
+
2920
+ def _pet_enhancer_route_for_name(name: str):
2921
+ clean = str(name or "").strip().lower()
2922
+ for route in _pet_enhancer_routes():
2923
+ if route["source"].lower() == clean or route["result"].lower() == clean:
2924
+ return route
2925
+ return None
2926
+
2927
+ def _pet_enhancer_tier(player_level: int, helper_odds: bool = False):
2928
+ if helper_odds:
2929
+ return _PET_ENHANCER_CHANCES[-1]
2930
+ try:
2931
+ level = max(0, int(player_level or 0))
2932
+ except (TypeError, ValueError):
2933
+ level = 0
2934
+ for row in _PET_ENHANCER_CHANCES:
2935
+ if level >= row["min"] and (row["max"] is None or level <= row["max"]):
2936
+ return row
2937
+ return _PET_ENHANCER_CHANCES[-1]
2938
+
2939
+ def _pet_enhancer_chance(route: dict, player_level: int, use_gold: bool, helper_odds: bool):
2940
+ tier = _pet_enhancer_tier(player_level, helper_odds)
2941
+ key = "gold" if use_gold else "free"
2942
+ index = int(route.get("index", 0) or 0)
2943
+ chance_pct = float(tier[key][index])
2944
+ return max(0.0, min(1.0, chance_pct / 100.0)), chance_pct, tier
2945
+
2946
+ def _binomial_distribution(n: int, p: float):
2947
+ n = max(0, int(n or 0))
2948
+ p = max(0.0, min(1.0, float(p or 0.0)))
2949
+ if n <= 0:
2950
+ return {0: 1.0}
2951
+ if p <= 0.0:
2952
+ return {0: 1.0}
2953
+ if p >= 1.0:
2954
+ return {n: 1.0}
2955
+ dist = {}
2956
+ for successes in range(n + 1):
2957
+ prob = math.exp(
2958
+ math.lgamma(n + 1)
2959
+ - math.lgamma(successes + 1)
2960
+ - math.lgamma(n - successes + 1)
2961
+ + successes * math.log(p)
2962
+ + (n - successes) * math.log1p(-p)
2963
+ )
2964
+ if prob > 1e-15:
2965
+ dist[successes] = prob
2966
+ return dist
2967
+
2968
  _KNOWN_ELF_ROUTES = {
2969
  ("sword", "Dragon's Poison"): "Dragon's Devil",
2970
  ("shield", "Dragon's Anger"): "Dragon's Soul",
 
3046
  "world": item.get("world", ""),
3047
  "source": item.get("source", ""),
3048
  })
3049
+ for route in _pet_enhancer_routes():
3050
+ items.append({
3051
+ "name": route["source"],
3052
+ "item_type": "pet",
3053
+ "rarity": "Pet",
3054
+ "world": "Enhancer",
3055
+ "source": "Pet Enhancer",
3056
+ "enhances_to": route["result"],
3057
+ })
3058
  return {
3059
  "items": items,
3060
  "swords": [
 
3065
  {"name": item.get("name"), "rarity": item.get("rarity"), "world": item.get("world", "")}
3066
  for item in (_GAME_CATALOG.get("shields") or [])
3067
  ],
3068
+ "pet_routes": _pet_enhancer_routes(),
3069
+ "pet_chance_tiers": _PET_ENHANCER_CHANCES,
3070
  "elf_routes": _elf_routes(),
3071
  "dwarf_luck": {
3072
  rarity: [
 
3082
  },
3083
  }
3084
 
3085
+ def _upgrade_lab_pet_calculate(data: UpgradeLabInput):
3086
+ item_name = str(data.item_name or "").strip()
3087
+ route = _pet_enhancer_route_for_name(item_name)
3088
+ if not route:
3089
+ raise HTTPException(400, f"{item_name!r} has no known Pet Enhancer route.")
3090
+ target_count = max(1, int(data.target_count or 1))
3091
+ counts = data.counts or {}
3092
+ normal_count = max(0, int(counts.get("pet", counts.get("1", counts.get(1, 0))) or 0))
3093
+ attempts = normal_count // 10
3094
+ leftovers = normal_count % 10
3095
+ player_level = 10001 if data.pet_helper_odds else max(0, int(data.player_level or 0))
3096
+ chance, chance_pct, tier = _pet_enhancer_chance(route, player_level, data.pet_use_gold, data.pet_helper_odds)
3097
+ result_dist = _binomial_distribution(attempts, chance)
3098
+ probability = _distribution_tail(result_dist, target_count)
3099
+ expected = attempts * chance
3100
+ gold_cost = attempts * 1e15 if data.pet_use_gold else 0.0
3101
+ return {
3102
+ "ok": True,
3103
+ "item": {
3104
+ "type": "pet",
3105
+ "name": route["source"],
3106
+ "rarity": "Pet",
3107
+ "world": "Enhancer",
3108
+ "source": "Pet Enhancer",
3109
+ },
3110
+ "prices": {},
3111
+ "goal": {
3112
+ "kind": "pet",
3113
+ "label": route["result"],
3114
+ "target_count": target_count,
3115
+ "source_name": route["source"],
3116
+ "result_name": route["result"],
3117
+ },
3118
+ "plan": {
3119
+ "strategy": "pet_enhancer",
3120
+ "probability": probability,
3121
+ "probability_pct": probability * 100.0,
3122
+ "expected_count": expected,
3123
+ "distribution": _distribution_rows(result_dist),
3124
+ "steps": [{
3125
+ "from_level": 0,
3126
+ "to_level": 0,
3127
+ "action": "enhance",
3128
+ "chance": chance,
3129
+ "chance_pct": chance_pct,
3130
+ "expected_input": float(normal_count),
3131
+ "expected_output": expected,
3132
+ "merge_required": 10,
3133
+ "from_label": f"10x {route['source']}",
3134
+ "to_label": route["result"],
3135
+ "attempts": attempts,
3136
+ "leftovers": leftovers,
3137
+ "gold_cost": gold_cost,
3138
+ "gold_cost_display": _price_fmt(gold_cost) if gold_cost else "0",
3139
+ }],
3140
+ },
3141
+ "valuation": {
3142
+ "gold_cost": gold_cost,
3143
+ "gold_cost_display": _price_fmt(gold_cost) if gold_cost else "0",
3144
+ },
3145
+ "pet": {
3146
+ "route": route,
3147
+ "source_count": normal_count,
3148
+ "attempts": attempts,
3149
+ "leftovers": leftovers,
3150
+ "player_level": player_level,
3151
+ "helper_odds": bool(data.pet_helper_odds),
3152
+ "use_gold": bool(data.pet_use_gold),
3153
+ "chance": chance,
3154
+ "chance_pct": chance_pct,
3155
+ "tier": tier,
3156
+ },
3157
+ "elf": {"eligible": False, "routes": _elf_routes(), "route": None},
3158
+ }
3159
+
3160
  def _upgrade_lab_calculate(data: UpgradeLabInput):
3161
  item_name = str(data.item_name or "").strip()
3162
+ target_kind = str(data.target_kind or "level").strip().lower()
3163
+ if target_kind in ("pet", "xpet", "pet_enhancer", "enhancer"):
3164
+ return _upgrade_lab_pet_calculate(data)
3165
  singular, category, item = _upgrade_item_lookup(item_name, data.item_type)
3166
  rarity = str(item.get("rarity") or "Common")
3167
  target_level = max(1, min(10, int(data.target_level or 1)))
3168
  target_count = max(1, int(data.target_count or 1))
 
3169
  strategy = str(data.strategy or "auto").strip().lower()
3170
  if strategy not in ("auto", "dwarf", "dwarft", "dwarfting", "merge", "merging"):
3171
  strategy = "auto"
static/index.html CHANGED
@@ -716,13 +716,26 @@ input::placeholder{color:var(--text3);}
716
  <select id="up-target-kind" onchange="upgradeGoalChanged()">
717
  <option value="level">Item Level</option>
718
  <option value="elf">Elf Legendary</option>
 
719
  </select>
720
  </div>
721
  <div id="up-target-level-wrap"><label>Target Level</label><input id="up-target-level" type="number" value="5" min="1" max="10"></div>
722
  <div><label>Target Count</label><input id="up-target-count" type="number" value="1" min="1"></div>
723
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
724
  <div id="up-elf-hint" class="upgrade-mini" style="display:none;margin-top:6px"></div>
725
- <div class="stitle">Items Owned By Level</div>
726
  <div id="up-count-grid" class="upgrade-count-grid"></div>
727
  <button class="btn btn-p" style="width:100%;justify-content:center;margin-top:12px;padding:9px" onclick="upgradeCalc()">CALCULATE</button>
728
  <div id="up-status" class="upgrade-mini" style="margin-top:9px;min-height:14px">Choose an item, enter counts, or import an account.</div>
@@ -1626,26 +1639,59 @@ function _upgradeRouteForItem(name){
1626
  return (UPGRADE_META?.elf_routes||[]).find(row=>String(row.source||'').toLowerCase()===clean)||null;
1627
  }
1628
 
 
 
 
 
 
 
 
1629
  function upgradeItemChanged(syncRoute=true){
1630
  const name=document.getElementById('up-item-name')?.value||'';
1631
  const route=_upgradeRouteForItem(name);
 
1632
  const goal=document.getElementById('up-target-kind');
1633
  if(goal&&goal.value==='elf'&&!route)goal.value='level';
 
 
1634
  upgradeGoalChanged();
1635
  }
1636
 
1637
  function upgradeGoalChanged(){
1638
  const goal=document.getElementById('up-target-kind');
1639
- const route=_upgradeRouteForItem(document.getElementById('up-item-name')?.value||'');
 
 
1640
  const elfOption=goal?.querySelector('option[value="elf"]');
1641
  if(elfOption)elfOption.disabled=!route;
 
 
1642
  if(goal&&goal.value==='elf'&&!route)goal.value='level';
 
 
1643
  const kind=goal?.value||'level';
1644
  const wrap=document.getElementById('up-target-level-wrap');
1645
- if(wrap)wrap.style.display=kind==='elf'?'none':'';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1646
  const hint=document.getElementById('up-elf-hint');
1647
  if(hint){
1648
- if(kind==='elf'&&route){
 
 
 
1649
  hint.style.display='';
1650
  hint.textContent=`Elf: 10x ${route.source} Lv5 -> ${route.result}. Success chance is 80%; success and fail both consume the 10 Epics.`;
1651
  }else if(route){
@@ -1677,6 +1723,13 @@ function _upgradeLoadedCountsFor(name){
1677
  const clean=String(name||'').trim().toLowerCase();
1678
  const counts={};
1679
  for(let level=1;level<=10;level++)counts[String(level)]=0;
 
 
 
 
 
 
 
1680
  for(const row of UPGRADE_LOADED_ITEMS||[]){
1681
  if(String(row.name||'').trim().toLowerCase()!==clean)continue;
1682
  const level=Math.max(1,Math.min(10,parseInt(row.level,10)||1));
@@ -1729,8 +1782,12 @@ async function upgradeLoadAccount(){
1729
  const d=bulk.results?.[name]||Object.values(bulk.results||{})[0];
1730
  if(!d||!d.ok)throw new Error(d?.error||'No live lookup result.');
1731
  const inv=d.inventory||{};
1732
- UPGRADE_LOADED_ITEMS=[...(inv.swords||[]),...(inv.shields||[])].filter(row=>row&&row.name);
1733
- if(!UPGRADE_LOADED_ITEMS.length)throw new Error('No swords or shields found in this live inventory.');
 
 
 
 
1734
  const input=document.getElementById('up-item-name');
1735
  if(input&&!input.value.trim()){
1736
  const best=_upgradeBestLoadedItem();
@@ -1743,8 +1800,8 @@ async function upgradeLoadAccount(){
1743
  if(status){
1744
  status.style.color=total>0?'var(--green)':'var(--gold)';
1745
  status.textContent=total>0
1746
- ? `Loaded ${UPGRADE_LOADED_ITEMS.length} swords/shields from ${d.username||name}. Showing ${total}x ${input?.value||'selected item'}.`
1747
- : `Loaded ${UPGRADE_LOADED_ITEMS.length} swords/shields from ${d.username||name}. No ${input?.value||'selected item'} found.`;
1748
  }
1749
  }catch(e){
1750
  if(status){status.style.color='var(--red)';status.textContent=`Import failed: ${e.message}`;}
@@ -1798,7 +1855,10 @@ async function upgradeCalc(){
1798
  const itemName=document.getElementById('up-item-name')?.value||'';
1799
  const meta=_upgradeItemMeta(itemName);
1800
  const route=_upgradeRouteForItem(itemName);
1801
- const kind=document.getElementById('up-target-kind')?.value||'level';
 
 
 
1802
  const payload={
1803
  item_type:meta?.item_type||'',
1804
  item_name:itemName,
@@ -1808,6 +1868,9 @@ async function upgradeCalc(){
1808
  target_level:parseInt(document.getElementById('up-target-level')?.value,10)||1,
1809
  target_count:parseInt(document.getElementById('up-target-count')?.value,10)||1,
1810
  elf_route:kind==='elf'&&route?route.key:null,
 
 
 
1811
  };
1812
  try{
1813
  const data=await post('/upgrade-lab',payload);
@@ -1835,12 +1898,14 @@ function renderUpgradeResult(data){
1835
  ? `${row.merge_required}x required`
1836
  : row.action==='merge+dwarf'
1837
  ? `${row.merge_required}x merge, leftovers dwarf ${Number(row.dwarf_chance_pct||0).toFixed(2)}%`
 
 
1838
  : row.action==='elf'
1839
  ? '10x Lv5, 80%, fail destroys inputs'
1840
  : `${Number(row.chance_pct||0).toFixed(2)}% success`;
1841
  return `<tr>
1842
  <td>${_escHtml(from)} -> ${_escHtml(to)}</td>
1843
- <td style="color:${row.action==='merge'?'var(--green)':row.action==='merge+dwarf'?'var(--gold)':row.action==='elf'?'var(--gold)':'var(--accent)'}">${_escHtml(action)}</td>
1844
  <td>${_escHtml(rule)}</td>
1845
  <td>${_numText(row.expected_input,3)}</td>
1846
  <td>${_numText(row.expected_output,3)}</td>
 
716
  <select id="up-target-kind" onchange="upgradeGoalChanged()">
717
  <option value="level">Item Level</option>
718
  <option value="elf">Elf Legendary</option>
719
+ <option value="pet">Pet Enhancer</option>
720
  </select>
721
  </div>
722
  <div id="up-target-level-wrap"><label>Target Level</label><input id="up-target-level" type="number" value="5" min="1" max="10"></div>
723
  <div><label>Target Count</label><input id="up-target-count" type="number" value="1" min="1"></div>
724
  </div>
725
+ <div id="up-pet-controls" class="fr c3" style="display:none;margin-top:7px">
726
+ <div><label>Player Level</label><input id="up-player-level" type="number" value="10000" min="0"></div>
727
+ <div><label>Odds</label>
728
+ <select id="up-pet-cost">
729
+ <option value="free">Free</option>
730
+ <option value="gold">With 1QT Gold</option>
731
+ </select>
732
+ </div>
733
+ <label style="display:flex;align-items:center;gap:6px;margin-top:17px;color:var(--text2);font-family:'Rajdhani',sans-serif;font-size:11px;letter-spacing:1px">
734
+ <input id="up-helper-odds" type="checkbox" style="width:auto"> 10K+ HELPER ODDS
735
+ </label>
736
+ </div>
737
  <div id="up-elf-hint" class="upgrade-mini" style="display:none;margin-top:6px"></div>
738
+ <div class="stitle" id="up-count-title">Items Owned By Level</div>
739
  <div id="up-count-grid" class="upgrade-count-grid"></div>
740
  <button class="btn btn-p" style="width:100%;justify-content:center;margin-top:12px;padding:9px" onclick="upgradeCalc()">CALCULATE</button>
741
  <div id="up-status" class="upgrade-mini" style="margin-top:9px;min-height:14px">Choose an item, enter counts, or import an account.</div>
 
1639
  return (UPGRADE_META?.elf_routes||[]).find(row=>String(row.source||'').toLowerCase()===clean)||null;
1640
  }
1641
 
1642
+ function _upgradePetRouteForItem(name){
1643
+ const clean=String(name||'').trim().toLowerCase();
1644
+ return (UPGRADE_META?.pet_routes||[]).find(row=>
1645
+ String(row.source||'').toLowerCase()===clean || String(row.result||'').toLowerCase()===clean
1646
+ )||null;
1647
+ }
1648
+
1649
  function upgradeItemChanged(syncRoute=true){
1650
  const name=document.getElementById('up-item-name')?.value||'';
1651
  const route=_upgradeRouteForItem(name);
1652
+ const petRoute=_upgradePetRouteForItem(name);
1653
  const goal=document.getElementById('up-target-kind');
1654
  if(goal&&goal.value==='elf'&&!route)goal.value='level';
1655
+ if(goal&&goal.value==='pet'&&!petRoute)goal.value='level';
1656
+ if(goal&&goal.value==='level'&&petRoute)goal.value='pet';
1657
  upgradeGoalChanged();
1658
  }
1659
 
1660
  function upgradeGoalChanged(){
1661
  const goal=document.getElementById('up-target-kind');
1662
+ const itemName=document.getElementById('up-item-name')?.value||'';
1663
+ const route=_upgradeRouteForItem(itemName);
1664
+ const petRoute=_upgradePetRouteForItem(itemName);
1665
  const elfOption=goal?.querySelector('option[value="elf"]');
1666
  if(elfOption)elfOption.disabled=!route;
1667
+ const petOption=goal?.querySelector('option[value="pet"]');
1668
+ if(petOption)petOption.disabled=!petRoute;
1669
  if(goal&&goal.value==='elf'&&!route)goal.value='level';
1670
+ if(goal&&goal.value==='pet'&&!petRoute)goal.value='level';
1671
+ if(goal&&goal.value==='level'&&petRoute)goal.value='pet';
1672
  const kind=goal?.value||'level';
1673
  const wrap=document.getElementById('up-target-level-wrap');
1674
+ if(wrap)wrap.style.display=(kind==='elf'||kind==='pet')?'none':'';
1675
+ const petControls=document.getElementById('up-pet-controls');
1676
+ if(petControls)petControls.style.display=kind==='pet'?'grid':'none';
1677
+ const countTitle=document.getElementById('up-count-title');
1678
+ if(countTitle)countTitle.textContent=kind==='pet'?'Normal Pets Owned':'Items Owned By Level';
1679
+ document.querySelectorAll('#up-count-grid>div').forEach((cell,index)=>{
1680
+ const label=cell.querySelector('label');
1681
+ if(kind==='pet'){
1682
+ cell.style.display=index===0?'':'none';
1683
+ if(label)label.textContent=petRoute?petRoute.source:'Normal Pet';
1684
+ }else{
1685
+ cell.style.display='';
1686
+ if(label)label.textContent=`Lv${index+1}`;
1687
+ }
1688
+ });
1689
  const hint=document.getElementById('up-elf-hint');
1690
  if(hint){
1691
+ if(kind==='pet'&&petRoute){
1692
+ hint.style.display='';
1693
+ hint.textContent=`Pet Enhancer: 10x ${petRoute.source} -> ${petRoute.result}. Failed attempts consume the 10 pets too.`;
1694
+ }else if(kind==='elf'&&route){
1695
  hint.style.display='';
1696
  hint.textContent=`Elf: 10x ${route.source} Lv5 -> ${route.result}. Success chance is 80%; success and fail both consume the 10 Epics.`;
1697
  }else if(route){
 
1723
  const clean=String(name||'').trim().toLowerCase();
1724
  const counts={};
1725
  for(let level=1;level<=10;level++)counts[String(level)]=0;
1726
+ const petRoute=_upgradePetRouteForItem(name);
1727
+ if(petRoute){
1728
+ counts['1']=(UPGRADE_LOADED_ITEMS||[]).filter(row=>
1729
+ row.item_type==='pet' && String(row.name||'').trim().toLowerCase()===String(petRoute.source||'').toLowerCase()
1730
+ ).length;
1731
+ return counts;
1732
+ }
1733
  for(const row of UPGRADE_LOADED_ITEMS||[]){
1734
  if(String(row.name||'').trim().toLowerCase()!==clean)continue;
1735
  const level=Math.max(1,Math.min(10,parseInt(row.level,10)||1));
 
1782
  const d=bulk.results?.[name]||Object.values(bulk.results||{})[0];
1783
  if(!d||!d.ok)throw new Error(d?.error||'No live lookup result.');
1784
  const inv=d.inventory||{};
1785
+ UPGRADE_LOADED_ITEMS=[
1786
+ ...(inv.swords||[]).map(row=>({...row,item_type:'sword'})),
1787
+ ...(inv.shields||[]).map(row=>({...row,item_type:'shield'})),
1788
+ ...(inv.pets||[]).map(row=>({...row,item_type:'pet'})),
1789
+ ].filter(row=>row&&row.name);
1790
+ if(!UPGRADE_LOADED_ITEMS.length)throw new Error('No swords, shields or pets found in this live inventory.');
1791
  const input=document.getElementById('up-item-name');
1792
  if(input&&!input.value.trim()){
1793
  const best=_upgradeBestLoadedItem();
 
1800
  if(status){
1801
  status.style.color=total>0?'var(--green)':'var(--gold)';
1802
  status.textContent=total>0
1803
+ ? `Loaded ${UPGRADE_LOADED_ITEMS.length} items from ${d.username||name}. Showing ${total}x ${input?.value||'selected item'}.`
1804
+ : `Loaded ${UPGRADE_LOADED_ITEMS.length} items from ${d.username||name}. No ${input?.value||'selected item'} found.`;
1805
  }
1806
  }catch(e){
1807
  if(status){status.style.color='var(--red)';status.textContent=`Import failed: ${e.message}`;}
 
1855
  const itemName=document.getElementById('up-item-name')?.value||'';
1856
  const meta=_upgradeItemMeta(itemName);
1857
  const route=_upgradeRouteForItem(itemName);
1858
+ const petRoute=_upgradePetRouteForItem(itemName);
1859
+ let kind=document.getElementById('up-target-kind')?.value||'level';
1860
+ if(petRoute&&kind==='level')kind='pet';
1861
+ if(kind==='pet'&&!petRoute)kind='level';
1862
  const payload={
1863
  item_type:meta?.item_type||'',
1864
  item_name:itemName,
 
1868
  target_level:parseInt(document.getElementById('up-target-level')?.value,10)||1,
1869
  target_count:parseInt(document.getElementById('up-target-count')?.value,10)||1,
1870
  elf_route:kind==='elf'&&route?route.key:null,
1871
+ player_level:parseInt(document.getElementById('up-player-level')?.value,10)||0,
1872
+ pet_use_gold:document.getElementById('up-pet-cost')?.value==='gold',
1873
+ pet_helper_odds:!!document.getElementById('up-helper-odds')?.checked,
1874
  };
1875
  try{
1876
  const data=await post('/upgrade-lab',payload);
 
1898
  ? `${row.merge_required}x required`
1899
  : row.action==='merge+dwarf'
1900
  ? `${row.merge_required}x merge, leftovers dwarf ${Number(row.dwarf_chance_pct||0).toFixed(2)}%`
1901
+ : row.action==='enhance'
1902
+ ? `${row.merge_required}x per try, ${Number(row.chance_pct||0).toFixed(2)}%${row.gold_cost?`, gold ${row.gold_cost_display}`:''}`
1903
  : row.action==='elf'
1904
  ? '10x Lv5, 80%, fail destroys inputs'
1905
  : `${Number(row.chance_pct||0).toFixed(2)}% success`;
1906
  return `<tr>
1907
  <td>${_escHtml(from)} -> ${_escHtml(to)}</td>
1908
+ <td style="color:${row.action==='merge'?'var(--green)':row.action==='merge+dwarf'?'var(--gold)':row.action==='elf'||row.action==='enhance'?'var(--gold)':'var(--accent)'}">${_escHtml(action)}</td>
1909
  <td>${_escHtml(rule)}</td>
1910
  <td>${_numText(row.expected_input,3)}</td>
1911
  <td>${_numText(row.expected_output,3)}</td>
tests/test_market_optimizer_logic.py CHANGED
@@ -994,11 +994,40 @@ class MarketOptimizerLogicTests(unittest.TestCase):
994
  self.assertAlmostEqual(distribution[1], 0.8)
995
  self.assertAlmostEqual(result["plan"]["expected_count"], 0.8)
996
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
997
  def test_frontend_flame_solver_uses_explicit_solved_flag(self):
998
  html = (Path(__file__).resolve().parents[1] / "static" / "index.html").read_text(encoding="utf-8")
999
  self.assertIn("let solved=false;", html)
1000
  self.assertIn("solved=true;break;", html)
1001
  self.assertNotIn("if(ticks===0){", html)
 
1002
 
1003
  def test_market_selection_keeps_current_setup_as_search_seed(self):
1004
  accounts = [self.account("A"), self.account("B")]
 
994
  self.assertAlmostEqual(distribution[1], 0.8)
995
  self.assertAlmostEqual(result["plan"]["expected_count"], 0.8)
996
 
997
+ def test_pet_enhancer_uses_level_gold_chance_table(self):
998
+ result = self.app._upgrade_lab_calculate(self.app.UpgradeLabInput(
999
+ item_name="Inferno",
1000
+ counts={"1": 20},
1001
+ target_kind="pet",
1002
+ target_count=1,
1003
+ player_level=10001,
1004
+ pet_use_gold=True,
1005
+ ))
1006
+ chance = 0.1238
1007
+ self.assertEqual(result["goal"]["result_name"], "XInferno")
1008
+ self.assertEqual(result["pet"]["attempts"], 2)
1009
+ self.assertAlmostEqual(result["pet"]["chance"], chance)
1010
+ self.assertAlmostEqual(result["plan"]["expected_count"], 2 * chance)
1011
+ self.assertAlmostEqual(result["plan"]["probability"], 1 - (1 - chance) ** 2)
1012
+
1013
+ def test_pet_enhancer_accepts_result_name_as_lookup_alias(self):
1014
+ result = self.app._upgrade_lab_calculate(self.app.UpgradeLabInput(
1015
+ item_name="XInferno",
1016
+ counts={"1": 10},
1017
+ target_kind="pet",
1018
+ target_count=1,
1019
+ pet_helper_odds=True,
1020
+ ))
1021
+ self.assertEqual(result["item"]["name"], "Inferno")
1022
+ self.assertEqual(result["goal"]["label"], "XInferno")
1023
+ self.assertEqual(result["pet"]["source_count"], 10)
1024
+
1025
  def test_frontend_flame_solver_uses_explicit_solved_flag(self):
1026
  html = (Path(__file__).resolve().parents[1] / "static" / "index.html").read_text(encoding="utf-8")
1027
  self.assertIn("let solved=false;", html)
1028
  self.assertIn("solved=true;break;", html)
1029
  self.assertNotIn("if(ticks===0){", html)
1030
+ self.assertIn('<option value="pet">Pet Enhancer</option>', html)
1031
 
1032
  def test_market_selection_keeps_current_setup_as_search_seed(self):
1033
  accounts = [self.account("A"), self.account("B")]