Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion drool/moves.csv
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ Iron Wall,Aurox,0,3,100,0,Metal,Self,"Until Aurox switches out, regenerate 50% o
Bull Rush,Aurox,140,2,100,0,Metal,Physical,Deals damage. Also deals 20% of max HP to self.,,none,0
Contagious Slumber,Xmon,0,2,100,0,Cosmic,Other,"Inflicts Sleep on self and opponent.",,none,0
Vital Siphon,Xmon,40,2,90,0,Cosmic,Special,"Deals damage, 50% chance to steal 1 stamina from opponent.",,none,0
Somniphobia,Xmon,0,1,100,0,Cosmic,Other,"For the next 8 turns, any mon that rests will take 1/8th of max HP as damage.",,none,0
Somniphobia,Xmon,0,1,100,0,Cosmic,Other,"For 4 turns, any active mon that gains stamina takes 1/8th of max HP per stack as damage. Stacks if used again.",,none,0
Night Terrors,Xmon,0,0,100,0,Cosmic,Special,Gain a Terror stack. Deals damage and costs stamina at end of turn for each Terror stack. Deals extra damage if opponent is alseep.,,none,0
Invoke Taboo,Xmon,0,1,100,-1,Cosmic,Other,"Brands the move the opponent just used. Until they switch out, if they use that move again they fall asleep.",,none,6
Bubble Bop,Ekineki,50,3,100,0,Liquid,Special,Hits twice. Each hit deals 50 base power.,,none,0
Sneak Attack,Ekineki,60,2,100,1,Liquid,Special,Hits any opponent mon (even non-active). Can only be used once per switch-in.,,opponent-mon,0
Nine Nine Nine,Ekineki,0,1,100,0,Math,Self,Sets crit rate to 90% on the next turn for all moves.,,none,0
Expand Down
22 changes: 15 additions & 7 deletions script/SetupMons.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {PreemptiveShock} from "../src/mons/volthare/PreemptiveShock.sol";
import {RoundTrip} from "../src/mons/volthare/RoundTrip.sol";
import {ContagiousSlumber} from "../src/mons/xmon/ContagiousSlumber.sol";
import {Dreamcatcher} from "../src/mons/xmon/Dreamcatcher.sol";
import {InvokeTaboo} from "../src/mons/xmon/InvokeTaboo.sol";
import {NightTerrors} from "../src/mons/xmon/NightTerrors.sol";
import {Somniphobia} from "../src/mons/xmon/Somniphobia.sol";
import {VitalSiphon} from "../src/mons/xmon/VitalSiphon.sol";
Expand Down Expand Up @@ -664,13 +665,13 @@ contract SetupMons is Script {
}

