Merge codex/bandit-intangible-pr
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1069,6 +1069,7 @@
|
|||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "피해를 10 줍니다. 치명타라면, 카드 보상을 추가로 얻습니다. 소멸.",
|
"desc": "피해를 10 줍니다. 치명타라면, 카드 보상을 추가로 얻습니다. 소멸.",
|
||||||
"damage": 10,
|
"damage": 10,
|
||||||
|
"rewardOnKill": 1,
|
||||||
"image": "b1360ed0c4b942309d240634b8f36872"
|
"image": "b1360ed0c4b942309d240634b8f36872"
|
||||||
},
|
},
|
||||||
"Murder": {
|
"Murder": {
|
||||||
@@ -1164,6 +1165,7 @@
|
|||||||
"rarity": "legend",
|
"rarity": "legend",
|
||||||
"desc": "이번 턴에 다음에 사용하는 스킬 카드가 1번 추가로 사용됩니다.",
|
"desc": "이번 턴에 다음에 사용하는 스킬 카드가 1번 추가로 사용됩니다.",
|
||||||
"draw": 1,
|
"draw": 1,
|
||||||
|
"nextSkillRepeatCount": 1,
|
||||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||||
},
|
},
|
||||||
"KnifeTrap": {
|
"KnifeTrap": {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## 구현됨
|
## 구현됨
|
||||||
|
|
||||||
`Neutralize`, `SilentStrike`, `Survivor`, `SilentDefend`, `Slice`, `DaggerSpray`, `DaggerThrow`, `PoisonedStab`, `SuckerPunch`, `LeadingStrike`, `FollowThrough`, `FlickFlack`, `Prepared`, `Deflect`, `BladeDance`, `Backflip`, `DodgeAndRoll`, `CloakAndDagger`, `DeadlyPoison`, `Snakebite`, `Untouchable`, `Backstab`, `PreciseCut`, `Finisher`, `MementoMori`, `Flechettes`, `Dash`, `Predator`, `CalculatedGamble`, `HiddenDaggers`, `Acrobatics`, `Blur`, `LegSweep`, `Reflex`, `Haze`, `Tactician`, `WellLaidPlans`, `InfiniteBlades`, `Footwork`, `GrandFinale`, `Adrenaline`, `ShadowStep`, `Assassinate`, `Nightmare`, `ToolsOfTheTrade`, `Afterimage`, `StormOfSteel`, `Abrasive`, `Suppress`, `Expertise`, `Shadowmeld`, `Pounce`, `Pinpoint`
|
`Neutralize`, `SilentStrike`, `Survivor`, `SilentDefend`, `Slice`, `DaggerSpray`, `DaggerThrow`, `PoisonedStab`, `SuckerPunch`, `LeadingStrike`, `FollowThrough`, `FlickFlack`, `Prepared`, `Deflect`, `BladeDance`, `Backflip`, `DodgeAndRoll`, `CloakAndDagger`, `DeadlyPoison`, `Snakebite`, `Untouchable`, `Backstab`, `PreciseCut`, `Finisher`, `MementoMori`, `Flechettes`, `Dash`, `Predator`, `CalculatedGamble`, `HiddenDaggers`, `Acrobatics`, `Blur`, `LegSweep`, `Reflex`, `Haze`, `Tactician`, `WellLaidPlans`, `InfiniteBlades`, `Footwork`, `GrandFinale`, `Adrenaline`, `ShadowStep`, `Assassinate`, `Nightmare`, `ToolsOfTheTrade`, `Afterimage`, `Burst`, `StormOfSteel`, `Abrasive`, `Suppress`, `Expertise`, `Shadowmeld`, `Pounce`, `Pinpoint`
|
||||||
|
|
||||||
공용 메모:
|
공용 메모:
|
||||||
|
|
||||||
@@ -73,8 +73,6 @@
|
|||||||
|
|
||||||
`BladeOfInk`: 전용 표창 생성
|
`BladeOfInk`: 전용 표창 생성
|
||||||
|
|
||||||
`Burst`: 다음 스킬 1회 추가 사용
|
|
||||||
|
|
||||||
`KnifeTrap`: 소멸된 표창 전부 사용
|
`KnifeTrap`: 소멸된 표창 전부 사용
|
||||||
|
|
||||||
`BulletTime`: 드로우 금지 + 손패 무료 사용
|
`BulletTime`: 드로우 금지 + 손패 무료 사용
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
- `nextTurnCopies`: 다음 턴에 손패에서 가져올 복사본 수
|
- `nextTurnCopies`: 다음 턴에 손패에서 가져올 복사본 수
|
||||||
- `nextTurnSelectHandCard`: 현재 손패에서 카드 1장 선택
|
- `nextTurnSelectHandCard`: 현재 손패에서 카드 1장 선택
|
||||||
- `nextTurnSelectPrompt`: 선택 UI 문구
|
- `nextTurnSelectPrompt`: 선택 UI 문구
|
||||||
|
- `nextSkillRepeatCount`: 다음 스킬 카드의 효과를 추가 횟수만큼 다시 적용
|
||||||
- `nextSkillCostZero`: 다음 스킬 카드 비용을 0으로 만듦
|
- `nextSkillCostZero`: 다음 스킬 카드 비용을 0으로 만듦
|
||||||
- `skillCostReductionThisTurn`: 이번 턴 스킬 카드 비용을 일정량 감소
|
- `skillCostReductionThisTurn`: 이번 턴 스킬 카드 비용을 일정량 감소
|
||||||
|
|
||||||
|
|||||||
12
docs/next-skill-repeat.md
Normal file
12
docs/next-skill-repeat.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Next Skill Repeat
|
||||||
|
|
||||||
|
`nextSkillRepeatCount`는 다음에 사용하는 스킬 카드의 효과를 추가 횟수만큼 다시 적용하는 공용 필드입니다.
|
||||||
|
|
||||||
|
현재 구현은 카드가 발동할 때 이 수치를 전역 상태에 누적해 두고, 다음 스킬 카드가 실제로 사용되면 그 효과를 같은 카드에 대해 다시 한 번 이상 적용합니다. 카드 종류는 고정하지 않았기 때문에, 같은 필드를 다른 카드에도 그대로 붙일 수 있습니다.
|
||||||
|
|
||||||
|
예시:
|
||||||
|
|
||||||
|
- `Burst`
|
||||||
|
- `nextSkillRepeatCount = 1`
|
||||||
|
- 다음 스킬을 한 번 더 적용
|
||||||
|
|
||||||
5
docs/reward-on-kill.md
Normal file
5
docs/reward-on-kill.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 처치 보상
|
||||||
|
|
||||||
|
`rewardOnKill`은 해당 카드가 적을 처치했을 때 전투 보상 화면을 한 번 더 이어서 보여주는 공용 필드입니다. 현재 보상 UI는 3장 선택을 유지하고, 보상 화면만 추가로 한 번 더 열립니다.
|
||||||
|
|
||||||
|
`TheHunt`는 이 규칙을 사용합니다. 같은 패턴이 필요한 다른 카드에도 그대로 붙일 수 있습니다.
|
||||||
@@ -153,12 +153,15 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
let handCostZeroThisTurn = false;
|
let handCostZeroThisTurn = false;
|
||||||
let drawDisabledThisTurn = false;
|
let drawDisabledThisTurn = false;
|
||||||
let nextSkillCostZero = false;
|
let nextSkillCostZero = false;
|
||||||
|
let nextSkillRepeatCount = 0;
|
||||||
let skillCostReductionThisTurn = 0;
|
let skillCostReductionThisTurn = 0;
|
||||||
let nextTurnBlock = 0, nextTurnDraw = 0, nextTurnKeepBlock = false;
|
let nextTurnBlock = 0, nextTurnDraw = 0, nextTurnKeepBlock = false;
|
||||||
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
|
let nextTurnAttackMultiplier = 1, turnAttackMultiplier = 1;
|
||||||
let nextTurnAddCards = [];
|
let nextTurnAddCards = [];
|
||||||
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
|
let turnAttackCardsPlayed = 0, turnDiscardedCards = 0;
|
||||||
let cardsDrawnThisCombat = 0;
|
let cardsDrawnThisCombat = 0;
|
||||||
|
let bonusRewardScreens = 0;
|
||||||
|
let activeKillReward = 0;
|
||||||
let energy = 0;
|
let energy = 0;
|
||||||
const powers = [];
|
const powers = [];
|
||||||
const mob = monsters.map((m) => ({
|
const mob = monsters.map((m) => ({
|
||||||
@@ -279,6 +282,7 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
let blockGained = 0;
|
let blockGained = 0;
|
||||||
if (c.blockGainMultiplier && c.blockGainMultiplier > 0) blockGainMultiplier *= c.blockGainMultiplier;
|
if (c.blockGainMultiplier && c.blockGainMultiplier > 0) blockGainMultiplier *= c.blockGainMultiplier;
|
||||||
if (c.nextSkillCostZero === true) nextSkillCostZero = true;
|
if (c.nextSkillCostZero === true) nextSkillCostZero = true;
|
||||||
|
if (c.nextSkillRepeatCount && c.nextSkillRepeatCount > 0) nextSkillRepeatCount += c.nextSkillRepeatCount;
|
||||||
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
|
if (c.skillCostReductionThisTurn && c.skillCostReductionThisTurn > 0) skillCostReductionThisTurn += c.skillCostReductionThisTurn;
|
||||||
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
|
if (c.handCostZeroThisTurn === true) handCostZeroThisTurn = true;
|
||||||
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
|
if (c.drawDisabledThisTurn === true) drawDisabledThisTurn = true;
|
||||||
@@ -303,7 +307,10 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
m2.hp = r2.hp; m2.block = r2.block;
|
m2.hp = r2.hp; m2.block = r2.block;
|
||||||
const attackPoison = powerFieldTotal('attackPoison');
|
const attackPoison = powerFieldTotal('attackPoison');
|
||||||
if (d2 > 0 && attackPoison > 0) m2.poison += attackPoison;
|
if (d2 > 0 && attackPoison > 0) m2.poison += attackPoison;
|
||||||
if (m2.hp <= 0) m2.alive = false;
|
if (m2.hp <= 0) {
|
||||||
|
m2.alive = false;
|
||||||
|
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
|
||||||
@@ -316,7 +323,10 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
}
|
}
|
||||||
const attackPoison = powerFieldTotal('attackPoison');
|
const attackPoison = powerFieldTotal('attackPoison');
|
||||||
if (dmg > 0 && attackPoison > 0) target.poison += attackPoison;
|
if (dmg > 0 && attackPoison > 0) target.poison += attackPoison;
|
||||||
if (target.hp <= 0) target.alive = false;
|
if (target.hp <= 0) {
|
||||||
|
target.alive = false;
|
||||||
|
if (c.rewardOnKill) bonusRewardScreens += c.rewardOnKill;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (c.block) blockGained = addBlock(c.block);
|
if (c.block) blockGained = addBlock(c.block);
|
||||||
@@ -338,6 +348,7 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (c.selfVuln) pVuln += c.selfVuln;
|
if (c.selfVuln) pVuln += c.selfVuln;
|
||||||
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
if (c.heal) pHp = Math.min(pHp + c.heal, PLAYER_HP);
|
||||||
if (c.gainEnergy) energy += c.gainEnergy;
|
if (c.gainEnergy) energy += c.gainEnergy;
|
||||||
|
activeKillReward = c.rewardOnKill || 0;
|
||||||
if (c.intangible) pIntangible += c.intangible;
|
if (c.intangible) pIntangible += c.intangible;
|
||||||
queueNextTurnEffects(c);
|
queueNextTurnEffects(c);
|
||||||
let drawnCards = [];
|
let drawnCards = [];
|
||||||
@@ -451,20 +462,66 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (idx < 0) break;
|
if (idx < 0) break;
|
||||||
const id = hand[idx], c = cards[id];
|
const id = hand[idx], c = cards[id];
|
||||||
const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
|
const skillFree = c.kind === 'Skill' && nextSkillCostZero === true;
|
||||||
|
const skillRepeat = c.kind === 'Skill' ? nextSkillRepeatCount : 0;
|
||||||
const baseCost = c.cost || 0;
|
const baseCost = c.cost || 0;
|
||||||
const cost = handCostZeroThisTurn === true ? 0 : (c.useAllEnergy === true ? energy : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost)));
|
const cost = handCostZeroThisTurn === true ? 0 : (c.useAllEnergy === true ? energy : (skillFree ? 0 : (c.kind === 'Skill' ? Math.max(0, baseCost - skillCostReductionThisTurn) : baseCost)));
|
||||||
energy -= cost;
|
energy -= cost;
|
||||||
resolveCardEffects(id, c, cost);
|
resolveCardEffects(id, c, cost);
|
||||||
if (c.kind === 'Attack') turnAttackCardsPlayed++;
|
|
||||||
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
|
|
||||||
const playedBlock = powerFieldTotal('cardPlayedBlock');
|
const playedBlock = powerFieldTotal('cardPlayedBlock');
|
||||||
if (playedBlock > 0) addBlock(playedBlock);
|
if (playedBlock > 0) addBlock(playedBlock);
|
||||||
|
if (c.cardPlayedDamage && alive.length) {
|
||||||
|
const target = chooseTarget(aliveList(), 0);
|
||||||
|
if (target && target.alive) {
|
||||||
|
target.hp -= c.cardPlayedDamage;
|
||||||
|
dmg += c.cardPlayedDamage;
|
||||||
|
if (target.hp <= 0) target.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.cardPlayedRandomDamage && alive.length) {
|
||||||
|
const pool = aliveList();
|
||||||
|
if (pool.length) {
|
||||||
|
const target = pool[Math.floor(rng() * pool.length)];
|
||||||
|
if (target) {
|
||||||
|
target.hp -= c.cardPlayedRandomDamage;
|
||||||
|
dmg += c.cardPlayedRandomDamage;
|
||||||
|
if (target.hp <= 0) target.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skillRepeat > 0) {
|
||||||
|
nextSkillRepeatCount = Math.max(0, nextSkillRepeatCount - skillRepeat);
|
||||||
|
for (let r = 0; r < skillRepeat; r++) {
|
||||||
|
resolveCardEffects(id, c, cost);
|
||||||
|
if (playedBlock > 0) addBlock(playedBlock);
|
||||||
|
if (c.cardPlayedDamage && alive.length) {
|
||||||
|
const target = chooseTarget(aliveList(), 0);
|
||||||
|
if (target && target.alive) {
|
||||||
|
target.hp -= c.cardPlayedDamage;
|
||||||
|
dmg += c.cardPlayedDamage;
|
||||||
|
if (target.hp <= 0) target.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.cardPlayedRandomDamage && alive.length) {
|
||||||
|
const pool = aliveList();
|
||||||
|
if (pool.length) {
|
||||||
|
const target = pool[Math.floor(rng() * pool.length)];
|
||||||
|
if (target) {
|
||||||
|
target.hp -= c.cardPlayedRandomDamage;
|
||||||
|
dmg += c.cardPlayedRandomDamage;
|
||||||
|
if (target.hp <= 0) target.alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c.kind === 'Attack') turnAttackCardsPlayed++;
|
||||||
|
if (skillFree === true && c.nextSkillCostZero !== true) nextSkillCostZero = false;
|
||||||
hand.splice(idx, 1);
|
hand.splice(idx, 1);
|
||||||
queueSelectedReserve(c);
|
queueSelectedReserve(c);
|
||||||
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
|
if (c.exhaust === true || String(c.desc || '').includes('소멸.')) exhaust.push(id);
|
||||||
else if (c.kind !== 'Power') discard.push(id);
|
else if (c.kind !== 'Power') discard.push(id);
|
||||||
applyDiscardEffects(c);
|
applyDiscardEffects(c);
|
||||||
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp };
|
if (aliveList().length === 0) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
|
||||||
}
|
}
|
||||||
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
|
// 화상(endTurnDamage) — 손패에 있으면 턴 종료 시 피해 (Lua EndPlayerTurn 동기화)
|
||||||
let burn = 0;
|
let burn = 0;
|
||||||
@@ -527,9 +584,9 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
if (pHp <= 0) return { win: false, turns, playerHpRemaining: 0 };
|
||||||
}
|
}
|
||||||
// 독 사망 등 적 페이즈 중 전멸 처리 (Lua FinishEnemyTurn→CheckCombatEnd 동기화)
|
// 독 사망 등 적 페이즈 중 전멸 처리 (Lua FinishEnemyTurn→CheckCombatEnd 동기화)
|
||||||
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp };
|
if (!mob.some((m) => m.alive)) return { win: true, turns, playerHpRemaining: pHp, bonusRewardScreens };
|
||||||
}
|
}
|
||||||
return { win: false, turns, playerHpRemaining: pHp, draw: true };
|
return { win: false, turns, playerHpRemaining: pHp, draw: true, bonusRewardScreens };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }
|
function mean(a) { return a.length ? a.reduce((s, x) => s + x, 0) / a.length : 0; }
|
||||||
|
|||||||
@@ -713,6 +713,28 @@ test("simulateCombat: nextSkillCostZero makes the next skill free", () => {
|
|||||||
assert.equal(r.playerHpRemaining, 80);
|
assert.equal(r.playerHpRemaining, 80);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: nextSkillRepeatCount repeats the next skill effect", () => {
|
||||||
|
const shared = {
|
||||||
|
cards: {
|
||||||
|
Burst: { name: "Burst", cost: 1, kind: "Skill", draw: 1, block: 5, nextSkillRepeatCount: 1 },
|
||||||
|
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Burst", "Guard"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 15 }] }],
|
||||||
|
};
|
||||||
|
const withBurst = simulateCombat(shared, () => 0.999999);
|
||||||
|
const withoutBurst = simulateCombat({
|
||||||
|
...shared,
|
||||||
|
cards: {
|
||||||
|
Burst: { name: "Burst", cost: 1, kind: "Skill", draw: 1, block: 5 },
|
||||||
|
Guard: shared.cards.Guard,
|
||||||
|
},
|
||||||
|
}, () => 0.999999);
|
||||||
|
assert.equal(withBurst.draw, true);
|
||||||
|
assert.equal(withBurst.playerHpRemaining, 80);
|
||||||
|
assert.ok(withBurst.playerHpRemaining > withoutBurst.playerHpRemaining);
|
||||||
|
});
|
||||||
|
|
||||||
test("chooseAction: skillCostReductionThisTurn allows discounted skills", () => {
|
test("chooseAction: skillCostReductionThisTurn allows discounted skills", () => {
|
||||||
const cards = {
|
const cards = {
|
||||||
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
|
Guard: { name: "Guard", cost: 2, kind: "Skill", block: 8 },
|
||||||
@@ -823,6 +845,19 @@ test("simulateCombat: cardPlayedRandomDamage hits a random enemy on card play",
|
|||||||
assert.equal(r.win, true);
|
assert.equal(r.win, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: rewardOnKill grants an extra reward screen when an attack kills", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
TheHunt: { name: "TheHunt", cost: 1, kind: "Attack", damage: 10, rewardOnKill: 1 },
|
||||||
|
},
|
||||||
|
starterDeck: ["TheHunt"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 10, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const r = simulateCombat(data, () => 0.999999);
|
||||||
|
assert.equal(r.win, true);
|
||||||
|
assert.equal(r.bonusRewardScreens, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test("simulateCombat: intangible cards reduce incoming damage and persist across turns", () => {
|
test("simulateCombat: intangible cards reduce incoming damage and persist across turns", () => {
|
||||||
const data = {
|
const data = {
|
||||||
cards: {
|
cards: {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ if self:CanPlayCardNow(c) ~= true then
|
|||||||
end
|
end
|
||||||
local cost = c.cost or 0
|
local cost = c.cost or 0
|
||||||
local skillFree = false
|
local skillFree = false
|
||||||
|
local skillRepeat = 0
|
||||||
if self.HandCostZeroThisTurn == true then
|
if self.HandCostZeroThisTurn == true then
|
||||||
cost = 0
|
cost = 0
|
||||||
elseif c.useAllEnergy == true then
|
elseif c.useAllEnergy == true then
|
||||||
@@ -58,12 +59,39 @@ end
|
|||||||
if c.kind == "Skill" and self.SkillCostReductionThisTurn ~= nil and self.SkillCostReductionThisTurn > 0 then
|
if c.kind == "Skill" and self.SkillCostReductionThisTurn ~= nil and self.SkillCostReductionThisTurn > 0 then
|
||||||
cost = math.max(0, cost - self.SkillCostReductionThisTurn)
|
cost = math.max(0, cost - self.SkillCostReductionThisTurn)
|
||||||
end
|
end
|
||||||
|
if c.kind == "Skill" and self.NextSkillRepeatCount ~= nil and self.NextSkillRepeatCount > 0 then
|
||||||
|
skillRepeat = self.NextSkillRepeatCount
|
||||||
|
end
|
||||||
if self.Energy < cost then
|
if self.Energy < cost then
|
||||||
self:Toast("에너지가 부족합니다")
|
self:Toast("에너지가 부족합니다")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.Energy = self.Energy - cost
|
self.Energy = self.Energy - cost
|
||||||
|
self.ActiveKillReward = c.rewardOnKill or 0
|
||||||
self:ResolveCardEffects(cardId, slot, c, false, cost)
|
self:ResolveCardEffects(cardId, slot, c, false, cost)
|
||||||
|
local function applyCardPlayHooks()
|
||||||
|
if self:HasPowerField("cardPlayedBlock") == true then
|
||||||
|
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
|
||||||
|
end
|
||||||
|
if c.cardPlayedDamage ~= nil and c.cardPlayedDamage > 0 then
|
||||||
|
self:DealDirectDamageToTarget(c.cardPlayedDamage)
|
||||||
|
end
|
||||||
|
if c.cardPlayedRandomDamage ~= nil and c.cardPlayedRandomDamage > 0 then
|
||||||
|
self:DealDirectDamageToRandomMonster(c.cardPlayedRandomDamage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
applyCardPlayHooks()
|
||||||
|
if skillRepeat > 0 then
|
||||||
|
local remaining = (self.NextSkillRepeatCount or 0) - skillRepeat
|
||||||
|
if remaining < 0 then
|
||||||
|
remaining = 0
|
||||||
|
end
|
||||||
|
self.NextSkillRepeatCount = remaining
|
||||||
|
for i = 1, skillRepeat do
|
||||||
|
self:ResolveCardEffects(cardId, slot, c, false, cost)
|
||||||
|
applyCardPlayHooks()
|
||||||
|
end
|
||||||
|
end
|
||||||
if c.kind == "Attack" then
|
if c.kind == "Attack" then
|
||||||
self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1
|
self.TurnAttackCardsPlayed = (self.TurnAttackCardsPlayed or 0) + 1
|
||||||
end
|
end
|
||||||
@@ -72,14 +100,8 @@ if skillFree == true then
|
|||||||
self.NextSkillCostZero = false
|
self.NextSkillCostZero = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if self:HasPowerField("cardPlayedBlock") == true then
|
if self.ActiveKillReward ~= nil and self.ActiveKillReward <= 0 then
|
||||||
self:AddCardBlock(self:AddPowerFieldTotal("cardPlayedBlock"))
|
self.ActiveKillReward = 0
|
||||||
end
|
|
||||||
if c.cardPlayedDamage ~= nil and c.cardPlayedDamage > 0 then
|
|
||||||
self:DealDirectDamageToTarget(c.cardPlayedDamage)
|
|
||||||
end
|
|
||||||
if c.cardPlayedRandomDamage ~= nil and c.cardPlayedRandomDamage > 0 then
|
|
||||||
self:DealDirectDamageToRandomMonster(c.cardPlayedRandomDamage)
|
|
||||||
end
|
end
|
||||||
table.remove(self.Hand, slot)
|
table.remove(self.Hand, slot)
|
||||||
if c.exhaust == true then
|
if c.exhaust == true then
|
||||||
@@ -235,7 +257,7 @@ end`, [
|
|||||||
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
|
||||||
]),
|
]),
|
||||||
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
|
||||||
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
||||||
if m == nil or m.alive ~= true then
|
if m == nil or m.alive ~= true then
|
||||||
m = nil
|
m = nil
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
@@ -243,7 +265,7 @@ if m == nil or m.alive ~= true then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if m == nil then
|
if m == nil then
|
||||||
return
|
return false
|
||||||
end
|
end
|
||||||
local dmg = amount
|
local dmg = amount
|
||||||
if m.vuln > 0 then
|
if m.vuln > 0 then
|
||||||
@@ -262,13 +284,16 @@ if dmg > 0 then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:MonsterHitMotion(m.slot)
|
self:MonsterHitMotion(m.slot)
|
||||||
|
local killed = false
|
||||||
if m.hp <= 0 then
|
if m.hp <= 0 then
|
||||||
m.hp = 0
|
m.hp = 0
|
||||||
self:KillMonster(m.slot)
|
self:KillMonster(m.slot)
|
||||||
end`, [
|
killed = true
|
||||||
|
end
|
||||||
|
return killed`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
|
||||||
]),
|
], 0, 'boolean'),
|
||||||
method('DealDirectDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
method('DealDirectDamageToTarget', `local m = self.Monsters[self.TargetIndex]
|
||||||
if m == nil or m.alive ~= true then
|
if m == nil or m.alive ~= true then
|
||||||
m = nil
|
m = nil
|
||||||
@@ -277,17 +302,20 @@ if m == nil or m.alive ~= true then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if m == nil then
|
if m == nil then
|
||||||
return
|
return false
|
||||||
end
|
end
|
||||||
m.hp = m.hp - amount
|
m.hp = m.hp - amount
|
||||||
self:ShowDmgPop(m.slot, amount)
|
self:ShowDmgPop(m.slot, amount)
|
||||||
self:MonsterHitMotion(m.slot)
|
self:MonsterHitMotion(m.slot)
|
||||||
|
local killed = false
|
||||||
if m.hp <= 0 then
|
if m.hp <= 0 then
|
||||||
m.hp = 0
|
m.hp = 0
|
||||||
self:KillMonster(m.slot)
|
self:KillMonster(m.slot)
|
||||||
end`, [
|
killed = true
|
||||||
|
end
|
||||||
|
return killed`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
]),
|
], 0, 'boolean'),
|
||||||
method('DealDirectDamageToRandomMonster', `local alive = {}
|
method('DealDirectDamageToRandomMonster', `local alive = {}
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
local m = self.Monsters[i]
|
local m = self.Monsters[i]
|
||||||
@@ -296,21 +324,24 @@ for i = 1, #self.Monsters do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #alive <= 0 then
|
if #alive <= 0 then
|
||||||
return
|
return false
|
||||||
end
|
end
|
||||||
local m = alive[math.random(1, #alive)]
|
local m = alive[math.random(1, #alive)]
|
||||||
if m == nil then
|
if m == nil then
|
||||||
return
|
return false
|
||||||
end
|
end
|
||||||
m.hp = m.hp - amount
|
m.hp = m.hp - amount
|
||||||
self:ShowDmgPop(m.slot, amount)
|
self:ShowDmgPop(m.slot, amount)
|
||||||
self:MonsterHitMotion(m.slot)
|
self:MonsterHitMotion(m.slot)
|
||||||
|
local killed = false
|
||||||
if m.hp <= 0 then
|
if m.hp <= 0 then
|
||||||
m.hp = 0
|
m.hp = 0
|
||||||
self:KillMonster(m.slot)
|
self:KillMonster(m.slot)
|
||||||
end`, [
|
killed = true
|
||||||
|
end
|
||||||
|
return killed`, [
|
||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
|
||||||
]),
|
], 0, 'boolean'),
|
||||||
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
|
method('PlayAttackFx', `local m = self.Monsters[targetIndex]
|
||||||
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
|
||||||
self:DealDamageToTarget(damage, pierce)
|
self:DealDamageToTarget(damage, pierce)
|
||||||
@@ -339,7 +370,11 @@ _TimerService:SetTimerOnce(function()
|
|||||||
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
|
||||||
shown = math.floor(damage * 1.5)
|
shown = math.floor(damage * 1.5)
|
||||||
end
|
end
|
||||||
self:DealDamageToTarget(damage, pierce)
|
local killed = self:DealDamageToTarget(damage, pierce)
|
||||||
|
if killed == true and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
|
||||||
|
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + self.ActiveKillReward
|
||||||
|
end
|
||||||
|
self.ActiveKillReward = 0
|
||||||
self:ShowDmgPop(targetIndex, shown)
|
self:ShowDmgPop(targetIndex, shown)
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()
|
self:CheckCombatEnd()
|
||||||
@@ -363,6 +398,7 @@ end
|
|||||||
_TimerService:SetTimerOnce(function()
|
_TimerService:SetTimerOnce(function()
|
||||||
if fx ~= nil then fx.Enable = false end
|
if fx ~= nil then fx.Enable = false end
|
||||||
self.FxBusy = false
|
self.FxBusy = false
|
||||||
|
local killCount = 0
|
||||||
for i = 1, #self.Monsters do
|
for i = 1, #self.Monsters do
|
||||||
local m = self.Monsters[i]
|
local m = self.Monsters[i]
|
||||||
if m ~= nil and m.alive == true then
|
if m ~= nil and m.alive == true then
|
||||||
@@ -387,9 +423,14 @@ _TimerService:SetTimerOnce(function()
|
|||||||
if m.hp <= 0 then
|
if m.hp <= 0 then
|
||||||
m.hp = 0
|
m.hp = 0
|
||||||
self:KillMonster(m.slot)
|
self:KillMonster(m.slot)
|
||||||
|
killCount = killCount + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if killCount > 0 and self.ActiveKillReward ~= nil and self.ActiveKillReward > 0 then
|
||||||
|
self.BonusRewardScreens = (self.BonusRewardScreens or 0) + (killCount * self.ActiveKillReward)
|
||||||
|
end
|
||||||
|
self.ActiveKillReward = 0
|
||||||
self:RenderCombat()
|
self:RenderCombat()
|
||||||
self:CheckCombatEnd()
|
self:CheckCombatEnd()
|
||||||
end, 0.35)`, [
|
end, 0.35)`, [
|
||||||
|
|||||||
@@ -382,6 +382,9 @@ end
|
|||||||
if c.nextSkillCostZero == true then
|
if c.nextSkillCostZero == true then
|
||||||
self.NextSkillCostZero = true
|
self.NextSkillCostZero = true
|
||||||
end
|
end
|
||||||
|
if c.nextSkillRepeatCount ~= nil and c.nextSkillRepeatCount > 0 then
|
||||||
|
self.NextSkillRepeatCount = (self.NextSkillRepeatCount or 0) + c.nextSkillRepeatCount
|
||||||
|
end
|
||||||
if c.skillCostReductionThisTurn ~= nil and c.skillCostReductionThisTurn > 0 then
|
if c.skillCostReductionThisTurn ~= nil and c.skillCostReductionThisTurn > 0 then
|
||||||
self.SkillCostReductionThisTurn = (self.SkillCostReductionThisTurn or 0) + c.skillCostReductionThisTurn
|
self.SkillCostReductionThisTurn = (self.SkillCostReductionThisTurn or 0) + c.skillCostReductionThisTurn
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ end`),
|
|||||||
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
|
||||||
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'cardId' },
|
||||||
]),
|
]),
|
||||||
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
method('PickReward', `if self.CombatOver ~= true or self.RunActive ~= true then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if slot ~= 0 and self.RewardChoices ~= nil then
|
if slot ~= 0 and self.RewardChoices ~= nil then
|
||||||
@@ -47,6 +47,11 @@ if slot ~= 0 and self.RewardChoices ~= nil then
|
|||||||
table.insert(self.RunDeck, id)
|
table.insert(self.RunDeck, id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if self.BonusRewardScreens ~= nil and self.BonusRewardScreens > 0 and slot ~= 0 then
|
||||||
|
self.BonusRewardScreens = self.BonusRewardScreens - 1
|
||||||
|
self:OfferReward()
|
||||||
|
return
|
||||||
|
end
|
||||||
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
|
local hud = _EntityService:GetEntityByPath("/ui/RunUIGroup/RewardHud")
|
||||||
if hud ~= nil then
|
if hud ~= nil then
|
||||||
hud.Enable = false
|
hud.Enable = false
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ self.CardsDrawnThisCombat = 0
|
|||||||
self.HandCostZeroThisTurn = false
|
self.HandCostZeroThisTurn = false
|
||||||
self.DrawDisabledThisTurn = false
|
self.DrawDisabledThisTurn = false
|
||||||
self.NextSkillCostZero = false
|
self.NextSkillCostZero = false
|
||||||
|
self.NextSkillRepeatCount = 0
|
||||||
self.SkillCostReductionThisTurn = 0
|
self.SkillCostReductionThisTurn = 0
|
||||||
self.PlayerStr = 0
|
self.PlayerStr = 0
|
||||||
self.PlayerDex = 0
|
self.PlayerDex = 0
|
||||||
@@ -79,6 +80,8 @@ self.PlayerThorns = 0
|
|||||||
self.PlayerWeak = 0
|
self.PlayerWeak = 0
|
||||||
self.PlayerVuln = 0
|
self.PlayerVuln = 0
|
||||||
self.PlayerIntangible = 0
|
self.PlayerIntangible = 0
|
||||||
|
self.BonusRewardScreens = 0
|
||||||
|
self.ActiveKillReward = 0
|
||||||
self.PlayerPowers = {}
|
self.PlayerPowers = {}
|
||||||
self.FightAttackCount = 0
|
self.FightAttackCount = 0
|
||||||
self.TurnAttackCardsPlayed = 0
|
self.TurnAttackCardsPlayed = 0
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ function luaCardsTable(cards) {
|
|||||||
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`);
|
if (c.damagePerTurn != null) fields.push(`damagePerTurn = ${c.damagePerTurn}`);
|
||||||
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
|
if (c.cardPlayedDamage != null) fields.push(`cardPlayedDamage = ${c.cardPlayedDamage}`);
|
||||||
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
|
if (c.cardPlayedRandomDamage != null) fields.push(`cardPlayedRandomDamage = ${c.cardPlayedRandomDamage}`);
|
||||||
|
if (c.rewardOnKill != null) fields.push(`rewardOnKill = ${c.rewardOnKill}`);
|
||||||
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
|
if (c.intangible != null) fields.push(`intangible = ${c.intangible}`);
|
||||||
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
|
if (c.endTurnDexLoss != null) fields.push(`endTurnDexLoss = ${c.endTurnDexLoss}`);
|
||||||
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`);
|
if (c.poisonPerTurn != null) fields.push(`poisonPerTurn = ${c.poisonPerTurn}`);
|
||||||
@@ -214,6 +215,7 @@ function luaCardsTable(cards) {
|
|||||||
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
|
if (c.nextTurnCopies != null) fields.push(`nextTurnCopies = ${c.nextTurnCopies}`);
|
||||||
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
|
if (c.nextTurnSelectHandCard === true) fields.push('nextTurnSelectHandCard = true');
|
||||||
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
|
if (c.nextTurnSelectPrompt != null) fields.push(`nextTurnSelectPrompt = ${luaStr(c.nextTurnSelectPrompt)}`);
|
||||||
|
if (c.nextSkillRepeatCount != null) fields.push(`nextSkillRepeatCount = ${c.nextSkillRepeatCount}`);
|
||||||
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
|
if (c.nextSkillCostZero === true) fields.push('nextSkillCostZero = true');
|
||||||
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`);
|
if (c.skillCostReductionThisTurn != null) fields.push(`skillCostReductionThisTurn = ${c.skillCostReductionThisTurn}`);
|
||||||
if (c.innate === true) fields.push('innate = true');
|
if (c.innate === true) fields.push('innate = true');
|
||||||
|
|||||||
Reference in New Issue
Block a user