367 lines
11 KiB
C#
367 lines
11 KiB
C#
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
|
||
|
namespace UPWG.Core
|
||
|
{
|
||
|
public enum WrestlerPosition
|
||
|
{
|
||
|
InRing,
|
||
|
OnRopes,
|
||
|
OutsideRing,
|
||
|
}
|
||
|
|
||
|
public enum MomentumLevel
|
||
|
{
|
||
|
None = 0,
|
||
|
Low = 1,
|
||
|
Medium = 2,
|
||
|
High = 3,
|
||
|
}
|
||
|
|
||
|
|
||
|
public enum HitResult
|
||
|
{
|
||
|
Miss,
|
||
|
Hit,
|
||
|
Critical,
|
||
|
}
|
||
|
|
||
|
public class MoveResult
|
||
|
{
|
||
|
public HitResult Result;
|
||
|
|
||
|
// what did this do to the recipient?
|
||
|
public uint RecipientDamage;
|
||
|
public int RecipientMomentumDelta;
|
||
|
public BodyParts RecipientRestrained;
|
||
|
|
||
|
// and what did this do to us?
|
||
|
public uint SelfDamage;
|
||
|
public int SelfMomentumDelta;
|
||
|
public int SelfPopularityDelta;
|
||
|
|
||
|
public MoveResult(
|
||
|
HitResult result,
|
||
|
uint recipientDamage,
|
||
|
int recipientMomentumDelta = 0,
|
||
|
BodyParts restrained = 0,
|
||
|
uint selfDamage = 0,
|
||
|
int selfMomentumDelta = 0,
|
||
|
int selfPopularityDelta = 0
|
||
|
)
|
||
|
{
|
||
|
Result = result;
|
||
|
RecipientDamage = recipientDamage;
|
||
|
RecipientMomentumDelta = recipientMomentumDelta;
|
||
|
RecipientRestrained = restrained;
|
||
|
SelfDamage = selfDamage;
|
||
|
SelfMomentumDelta = selfMomentumDelta;
|
||
|
SelfPopularityDelta = selfPopularityDelta;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
interface IGoal
|
||
|
{
|
||
|
public void ProcessMove(Move move, MoveResult result);
|
||
|
public bool IsComplete();
|
||
|
}
|
||
|
|
||
|
class MaxMomentumGoal : IGoal
|
||
|
{
|
||
|
public int GoalMomentum { get; }
|
||
|
private int _maxMomentum;
|
||
|
private int _curMomentum;
|
||
|
|
||
|
MaxMomentumGoal(int goalMomentum)
|
||
|
{
|
||
|
GoalMomentum = goalMomentum;
|
||
|
}
|
||
|
|
||
|
public void ProcessMove(Move move, MoveResult result)
|
||
|
{
|
||
|
_curMomentum += result.SelfMomentumDelta;
|
||
|
if (_curMomentum > _maxMomentum)
|
||
|
_maxMomentum = _curMomentum;
|
||
|
}
|
||
|
|
||
|
public bool IsComplete()
|
||
|
{
|
||
|
return _maxMomentum >= GoalMomentum;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public enum MatchStatus
|
||
|
{
|
||
|
InProgress,
|
||
|
Pinfall,
|
||
|
Submission,
|
||
|
CountOut,
|
||
|
Disqualification,
|
||
|
}
|
||
|
|
||
|
public class MoveOptions
|
||
|
{
|
||
|
// this class represents the available moves
|
||
|
public string Strike { get; set; }
|
||
|
public string Grapple { get; set; }
|
||
|
public string Other { get; set; }
|
||
|
public string Special { get; set; }
|
||
|
}
|
||
|
|
||
|
public class Battle
|
||
|
{
|
||
|
private static Random _random = new Random();
|
||
|
|
||
|
public enum ParticipantRole
|
||
|
{
|
||
|
LeftSingle,
|
||
|
RightSingle,
|
||
|
LeftTag1,
|
||
|
LeftTag2,
|
||
|
RightTag1,
|
||
|
RightTag2,
|
||
|
}
|
||
|
|
||
|
public struct Participant
|
||
|
{
|
||
|
public Wrestler Wrestler;
|
||
|
public WrestlerPosition Position;
|
||
|
public BodyParts Restrained;
|
||
|
public BodyParts Injured;
|
||
|
public MomentumLevel Momentum;
|
||
|
public uint MaxFatigue;
|
||
|
public uint CurFatigue;
|
||
|
|
||
|
public Participant(Wrestler wrestler)
|
||
|
{
|
||
|
Wrestler = wrestler;
|
||
|
Position = WrestlerPosition.InRing;
|
||
|
Restrained = 0;
|
||
|
Injured = 0;
|
||
|
Momentum = MomentumLevel.None;
|
||
|
CurFatigue = MaxFatigue = wrestler.Fortitude * 10;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private uint _turnClock;
|
||
|
private ParticipantRole _playerRole;
|
||
|
|
||
|
public Dictionary<ParticipantRole, Participant> Participants { get; }
|
||
|
public MatchStatus Status { get; private set; }
|
||
|
public ParticipantRole[] UpcomingTurns { get; set; }
|
||
|
public MoveOptions CurrentMoveOptions { get; private set; }
|
||
|
|
||
|
private void InitializeState()
|
||
|
{
|
||
|
_turnClock = 1;
|
||
|
Status = MatchStatus.InProgress;
|
||
|
UpcomingTurns = new ParticipantRole[5];
|
||
|
CalcUpcomingTurns();
|
||
|
CurrentMoveOptions = new MoveOptions();
|
||
|
CalcPlayerMoveOptions();
|
||
|
}
|
||
|
|
||
|
public Battle(Wrestler leftWrestler, Wrestler rightWrestler)
|
||
|
{
|
||
|
Participants = new Dictionary<ParticipantRole, Participant>
|
||
|
{
|
||
|
[ParticipantRole.LeftSingle] = new Participant(leftWrestler),
|
||
|
[ParticipantRole.RightSingle] = new Participant(rightWrestler)
|
||
|
};
|
||
|
_playerRole = ParticipantRole.LeftSingle;
|
||
|
InitializeState();
|
||
|
}
|
||
|
|
||
|
public Battle(Wrestler leftOne, Wrestler leftTwo, Wrestler rightOne, Wrestler rightTwo)
|
||
|
{
|
||
|
Participants = new Dictionary<ParticipantRole, Participant>
|
||
|
{
|
||
|
[ParticipantRole.LeftTag1] = new Participant(leftOne),
|
||
|
[ParticipantRole.LeftTag2] = new Participant(leftTwo),
|
||
|
[ParticipantRole.RightTag1] = new Participant(rightOne),
|
||
|
[ParticipantRole.RightTag2] = new Participant(rightTwo)
|
||
|
};
|
||
|
_playerRole = ParticipantRole.LeftTag1;
|
||
|
InitializeState();
|
||
|
}
|
||
|
|
||
|
// Player Interaction
|
||
|
|
||
|
public void CalcPlayerMoveOptions()
|
||
|
{
|
||
|
// TODO: revisit for other match types
|
||
|
var player = Participants[_playerRole];
|
||
|
var target = Participants[ParticipantRole.RightSingle];
|
||
|
// TODO: revisit for other move types
|
||
|
CurrentMoveOptions.Strike = GetBestAvailableMove(player, MoveType.Strike, target);
|
||
|
CurrentMoveOptions.Grapple = GetBestAvailableMove(player, MoveType.Aerial, target);
|
||
|
CurrentMoveOptions.Special = "Pin";
|
||
|
CurrentMoveOptions.Other = "Enzugiri **";
|
||
|
}
|
||
|
|
||
|
public MoveResult ApplyMove(string moveName)
|
||
|
{
|
||
|
var move = MoveDatabase.Lookup(moveName);
|
||
|
return ApplyMove(_playerRole, move, ParticipantRole.RightSingle);
|
||
|
}
|
||
|
|
||
|
public bool IsPlayerTurn()
|
||
|
{
|
||
|
return UpcomingTurns[0] == _playerRole;
|
||
|
}
|
||
|
|
||
|
public void EndTurn()
|
||
|
{
|
||
|
_turnClock += 1;
|
||
|
}
|
||
|
|
||
|
// CPU Control
|
||
|
|
||
|
public void GetCpuMove(out string move, out ParticipantRole target)
|
||
|
{
|
||
|
move = "Chop 1";
|
||
|
target = ParticipantRole.LeftSingle;
|
||
|
}
|
||
|
|
||
|
// Internals
|
||
|
|
||
|
private MoveResult ApplyMove(ParticipantRole performerPos, Move move, ParticipantRole recipientPos)
|
||
|
{
|
||
|
var performer = Participants[performerPos];
|
||
|
var recipient = Participants[recipientPos];
|
||
|
|
||
|
// can this move be done?
|
||
|
if (!MoveIsValid(performer, move, recipient))
|
||
|
throw new InvalidMoveException();
|
||
|
|
||
|
var hitResult = CheckForHit(performer, move, recipient);
|
||
|
|
||
|
// if move misses, no damage
|
||
|
if (hitResult == HitResult.Miss)
|
||
|
return new MoveResult(hitResult, 0);
|
||
|
|
||
|
uint damage = 0;
|
||
|
uint selfDamage = 0;
|
||
|
switch (move.Type)
|
||
|
{
|
||
|
case MoveType.Strike:
|
||
|
damage = (uint) (move.BaseOffense * performer.Wrestler.DamageMult);
|
||
|
break;
|
||
|
case MoveType.Aerial:
|
||
|
damage = (uint) (move.BaseOffense * performer.Wrestler.DamageMult);
|
||
|
selfDamage = 5;
|
||
|
break;
|
||
|
case MoveType.Hold:
|
||
|
break;
|
||
|
case MoveType.Throw:
|
||
|
damage = (uint) (move.BaseOffense * performer.Wrestler.DamageMult);
|
||
|
break;
|
||
|
case MoveType.Pin:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// TODO: how does momentum/popularity work?
|
||
|
|
||
|
return new MoveResult(hitResult, damage, 0, 0, selfDamage, 0, 0);
|
||
|
}
|
||
|
|
||
|
private void CalcUpcomingTurns()
|
||
|
{
|
||
|
for (var i = 0; i < UpcomingTurns.Length; i++)
|
||
|
{
|
||
|
// TODO: use speed to determine turn order
|
||
|
if ((_turnClock + i) % 2 == 1)
|
||
|
{
|
||
|
UpcomingTurns[i] = ParticipantRole.LeftSingle;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
UpcomingTurns[i] = ParticipantRole.RightSingle;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static readonly double[] HitMod = {0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4};
|
||
|
private static readonly double[] EvadeMod = {-0.1, -0.1, -0.05, -0.05, 0, 0.05, 0.05, 0.1, 0.1, 0.15};
|
||
|
|
||
|
private static HitResult CheckForHit(Participant performer, Move move, Participant recipient)
|
||
|
{
|
||
|
var critChance = 0.05;
|
||
|
uint attackStat = 0;
|
||
|
|
||
|
switch (move.Type)
|
||
|
{
|
||
|
case MoveType.Strike:
|
||
|
attackStat = performer.Wrestler.Strength;
|
||
|
break;
|
||
|
case MoveType.Aerial:
|
||
|
attackStat = performer.Wrestler.Agility;
|
||
|
break;
|
||
|
case MoveType.Pin:
|
||
|
attackStat = performer.Wrestler.Strength > performer.Wrestler.Agility
|
||
|
? performer.Wrestler.Strength
|
||
|
: performer.Wrestler.Agility;
|
||
|
break;
|
||
|
case MoveType.Hold:
|
||
|
attackStat = performer.Wrestler.Grappling;
|
||
|
break;
|
||
|
case MoveType.Throw:
|
||
|
attackStat = performer.Wrestler.Grappling;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
var hitChance = move.BaseChance * HitMod[attackStat] - EvadeMod[recipient.Wrestler.Agility];
|
||
|
|
||
|
var hitRoll = _random.NextDouble();
|
||
|
if (hitRoll < critChance)
|
||
|
return HitResult.Critical;
|
||
|
else if (hitRoll <= hitChance)
|
||
|
return HitResult.Hit;
|
||
|
else
|
||
|
return HitResult.Miss;
|
||
|
}
|
||
|
|
||
|
private static bool MoveIsValid(Participant performer, Move move, Participant recipient)
|
||
|
{
|
||
|
// TODO: check position of performer and recipient
|
||
|
|
||
|
// is there an issue affecting the required body part
|
||
|
var requiredPartsRestrained = performer.Restrained & move.PartsUtilized;
|
||
|
if (requiredPartsRestrained != 0)
|
||
|
return false;
|
||
|
|
||
|
// does wrestler have momentum?
|
||
|
if (performer.Momentum < (MomentumLevel) (move.Rank - 1))
|
||
|
return false;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private static string GetBestAvailableMove(Participant participant, MoveType type, Participant recipient)
|
||
|
{
|
||
|
List<Move> best = new();
|
||
|
var bestRank = -1;
|
||
|
foreach (var (moveName, expertise) in participant.Wrestler.MoveSet)
|
||
|
{
|
||
|
var move = MoveDatabase.Lookup(moveName);
|
||
|
// accumulate list of best valid moves of the type
|
||
|
if (move.Type == type && MoveIsValid(participant, move, recipient))
|
||
|
{
|
||
|
// for now just use rank, eventually this may be dynamic
|
||
|
if (move.Rank > bestRank)
|
||
|
{
|
||
|
bestRank = (int) move.Rank;
|
||
|
best.Clear();
|
||
|
best.Add(move);
|
||
|
}
|
||
|
else if (move.Rank == bestRank)
|
||
|
{
|
||
|
best.Add(move);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return best[_random.Next(best.Count)].Name;
|
||
|
}
|
||
|
}
|
||
|
}
|