카드 설명 키워드 하이라이트 추가
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -803,8 +803,8 @@
|
|||||||
"class": "bandit",
|
"class": "bandit",
|
||||||
"rarity": "unique",
|
"rarity": "unique",
|
||||||
"desc": "카드를 1장 뽑습니다. 뽑은 카드가 스킬 카드라면, 방어도를 3 얻습니다.",
|
"desc": "카드를 1장 뽑습니다. 뽑은 카드가 스킬 카드라면, 방어도를 3 얻습니다.",
|
||||||
"block": 3,
|
|
||||||
"draw": 1,
|
"draw": 1,
|
||||||
|
"drawSkillBlock": 3,
|
||||||
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
"image": "91a2d1c16cb041549adbf1a0d7b1f37f"
|
||||||
},
|
},
|
||||||
"Acrobatics": {
|
"Acrobatics": {
|
||||||
|
|||||||
22
docs/draw-skill-block.md
Normal file
22
docs/draw-skill-block.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 드로우 연동 효과
|
||||||
|
|
||||||
|
드로우 결과를 받아 후속 효과를 처리하는 공용 패턴을 정리합니다.
|
||||||
|
|
||||||
|
## 현재 구현
|
||||||
|
|
||||||
|
- `draw`: 카드를 뽑음
|
||||||
|
- `drawUntilHandSize`: 손패가 지정 수치가 될 때까지 뽑음
|
||||||
|
- `drawSkillBlock`: 이번 카드로 뽑힌 카드 중 스킬 카드마다 방어도를 얻음
|
||||||
|
|
||||||
|
## 동작 방식
|
||||||
|
|
||||||
|
- 드로우 함수는 이번에 뽑힌 카드 ID 목록을 반환합니다.
|
||||||
|
- 카드 효과는 그 목록을 보고 조건을 판정합니다.
|
||||||
|
- 그래서 `EscapePlan` 같은 카드뿐 아니라, 나중에 같은 규칙이 필요한 카드에도 같은 필드를 붙이면 됩니다.
|
||||||
|
|
||||||
|
## 예시
|
||||||
|
|
||||||
|
- `EscapePlan`
|
||||||
|
- `draw = 1`
|
||||||
|
- `drawSkillBlock = 3`
|
||||||
|
|
||||||
@@ -161,16 +161,19 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
let turns = 0;
|
let turns = 0;
|
||||||
|
|
||||||
function draw(n) {
|
function draw(n) {
|
||||||
|
const drawn = [];
|
||||||
for (let k = 0; k < n; k++) {
|
for (let k = 0; k < n; k++) {
|
||||||
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
|
if (drawPile.length === 0) { drawPile = shuffle(discard, rng); discard = []; }
|
||||||
if (drawPile.length === 0) break;
|
if (drawPile.length === 0) break;
|
||||||
const card = drawPile.pop();
|
const card = drawPile.pop();
|
||||||
|
drawn.push(card);
|
||||||
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
|
// 손패 10장 상한 — 초과 드로는 자동 버림 (Lua DrawCards 동기화)
|
||||||
if (hand.length >= 10) {
|
if (hand.length >= 10) {
|
||||||
discard.push(card);
|
discard.push(card);
|
||||||
triggerSly(card);
|
triggerSly(card);
|
||||||
} else hand.push(card);
|
} else hand.push(card);
|
||||||
}
|
}
|
||||||
|
return drawn;
|
||||||
}
|
}
|
||||||
function addCardsToHand(id, n) {
|
function addCardsToHand(id, n) {
|
||||||
for (let k = 0; k < n; k++) {
|
for (let k = 0; k < n; k++) {
|
||||||
@@ -318,10 +321,16 @@ export function simulateCombat(data, rng, stats) {
|
|||||||
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;
|
||||||
queueNextTurnEffects(c);
|
queueNextTurnEffects(c);
|
||||||
if (c.draw) draw(c.draw);
|
let drawnCards = [];
|
||||||
|
if (c.draw) drawnCards = drawnCards.concat(draw(c.draw));
|
||||||
if (c.drawUntilHandSize) {
|
if (c.drawUntilHandSize) {
|
||||||
const need = c.drawUntilHandSize - Math.max(0, hand.length - 1);
|
const need = c.drawUntilHandSize - Math.max(0, hand.length - 1);
|
||||||
if (need > 0) draw(need);
|
if (need > 0) drawnCards = drawnCards.concat(draw(need));
|
||||||
|
}
|
||||||
|
if (c.drawSkillBlock && c.drawSkillBlock > 0) {
|
||||||
|
for (const drawnId of drawnCards) {
|
||||||
|
if (cards[drawnId]?.kind === 'Skill') blockGained += addBlock(c.drawSkillBlock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
|
if (c.addShiv && !c.discard && c.discardAll !== true) addCardsToHand('Shiv', c.addShiv);
|
||||||
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
|
if (recordStats && stats) stats[id] = bump(stats[id], costSpent, dmg, blockGained);
|
||||||
|
|||||||
@@ -720,3 +720,22 @@ test("chooseAction: skillCostReductionThisTurn allows discounted skills", () =>
|
|||||||
assert.equal(chooseAction(["Guard"], cards, 1, { skillCostReductionThisTurn: 1 }), 0);
|
assert.equal(chooseAction(["Guard"], cards, 1, { skillCostReductionThisTurn: 1 }), 0);
|
||||||
assert.equal(chooseAction(["Guard"], cards, 1, {}), -1);
|
assert.equal(chooseAction(["Guard"], cards, 1, {}), -1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("simulateCombat: drawSkillBlock grants block for each drawn skill", () => {
|
||||||
|
const data = {
|
||||||
|
cards: {
|
||||||
|
Escape: { name: "EscapePlan", cost: 0, kind: "Skill", draw: 1, drawSkillBlock: 3, innate: true, exhaust: true },
|
||||||
|
Filler1: { name: "Filler1", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler2: { name: "Filler2", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler3: { name: "Filler3", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler4: { name: "Filler4", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
Filler5: { name: "Filler5", cost: 99, kind: "Skill", block: 0 },
|
||||||
|
},
|
||||||
|
starterDeck: ["Escape", "Filler1", "Filler2", "Filler3", "Filler4", "Filler5"],
|
||||||
|
monsters: [{ name: "Dummy", maxHp: 9999, intents: [{ kind: "Attack", value: 0 }] }],
|
||||||
|
};
|
||||||
|
const stats = {};
|
||||||
|
const r = simulateCombat(data, () => 0.999999, stats);
|
||||||
|
assert.equal(r.draw, true);
|
||||||
|
assert.equal(stats.Escape.block, 3);
|
||||||
|
});
|
||||||
|
|||||||
@@ -415,7 +415,8 @@ if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
|
|||||||
self:RenderHand(false)
|
self:RenderHand(false)
|
||||||
self:RenderPiles()
|
self:RenderPiles()
|
||||||
self:EnemyTurn()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'retainSlot' }]),
|
self:EnemyTurn()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'retainSlot' }]),
|
||||||
method('DrawCards', `local drawnSlots = {}
|
method('DrawCards', `local drawnSlots = {}
|
||||||
|
local drawnCards = {}
|
||||||
local drewAny = false
|
local drewAny = false
|
||||||
for i = 1, amount do
|
for i = 1, amount do
|
||||||
\tif #self.DrawPile <= 0 then
|
\tif #self.DrawPile <= 0 then
|
||||||
@@ -425,6 +426,7 @@ for i = 1, amount do
|
|||||||
\t\tbreak
|
\t\tbreak
|
||||||
\tend
|
\tend
|
||||||
\tlocal cardId = table.remove(self.DrawPile)
|
\tlocal cardId = table.remove(self.DrawPile)
|
||||||
|
\ttable.insert(drawnCards, cardId)
|
||||||
\tif #self.Hand >= 10 then
|
\tif #self.Hand >= 10 then
|
||||||
\t\ttable.insert(self.DiscardPile, cardId)
|
\t\ttable.insert(self.DiscardPile, cardId)
|
||||||
\t\tself:TriggerSly(cardId)
|
\t\tself:TriggerSly(cardId)
|
||||||
@@ -444,10 +446,11 @@ if animate == true and #drawnSlots > 0 then
|
|||||||
\t\tlocal slot = drawnSlots[i]
|
\t\tlocal slot = drawnSlots[i]
|
||||||
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
|
\t\tself:AnimateCardFrom(slot, drawStart, Vector2(self:GetHandSlotX(slot), 0), 0.08 + i * 0.045)
|
||||||
\tend
|
\tend
|
||||||
|
return drawnCards
|
||||||
end`, [
|
end`, [
|
||||||
{ 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: 'animate' },
|
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'animate' },
|
||||||
]),
|
], 0, 'any'),
|
||||||
method('AddCardsToHand', `if self.Hand == nil then
|
method('AddCardsToHand', `if self.Hand == nil then
|
||||||
self.Hand = {}
|
self.Hand = {}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
|||||||
end
|
end
|
||||||
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
self:SetText(base .. "/Cost", string.format("%d", c.cost))
|
||||||
self:SetText(base .. "/Name", c.name)
|
self:SetText(base .. "/Name", c.name)
|
||||||
self:SetText(base .. "/Desc", c.desc)
|
self:SetText(base .. "/Desc", self:FormatCardDescription(c.desc))
|
||||||
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
local art = _EntityService:GetEntityByPath(base .. "/Art")
|
||||||
if art ~= nil then
|
if art ~= nil then
|
||||||
if c.image ~= nil and c.image ~= "" then
|
if c.image ~= nil and c.image ~= "" then
|
||||||
@@ -459,8 +459,9 @@ if c.weak ~= nil or c.vuln ~= nil or c.poison ~= nil then
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
local drawnCards = {}
|
||||||
if c.draw ~= nil then
|
if c.draw ~= nil then
|
||||||
self:DrawCards(c.draw, true)
|
drawnCards = self:DrawCards(c.draw, true) or {}
|
||||||
end
|
end
|
||||||
if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then
|
if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then
|
||||||
local currentHand = 0
|
local currentHand = 0
|
||||||
@@ -472,7 +473,18 @@ if c.drawUntilHandSize ~= nil and c.drawUntilHandSize > 0 then
|
|||||||
end
|
end
|
||||||
local need = c.drawUntilHandSize - currentHand
|
local need = c.drawUntilHandSize - currentHand
|
||||||
if need > 0 then
|
if need > 0 then
|
||||||
self:DrawCards(need, true)
|
local moreDrawnCards = self:DrawCards(need, true) or {}
|
||||||
|
for i = 1, #moreDrawnCards do
|
||||||
|
table.insert(drawnCards, moreDrawnCards[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if c.drawSkillBlock ~= nil and c.drawSkillBlock > 0 then
|
||||||
|
for i = 1, #drawnCards do
|
||||||
|
local drawnCard = self.Cards[drawnCards[i]]
|
||||||
|
if drawnCard ~= nil and drawnCard.kind == "Skill" then
|
||||||
|
self:AddCardBlock(c.drawSkillBlock)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
if c.addShiv ~= nil and c.discard == nil and c.discardAll ~= true then
|
||||||
|
|||||||
@@ -3,6 +3,47 @@ import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_
|
|||||||
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
|
||||||
|
|
||||||
export const tooltipMethods = [
|
export const tooltipMethods = [
|
||||||
|
method('FormatCardDescription', `if desc == nil or desc == "" then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
local function replacePlain(text, needle, replacement)
|
||||||
|
local out = ""
|
||||||
|
local pos = 1
|
||||||
|
while true do
|
||||||
|
local s, e = string.find(text, needle, pos, true)
|
||||||
|
if s == nil then
|
||||||
|
out = out .. string.sub(text, pos)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
out = out .. string.sub(text, pos, s - 1) .. replacement
|
||||||
|
pos = e + 1
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
local terms = {
|
||||||
|
"교활",
|
||||||
|
"보존",
|
||||||
|
"민첩",
|
||||||
|
"가시",
|
||||||
|
"소멸",
|
||||||
|
"선천성",
|
||||||
|
"취약",
|
||||||
|
"약화",
|
||||||
|
"독",
|
||||||
|
"광역",
|
||||||
|
"관통",
|
||||||
|
"방어도",
|
||||||
|
"힘",
|
||||||
|
"스킬",
|
||||||
|
"공격",
|
||||||
|
"파워",
|
||||||
|
}
|
||||||
|
local out = desc
|
||||||
|
for i = 1, #terms do
|
||||||
|
local term = terms[i]
|
||||||
|
out = replacePlain(out, term, "<color=#70D6FF>" .. term .. "</color>")
|
||||||
|
end
|
||||||
|
return out`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'desc' }], 0, 'string'),
|
||||||
method('BuildCardKeywordTooltip', `if c == nil then
|
method('BuildCardKeywordTooltip', `if c == nil then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ function luaCardsTable(cards) {
|
|||||||
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
|
||||||
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
if (c.draw != null) fields.push(`draw = ${c.draw}`);
|
||||||
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
|
if (c.drawUntilHandSize != null) fields.push(`drawUntilHandSize = ${c.drawUntilHandSize}`);
|
||||||
|
if (c.drawSkillBlock != null) fields.push(`drawSkillBlock = ${c.drawSkillBlock}`);
|
||||||
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
if (c.heal != null) fields.push(`heal = ${c.heal}`);
|
||||||
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
|
if (c.gainEnergy != null) fields.push(`gainEnergy = ${c.gainEnergy}`);
|
||||||
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
if (c.poison != null) fields.push(`poison = ${c.poison}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user