function deployXmon(GachaTeamRegistry registry) internal returns (DeployData[] memory) {
DeployData[] memory deployedContracts = new DeployData[](5);
DeployData[] memory deployedContracts = new DeployData[](6);

// Cache commonly used addresses
address sleepstatus = vm.envAddress("SLEEP_STATUS");
address typecalculator = vm.envAddress("TYPE_CALCULATOR");

address[5] memory addrs;
address[6] memory addrs;

{
addrs[0] = address(new ContagiousSlumber(IEffect(sleepstatus)));
Expand All @@ -689,16 +690,20 @@ contract SetupMons is Script {
deployedContracts[3] = DeployData({name: "Night Terrors", contractAddress: addrs[3]});
}
{
addrs[4] = address(new Dreamcatcher());
deployedContracts[4] = DeployData({name: "Dreamcatcher", contractAddress: addrs[4]});
addrs[4] = address(new InvokeTaboo(IEffect(sleepstatus)));
deployedContracts[4] = DeployData({name: "Invoke Taboo", contractAddress: addrs[4]});
}
{
addrs[5] = address(new Dreamcatcher());
deployedContracts[5] = DeployData({name: "Dreamcatcher", contractAddress: addrs[5]});
}

_registerXmon(registry, addrs);

return deployedContracts;
}

function _registerXmon(GachaTeamRegistry registry, address[5] memory addrs) internal {
function _registerXmon(GachaTeamRegistry registry, address[6] memory addrs) internal {
MonStats memory stats = MonStats({
hp: 311,
stamina: 5,
Expand All @@ -710,13 +715,16 @@ contract SetupMons is Script {
type1: Type.Cosmic,
type2: Type.None
});
uint256[] memory moves = new uint256[](4);
// Lanes 0-3 are the level-0 battle moves; lane 4 (Invoke Taboo) is a higher-pool move that
// unlocks at level 6 (see moves.csv UnlockLevel and the MonExp by-lane gating).
uint256[] memory moves = new uint256[](5);
moves[0] = uint256(uint160(addrs[0]));
moves[1] = uint256(uint160(addrs[1]));
moves[2] = uint256(uint160(addrs[2]));
moves[3] = uint256(uint160(addrs[3]));
moves[4] = uint256(uint160(addrs[4]));
uint256[] memory abilities = new uint256[](1);
abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[4]));
abilities[0] = (uint256(1) << 248) | uint256(uint160(addrs[5]));
bytes32[] memory keys = new bytes32[](0);
bytes32[] memory values = new bytes32[](0);
registry.createMon(10, stats, moves, abilities, keys, values);
Expand Down
122 changes: 122 additions & 0 deletions src/mons/xmon/InvokeTaboo.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-License-Identifier: AGPL-3.0

pragma solidity ^0.8.0;

import {ALWAYS_APPLIES_BIT, DEFAULT_PRIORITY, MOVE_INDEX_MASK, SWITCH_MOVE_INDEX} from "../../Constants.sol";
import {ExtraDataType, MoveClass, Type} from "../../Enums.sol";
import {MoveDecision, MoveMeta} from "../../Structs.sol";

import {IEngine} from "../../IEngine.sol";
import {IMoveSet} from "../../moves/IMoveSet.sol";
import {IEffect} from "../../effects/IEffect.sol";
import {BasicEffect} from "../../effects/BasicEffect.sol";

contract InvokeTaboo is IMoveSet, BasicEffect {
IEffect immutable SLEEP_STATUS;

constructor(IEffect _SLEEP_STATUS) {
SLEEP_STATUS = _SLEEP_STATUS;
}

function name() public pure override(IMoveSet, BasicEffect) returns (string memory) {
return "Invoke Taboo";
}

function move(
IEngine engine,
bytes32 battleKey,
uint256 attackerPlayerIndex,
uint256,
uint256 defenderMonIndex,
uint16,
uint256
) external {
uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2;

MoveDecision memory moveDecision = engine.getMoveDecisionForBattleState(battleKey, defenderPlayerIndex);
uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK;

// Only brand regular move slots, not a switch (125) or no-op (126).
if (moveIndex >= SWITCH_MOVE_INDEX) {
return;
}

bytes32 tabooData = bytes32(uint256(moveIndex));
(bool exists, uint256 effectIndex,) =
engine.getEffectData(battleKey, defenderPlayerIndex, defenderMonIndex, address(this));
if (exists) {
engine.editEffect(defenderPlayerIndex, effectIndex, tabooData);
} else {
engine.addEffect(defenderPlayerIndex, defenderMonIndex, this, tabooData);
}
}

function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) {
return 1;
}

function priority(IEngine, bytes32, uint256) public pure returns (uint32) {
return DEFAULT_PRIORITY - 1;
}

function moveType(IEngine, bytes32) public pure returns (Type) {
return Type.Cosmic;
}

function moveClass(IEngine, bytes32) public pure returns (MoveClass) {
return MoveClass.Other;
}

function extraDataType() public pure returns (ExtraDataType) {
return ExtraDataType.None;
}

// Steps: OnMonSwitchOut (0x20), AfterMove (0x80), ALWAYS_APPLIES (0x8000)
function getStepsBitmap() external pure override returns (uint16) {
return ALWAYS_APPLIES_BIT | 0x00A0;
}

function onAfterMove(
IEngine engine,
bytes32 battleKey,
uint256,
bytes32 extraData,
uint256 targetIndex,
uint256 monIndex,
uint256,
uint256
) external override returns (bytes32, bool) {
MoveDecision memory moveDecision = engine.getMoveDecisionForBattleState(battleKey, targetIndex);
uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK;
uint8 tabooMoveIndex = uint8(uint256(extraData));

if (moveIndex == tabooMoveIndex) {
engine.addEffect(targetIndex, monIndex, SLEEP_STATUS, "");
}
return (extraData, false);
}

function onMonSwitchOut(IEngine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256)
external
pure
override
returns (bytes32, bool)
{
return (extraData, true);
}

function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex)
external
pure
returns (MoveMeta memory)
{
return MoveMeta({
moveType: moveType(engine, battleKey),
moveClass: moveClass(engine, battleKey),
extraDataType: extraDataType(),
priority: priority(engine, battleKey, attackerPlayerIndex),
stamina: stamina(engine, battleKey, attackerPlayerIndex, attackerMonIndex),
basePower: 0
});
}
}
112 changes: 80 additions & 32 deletions src/mons/xmon/Somniphobia.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,47 @@

