2026-11-05 12:11:10 +00:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace TKS.OutGame
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>스테이지 1회 결과(보상 · 별 · 조건) — 결과 화면·소탕 팝업 표시용.</summary>
|
|
|
|
|
|
public class StageReward
|
|
|
|
|
|
{
|
|
|
|
|
|
public bool win;
|
|
|
|
|
|
public bool sweep;
|
|
|
|
|
|
public bool first;
|
|
|
|
|
|
public bool boss;
|
|
|
|
|
|
public int chapter;
|
|
|
|
|
|
public int stage;
|
|
|
|
|
|
public int gold;
|
|
|
|
|
|
public int exp;
|
|
|
|
|
|
public int gem;
|
|
|
|
|
|
public string gearId;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
/// <summary>보물 상자(stage_chest · stage_chest_pct · PD #1093 C) — 상자 등급 개발 반영 뒤 id.</summary>
|
|
|
|
|
|
public string chest;
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public int stamina;
|
|
|
|
|
|
public int stars;
|
|
|
|
|
|
/// <summary>별 조건 ① 클리어 ② 아군 전원 생존 ③ star_turns 턴 이내.</summary>
|
|
|
|
|
|
public bool[] conds = new bool[3];
|
|
|
|
|
|
public int starTurns;
|
|
|
|
|
|
public int turns;
|
|
|
|
|
|
public int bestStars;
|
|
|
|
|
|
public List<string> expTo = new List<string>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>군량(§2) · 일일 횟수 · 스테이지 보상(§6) · 소탕 · 골드 장비 상점(S15). 수치 = economy.csv · stage_rewards.csv · gear_items.csv.</summary>
|
|
|
|
|
|
public static class EconomyRules
|
|
|
|
|
|
{
|
|
|
|
|
|
// ---------- 일일 ----------
|
|
|
|
|
|
|
|
|
|
|
|
public static string DayKey(DateTime nowUtc)
|
|
|
|
|
|
{
|
|
|
|
|
|
return nowUtc.ToLocalTime().ToString("yyyyMMdd", CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>날짜가 바뀌었으면 광고·보옥 구매 횟수를 초기화.</summary>
|
|
|
|
|
|
public static void RollDaily(SaveData d, DateTime nowUtc)
|
|
|
|
|
|
{
|
|
|
|
|
|
var day = DayKey(nowUtc);
|
|
|
|
|
|
if (d.adsUsed.day == day) return;
|
|
|
|
|
|
d.adsUsed = new AdsUsed { day = day };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>다음 날 0시(현지)까지 남은 시간.</summary>
|
|
|
|
|
|
public static TimeSpan UntilNextDay(DateTime nowUtc)
|
|
|
|
|
|
{
|
|
|
|
|
|
var local = nowUtc.ToLocalTime();
|
|
|
|
|
|
return local.Date.AddDays(1) - local;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 군량 ----------
|
|
|
|
|
|
|
2026-11-05 15:22:54 +00:00
|
|
|
|
/// <summary>군량 최대 = stamina_max + 농업 「군량 상한」(PD #1093 C).</summary>
|
|
|
|
|
|
public static int StaminaMax(OutGameData db, SaveData d)
|
|
|
|
|
|
{
|
|
|
|
|
|
return db.Economy.StaminaMax + Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.FoodCap));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>군량 1 회복 초 = stamina_regen_sec − 농업 「군량 회복」(최소 1).</summary>
|
|
|
|
|
|
public static int RegenSec(OutGameData db, SaveData d)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Mathf.Max(1, db.Economy.StaminaRegenSec - Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.FoodRegen)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
/// <summary>회복 계산(오프라인 포함): 최대 미만이면 stamina_regen_sec 마다 1 · 최대 이상(구매·광고로 넘침)이면 멈추고 기준 시각만 당긴다.</summary>
|
|
|
|
|
|
public static void TickStamina(OutGameData db, SaveData d, DateTime nowUtc)
|
|
|
|
|
|
{
|
2026-11-05 15:22:54 +00:00
|
|
|
|
int max = StaminaMax(db, d);
|
|
|
|
|
|
int regen = RegenSec(db, d);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
var at = SaveStore.ParseIso(d.staminaAt, nowUtc);
|
|
|
|
|
|
if (at > nowUtc) at = nowUtc; // 기기 시각을 되돌린 경우
|
2026-11-05 15:22:54 +00:00
|
|
|
|
if (d.stamina >= max)
|
2026-11-05 12:11:10 +00:00
|
|
|
|
{
|
|
|
|
|
|
d.staminaAt = SaveStore.Iso(nowUtc);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
long elapsed = (long)(nowUtc - at).TotalSeconds;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
long gained = elapsed / Mathf.Max(1, regen);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
if (gained <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
d.staminaAt = SaveStore.Iso(at);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
long next = d.stamina + gained;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
if (next >= max)
|
2026-11-05 12:11:10 +00:00
|
|
|
|
{
|
2026-11-05 15:22:54 +00:00
|
|
|
|
d.stamina = max;
|
2026-11-05 12:11:10 +00:00
|
|
|
|
d.staminaAt = SaveStore.Iso(nowUtc);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
d.stamina = (int)next;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
d.staminaAt = SaveStore.Iso(at.AddSeconds(gained * regen));
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>다음 1 회복까지 초(최대 이상 = 0).</summary>
|
|
|
|
|
|
public static int SecondsToNext(OutGameData db, SaveData d, DateTime nowUtc)
|
|
|
|
|
|
{
|
2026-11-05 15:22:54 +00:00
|
|
|
|
if (d.stamina >= StaminaMax(db, d)) return 0;
|
|
|
|
|
|
int regen = RegenSec(db, d);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
var at = SaveStore.ParseIso(d.staminaAt, nowUtc);
|
2026-11-05 15:22:54 +00:00
|
|
|
|
int left = regen - (int)(nowUtc - at).TotalSeconds;
|
|
|
|
|
|
return Mathf.Clamp(left, 0, regen);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static int StageCost(OutGameData db, BalanceConfig.StageDef s)
|
|
|
|
|
|
{
|
|
|
|
|
|
return s.kind == StageKind.Boss ? db.Economy.BossCost : db.Economy.StageCost;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 15:22:54 +00:00
|
|
|
|
/// <summary>입장 군량 = 표 비용 − 농업 「군량 비용 할인」(누적 값의 정수 부분 · 최소 1 · PD 확인 대상).</summary>
|
|
|
|
|
|
public static int StageCost(OutGameData db, SaveData d, BalanceConfig.StageDef s)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Mathf.Max(1, StageCost(db, s) - Mathf.FloorToInt(DevRules.Mod(db, d, DevRules.FoodCost) + 1e-4f));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static bool SpendStamina(OutGameData db, SaveData d, int n, DateTime nowUtc)
|
|
|
|
|
|
{
|
|
|
|
|
|
TickStamina(db, d, nowUtc);
|
|
|
|
|
|
if (d.stamina < n) return false;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
int max = StaminaMax(db, d);
|
|
|
|
|
|
bool wasFull = d.stamina >= max;
|
2026-11-05 12:11:10 +00:00
|
|
|
|
d.stamina -= n;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
if (wasFull && d.stamina < max) d.staminaAt = SaveStore.Iso(nowUtc); // 가득 찬 상태에서 쓰면 그때부터 회복
|
2026-11-05 12:11:10 +00:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void AddStamina(OutGameData db, SaveData d, int n, DateTime nowUtc)
|
|
|
|
|
|
{
|
|
|
|
|
|
TickStamina(db, d, nowUtc);
|
|
|
|
|
|
d.stamina += n;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
if (d.stamina >= StaminaMax(db, d)) d.staminaAt = SaveStore.Iso(nowUtc);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>보옥 gem_stamina_cost → 군량 gem_stamina_amount (gem_stamina_daily 회/일). null = 성공.</summary>
|
|
|
|
|
|
public static string BuyStaminaWithGem(OutGameData db, SaveData d, DateTime nowUtc)
|
|
|
|
|
|
{
|
|
|
|
|
|
var e = db.Economy;
|
|
|
|
|
|
RollDaily(d, nowUtc);
|
|
|
|
|
|
if (d.adsUsed.gemStamina >= e.GemStaminaDaily) return "오늘 구매 " + e.GemStaminaDaily + "회를 모두 썼습니다";
|
|
|
|
|
|
if (d.gem < e.GemStaminaCost) return "보옥 부족 " + d.gem + " / " + e.GemStaminaCost;
|
|
|
|
|
|
d.gem -= e.GemStaminaCost;
|
|
|
|
|
|
d.adsUsed.gemStamina++;
|
|
|
|
|
|
AddStamina(db, d, e.GemStaminaAmount, nowUtc);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>광고 보상 군량 +ad_stamina (ad_stamina_daily 회/일) — 광고 SDK 미연동 스텁 = 즉시 지급.</summary>
|
|
|
|
|
|
public static string AdStamina(OutGameData db, SaveData d, DateTime nowUtc)
|
|
|
|
|
|
{
|
|
|
|
|
|
var e = db.Economy;
|
|
|
|
|
|
RollDaily(d, nowUtc);
|
|
|
|
|
|
if (d.adsUsed.stamina >= e.AdStaminaDaily) return "오늘 광고 " + e.AdStaminaDaily + "회를 모두 봤습니다";
|
|
|
|
|
|
d.adsUsed.stamina++;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
AddStamina(db, d, e.AdStamina + Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.AdFood)), nowUtc);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 스테이지 보상(§6) ----------
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 골드 = gold_base + stage × gold_per_stage(첫 클리어 × first_clear_gold_mult · 반복 × repeat_reward_mult) · 경험 = 출전 전원 exp_base(반복 ×0.5) ·
|
|
|
|
|
|
/// 보옥 = 첫 클리어 gem_first + 보스 gem_boss · 장비 1개 = gear_drop_pct(보스 boss_gear_drop_pct) · 등급 ≤ gear_grade_max · drop_from_chapter ≤ 챕터 ·
|
|
|
|
|
|
/// 별 = 클리어 · 전원 생존 · star_turns 턴 이내. 소탕 = 반복 보상(별 조건 없음).
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static StageReward Compute(OutGameData db, SaveData d, BalanceConfig.StageDef s, bool win, bool allAlive, int turns, bool sweep, System.Random rng)
|
|
|
|
|
|
{
|
|
|
|
|
|
var e = db.Economy;
|
|
|
|
|
|
var row = db.Rewards.For(s.chapter);
|
|
|
|
|
|
var st = d.Stage(s.chapter, s.stage, false);
|
|
|
|
|
|
var r = new StageReward
|
|
|
|
|
|
{
|
|
|
|
|
|
win = win, sweep = sweep, chapter = s.chapter, stage = s.stage, boss = s.kind == StageKind.Boss,
|
|
|
|
|
|
first = !sweep && (st == null || st.clears == 0), turns = turns,
|
|
|
|
|
|
starTurns = row != null ? row.starTurns : 0, bestStars = st != null ? st.stars : 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (!win || row == null) return r;
|
|
|
|
|
|
float baseGold = row.goldBase + s.stage * e.GoldPerStage;
|
|
|
|
|
|
float mult = r.first ? e.FirstClearGoldMult : e.RepeatRewardMult;
|
2026-11-05 15:22:54 +00:00
|
|
|
|
// 개발 효과(PD #1093 C) = 골드 획득 · 경험 획득 · 첫 클리어 보옥 · 보옥 보너스 · 장비 드랍(%p) — 개발 0 이면 A 단계 값 그대로
|
|
|
|
|
|
r.gold = Mathf.RoundToInt(baseGold * mult * DevRules.Mult(db, d, DevRules.GoldGain));
|
|
|
|
|
|
r.exp = Mathf.RoundToInt(row.expBase * (r.first ? 1f : e.RepeatRewardMult) * DevRules.Mult(db, d, DevRules.ExpGain));
|
|
|
|
|
|
r.gem = r.first ? Mathf.RoundToInt((row.gemFirst + (r.boss ? row.gemBoss : 0)) * DevRules.Mult(db, d, DevRules.FirstClearGem) * DevRules.Mult(db, d, DevRules.GemBonus)) : 0;
|
|
|
|
|
|
float drop = (r.boss ? e.BossGearDropPct : row.gearDropPct) + DevRules.Mod(db, d, DevRules.GearDrop) / 100f;
|
2026-11-05 12:11:10 +00:00
|
|
|
|
if (rng.NextDouble() < drop) r.gearId = RollGear(db, s.chapter, row.gearGradeMax, rng);
|
2026-11-05 15:22:54 +00:00
|
|
|
|
if (db.Chests != null && rng.NextDouble() < e.StageChestPct) r.chest = TreasureRules.RollTier(db, d, e.StageChest, rng);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
if (!sweep)
|
|
|
|
|
|
{
|
|
|
|
|
|
r.conds[0] = true;
|
|
|
|
|
|
r.conds[1] = allAlive;
|
|
|
|
|
|
r.conds[2] = turns <= row.starTurns;
|
|
|
|
|
|
foreach (var c in r.conds)
|
|
|
|
|
|
if (c) r.stars++;
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static string RollGear(OutGameData db, int chapter, string gradeMax, System.Random rng)
|
|
|
|
|
|
{
|
|
|
|
|
|
int maxOrder = db.Gear.OrderOf(gradeMax);
|
|
|
|
|
|
var pool = new List<string>();
|
|
|
|
|
|
foreach (var it in db.Gear.items)
|
|
|
|
|
|
if (it.order <= maxOrder && it.dropFromChapter <= chapter) pool.Add(it.id);
|
|
|
|
|
|
return pool.Count > 0 ? pool[rng.Next(pool.Count)] : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>보상을 세이브에 넣는다(재화 · 경험 = expTo 전원 · 장비 · 스테이지 기록 · 클리어 최고 챕터).</summary>
|
|
|
|
|
|
public static void Apply(SaveData d, StageReward r, IEnumerable<string> expTo)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!r.win) return;
|
|
|
|
|
|
d.gold += r.gold;
|
|
|
|
|
|
d.gem += r.gem;
|
|
|
|
|
|
if (expTo != null)
|
|
|
|
|
|
foreach (var id in expTo)
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = d.Level(id);
|
|
|
|
|
|
if (s == null || r.expTo.Contains(id)) continue;
|
|
|
|
|
|
HeroRules.GrantExp(s, r.exp);
|
|
|
|
|
|
r.expTo.Add(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!string.IsNullOrEmpty(r.gearId)) d.inv.AddGear(r.gearId, 1);
|
2026-11-05 15:22:54 +00:00
|
|
|
|
if (!string.IsNullOrEmpty(r.chest)) Counts.Add(d.inv.chests, r.chest, 1);
|
2026-11-05 12:11:10 +00:00
|
|
|
|
var st = d.Stage(r.chapter, r.stage, true);
|
|
|
|
|
|
st.clears++;
|
|
|
|
|
|
st.stars = Mathf.Max(st.stars, r.stars);
|
|
|
|
|
|
r.bestStars = st.stars;
|
|
|
|
|
|
d.chapterMax = Mathf.Max(d.chapterMax, r.chapter);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static int StarsOf(SaveData d, BalanceConfig.StageDef s)
|
|
|
|
|
|
{
|
|
|
|
|
|
var st = d.Stage(s.chapter, s.stage, false);
|
|
|
|
|
|
return st != null ? st.stars : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>소탕 = 별 3 스테이지만(최대 별 = 조건 수).</summary>
|
|
|
|
|
|
public static bool CanSweep(SaveData d, BalanceConfig.StageDef s)
|
|
|
|
|
|
{
|
|
|
|
|
|
return StarsOf(d, s) >= 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 골드 장비 상점 ----------
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>날짜가 바뀌었거나 비었으면 shop_grades 장비 shop_slots 종(겹침 없음 · 가격 = sell_gold × shop_price_mult)을 새로 깐다.</summary>
|
|
|
|
|
|
public static void EnsureShop(OutGameData db, SaveData d, DateTime nowUtc, System.Random rng)
|
|
|
|
|
|
{
|
|
|
|
|
|
var day = DayKey(nowUtc);
|
|
|
|
|
|
if (d.shop.day == day && d.shop.items.Count > 0) return;
|
|
|
|
|
|
d.shop.day = day;
|
|
|
|
|
|
d.shop.refreshes = 0;
|
|
|
|
|
|
Restock(db, d, rng);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void Restock(OutGameData db, SaveData d, System.Random rng)
|
|
|
|
|
|
{
|
|
|
|
|
|
var e = db.Economy;
|
|
|
|
|
|
var grades = new HashSet<string>(e.ShopGrades);
|
|
|
|
|
|
var pool = new List<GearDb.Item>();
|
|
|
|
|
|
foreach (var it in db.Gear.items)
|
|
|
|
|
|
if (grades.Contains(it.grade)) pool.Add(it);
|
|
|
|
|
|
d.shop.items.Clear();
|
|
|
|
|
|
for (int i = 0; i < e.ShopSlots && pool.Count > 0; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int k = rng.Next(pool.Count);
|
|
|
|
|
|
var it = pool[k];
|
|
|
|
|
|
pool.RemoveAt(k);
|
2026-11-05 15:22:54 +00:00
|
|
|
|
float discount = Mathf.Max(0f, 1f - DevRules.Mod(db, d, DevRules.TradeDiscount) / 100f); // 상업 「거래 할인」(PD #1093 C)
|
|
|
|
|
|
d.shop.items.Add(new ShopItem { gearId = it.id, price = Mathf.RoundToInt(it.sellGold * e.ShopPriceMult * discount), sold = false });
|
2026-11-05 12:11:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>보옥 shop_refresh_gem 으로 다시 깔기. null = 성공.</summary>
|
|
|
|
|
|
public static string RefreshShop(OutGameData db, SaveData d, System.Random rng)
|
|
|
|
|
|
{
|
2026-11-05 15:22:54 +00:00
|
|
|
|
int cost = FreeRefreshesLeft(db, d) > 0 ? 0 : db.Economy.ShopRefreshGem; // 시장 개설 = 하루 무료 새로고침(PD #1093 C)
|
2026-11-05 12:11:10 +00:00
|
|
|
|
if (d.gem < cost) return "보옥 부족 " + d.gem + " / " + cost;
|
|
|
|
|
|
d.gem -= cost;
|
|
|
|
|
|
d.shop.refreshes++;
|
|
|
|
|
|
Restock(db, d, rng);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 15:22:54 +00:00
|
|
|
|
/// <summary>오늘 남은 무료 새로고침(상업 「시장 개설」 Lv · 상점 refreshes = 그날 새로고침 수).</summary>
|
|
|
|
|
|
public static int FreeRefreshesLeft(OutGameData db, SaveData d)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Mathf.Max(0, Mathf.RoundToInt(DevRules.Mod(db, d, DevRules.TradeFreeRefresh)) - d.shop.refreshes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:11:10 +00:00
|
|
|
|
public static string BuyShop(SaveData d, int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (index < 0 || index >= d.shop.items.Count) return "없는 칸";
|
|
|
|
|
|
var it = d.shop.items[index];
|
|
|
|
|
|
if (it.sold) return "품절";
|
|
|
|
|
|
if (d.gold < it.price) return "골드 부족 " + d.gold + " / " + it.price;
|
|
|
|
|
|
d.gold -= it.price;
|
|
|
|
|
|
it.sold = true;
|
|
|
|
|
|
d.inv.AddGear(it.gearId, 1);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|