pragma solidity ^0.8.0;

import {NO_OP_MOVE_INDEX, DEFAULT_PRIORITY, MOVE_INDEX_MASK} from "../../Constants.sol";
import {ExtraDataType, MoveClass, Type} from "../../Enums.sol";
import { MoveDecision, MonStateIndexName, MoveMeta } from "../../Structs.sol";
import {ALWAYS_APPLIES_BIT, DEFAULT_PRIORITY} from "../../Constants.sol";
import {ExtraDataType, MoveClass, Type, MonStateIndexName} from "../../Enums.sol";
import {MoveMeta} from "../../Structs.sol";

import {IEngine} from "../../IEngine.sol";
import {IMoveSet} from "../../moves/IMoveSet.sol";
import {BasicEffect} from "../../effects/BasicEffect.sol";

contract Somniphobia is IMoveSet, BasicEffect {
uint256 public constant DURATION = 8;
uint256 public constant DURATION = 4;
int32 public constant DAMAGE_DENOM = 8;

// Global-coordinator data: [stack: bits 8-15 | remainingDuration: bits 0-7].
// Per-mon-punisher data: this marker bit set (distinguishes the two roles, which share a contract).
uint256 internal constant PUNISHER_MARKER = 1 << 255;

function name() public pure override(IMoveSet, BasicEffect) returns (string memory) {
return "Somniphobia";
}

function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external {
// Add effect globally for 6 turns (only if it's not already in global effects)
(bool exists,,) = engine.getEffectData(battleKey, 2, 2, address(this));
function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, uint16, uint256) external {
uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2;

(bool exists, uint256 effectIndex, bytes32 data) = engine.getEffectData(battleKey, 2, 2, address(this));
if (exists) {
return;
// Bump the stack but keep the original countdown; the effect must fade before it resets.
uint256 stack = ((uint256(data) >> 8) & 0xFF) + 1;
engine.editEffect(2, effectIndex, bytes32((stack << 8) | (uint256(data) & 0xFF)));
} else {
engine.addEffect(2, attackerPlayerIndex, this, bytes32((uint256(1) << 8) | DURATION));
}

_applyPunisher(engine, battleKey, attackerPlayerIndex, attackerMonIndex);
_applyPunisher(engine, battleKey, defenderPlayerIndex, defenderMonIndex);
}

function _applyPunisher(IEngine engine, bytes32 battleKey, uint256 playerIndex, uint256 monIndex) internal {
(bool exists,,) = engine.getEffectData(battleKey, playerIndex, monIndex, address(this));
if (!exists) {
engine.addEffect(playerIndex, monIndex, this, bytes32(PUNISHER_MARKER));
}
engine.addEffect(2, attackerPlayerIndex, this, bytes32(DURATION));
}

function stamina(IEngine, bytes32, uint256, uint256) public pure returns (uint32) {
Expand All @@ -47,46 +65,76 @@ contract Somniphobia is IMoveSet, BasicEffect {
return ExtraDataType.None;
}

// Steps: RoundEnd, AfterMove
// Steps: RoundEnd (0x04), OnMonSwitchIn (0x10), OnMonSwitchOut (0x20), OnUpdateMonState (0x100), ALWAYS_APPLIES (0x8000)
function getStepsBitmap() external pure override returns (uint16) {
return 0x8084;
return ALWAYS_APPLIES_BIT | 0x0134;
}

function onAfterMove(IEngine engine, bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256)
function onUpdateMonState(
IEngine engine,
bytes32 battleKey,
uint256,
bytes32 extraData,
uint256 playerIndex,
uint256 monIndex,
uint256,
uint256,
MonStateIndexName stateVarIndex,
int32 valueToAdd
) external override returns (bytes32, bool) {
if (stateVarIndex == MonStateIndexName.Stamina && valueToAdd > 0) {
(bool exists,, bytes32 data) = engine.getEffectData(battleKey, 2, 2, address(this));
if (exists) {
int32 stack = int32(uint32((uint256(data) >> 8) & 0xFF));
uint32 maxHp = engine.getMonValueForBattle(battleKey, playerIndex, monIndex, MonStateIndexName.Hp);
int32 damage = int32(uint32(maxHp)) / DAMAGE_DENOM * stack;
if (damage > 0) {
engine.dealDamage(playerIndex, monIndex, damage);
}
}
}
return (extraData, false);
}

function onMonSwitchIn(IEngine engine, bytes32 battleKey, uint256, bytes32 extraData, uint256 targetIndex, uint256 monIndex, uint256, uint256)
external
override
returns (bytes32, bool)
{
MoveDecision memory moveDecision = engine.getMoveDecisionForBattleState(battleKey, targetIndex);

// Unpack the move index from packedMoveIndex
uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK;

// If this player rested (NO_OP), deal damage
if (moveIndex == NO_OP_MOVE_INDEX) {
uint32 maxHp = engine.getMonValueForBattle(battleKey, targetIndex, monIndex, MonStateIndexName.Hp);
int32 damage = int32(uint32(maxHp)) / DAMAGE_DENOM;

if (damage > 0) {
engine.dealDamage(targetIndex, monIndex, damage);
}
// Global coordinator only: apply the punisher to the mon that just switched in.
if (uint256(extraData) & PUNISHER_MARKER == 0) {
_applyPunisher(engine, battleKey, targetIndex, monIndex);
}

return (extraData, false);
}

function onRoundEnd(IEngine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256)
function onMonSwitchOut(IEngine, bytes32, uint256, bytes32 extraData, uint256, uint256, uint256, uint256)
external
pure
override
returns (bytes32, bool removeAfterRun)
returns (bytes32, bool)
{
uint256 turnsLeft = uint256(extraData);
if (turnsLeft == 1) {
// Punisher clears with its mon; the global coordinator persists.
return (extraData, uint256(extraData) & PUNISHER_MARKER != 0);
}

function onRoundEnd(IEngine engine, bytes32 battleKey, uint256, bytes32 extraData, uint256, uint256, uint256, uint256)
external
view
override
returns (bytes32, bool)
{
if (uint256(extraData) & PUNISHER_MARKER != 0) {
// Punisher: drop self once the coordinator is gone.
(bool exists,,) = engine.getEffectData(battleKey, 2, 2, address(this));
return (extraData, !exists);
}
// Global coordinator: count down, preserving the stack.
uint256 duration = uint256(extraData) & 0xFF;
if (duration <= 1) {
return (extraData, true);
} else {
return (bytes32(turnsLeft - 1), false);
}
return (bytes32((uint256(extraData) & 0xFF00) | (duration - 1)), false);
}

function getMeta(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256 attackerMonIndex)
Expand Down
Loading