impl
This commit is contained in:
+363
@@ -0,0 +1,363 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Oo]ut/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Coverlet is a free, cross platform Code Coverage Tool
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||||
|
MigrationBackup/
|
||||||
|
|
||||||
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
|
.ionide/
|
||||||
|
|
||||||
|
# Fody - auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0CF24C60-F57A-466D-989F-EB2ABD76AC23}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaySharp.Core", "src\SlaySharp.Core\SlaySharp.Core.csproj", "{7B125201-7E42-4871-8E4B-F179EDE8A820}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaySharp.Web", "src\SlaySharp.Web\SlaySharp.Web.csproj", "{198A4D46-79A8-40F3-AEEC-372074AA55F5}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0E80BAC8-E9BA-4EB5-B4A9-AAAB8E820D90}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaySharp.Tests", "tests\SlaySharp.Tests\SlaySharp.Tests.csproj", "{E74D26AC-FBF1-4C3C-80BB-842BA802D969}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{7B125201-7E42-4871-8E4B-F179EDE8A820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7B125201-7E42-4871-8E4B-F179EDE8A820}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7B125201-7E42-4871-8E4B-F179EDE8A820}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7B125201-7E42-4871-8E4B-F179EDE8A820}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{198A4D46-79A8-40F3-AEEC-372074AA55F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{198A4D46-79A8-40F3-AEEC-372074AA55F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{198A4D46-79A8-40F3-AEEC-372074AA55F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{198A4D46-79A8-40F3-AEEC-372074AA55F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E74D26AC-FBF1-4C3C-80BB-842BA802D969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E74D26AC-FBF1-4C3C-80BB-842BA802D969}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E74D26AC-FBF1-4C3C-80BB-842BA802D969}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E74D26AC-FBF1-4C3C-80BB-842BA802D969}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{7B125201-7E42-4871-8E4B-F179EDE8A820} = {0CF24C60-F57A-466D-989F-EB2ABD76AC23}
|
||||||
|
{198A4D46-79A8-40F3-AEEC-372074AA55F5} = {0CF24C60-F57A-466D-989F-EB2ABD76AC23}
|
||||||
|
{E74D26AC-FBF1-4C3C-80BB-842BA802D969} = {0E80BAC8-E9BA-4EB5-B4A9-AAAB8E820D90}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "8.0.100-preview.7.23376.3",
|
||||||
|
"rollForward": "latestFeature"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
using SlaySharp.Core.Combat;
|
||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
using SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Catalogs;
|
||||||
|
|
||||||
|
public class ConsoleWriteLineCard : Card
|
||||||
|
{
|
||||||
|
private int _damage = 6;
|
||||||
|
|
||||||
|
public ConsoleWriteLineCard() : base(
|
||||||
|
"Console.WriteLine",
|
||||||
|
"Console.WriteLine(target);",
|
||||||
|
"target.Hit(6);",
|
||||||
|
"Deal 6 damage.",
|
||||||
|
cost: 1,
|
||||||
|
type: CardType.Attack,
|
||||||
|
rarity: CardRarity.Basic,
|
||||||
|
targetType: TargetType.SingleEnemy
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_damage = 9;
|
||||||
|
Description = "Deal 9 damage.";
|
||||||
|
CodeSnippet = "target.Hit(9);";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
if (target == null) return;
|
||||||
|
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
|
||||||
|
var dealt = target.TakeDamage(totalDmg);
|
||||||
|
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
|
||||||
|
engine.Log.AddInfo($"[Console.WriteLine] {target.Name} took {dealt} dmg.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new ConsoleWriteLineCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_damage = this._damage,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TryCatchCard : Card
|
||||||
|
{
|
||||||
|
private int _shield = 5;
|
||||||
|
|
||||||
|
public TryCatchCard() : base(
|
||||||
|
"try { ... } catch",
|
||||||
|
"try { Guard(); } catch { }",
|
||||||
|
"shield += 5;",
|
||||||
|
"Gain 5 Shield.",
|
||||||
|
cost: 1,
|
||||||
|
type: CardType.Skill,
|
||||||
|
rarity: CardRarity.Basic,
|
||||||
|
targetType: TargetType.Self
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_shield = 8;
|
||||||
|
Description = "Gain 8 Shield.";
|
||||||
|
CodeSnippet = "shield += 8;";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
player.GainShield(_shield);
|
||||||
|
engine.NotifyShield(player.Id, player.Name, _shield, Name);
|
||||||
|
engine.Log.AddInfo($"[try/catch] +{_shield} Shield.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new TryCatchCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_shield = this._shield,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GcCollectCard : Card
|
||||||
|
{
|
||||||
|
private int _baseDamage = 8;
|
||||||
|
|
||||||
|
public GcCollectCard() : base(
|
||||||
|
"GC.Collect()",
|
||||||
|
"GC.Collect(2);",
|
||||||
|
"target.Hit(8 + Discard * 2);",
|
||||||
|
"Deal 8 damage + 2 per Discard card.",
|
||||||
|
cost: 2,
|
||||||
|
type: CardType.Attack,
|
||||||
|
rarity: CardRarity.Common,
|
||||||
|
targetType: TargetType.SingleEnemy
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_baseDamage = 12;
|
||||||
|
Description = "Deal 12 damage + 2 per Discard card.";
|
||||||
|
CodeSnippet = "target.Hit(12 + Discard * 2);";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
if (target == null) return;
|
||||||
|
var discardCount = player.DiscardPile.Count;
|
||||||
|
var rawDmg = _baseDamage + (discardCount * 2);
|
||||||
|
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var totalDmg = player.Statuses.CalculateOutgoingDamage(rawDmg);
|
||||||
|
var dealt = target.TakeDamage(totalDmg);
|
||||||
|
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
|
||||||
|
engine.Log.AddSuccess($"[GC.Collect] Swept {discardCount} objects! Dealt {dealt} dmg.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new GcCollectCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_baseDamage = this._baseDamage,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SpanSliceCard : Card
|
||||||
|
{
|
||||||
|
private int _damage = 8;
|
||||||
|
|
||||||
|
public SpanSliceCard() : base(
|
||||||
|
"Span<T>.Slice()",
|
||||||
|
"span.Slice(0, 8);",
|
||||||
|
"target.DirectHit(8);",
|
||||||
|
"Deal 8 piercing damage (ignores shield).",
|
||||||
|
cost: 1,
|
||||||
|
type: CardType.Attack,
|
||||||
|
rarity: CardRarity.Common,
|
||||||
|
targetType: TargetType.SingleEnemy
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_damage = 11;
|
||||||
|
Description = "Deal 11 piercing damage (ignores shield).";
|
||||||
|
CodeSnippet = "target.DirectHit(11);";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
if (target == null) return;
|
||||||
|
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
|
||||||
|
var dealt = target.TakeDamage(totalDmg, isPiercing: true);
|
||||||
|
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: true, isCrit: isCrit, Name);
|
||||||
|
engine.Log.AddSuccess($"[Span.Slice] Pierced shield! Dealt {dealt} dmg.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new SpanSliceCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_damage = this._damage,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThrowExceptionCard : Card
|
||||||
|
{
|
||||||
|
private int _damage = 16;
|
||||||
|
|
||||||
|
public ThrowExceptionCard() : base(
|
||||||
|
"throw Exception()",
|
||||||
|
"throw new PanicException();",
|
||||||
|
"target.Hit(16); // +1 Leak",
|
||||||
|
"Deal 16 damage. Gain 1 Memory Leak.",
|
||||||
|
cost: 2,
|
||||||
|
type: CardType.Attack,
|
||||||
|
rarity: CardRarity.Uncommon,
|
||||||
|
targetType: TargetType.SingleEnemy
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_damage = 22;
|
||||||
|
Description = "Deal 22 damage. Gain 1 Memory Leak.";
|
||||||
|
CodeSnippet = "target.Hit(22); // +1 Leak";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
if (target == null) return;
|
||||||
|
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var totalDmg = player.Statuses.CalculateOutgoingDamage(_damage);
|
||||||
|
var dealt = target.TakeDamage(totalDmg);
|
||||||
|
player.Statuses.Add(StatusType.MemoryLeak, 1);
|
||||||
|
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, Name);
|
||||||
|
engine.Log.AddWarning($"[throw Exception] Dealt {dealt} dmg. +1 Memory Leak.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new ThrowExceptionCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_damage = this._damage,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TieredCompilationCard : Card
|
||||||
|
{
|
||||||
|
private int _strength = 2;
|
||||||
|
|
||||||
|
public TieredCompilationCard() : base(
|
||||||
|
"Tiered Compilation",
|
||||||
|
"[AggressiveOptimization]",
|
||||||
|
"Player.Strength += 2;",
|
||||||
|
"Power: +2 Strength (All attacks deal +2 damage).",
|
||||||
|
cost: 1,
|
||||||
|
type: CardType.Power,
|
||||||
|
rarity: CardRarity.Rare,
|
||||||
|
targetType: TargetType.Self,
|
||||||
|
exhausts: true
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_strength = 3;
|
||||||
|
Description = "Power: +3 Strength (All attacks deal +3 damage).";
|
||||||
|
CodeSnippet = "Player.Strength += 3;";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
player.Statuses.Add(StatusType.Strength, _strength);
|
||||||
|
engine.Log.AddSuccess($"[JIT] +{_strength} Strength permanently.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new TieredCompilationCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_strength = this._strength,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThreadSleepCard : Card
|
||||||
|
{
|
||||||
|
public ThreadSleepCard() : base(
|
||||||
|
"Thread.Sleep()",
|
||||||
|
"await Task.Yield();",
|
||||||
|
"CPU += 2; Weak += 1;",
|
||||||
|
"Gain +2 CPU this turn. Gain 1 Weak.",
|
||||||
|
cost: 0,
|
||||||
|
type: CardType.Skill,
|
||||||
|
rarity: CardRarity.Common,
|
||||||
|
targetType: TargetType.Self
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
Description = "Gain +3 CPU this turn. Gain 1 Weak.";
|
||||||
|
CodeSnippet = "CPU += 3; Weak += 1;";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
var energyBonus = Upgraded ? 3 : 2;
|
||||||
|
player.CurrentEnergy += energyBonus;
|
||||||
|
player.Statuses.Add(StatusType.Weak, 1);
|
||||||
|
engine.Log.AddInfo($"[Thread.Sleep] +{energyBonus} CPU, +1 Weak.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new ThreadSleepCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParallelForEachCard : Card
|
||||||
|
{
|
||||||
|
private int _dmgPerHit = 4;
|
||||||
|
|
||||||
|
public ParallelForEachCard() : base(
|
||||||
|
"Parallel.ForEach()",
|
||||||
|
"Parallel.For(0, 3);",
|
||||||
|
"target.Hit(4); // x3 hits",
|
||||||
|
"Attack 3 times for 4 damage each.",
|
||||||
|
cost: 2,
|
||||||
|
type: CardType.Attack,
|
||||||
|
rarity: CardRarity.Uncommon,
|
||||||
|
targetType: TargetType.SingleEnemy
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_dmgPerHit = 5;
|
||||||
|
Description = "Attack 3 times for 5 damage each.";
|
||||||
|
CodeSnippet = "target.Hit(5); // x3 hits";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
if (target == null) return;
|
||||||
|
int totalHits = 3;
|
||||||
|
int totalDamage = 0;
|
||||||
|
var isCrit = target.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
for (int i = 0; i < totalHits; i++)
|
||||||
|
{
|
||||||
|
if (target.IsDead) break;
|
||||||
|
var hitDmg = player.Statuses.CalculateOutgoingDamage(_dmgPerHit);
|
||||||
|
var dealt = target.TakeDamage(hitDmg);
|
||||||
|
totalDamage += dealt;
|
||||||
|
engine.NotifyDamage(target.Id, target.Name, dealt, isPiercing: false, isCrit: isCrit, $"{Name} #{i+1}");
|
||||||
|
}
|
||||||
|
engine.Log.AddSuccess($"[Parallel.ForEach] 3 hits: total {totalDamage} dmg.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new ParallelForEachCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_dmgPerHit = this._dmgPerHit,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LinqWhereCard : Card
|
||||||
|
{
|
||||||
|
private int _shield = 6;
|
||||||
|
|
||||||
|
public LinqWhereCard() : base(
|
||||||
|
"collection.Where()",
|
||||||
|
"data.Where(x => x.IsSafe);",
|
||||||
|
"shield += 6; Draw(1);",
|
||||||
|
"Gain 6 Shield. Draw 1 card.",
|
||||||
|
cost: 1,
|
||||||
|
type: CardType.Skill,
|
||||||
|
rarity: CardRarity.Common,
|
||||||
|
targetType: TargetType.Self
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
_shield = 9;
|
||||||
|
Description = "Gain 9 Shield. Draw 1 card.";
|
||||||
|
CodeSnippet = "shield += 9; Draw(1);";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
player.GainShield(_shield);
|
||||||
|
engine.NotifyShield(player.Id, player.Name, _shield, Name);
|
||||||
|
player.DrawCards(1, engine.Log);
|
||||||
|
engine.Log.AddInfo($"[LINQ .Where()] +{_shield} Shield, drew 1 card.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new LinqWhereCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description,
|
||||||
|
_shield = this._shield,
|
||||||
|
CodeSnippet = this.CodeSnippet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NullCoalescingCard : Card
|
||||||
|
{
|
||||||
|
public NullCoalescingCard() : base(
|
||||||
|
"fallback ?? default",
|
||||||
|
"val ?? FallbackState;",
|
||||||
|
"ClearDebuffs(); shield += 6;",
|
||||||
|
"Remove all debuffs. Gain 6 Shield.",
|
||||||
|
cost: 1,
|
||||||
|
type: CardType.Skill,
|
||||||
|
rarity: CardRarity.Uncommon,
|
||||||
|
targetType: TargetType.Self
|
||||||
|
) { }
|
||||||
|
|
||||||
|
protected override void OnUpgrade()
|
||||||
|
{
|
||||||
|
Description = "Remove all debuffs. Gain 8 Shield.";
|
||||||
|
CodeSnippet = "ClearDebuffs(); shield += 8;";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyEffect(CombatEngine engine, Player player, Character? target)
|
||||||
|
{
|
||||||
|
var stacksToRemove = Upgraded ? 2 : 1;
|
||||||
|
var shield = Upgraded ? 8 : 6;
|
||||||
|
player.Statuses.Remove(StatusType.Vulnerable, stacksToRemove);
|
||||||
|
player.Statuses.Remove(StatusType.Weak, stacksToRemove);
|
||||||
|
player.Statuses.Remove(StatusType.MemoryLeak, stacksToRemove);
|
||||||
|
player.GainShield(shield);
|
||||||
|
engine.NotifyShield(player.Id, player.Name, shield, Name);
|
||||||
|
engine.Log.AddSuccess($"[Null Coalescing ??] Cleared debuffs, +{shield} Shield.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Card Clone() => new NullCoalescingCard
|
||||||
|
{
|
||||||
|
Upgraded = this.Upgraded,
|
||||||
|
Name = this.Name,
|
||||||
|
Description = this.Description
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
using SlaySharp.Core.Combat;
|
||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
using SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Catalogs;
|
||||||
|
|
||||||
|
public class NullReferenceExceptionEnemy : Enemy
|
||||||
|
{
|
||||||
|
private int _patternStep = 0;
|
||||||
|
|
||||||
|
public NullReferenceExceptionEnemy()
|
||||||
|
: base("NullReferenceException", 45, "💥", "Unhandled null pointer reference causing sudden application crashes.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DecideNextIntent(CombatEngine engine)
|
||||||
|
{
|
||||||
|
CurrentIntent = _patternStep switch
|
||||||
|
{
|
||||||
|
0 => new Intent(IntentType.Attack, 7, 1, "Dereference Null Pointer (7 dmg)"),
|
||||||
|
1 => new Intent(IntentType.AttackDefend, 6, 1, "Corrupt Stack Frame (6 dmg & +7 Shield)"),
|
||||||
|
2 => new Intent(IntentType.Debuff, 0, 1, "Expose Null Pointer (+2 Vulnerable)", StatusType.Vulnerable, 2),
|
||||||
|
3 => new Intent(IntentType.Attack, 14, 1, "FATAL PROCESS TERMINATION (14 dmg)"),
|
||||||
|
_ => new Intent(IntentType.Attack, 8, 1, "Null Strike (8 dmg)")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ExecuteIntent(CombatEngine engine)
|
||||||
|
{
|
||||||
|
if (CurrentIntent == null) return;
|
||||||
|
|
||||||
|
switch (_patternStep)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
var dmg0 = Statuses.CalculateOutgoingDamage(CurrentIntent.PrimaryValue);
|
||||||
|
var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var dealt0 = engine.Player.TakeDamage(dmg0);
|
||||||
|
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, isPiercing: false, isCrit: isCrit0, Name);
|
||||||
|
engine.Log.AddError($"[{Name}] dereferenced null pointer! Dealt {dealt0} damage to Player.");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
var dmg1 = Statuses.CalculateOutgoingDamage(CurrentIntent.PrimaryValue);
|
||||||
|
var isCrit1 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var dealt1 = engine.Player.TakeDamage(dmg1);
|
||||||
|
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt1, isPiercing: false, isCrit: isCrit1, Name);
|
||||||
|
GainShield(7);
|
||||||
|
engine.NotifyShield(Id, Name, 7, Name);
|
||||||
|
engine.Log.AddError($"[{Name}] corrupted call stack! Dealt {dealt1} damage and gained +7 Shield.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
engine.Player.Statuses.Add(StatusType.Vulnerable, 2);
|
||||||
|
GainShield(5);
|
||||||
|
engine.NotifyShield(Id, Name, 5, Name);
|
||||||
|
engine.Log.AddWarning($"[{Name}] exposed unhandled pointer! Player is now Vulnerable for 2 turns.");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
var dmg3 = Statuses.CalculateOutgoingDamage(CurrentIntent.PrimaryValue);
|
||||||
|
var isCrit3 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var dealt3 = engine.Player.TakeDamage(dmg3);
|
||||||
|
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt3, isPiercing: false, isCrit: isCrit3, Name);
|
||||||
|
engine.Log.AddFatal($"[{Name}] TRIGGERED PROCESS CRASH! Massive {dealt3} damage dealt!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_patternStep = (_patternStep + 1) % 4;
|
||||||
|
DecideNextIntent(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OffByOneBugEnemy : Enemy
|
||||||
|
{
|
||||||
|
private int _patternStep = 0;
|
||||||
|
|
||||||
|
public OffByOneBugEnemy()
|
||||||
|
: base("Off-By-One Bug", 22, "🐛", "Iterative boundary bug indexing `for (int i = 0; i <= arr.Length; i++)`.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DecideNextIntent(CombatEngine engine)
|
||||||
|
{
|
||||||
|
CurrentIntent = _patternStep switch
|
||||||
|
{
|
||||||
|
0 => new Intent(IntentType.Attack, 3, 3, "IndexOutOfRangeException (3x3 dmg)"),
|
||||||
|
1 => new Intent(IntentType.Defend, 6, 1, "Loop Boundary Shield (+6 Shield)"),
|
||||||
|
_ => new Intent(IntentType.Attack, 4, 2, "Array Overflow (4x2 dmg)")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ExecuteIntent(CombatEngine engine)
|
||||||
|
{
|
||||||
|
if (CurrentIntent == null) return;
|
||||||
|
|
||||||
|
switch (_patternStep)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
int totalDmg = 0;
|
||||||
|
var isCrit = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var dmg = Statuses.CalculateOutgoingDamage(3);
|
||||||
|
var dealt = engine.Player.TakeDamage(dmg);
|
||||||
|
totalDmg += dealt;
|
||||||
|
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt, isPiercing: false, isCrit: isCrit, $"{Name} #{i+1}");
|
||||||
|
}
|
||||||
|
engine.Log.AddWarning($"[{Name}] looped past boundary! 3 hits for total {totalDmg} damage!");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
GainShield(6);
|
||||||
|
Statuses.Add(StatusType.Strength, 1);
|
||||||
|
engine.NotifyShield(Id, Name, 6, Name);
|
||||||
|
engine.Log.AddInfo($"[{Name}] reinforced iteration bound! Gained +6 Shield and +1 Strength.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_patternStep = (_patternStep + 1) % 2;
|
||||||
|
DecideNextIntent(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MemoryLeakSpectreEnemy : Enemy
|
||||||
|
{
|
||||||
|
private int _turn = 0;
|
||||||
|
|
||||||
|
public MemoryLeakSpectreEnemy()
|
||||||
|
: base("Memory Leak Spectre", 34, "👻", "Unmanaged IDisposable and dangling event handlers slowly devouring RAM.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DecideNextIntent(CombatEngine engine)
|
||||||
|
{
|
||||||
|
CurrentIntent = (_turn % 3) switch
|
||||||
|
{
|
||||||
|
0 => new Intent(IntentType.Attack, 6, 1, "Heap Allocation Spurt (6 dmg)"),
|
||||||
|
1 => new Intent(IntentType.Debuff, 0, 1, "Dangling Event Handler (+1 Memory Leak)", StatusType.MemoryLeak, 1),
|
||||||
|
2 => new Intent(IntentType.Attack, 11, 1, "Out Of Memory Wave (11 dmg)"),
|
||||||
|
_ => new Intent(IntentType.Attack, 6, 1, "Heap Bloat (6 dmg)")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ExecuteIntent(CombatEngine engine)
|
||||||
|
{
|
||||||
|
if (CurrentIntent == null) return;
|
||||||
|
|
||||||
|
switch (_turn % 3)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
var dmg0 = Statuses.CalculateOutgoingDamage(6);
|
||||||
|
var isCrit0 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var dealt0 = engine.Player.TakeDamage(dmg0);
|
||||||
|
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt0, isPiercing: false, isCrit: isCrit0, Name);
|
||||||
|
engine.Log.AddWarning($"[{Name}] allocated 200MB unmanaged memory! Dealt {dealt0} dmg.");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
engine.Player.Statuses.Add(StatusType.MemoryLeak, 1);
|
||||||
|
engine.Log.AddError($"[{Name}] hooked persistent event handler! Player gained +1 Memory Leak.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
var dmg2 = Statuses.CalculateOutgoingDamage(11);
|
||||||
|
var isCrit2 = engine.Player.Statuses.Get(StatusType.Vulnerable) > 0;
|
||||||
|
var dealt2 = engine.Player.TakeDamage(dmg2);
|
||||||
|
engine.NotifyDamage(engine.Player.Id, engine.Player.Name, dealt2, isPiercing: false, isCrit: isCrit2, Name);
|
||||||
|
engine.Log.AddFatal($"[{Name}] triggered GC Thrashing OOM! Dealt {dealt2} damage.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_turn++;
|
||||||
|
DecideNextIntent(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Catalogs;
|
||||||
|
|
||||||
|
public static class StarterDecks
|
||||||
|
{
|
||||||
|
public static List<Card> CreateCsharpArchitectDeck()
|
||||||
|
{
|
||||||
|
return new List<Card>
|
||||||
|
{
|
||||||
|
new ConsoleWriteLineCard(),
|
||||||
|
new ConsoleWriteLineCard(),
|
||||||
|
new ConsoleWriteLineCard(),
|
||||||
|
new ConsoleWriteLineCard(),
|
||||||
|
new TryCatchCard(),
|
||||||
|
new TryCatchCard(),
|
||||||
|
new TryCatchCard(),
|
||||||
|
new TryCatchCard(),
|
||||||
|
new GcCollectCard(),
|
||||||
|
new SpanSliceCard(),
|
||||||
|
new LinqWhereCard(),
|
||||||
|
new TieredCompilationCard()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
using SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Combat;
|
||||||
|
|
||||||
|
public class CombatEngine
|
||||||
|
{
|
||||||
|
public Player Player { get; private set; } = null!;
|
||||||
|
public List<Enemy> Enemies { get; private set; } = new();
|
||||||
|
public int TurnNumber { get; private set; }
|
||||||
|
public CombatPhase Phase { get; private set; } = CombatPhase.PlayerTurn;
|
||||||
|
public CombatLog Log { get; } = new();
|
||||||
|
public string? ActiveActingEnemyId { get; private set; }
|
||||||
|
|
||||||
|
public event Action? OnStateChanged;
|
||||||
|
public event Action<DamageEvent>? OnDamageDealt;
|
||||||
|
public event Action<ShieldEvent>? OnShieldGained;
|
||||||
|
public event Action<CardPlayAnimationEvent>? OnCardPlayAnimation;
|
||||||
|
|
||||||
|
public void NotifyDamage(string targetId, string targetName, int amount, bool isPiercing, bool isCrit, string sourceName)
|
||||||
|
{
|
||||||
|
OnDamageDealt?.Invoke(new DamageEvent(targetId, targetName, amount, isPiercing, isCrit, sourceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyShield(string targetId, string targetName, int amount, string sourceName)
|
||||||
|
{
|
||||||
|
OnShieldGained?.Invoke(new ShieldEvent(targetId, targetName, amount, sourceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartCombat(Player player, List<Enemy> enemies)
|
||||||
|
{
|
||||||
|
Player = player;
|
||||||
|
Enemies = enemies;
|
||||||
|
TurnNumber = 1;
|
||||||
|
Phase = CombatPhase.PlayerTurn;
|
||||||
|
ActiveActingEnemyId = null;
|
||||||
|
|
||||||
|
Log.Clear();
|
||||||
|
Log.AddInfo($"=== Starting Incident Resolution: {enemies.Count} Bugs Detected ===");
|
||||||
|
|
||||||
|
Player.InitCombatDeck();
|
||||||
|
|
||||||
|
foreach (var enemy in Enemies)
|
||||||
|
{
|
||||||
|
enemy.Statuses.Clear();
|
||||||
|
enemy.Shield = 0;
|
||||||
|
enemy.DecideNextIntent(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var relic in Player.Relics)
|
||||||
|
{
|
||||||
|
relic.OnCombatStart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPlayerTurn(isFirstTurn: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartPlayerTurn(bool isFirstTurn = false)
|
||||||
|
{
|
||||||
|
Phase = CombatPhase.PlayerTurn;
|
||||||
|
ActiveActingEnemyId = null;
|
||||||
|
Player.ResetShield();
|
||||||
|
Player.ResetTurnEnergy();
|
||||||
|
|
||||||
|
Log.AddInfo($"--- Turn {TurnNumber}: Player Execution Phase ---");
|
||||||
|
|
||||||
|
foreach (var relic in Player.Relics)
|
||||||
|
{
|
||||||
|
relic.OnTurnStart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
int drawCount = 5;
|
||||||
|
Player.DrawCards(drawCount, Log);
|
||||||
|
|
||||||
|
NotifyStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PlayCard(Card card, Character? target = null)
|
||||||
|
{
|
||||||
|
if (Phase != CombatPhase.PlayerTurn)
|
||||||
|
{
|
||||||
|
Log.AddWarning("Cannot play cards outside of player turn.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Player.Hand.Contains(card))
|
||||||
|
{
|
||||||
|
Log.AddError($"Card '{card.Name}' is not in player hand.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Player.CurrentEnergy < card.Cost)
|
||||||
|
{
|
||||||
|
Log.AddWarning($"Insufficient CPU cycles! Card requires {card.Cost} CPU, available: {Player.CurrentEnergy}.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (card.TargetType == TargetType.SingleEnemy)
|
||||||
|
{
|
||||||
|
if (target is not Enemy enemyTarget || enemyTarget.IsDead || !Enemies.Contains(enemyTarget))
|
||||||
|
{
|
||||||
|
Log.AddWarning("Please select a valid bug target for this card.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Player.CurrentEnergy -= card.Cost;
|
||||||
|
Player.Hand.Remove(card);
|
||||||
|
|
||||||
|
OnCardPlayAnimation?.Invoke(new CardPlayAnimationEvent(
|
||||||
|
card.Id,
|
||||||
|
card.Name,
|
||||||
|
target?.Id,
|
||||||
|
card.TargetType == TargetType.AllEnemies
|
||||||
|
));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
card.ApplyEffect(this, Player, target);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.AddFatal($"Runtime Exception while executing card effect: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var relic in Player.Relics)
|
||||||
|
{
|
||||||
|
relic.OnCardPlayed(this, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (card.Exhausts)
|
||||||
|
{
|
||||||
|
Player.ExhaustPile.Add(card);
|
||||||
|
Log.AddInfo($"[Exhaust] Card '{card.Name}' was finalized and unallocated (Exhaust Pile).");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Player.DiscardPile.Add(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanupDeadEnemies();
|
||||||
|
CheckBattleConditions();
|
||||||
|
NotifyStateChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndPlayerTurn()
|
||||||
|
{
|
||||||
|
_ = EndPlayerTurnAsync(stepDelayMs: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EndPlayerTurnAsync(
|
||||||
|
Func<Enemy, Task>? onBeforeEnemyActs = null,
|
||||||
|
Func<Enemy, Task>? onAfterEnemyActs = null,
|
||||||
|
int stepDelayMs = 1000)
|
||||||
|
{
|
||||||
|
if (Phase != CombatPhase.PlayerTurn) return;
|
||||||
|
|
||||||
|
Log.AddInfo("Ending player turn. Discarding active memory...");
|
||||||
|
Player.DiscardHand(Log);
|
||||||
|
NotifyStateChanged();
|
||||||
|
|
||||||
|
if (stepDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
Player.Statuses.OnTurnEnd(
|
||||||
|
logMsg => Log.AddWarning(logMsg),
|
||||||
|
dmg =>
|
||||||
|
{
|
||||||
|
var taken = Player.TakeDamage(dmg, isPiercing: true);
|
||||||
|
NotifyDamage(Player.Id, Player.Name, taken, isPiercing: true, isCrit: false, "MemoryLeak");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var relic in Player.Relics)
|
||||||
|
{
|
||||||
|
relic.OnTurnEnd(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Player.IsDead)
|
||||||
|
{
|
||||||
|
Phase = CombatPhase.Defeat;
|
||||||
|
Log.AddFatal("=== PRODUCTION CRASH: Uptime 0%, All Services Offline ===");
|
||||||
|
NotifyStateChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBattleConditions();
|
||||||
|
if (Phase == CombatPhase.Victory) return;
|
||||||
|
|
||||||
|
await ExecuteEnemyTurnsAsync(onBeforeEnemyActs, onAfterEnemyActs, stepDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteEnemyTurnsAsync(
|
||||||
|
Func<Enemy, Task>? onBeforeEnemyActs = null,
|
||||||
|
Func<Enemy, Task>? onAfterEnemyActs = null,
|
||||||
|
int stepDelayMs = 1000)
|
||||||
|
{
|
||||||
|
Phase = CombatPhase.EnemyTurn;
|
||||||
|
Log.AddInfo($"--- Turn {TurnNumber}: Bug Execution Phase ---");
|
||||||
|
NotifyStateChanged();
|
||||||
|
|
||||||
|
if (stepDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var enemy in Enemies.ToList())
|
||||||
|
{
|
||||||
|
if (enemy.IsDead) continue;
|
||||||
|
|
||||||
|
ActiveActingEnemyId = enemy.Id;
|
||||||
|
NotifyStateChanged();
|
||||||
|
|
||||||
|
if (onBeforeEnemyActs != null)
|
||||||
|
{
|
||||||
|
await onBeforeEnemyActs(enemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(stepDelayMs / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
enemy.ResetShield();
|
||||||
|
enemy.Statuses.OnTurnEnd(
|
||||||
|
logMsg => Log.AddInfo($"[{enemy.Name}] {logMsg}"),
|
||||||
|
dmg =>
|
||||||
|
{
|
||||||
|
var taken = enemy.TakeDamage(dmg, isPiercing: true);
|
||||||
|
NotifyDamage(enemy.Id, enemy.Name, taken, isPiercing: true, isCrit: false, "Debuff");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (enemy.IsDead)
|
||||||
|
{
|
||||||
|
CleanupDeadEnemies();
|
||||||
|
ActiveActingEnemyId = null;
|
||||||
|
NotifyStateChanged();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
enemy.ExecuteIntent(this);
|
||||||
|
NotifyStateChanged();
|
||||||
|
|
||||||
|
if (onAfterEnemyActs != null)
|
||||||
|
{
|
||||||
|
await onAfterEnemyActs(enemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(stepDelayMs / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Player.IsDead)
|
||||||
|
{
|
||||||
|
Phase = CombatPhase.Defeat;
|
||||||
|
ActiveActingEnemyId = null;
|
||||||
|
Log.AddFatal("=== PRODUCTION CRASH: Uptime 0%, All Services Offline ===");
|
||||||
|
NotifyStateChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveActingEnemyId = null;
|
||||||
|
CleanupDeadEnemies();
|
||||||
|
CheckBattleConditions();
|
||||||
|
|
||||||
|
if (Phase != CombatPhase.Victory && Phase != CombatPhase.Defeat)
|
||||||
|
{
|
||||||
|
if (stepDelayMs > 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(350);
|
||||||
|
}
|
||||||
|
TurnNumber++;
|
||||||
|
StartPlayerTurn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupDeadEnemies()
|
||||||
|
{
|
||||||
|
for (int i = Enemies.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Enemies[i].IsDead)
|
||||||
|
{
|
||||||
|
Log.AddSuccess($"[RESOLVED] Bug '{Enemies[i].Name}' was successfully patched and disposed!");
|
||||||
|
Enemies.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckBattleConditions()
|
||||||
|
{
|
||||||
|
if (Player.IsDead)
|
||||||
|
{
|
||||||
|
Phase = CombatPhase.Defeat;
|
||||||
|
}
|
||||||
|
else if (Enemies.Count == 0)
|
||||||
|
{
|
||||||
|
Phase = CombatPhase.Victory;
|
||||||
|
Log.AddSuccess("=== ALL BUGS RESOLVED! Build Deployed Successfully! ===");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyStateChanged() => OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace SlaySharp.Core.Combat;
|
||||||
|
|
||||||
|
public record DamageEvent(
|
||||||
|
string TargetId,
|
||||||
|
string TargetName,
|
||||||
|
int Amount,
|
||||||
|
bool IsPiercing,
|
||||||
|
bool IsCrit,
|
||||||
|
string SourceName
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ShieldEvent(
|
||||||
|
string TargetId,
|
||||||
|
string TargetName,
|
||||||
|
int Amount,
|
||||||
|
string SourceName
|
||||||
|
);
|
||||||
|
|
||||||
|
public record CardPlayAnimationEvent(
|
||||||
|
string CardId,
|
||||||
|
string CardName,
|
||||||
|
string? TargetId,
|
||||||
|
bool IsAoe
|
||||||
|
);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace SlaySharp.Core.Combat;
|
||||||
|
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Success,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
public record LogEntry(DateTime Timestamp, LogLevel Level, string Message)
|
||||||
|
{
|
||||||
|
public string FormattedTime => Timestamp.ToString("HH:mm:ss.fff");
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CombatLog
|
||||||
|
{
|
||||||
|
private readonly List<LogEntry> _entries = new();
|
||||||
|
public IReadOnlyList<LogEntry> Entries => _entries;
|
||||||
|
|
||||||
|
public event Action? OnLogAdded;
|
||||||
|
|
||||||
|
public void Add(LogLevel level, string message)
|
||||||
|
{
|
||||||
|
_entries.Add(new LogEntry(DateTime.Now, level, message));
|
||||||
|
OnLogAdded?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddInfo(string message) => Add(LogLevel.Info, message);
|
||||||
|
public void AddSuccess(string message) => Add(LogLevel.Success, message);
|
||||||
|
public void AddWarning(string message) => Add(LogLevel.Warning, message);
|
||||||
|
public void AddError(string message) => Add(LogLevel.Error, message);
|
||||||
|
public void AddFatal(string message) => Add(LogLevel.Fatal, message);
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_entries.Clear();
|
||||||
|
OnLogAdded?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
namespace SlaySharp.Core.Enums;
|
||||||
|
|
||||||
|
public enum CardType
|
||||||
|
{
|
||||||
|
Attack,
|
||||||
|
Skill,
|
||||||
|
Power,
|
||||||
|
Curse
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CardRarity
|
||||||
|
{
|
||||||
|
Basic,
|
||||||
|
Common,
|
||||||
|
Uncommon,
|
||||||
|
Rare,
|
||||||
|
Curse
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TargetType
|
||||||
|
{
|
||||||
|
SingleEnemy,
|
||||||
|
AllEnemies,
|
||||||
|
Self,
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StatusType
|
||||||
|
{
|
||||||
|
Strength,
|
||||||
|
Vulnerable,
|
||||||
|
Weak,
|
||||||
|
MemoryLeak,
|
||||||
|
JitTier,
|
||||||
|
ShieldRetain,
|
||||||
|
Overclocked
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum IntentType
|
||||||
|
{
|
||||||
|
Attack,
|
||||||
|
Defend,
|
||||||
|
Buff,
|
||||||
|
Debuff,
|
||||||
|
AttackDefend,
|
||||||
|
Special
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CombatPhase
|
||||||
|
{
|
||||||
|
PlayerTurn,
|
||||||
|
EnemyTurn,
|
||||||
|
Victory,
|
||||||
|
Defeat
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using SlaySharp.Core.Combat;
|
||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
public abstract class Card
|
||||||
|
{
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
|
||||||
|
public string Name { get; protected set; }
|
||||||
|
public string SyntaxSignature { get; protected set; }
|
||||||
|
public string CodeSnippet { get; protected set; }
|
||||||
|
public string Description { get; protected set; }
|
||||||
|
public int Cost { get; protected set; }
|
||||||
|
public CardType Type { get; }
|
||||||
|
public CardRarity Rarity { get; }
|
||||||
|
public TargetType TargetType { get; }
|
||||||
|
public bool Upgraded { get; protected set; }
|
||||||
|
public bool Exhausts { get; protected set; }
|
||||||
|
|
||||||
|
protected Card(
|
||||||
|
string name,
|
||||||
|
string syntaxSignature,
|
||||||
|
string codeSnippet,
|
||||||
|
string description,
|
||||||
|
int cost,
|
||||||
|
CardType type,
|
||||||
|
CardRarity rarity,
|
||||||
|
TargetType targetType,
|
||||||
|
bool exhausts = false)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
SyntaxSignature = syntaxSignature;
|
||||||
|
CodeSnippet = codeSnippet;
|
||||||
|
Description = description;
|
||||||
|
Cost = cost;
|
||||||
|
Type = type;
|
||||||
|
Rarity = rarity;
|
||||||
|
TargetType = targetType;
|
||||||
|
Exhausts = exhausts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool CanPlay(CombatEngine engine)
|
||||||
|
{
|
||||||
|
return engine.Player.CurrentEnergy >= Cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void ApplyEffect(CombatEngine engine, Player player, Character? target);
|
||||||
|
|
||||||
|
public virtual void Upgrade()
|
||||||
|
{
|
||||||
|
if (Upgraded) return;
|
||||||
|
Upgraded = true;
|
||||||
|
Name += "+";
|
||||||
|
OnUpgrade();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnUpgrade() { }
|
||||||
|
|
||||||
|
public abstract Card Clone();
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
public abstract class Character
|
||||||
|
{
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int MaxHp { get; set; }
|
||||||
|
public int CurrentHp { get; set; }
|
||||||
|
public int Shield { get; set; }
|
||||||
|
public StatusCollection Statuses { get; } = new();
|
||||||
|
|
||||||
|
public bool IsDead => CurrentHp <= 0;
|
||||||
|
|
||||||
|
protected Character(string name, int maxHp)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
MaxHp = maxHp;
|
||||||
|
CurrentHp = maxHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual int TakeDamage(int rawAmount, bool isPiercing = false)
|
||||||
|
{
|
||||||
|
if (IsDead || rawAmount <= 0) return 0;
|
||||||
|
|
||||||
|
var modifiedAmount = Statuses.CalculateIncomingDamage(rawAmount);
|
||||||
|
var damageToHp = modifiedAmount;
|
||||||
|
|
||||||
|
if (!isPiercing && Shield > 0)
|
||||||
|
{
|
||||||
|
if (Shield >= modifiedAmount)
|
||||||
|
{
|
||||||
|
Shield -= modifiedAmount;
|
||||||
|
damageToHp = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
damageToHp = modifiedAmount - Shield;
|
||||||
|
Shield = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentHp = Math.Max(0, CurrentHp - damageToHp);
|
||||||
|
return damageToHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void GainShield(int amount)
|
||||||
|
{
|
||||||
|
if (IsDead || amount <= 0) return;
|
||||||
|
Shield += amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void ResetShield()
|
||||||
|
{
|
||||||
|
if (Statuses.Get(StatusType.ShieldRetain) == 0)
|
||||||
|
{
|
||||||
|
Shield = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Heal(int amount)
|
||||||
|
{
|
||||||
|
if (IsDead || amount <= 0) return;
|
||||||
|
CurrentHp = Math.Min(MaxHp, CurrentHp + amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using SlaySharp.Core.Combat;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
public abstract class Enemy : Character
|
||||||
|
{
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string Icon { get; set; } = "👾";
|
||||||
|
public Intent? CurrentIntent { get; protected set; }
|
||||||
|
|
||||||
|
protected Enemy(string name, int maxHp, string icon = "👾", string description = "")
|
||||||
|
: base(name, maxHp)
|
||||||
|
{
|
||||||
|
Icon = icon;
|
||||||
|
Description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void DecideNextIntent(CombatEngine engine);
|
||||||
|
public abstract void ExecuteIntent(CombatEngine engine);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
public record Intent(
|
||||||
|
IntentType Type,
|
||||||
|
int PrimaryValue,
|
||||||
|
int Hits = 1,
|
||||||
|
string Description = "",
|
||||||
|
StatusType? DebuffToApply = null,
|
||||||
|
int DebuffStacks = 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public string DisplayText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Type switch
|
||||||
|
{
|
||||||
|
IntentType.Attack => Hits > 1 ? $"{PrimaryValue} x {Hits} dmg" : $"{PrimaryValue} dmg",
|
||||||
|
IntentType.Defend => $"+{PrimaryValue} Shield",
|
||||||
|
IntentType.AttackDefend => $"{PrimaryValue} dmg & +{PrimaryValue} Shield",
|
||||||
|
IntentType.Debuff => DebuffToApply.HasValue ? $"Debuff: {DebuffToApply.Value} ({DebuffStacks})" : "Debuff",
|
||||||
|
IntentType.Buff => $"+{PrimaryValue} Buff",
|
||||||
|
IntentType.Special => Description,
|
||||||
|
_ => Description
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using SlaySharp.Core.Combat;
|
||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
using SlaySharp.Core.Relics;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
public class Player : Character
|
||||||
|
{
|
||||||
|
public int MaxEnergy { get; set; } = 3;
|
||||||
|
public int CurrentEnergy { get; set; } = 3;
|
||||||
|
|
||||||
|
public List<Card> MasterDeck { get; } = new();
|
||||||
|
public List<Card> DrawPile { get; } = new();
|
||||||
|
public List<Card> Hand { get; } = new();
|
||||||
|
public List<Card> DiscardPile { get; } = new();
|
||||||
|
public List<Card> ExhaustPile { get; } = new();
|
||||||
|
public List<Relic> Relics { get; } = new();
|
||||||
|
|
||||||
|
private readonly Random _rng;
|
||||||
|
|
||||||
|
public Player(string name, int maxHp, int maxEnergy = 3, int? seed = null)
|
||||||
|
: base(name, maxHp)
|
||||||
|
{
|
||||||
|
MaxEnergy = maxEnergy;
|
||||||
|
CurrentEnergy = maxEnergy;
|
||||||
|
_rng = seed.HasValue ? new Random(seed.Value) : new Random();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitCombatDeck()
|
||||||
|
{
|
||||||
|
DrawPile.Clear();
|
||||||
|
Hand.Clear();
|
||||||
|
DiscardPile.Clear();
|
||||||
|
ExhaustPile.Clear();
|
||||||
|
Shield = 0;
|
||||||
|
Statuses.Clear();
|
||||||
|
|
||||||
|
foreach (var card in MasterDeck)
|
||||||
|
{
|
||||||
|
DrawPile.Add(card.Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
ShuffleDrawPile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShuffleDrawPile()
|
||||||
|
{
|
||||||
|
int n = DrawPile.Count;
|
||||||
|
while (n > 1)
|
||||||
|
{
|
||||||
|
n--;
|
||||||
|
int k = _rng.Next(n + 1);
|
||||||
|
(DrawPile[k], DrawPile[n]) = (DrawPile[n], DrawPile[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawCards(int count, CombatLog? log = null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (Hand.Count >= 10)
|
||||||
|
{
|
||||||
|
log?.AddWarning("Hand buffer overflow! Max 10 cards in hand.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DrawPile.Count == 0)
|
||||||
|
{
|
||||||
|
if (DiscardPile.Count == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
log?.AddInfo("Recompiling draw pile from deallocated heap (Discard Pile)...");
|
||||||
|
DrawPile.AddRange(DiscardPile);
|
||||||
|
DiscardPile.Clear();
|
||||||
|
ShuffleDrawPile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DrawPile.Count > 0)
|
||||||
|
{
|
||||||
|
var card = DrawPile[0];
|
||||||
|
DrawPile.RemoveAt(0);
|
||||||
|
Hand.Add(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DiscardHand(CombatLog? log = null)
|
||||||
|
{
|
||||||
|
foreach (var card in Hand)
|
||||||
|
{
|
||||||
|
DiscardPile.Add(card);
|
||||||
|
}
|
||||||
|
Hand.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetTurnEnergy()
|
||||||
|
{
|
||||||
|
CurrentEnergy = MaxEnergy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Models;
|
||||||
|
|
||||||
|
public class StatusCollection
|
||||||
|
{
|
||||||
|
private readonly Dictionary<StatusType, int> _statuses = new();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<StatusType, int> All => _statuses;
|
||||||
|
|
||||||
|
public int Get(StatusType type) => _statuses.TryGetValue(type, out var val) ? val : 0;
|
||||||
|
|
||||||
|
public void Add(StatusType type, int amount)
|
||||||
|
{
|
||||||
|
if (amount <= 0) return;
|
||||||
|
_statuses[type] = Get(type) + amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(StatusType type, int amount)
|
||||||
|
{
|
||||||
|
if (!_statuses.TryGetValue(type, out var current)) return;
|
||||||
|
var newVal = current - amount;
|
||||||
|
if (newVal <= 0)
|
||||||
|
{
|
||||||
|
_statuses.Remove(type);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_statuses[type] = newVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear() => _statuses.Clear();
|
||||||
|
|
||||||
|
public int CalculateOutgoingDamage(int baseDamage)
|
||||||
|
{
|
||||||
|
var dmg = (double)baseDamage;
|
||||||
|
dmg += Get(StatusType.Strength);
|
||||||
|
|
||||||
|
if (Get(StatusType.Weak) > 0)
|
||||||
|
{
|
||||||
|
dmg *= 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Max(0, (int)Math.Round(dmg, MidpointRounding.AwayFromZero));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CalculateIncomingDamage(int incomingDamage)
|
||||||
|
{
|
||||||
|
var dmg = (double)incomingDamage;
|
||||||
|
if (Get(StatusType.Vulnerable) > 0)
|
||||||
|
{
|
||||||
|
dmg *= 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Max(0, (int)Math.Round(dmg, MidpointRounding.AwayFromZero));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnTurnEnd(Action<string>? logAction = null, Action<int>? takeDamageAction = null)
|
||||||
|
{
|
||||||
|
// Decay standard turn debuffs
|
||||||
|
if (Get(StatusType.Vulnerable) > 0)
|
||||||
|
{
|
||||||
|
Remove(StatusType.Vulnerable, 1);
|
||||||
|
}
|
||||||
|
if (Get(StatusType.Weak) > 0)
|
||||||
|
{
|
||||||
|
Remove(StatusType.Weak, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory leak causes damage over time
|
||||||
|
var leaks = Get(StatusType.MemoryLeak);
|
||||||
|
if (leaks > 0)
|
||||||
|
{
|
||||||
|
logAction?.Invoke($"MemoryLeak triggered! Taking {leaks} damage from unmanaged resources.");
|
||||||
|
takeDamageAction?.Invoke(leaks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using SlaySharp.Core.Combat;
|
||||||
|
|
||||||
|
namespace SlaySharp.Core.Relics;
|
||||||
|
|
||||||
|
public abstract class Relic
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string PackageVersion { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public string Icon { get; }
|
||||||
|
|
||||||
|
protected Relic(string id, string name, string packageVersion, string description, string icon = "📦")
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
Name = name;
|
||||||
|
PackageVersion = packageVersion;
|
||||||
|
Description = description;
|
||||||
|
Icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void OnCombatStart(CombatEngine engine) { }
|
||||||
|
public virtual void OnTurnStart(CombatEngine engine) { }
|
||||||
|
public virtual void OnTurnEnd(CombatEngine engine) { }
|
||||||
|
public virtual void OnCardPlayed(CombatEngine engine, Models.Card card) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NewtonsoftJsonRelic : Relic
|
||||||
|
{
|
||||||
|
public NewtonsoftJsonRelic() : base(
|
||||||
|
"Newtonsoft.Json",
|
||||||
|
"Newtonsoft.Json",
|
||||||
|
"v13.0.3",
|
||||||
|
"Draw +1 extra card at the start of combat (Memory deserialization buffer).",
|
||||||
|
"📜"
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public override void OnCombatStart(CombatEngine engine)
|
||||||
|
{
|
||||||
|
engine.Player.DrawCards(1, engine.Log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BenchmarkDotNetRelic : Relic
|
||||||
|
{
|
||||||
|
private int _turnCounter;
|
||||||
|
|
||||||
|
public BenchmarkDotNetRelic() : base(
|
||||||
|
"BenchmarkDotNet",
|
||||||
|
"BenchmarkDotNet",
|
||||||
|
"v0.13.12",
|
||||||
|
"Every 2nd turn, micro-benchmark optimizations grant +1 extra CPU Cycle.",
|
||||||
|
"⚡"
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public override void OnTurnStart(CombatEngine engine)
|
||||||
|
{
|
||||||
|
_turnCounter++;
|
||||||
|
if (_turnCounter % 2 == 0)
|
||||||
|
{
|
||||||
|
engine.Player.CurrentEnergy += 1;
|
||||||
|
engine.Log.AddInfo($"[BenchmarkDotNet] Micro-benchmark boost! +1 CPU Cycle (Available: {engine.Player.CurrentEnergy})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SerilogRelic : Relic
|
||||||
|
{
|
||||||
|
public SerilogRelic() : base(
|
||||||
|
"Serilog",
|
||||||
|
"Serilog",
|
||||||
|
"v4.0.0",
|
||||||
|
"Whenever you play an Attack card, gain +2 Shield (Structured diagnostic logging).",
|
||||||
|
"📊"
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public override void OnCardPlayed(CombatEngine engine, Models.Card card)
|
||||||
|
{
|
||||||
|
if (card.Type == Enums.CardType.Attack)
|
||||||
|
{
|
||||||
|
engine.Player.GainShield(2);
|
||||||
|
engine.Log.AddInfo("[Serilog] Diagnostic trace emitted: +2 Shield.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<Router AppAssembly="@typeof(App).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<PageTitle>Not found</PageTitle>
|
||||||
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
|
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||||
|
</LayoutView>
|
||||||
|
</NotFound>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
@using SlaySharp.Core.Models
|
||||||
|
@using SlaySharp.Core.Enums
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<div class="game-card @GetCardClasses()" @onclick="HandleCardClick">
|
||||||
|
<!-- VS Code Tab Header -->
|
||||||
|
<div class="card-vsc-header">
|
||||||
|
<div class="card-tab-label font-mono overflow-hidden">
|
||||||
|
<span style="color: #61afef; font-size: 0.95rem; font-weight: 900;">#</span>
|
||||||
|
<span class="text-truncate" title="@Card.Name" style="font-weight: 800; font-size: 0.82rem;">
|
||||||
|
@Card.Name.Replace(" ", "").Replace("()", "")@(Card.Upgraded ? "_v2.cs" : ".cs")
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-1">
|
||||||
|
<div class="card-cost-badge font-mono" title="@($"{Card.Cost} CPU Cycles")">
|
||||||
|
@Card.Cost CPU
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Visual Art Window -->
|
||||||
|
<div class="card-art-box">
|
||||||
|
<span class="card-art-icon">@GetCardArtIcon(Card.Name)</span>
|
||||||
|
@if (Card.Upgraded)
|
||||||
|
{
|
||||||
|
<div class="position-absolute top-0 end-0 m-1 badge bg-warning text-dark font-mono" style="font-size: 0.62rem; font-weight: 900;">
|
||||||
|
TIER-1 +
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mini Code Editor with Gutter and Line Numbers -->
|
||||||
|
<div class="card-editor-viewport font-mono">
|
||||||
|
<!-- Line Numbers Gutter -->
|
||||||
|
<div class="card-gutter">
|
||||||
|
<span>1</span>
|
||||||
|
<span>2</span>
|
||||||
|
<span>3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- C# Code Content with Syntax Coloring -->
|
||||||
|
<div class="card-code-content">
|
||||||
|
<div style="color: var(--syn-comment); font-size: 0.68rem;">
|
||||||
|
// @(Card.Upgraded ? "Optimized IL" : "Managed code")
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.76rem; font-weight: 800;">
|
||||||
|
@FormatCodeSnippet(Card.CodeSnippet)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Punchy, Large, Bold Card Effect Footer -->
|
||||||
|
<div class="card-vsc-footer font-mono">
|
||||||
|
<span style="font-size: 0.92rem; font-weight: 900; line-height: 1.25;">@Card.Description</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public Card Card { get; set; } = null!;
|
||||||
|
[Parameter] public bool IsPlayable { get; set; } = true;
|
||||||
|
[Parameter] public bool IsSelected { get; set; } = false;
|
||||||
|
[Parameter] public EventCallback<Card> OnCardClicked { get; set; }
|
||||||
|
|
||||||
|
private string GetCardClasses()
|
||||||
|
{
|
||||||
|
var classes = new List<string>();
|
||||||
|
|
||||||
|
classes.Add(Card.Type switch
|
||||||
|
{
|
||||||
|
CardType.Attack => "type-attack",
|
||||||
|
CardType.Skill => "type-skill",
|
||||||
|
CardType.Power => "type-power",
|
||||||
|
CardType.Curse => "type-curse",
|
||||||
|
_ => ""
|
||||||
|
});
|
||||||
|
|
||||||
|
if (IsSelected) classes.Add("selected");
|
||||||
|
if (IsPlayable) classes.Add("playable");
|
||||||
|
else classes.Add("unaffordable");
|
||||||
|
|
||||||
|
return string.Join(" ", classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCardArtIcon(string name)
|
||||||
|
{
|
||||||
|
if (name.Contains("WriteLine", StringComparison.OrdinalIgnoreCase)) return "💻";
|
||||||
|
if (name.Contains("try", StringComparison.OrdinalIgnoreCase)) return "🛡️";
|
||||||
|
if (name.Contains("Collect", StringComparison.OrdinalIgnoreCase)) return "♻️";
|
||||||
|
if (name.Contains("Slice", StringComparison.OrdinalIgnoreCase)) return "⚡";
|
||||||
|
if (name.Contains("Exception", StringComparison.OrdinalIgnoreCase)) return "💥";
|
||||||
|
if (name.Contains("Compilation", StringComparison.OrdinalIgnoreCase)) return "🚀";
|
||||||
|
if (name.Contains("Sleep", StringComparison.OrdinalIgnoreCase)) return "⏳";
|
||||||
|
if (name.Contains("Parallel", StringComparison.OrdinalIgnoreCase)) return "🔀";
|
||||||
|
if (name.Contains("Where", StringComparison.OrdinalIgnoreCase)) return "🔍";
|
||||||
|
if (name.Contains("fallback", StringComparison.OrdinalIgnoreCase) || name.Contains("??")) return "🛟";
|
||||||
|
return "📄";
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarkupString FormatCodeSnippet(string snippet)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(snippet)) return new MarkupString("");
|
||||||
|
|
||||||
|
var highlighted = snippet
|
||||||
|
.Replace("public", "<span style='color: var(--syn-keyword);'>public</span>")
|
||||||
|
.Replace("void", "<span style='color: var(--syn-keyword);'>void</span>")
|
||||||
|
.Replace("var", "<span style='color: var(--syn-keyword);'>var</span>")
|
||||||
|
.Replace("throw", "<span style='color: var(--syn-control);'>throw</span>")
|
||||||
|
.Replace("new", "<span style='color: var(--syn-keyword);'>new</span>")
|
||||||
|
.Replace("try", "<span style='color: var(--syn-control);'>try</span>")
|
||||||
|
.Replace("catch", "<span style='color: var(--syn-control);'>catch</span>")
|
||||||
|
.Replace("await", "<span style='color: var(--syn-control);'>await</span>")
|
||||||
|
.Replace("return", "<span style='color: var(--syn-control);'>return</span>")
|
||||||
|
.Replace("Bug", "<span style='color: var(--syn-type);'>Bug</span>")
|
||||||
|
.Replace("Exception", "<span style='color: var(--syn-type);'>Exception</span>");
|
||||||
|
|
||||||
|
return new MarkupString(highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleCardClick()
|
||||||
|
{
|
||||||
|
if (IsPlayable)
|
||||||
|
{
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playCardSound");
|
||||||
|
await OnCardClicked.InvokeAsync(Card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
@using SlaySharp.Core.Combat
|
||||||
|
@using SlaySharp.Core.Models
|
||||||
|
@using SlaySharp.Core.Enums
|
||||||
|
@using SlaySharp.Core.Catalogs
|
||||||
|
@using SlaySharp.Core.Relics
|
||||||
|
@using SlaySharp.Web.Models
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<div class="d-flex flex-column h-100" style="background-color: var(--vscode-bg-editor);">
|
||||||
|
<!-- VS Code Editor Tabs Bar -->
|
||||||
|
<div class="vsc-tabs-bar font-mono">
|
||||||
|
<div class="vsc-tab active">
|
||||||
|
<span style="color: #61afef; font-weight: 900; font-size: 1rem;">#</span>
|
||||||
|
<span>Encounter.cs</span>
|
||||||
|
<span style="color: #828997; font-size: 0.75rem;">✕</span>
|
||||||
|
</div>
|
||||||
|
<div class="vsc-tab" @onclick="() => OpenPileModal(PileType.Draw)">
|
||||||
|
<span style="color: #56b6c2; font-size: 1rem;">📥</span>
|
||||||
|
<span>DrawPile (@Engine.Player.DrawPile.Count)</span>
|
||||||
|
</div>
|
||||||
|
<div class="vsc-tab" @onclick="() => OpenPileModal(PileType.Discard)">
|
||||||
|
<span style="color: #e5c07b; font-size: 1rem;">🗑️</span>
|
||||||
|
<span>Discard (@Engine.Player.DiscardPile.Count)</span>
|
||||||
|
</div>
|
||||||
|
@if (Engine.Player.ExhaustPile.Any())
|
||||||
|
{
|
||||||
|
<div class="vsc-tab" @onclick="() => OpenPileModal(PileType.Exhaust)">
|
||||||
|
<span style="color: #e06c75; font-size: 1rem;">🔥</span>
|
||||||
|
<span>Finalized (@Engine.Player.ExhaustPile.Count)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS Code Breadcrumbs Bar & Floating Debug Toolbar -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between vsc-breadcrumbs font-mono">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span>SlaySharp</span>
|
||||||
|
<span>›</span>
|
||||||
|
<span>src</span>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: #ffffff; font-weight: 800;">Encounter.cs</span>
|
||||||
|
<span>›</span>
|
||||||
|
<span style="color: #e5c07b; font-weight: 800;">DebugIncident()</span>
|
||||||
|
<span class="badge bg-dark border border-secondary text-info ms-2" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
Round #@Engine.TurnNumber
|
||||||
|
</span>
|
||||||
|
@if (Engine.Phase == CombatPhase.EnemyTurn)
|
||||||
|
{
|
||||||
|
<span class="badge bg-danger text-white ms-2 font-mono" style="font-size: 0.85rem; font-weight: 900;">
|
||||||
|
👾 BUGS EXECUTING...
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS Code Floating Debug Toolbar -->
|
||||||
|
<div class="vsc-debug-toolbar font-mono">
|
||||||
|
<button class="vsc-debug-btn"
|
||||||
|
disabled="@(Engine.Phase != CombatPhase.PlayerTurn || _isProcessingTurn)"
|
||||||
|
@onclick="HandleEndTurnAsync"
|
||||||
|
title="Continue / Compile Turn (F5)">
|
||||||
|
<span style="color: #98c379; font-size: 1.2rem;">▶</span>
|
||||||
|
<span style="font-size: 0.88rem; font-weight: 900;">F5 Continue</span>
|
||||||
|
</button>
|
||||||
|
<span style="color: #555555;">|</span>
|
||||||
|
<button class="vsc-debug-btn" @onclick="RestartCombat" title="Restart Incident (Ctrl+Shift+F5)">
|
||||||
|
<span style="color: #61afef; font-size: 1.2rem;">↻</span>
|
||||||
|
</button>
|
||||||
|
<button class="vsc-debug-btn" @onclick="RestartCombat" title="Stop Incident (Shift+F5)">
|
||||||
|
<span style="color: #e06c75; font-size: 1.2rem;">⏹</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Battle Arena Area -->
|
||||||
|
<div class="flex-grow-1 d-flex flex-column justify-content-between p-3 overflow-y-auto" style="min-height: 0;">
|
||||||
|
<!-- Enemies Area with Floating Damage Popups -->
|
||||||
|
<div class="d-flex justify-content-center align-items-center gap-4 my-auto py-2">
|
||||||
|
@if (Engine.Enemies.Any())
|
||||||
|
{
|
||||||
|
@foreach (var enemy in Engine.Enemies)
|
||||||
|
{
|
||||||
|
var enemyTexts = _floatingTexts.TryGetValue(enemy.Id, out var texts) ? texts : null;
|
||||||
|
var isAttacking = Engine.ActiveActingEnemyId == enemy.Id;
|
||||||
|
<EnemyView Enemy="@enemy"
|
||||||
|
IsTargeted="@(_selectedCard != null && _selectedCard.TargetType == TargetType.SingleEnemy)"
|
||||||
|
IsActiveAttacker="@isAttacking"
|
||||||
|
FloatingTexts="@enemyTexts"
|
||||||
|
OnEnemyClicked="HandleEnemyClicked" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Engine.Phase == CombatPhase.Victory)
|
||||||
|
{
|
||||||
|
<div class="card p-5 text-center shadow-lg" style="border-radius: 14px; max-width: 580px; background-color: #21252b; border: 2px solid #98c379;">
|
||||||
|
<div style="font-size: 4rem;">🎉</div>
|
||||||
|
<h1 class="text-success font-mono fw-bold mb-2">BUILD SUCCESSFUL</h1>
|
||||||
|
<p class="text-white font-mono fs-5">0 Errors, 0 Exceptions. All bugs patched!</p>
|
||||||
|
<button class="btn btn-success btn-lg font-mono fw-bold mt-3 px-5 py-3" @onclick="RestartCombat">
|
||||||
|
🚀 Next Incident
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Defeat Banner -->
|
||||||
|
@if (Engine.Phase == CombatPhase.Defeat)
|
||||||
|
{
|
||||||
|
<div class="card p-5 text-center my-2 mx-auto shadow-lg" style="border-radius: 14px; max-width: 620px; background-color: #21252b; border: 2px solid #e06c75;">
|
||||||
|
<div style="font-size: 4rem;">💥</div>
|
||||||
|
<h1 class="text-danger font-mono fw-bold mb-2">FATAL CRASH</h1>
|
||||||
|
<p class="text-white font-mono fs-5">Service SLA reached 0%. Access violation crash.</p>
|
||||||
|
<button class="btn btn-danger btn-lg font-mono fw-bold mt-3 px-5 py-3" @onclick="RestartCombat">
|
||||||
|
🔄 Restart Run
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Bottom Controls Dock -->
|
||||||
|
<div class="d-flex flex-column gap-2 mt-auto">
|
||||||
|
<!-- Middle Row: Player Info, CPU Reactor, End Turn button -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
@{
|
||||||
|
var playerTexts = _floatingTexts.TryGetValue(Engine.Player.Id, out var pTexts) ? pTexts : null;
|
||||||
|
}
|
||||||
|
<PlayerView Player="@Engine.Player" FloatingTexts="@playerTexts" />
|
||||||
|
|
||||||
|
<!-- Central Energy Reactor Core -->
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div class="cpu-reactor" title="Available CPU Cycles (Energy)">
|
||||||
|
<span class="font-mono text-white fw-bolder" style="font-size: 2.2rem; line-height: 1;">
|
||||||
|
@Engine.Player.CurrentEnergy
|
||||||
|
</span>
|
||||||
|
<span class="font-mono text-info fw-bold" style="font-size: 0.8rem; margin-top: 2px;">
|
||||||
|
/ @Engine.Player.MaxEnergy CPU
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_selectedCard != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info py-2 px-3 m-0 font-mono d-flex align-items-center gap-3 border-0 shadow-lg"
|
||||||
|
style="font-size: 0.95rem; font-weight: 800; background-color: #21252b; color: #61afef; border-left: 4px solid #007acc !important;">
|
||||||
|
<span>🎯 Target bug for: <strong class="text-white">@_selectedCard.Name</strong></span>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary py-0 fw-bold" @onclick="() => _selectedCard = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Run / Compile Button -->
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-success font-mono px-5 py-3 d-flex align-items-center gap-2 shadow-lg"
|
||||||
|
style="font-weight: 900; font-size: 1.05rem; background-color: #238636; border-color: #2ea043; border-radius: 10px;"
|
||||||
|
disabled="@(Engine.Phase != CombatPhase.PlayerTurn || _isProcessingTurn)"
|
||||||
|
@onclick="HandleEndTurnAsync">
|
||||||
|
<span style="font-size: 1.2rem;">▶</span>
|
||||||
|
<span>@(_isProcessingTurn ? "Executing Bugs..." : "F5 Build & End Turn")</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hand Deck: Fan of VS Code Editor Cards -->
|
||||||
|
<div class="d-flex justify-content-center align-items-end gap-2 px-2 overflow-x-auto py-1" style="min-height: 345px;">
|
||||||
|
@{
|
||||||
|
var hand = Engine.Player.Hand;
|
||||||
|
var count = hand.Count;
|
||||||
|
}
|
||||||
|
@for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var card = hand[i];
|
||||||
|
var isPlayable = Engine.Phase == CombatPhase.PlayerTurn && !_isProcessingTurn && card.CanPlay(Engine);
|
||||||
|
var isSelected = _selectedCard == card;
|
||||||
|
var isLaunching = _launchingCardId == card.Id;
|
||||||
|
|
||||||
|
var angle = count > 1 ? (i - (count - 1) / 2.0) * 3.0 : 0;
|
||||||
|
var style = $"transform-origin: bottom center; transform: rotate({angle}deg);";
|
||||||
|
|
||||||
|
<div style="@style" class="@(isLaunching ? "card-launched" : "")">
|
||||||
|
<CardView Card="@card"
|
||||||
|
IsPlayable="@isPlayable"
|
||||||
|
IsSelected="@isSelected"
|
||||||
|
OnCardClicked="HandleCardClicked" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS Code Terminal & Diagnostic Log Output -->
|
||||||
|
<DebugConsole Log="@Engine.Log" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Pile Inspector Modal -->
|
||||||
|
@if (_activeModalPile.HasValue)
|
||||||
|
{
|
||||||
|
<div class="modal d-block" tabindex="-1" style="background: rgba(0,0,0,0.85); backdrop-filter: blur(6px);">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||||
|
<div class="modal-content text-white" style="background-color: #181a1f; border: 2px solid #383e4a; border-radius: 12px;">
|
||||||
|
<div class="modal-header border-secondary py-3" style="background-color: #21252b;">
|
||||||
|
<h5 class="modal-title font-mono fw-bold">
|
||||||
|
📂 File Inspector: @_activeModalPile.Value Pile (@GetModalCards().Count cards)
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" @onclick="() => _activeModalPile = null"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-4">
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||||
|
@foreach (var card in GetModalCards())
|
||||||
|
{
|
||||||
|
<CardView Card="@card" IsPlayable="false" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public CombatEngine Engine { get; set; } = null!;
|
||||||
|
[Parameter] public EventCallback OnRestartRequested { get; set; }
|
||||||
|
|
||||||
|
private Card? _selectedCard;
|
||||||
|
private PileType? _activeModalPile;
|
||||||
|
private string? _launchingCardId;
|
||||||
|
private bool _isProcessingTurn = false;
|
||||||
|
private readonly Dictionary<string, List<FloatingTextItem>> _floatingTexts = new();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Engine.OnStateChanged += StateHasChanged;
|
||||||
|
Engine.OnDamageDealt += HandleDamageDealt;
|
||||||
|
Engine.OnShieldGained += HandleShieldGained;
|
||||||
|
Engine.OnCardPlayAnimation += HandleCardPlayAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDamageDealt(DamageEvent e)
|
||||||
|
{
|
||||||
|
var text = e.IsPiercing ? $"-{e.Amount} PIERCE" : (e.IsCrit ? $"-{e.Amount} CRIT!" : $"-{e.Amount}");
|
||||||
|
var cssClass = e.IsPiercing ? "pierce" : (e.IsCrit ? "crit" : "");
|
||||||
|
AddFloatingText(e.TargetId, text, cssClass);
|
||||||
|
if (e.TargetId == Engine.Player.Id)
|
||||||
|
{
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playHitSound");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleShieldGained(ShieldEvent e)
|
||||||
|
{
|
||||||
|
AddFloatingText(e.TargetId, $"+{e.Amount} 🛡️", "shield");
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playShieldSound");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCardPlayAnimation(CardPlayAnimationEvent e)
|
||||||
|
{
|
||||||
|
_launchingCardId = e.CardId;
|
||||||
|
StateHasChanged();
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(400);
|
||||||
|
_launchingCardId = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddFloatingText(string targetId, string text, string cssClass)
|
||||||
|
{
|
||||||
|
if (!_floatingTexts.ContainsKey(targetId))
|
||||||
|
{
|
||||||
|
_floatingTexts[targetId] = new List<FloatingTextItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new FloatingTextItem(Guid.NewGuid().ToString("N"), text, cssClass, DateTime.Now);
|
||||||
|
_floatingTexts[targetId].Add(item);
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(1300);
|
||||||
|
if (_floatingTexts.TryGetValue(targetId, out var list))
|
||||||
|
{
|
||||||
|
list.Remove(item);
|
||||||
|
}
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCardClicked(Card card)
|
||||||
|
{
|
||||||
|
if (Engine.Phase != CombatPhase.PlayerTurn || _isProcessingTurn) return;
|
||||||
|
|
||||||
|
if (card.TargetType == TargetType.Self || card.TargetType == TargetType.AllEnemies || card.TargetType == TargetType.None)
|
||||||
|
{
|
||||||
|
_selectedCard = null;
|
||||||
|
if (card.Type == CardType.Skill)
|
||||||
|
{
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playShieldSound");
|
||||||
|
}
|
||||||
|
Engine.PlayCard(card);
|
||||||
|
CheckVictorySound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single enemy target
|
||||||
|
if (Engine.Enemies.Count == 1)
|
||||||
|
{
|
||||||
|
_selectedCard = null;
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playHitSound");
|
||||||
|
Engine.PlayCard(card, Engine.Enemies[0]);
|
||||||
|
CheckVictorySound();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectedCard = _selectedCard == card ? null : card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleEnemyClicked(Enemy enemy)
|
||||||
|
{
|
||||||
|
if (_selectedCard != null && !_isProcessingTurn)
|
||||||
|
{
|
||||||
|
var card = _selectedCard;
|
||||||
|
_selectedCard = null;
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playHitSound");
|
||||||
|
Engine.PlayCard(card, enemy);
|
||||||
|
CheckVictorySound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckVictorySound()
|
||||||
|
{
|
||||||
|
if (Engine.Phase == CombatPhase.Victory)
|
||||||
|
{
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playVictorySound");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleEndTurnAsync()
|
||||||
|
{
|
||||||
|
if (_isProcessingTurn) return;
|
||||||
|
_isProcessingTurn = true;
|
||||||
|
_selectedCard = null;
|
||||||
|
|
||||||
|
await Engine.EndPlayerTurnAsync(
|
||||||
|
onBeforeEnemyActs: async enemy =>
|
||||||
|
{
|
||||||
|
await Task.Delay(200);
|
||||||
|
},
|
||||||
|
onAfterEnemyActs: async enemy =>
|
||||||
|
{
|
||||||
|
await Task.Delay(300);
|
||||||
|
},
|
||||||
|
stepDelayMs: 1200
|
||||||
|
);
|
||||||
|
|
||||||
|
_isProcessingTurn = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RestartCombat()
|
||||||
|
{
|
||||||
|
_selectedCard = null;
|
||||||
|
_activeModalPile = null;
|
||||||
|
_isProcessingTurn = false;
|
||||||
|
_floatingTexts.Clear();
|
||||||
|
await OnRestartRequested.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum PileType { Draw, Discard, Exhaust }
|
||||||
|
|
||||||
|
private void OpenPileModal(PileType pile)
|
||||||
|
{
|
||||||
|
_activeModalPile = pile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Card> GetModalCards() => _activeModalPile switch
|
||||||
|
{
|
||||||
|
PileType.Draw => Engine.Player.DrawPile,
|
||||||
|
PileType.Discard => Engine.Player.DiscardPile,
|
||||||
|
PileType.Exhaust => Engine.Player.ExhaustPile,
|
||||||
|
_ => new List<Card>()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@using SlaySharp.Core.Combat
|
||||||
|
|
||||||
|
<div class="d-flex flex-column font-mono" style="height: 155px; width: 100%; background-color: #1e1e1e; border-top: 1px solid #333333; overflow: hidden;">
|
||||||
|
<!-- VS Code Bottom Panel Tabs Header -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between px-3 py-1 border-bottom" style="background-color: #252526; border-color: #333333 !important; font-size: 0.72rem;">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="text-muted" style="cursor: pointer;">PROBLEMS (2)</span>
|
||||||
|
<span class="fw-bold text-white border-bottom border-2 border-primary pb-1" style="cursor: pointer;">
|
||||||
|
OUTPUT: Roslyn Diagnostic Trace
|
||||||
|
</span>
|
||||||
|
<span class="text-muted" style="cursor: pointer;">DEBUG CONSOLE</span>
|
||||||
|
<span class="text-muted" style="cursor: pointer;">TERMINAL (pwsh)</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-sm btn-dark p-0 px-2 border-secondary text-muted" style="font-size: 0.65rem;" @onclick="ClearLogs">
|
||||||
|
Clear Console
|
||||||
|
</button>
|
||||||
|
<span class="text-muted" style="font-size: 0.7rem;">✕</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scrollable Diagnostic Log Stream -->
|
||||||
|
<div class="flex-grow-1 p-2 overflow-y-auto d-flex flex-column-reverse" style="font-size: 0.7rem; line-height: 1.4; background-color: #1e1e1e;">
|
||||||
|
@foreach (var entry in Log.Entries.Reverse())
|
||||||
|
{
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<span class="text-muted" style="font-size: 0.65rem;">[@entry.FormattedTime]</span>
|
||||||
|
<span class="@GetLogLevelClass(entry.Level)">@entry.Message</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public CombatLog Log { get; set; } = null!;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Log.OnLogAdded += StateHasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearLogs()
|
||||||
|
{
|
||||||
|
Log.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLogLevelClass(LogLevel level) => level switch
|
||||||
|
{
|
||||||
|
LogLevel.Info => "text-info",
|
||||||
|
LogLevel.Success => "text-success",
|
||||||
|
LogLevel.Warning => "text-warning",
|
||||||
|
LogLevel.Error => "text-danger fw-bold",
|
||||||
|
LogLevel.Fatal => "text-danger fw-bolder bg-danger bg-opacity-25 px-1 rounded",
|
||||||
|
_ => "text-light"
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
@using SlaySharp.Core.Models
|
||||||
|
@using SlaySharp.Core.Enums
|
||||||
|
@using SlaySharp.Web.Models
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<div class="enemy-card @(IsTargeted ? "targeted" : "") @(IsActiveAttacker ? "enemy-attacking-lunge" : "") @(_isHit ? "hit-shake" : "") position-relative" @onclick="HandleClick">
|
||||||
|
<!-- Active Attacker Indicator Banner -->
|
||||||
|
@if (IsActiveAttacker)
|
||||||
|
{
|
||||||
|
<div class="acting-enemy-banner font-mono">
|
||||||
|
⚡ EXECUTING INTENT...
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Floating Damage / Shield Numbers Overlay -->
|
||||||
|
@if (FloatingTexts != null && FloatingTexts.Any())
|
||||||
|
{
|
||||||
|
<div class="floating-vfx-container">
|
||||||
|
@foreach (var item in FloatingTexts)
|
||||||
|
{
|
||||||
|
<div class="floating-dmg-number @item.CssClass">
|
||||||
|
@item.Text
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Flash Impact Overlay -->
|
||||||
|
@if (_isHit)
|
||||||
|
{
|
||||||
|
<div class="impact-flash"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Intent Forecast Ribbon -->
|
||||||
|
@if (Enemy.CurrentIntent != null)
|
||||||
|
{
|
||||||
|
<div class="intent-badge @GetIntentClass(Enemy.CurrentIntent.Type) font-mono" title="@Enemy.CurrentIntent.Description">
|
||||||
|
<span style="font-size: 1.1rem;">@GetIntentIcon(Enemy.CurrentIntent.Type)</span>
|
||||||
|
<span style="font-size: 0.92rem; font-weight: 900;">@Enemy.CurrentIntent.DisplayText</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Enemy Icon Avatar -->
|
||||||
|
<div class="enemy-avatar-wrap">
|
||||||
|
@Enemy.Icon
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bug Title with VS Code Error Squiggle -->
|
||||||
|
<div class="text-center mb-2">
|
||||||
|
<div class="fw-bold font-mono error-squiggle" style="font-size: 1.05rem; color: #ffffff; letter-spacing: -0.3px;">
|
||||||
|
@Enemy.Name
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Severity HP Bar -->
|
||||||
|
<div class="w-100 mb-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1 font-mono" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span style="color: #abb2bf;">Severity:</span>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
@if (Enemy.Shield > 0)
|
||||||
|
{
|
||||||
|
<span class="badge bg-primary font-mono" style="font-size: 0.85rem;" title="Try-Catch Shield">🛡️ @Enemy.Shield</span>
|
||||||
|
}
|
||||||
|
<span class="fw-bold text-white fs-6">@Enemy.CurrentHp / @Enemy.MaxHp HP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hp-bar-container">
|
||||||
|
<div class="hp-bar-fill" style="width: @(Math.Clamp((double)Enemy.CurrentHp / Enemy.MaxHp * 100, 0, 100))%;"></div>
|
||||||
|
<div class="hp-bar-text font-mono">@Enemy.CurrentHp / @Enemy.MaxHp</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Badges -->
|
||||||
|
<StatusBadgeView Statuses="@Enemy.Statuses" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public Enemy Enemy { get; set; } = null!;
|
||||||
|
[Parameter] public bool IsTargeted { get; set; } = false;
|
||||||
|
[Parameter] public bool IsActiveAttacker { get; set; } = false;
|
||||||
|
[Parameter] public List<FloatingTextItem>? FloatingTexts { get; set; }
|
||||||
|
[Parameter] public EventCallback<Enemy> OnEnemyClicked { get; set; }
|
||||||
|
|
||||||
|
private bool _isHit = false;
|
||||||
|
|
||||||
|
public async Task TriggerHitAnimation()
|
||||||
|
{
|
||||||
|
_isHit = true;
|
||||||
|
StateHasChanged();
|
||||||
|
await Task.Delay(400);
|
||||||
|
_isHit = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleClick()
|
||||||
|
{
|
||||||
|
if (IsTargeted)
|
||||||
|
{
|
||||||
|
_ = JS.InvokeVoidAsync("slayAudio.playHitSound");
|
||||||
|
}
|
||||||
|
await OnEnemyClicked.InvokeAsync(Enemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetIntentIcon(IntentType type) => type switch
|
||||||
|
{
|
||||||
|
IntentType.Attack => "⚔️",
|
||||||
|
IntentType.Defend => "🛡️",
|
||||||
|
IntentType.AttackDefend => "⚔️🛡️",
|
||||||
|
IntentType.Debuff => "⚠️",
|
||||||
|
IntentType.Buff => "💪",
|
||||||
|
_ => "⚡"
|
||||||
|
};
|
||||||
|
|
||||||
|
private string GetIntentClass(IntentType type) => type switch
|
||||||
|
{
|
||||||
|
IntentType.Attack => "intent-attack",
|
||||||
|
IntentType.Defend => "intent-defend",
|
||||||
|
IntentType.AttackDefend => "intent-attack",
|
||||||
|
IntentType.Debuff => "intent-debuff",
|
||||||
|
IntentType.Buff => "intent-buff",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
@using SlaySharp.Core.Models
|
||||||
|
@using SlaySharp.Core.Enums
|
||||||
|
@using SlaySharp.Web.Models
|
||||||
|
|
||||||
|
<div class="card p-3 d-flex flex-column gap-2 position-relative @(_isHit ? "hit-shake" : "")"
|
||||||
|
style="min-width: 310px; max-width: 360px; background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px;">
|
||||||
|
<!-- Floating Damage / Shield Numbers Overlay for Player -->
|
||||||
|
@if (FloatingTexts != null && FloatingTexts.Any())
|
||||||
|
{
|
||||||
|
<div class="floating-vfx-container">
|
||||||
|
@foreach (var item in FloatingTexts)
|
||||||
|
{
|
||||||
|
<div class="floating-dmg-number @item.CssClass">
|
||||||
|
@item.Text
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Player Header & Role -->
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span style="font-size: 2.4rem;">👨💻</span>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold font-mono" style="font-size: 1.15rem; color: #61afef; letter-spacing: -0.5px;">@Player.Name</div>
|
||||||
|
<div class="text-muted font-mono" style="font-size: 0.76rem; font-weight: 800;">.NET Principal Architect</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (Player.Shield > 0)
|
||||||
|
{
|
||||||
|
<div class="shield-pill font-mono" title="Try/Catch Barrier">
|
||||||
|
🛡️ @Player.Shield
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Production SLA Health Bar -->
|
||||||
|
<div>
|
||||||
|
<div class="d-flex justify-content-between font-mono mb-1" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span style="color: #abb2bf;">Production SLA:</span>
|
||||||
|
<span class="fw-bold text-white fs-6">@Player.CurrentHp / @Player.MaxHp HP (@Math.Round((double)Player.CurrentHp / Player.MaxHp * 100, 0)%)</span>
|
||||||
|
</div>
|
||||||
|
<div class="hp-bar-container">
|
||||||
|
<div class="hp-bar-fill" style="width: @(Math.Clamp((double)Player.CurrentHp / Player.MaxHp * 100, 0, 100))%;"></div>
|
||||||
|
<div class="hp-bar-text font-mono">@Player.CurrentHp / @Player.MaxHp HP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Status Effects -->
|
||||||
|
<StatusBadgeView Statuses="@Player.Statuses" />
|
||||||
|
|
||||||
|
<!-- Active NuGet Packages (Relics) -->
|
||||||
|
@if (Player.Relics.Any())
|
||||||
|
{
|
||||||
|
<div class="pt-2 border-top border-secondary mt-1">
|
||||||
|
<div class="text-muted mb-1 font-mono" style="font-size: 0.72rem; font-weight: 800; letter-spacing: 0.5px;">INSTALLED NUGETS:</div>
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
@foreach (var relic in Player.Relics)
|
||||||
|
{
|
||||||
|
<div class="badge bg-dark border border-secondary p-1 px-2 d-flex align-items-center gap-1 font-mono text-white"
|
||||||
|
title="@($"{relic.Name} ({relic.PackageVersion}): {relic.Description}")"
|
||||||
|
style="font-size: 0.78rem; font-weight: 800; cursor: help;">
|
||||||
|
<span>@relic.Icon</span>
|
||||||
|
<span>@relic.Name</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public Player Player { get; set; } = null!;
|
||||||
|
[Parameter] public List<FloatingTextItem>? FloatingTexts { get; set; }
|
||||||
|
|
||||||
|
private bool _isHit = false;
|
||||||
|
|
||||||
|
public async Task TriggerHitAnimation()
|
||||||
|
{
|
||||||
|
_isHit = true;
|
||||||
|
StateHasChanged();
|
||||||
|
await Task.Delay(400);
|
||||||
|
_isHit = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
@using SlaySharp.Core.Enums
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||||
|
@if (Statuses != null)
|
||||||
|
{
|
||||||
|
@foreach (var (status, stacks) in Statuses.All)
|
||||||
|
{
|
||||||
|
if (stacks <= 0) continue;
|
||||||
|
|
||||||
|
var (badgeClass, icon, label, tip) = GetStatusDetails(status, stacks);
|
||||||
|
<div class="status-pill @badgeClass font-mono" style="font-size: 0.82rem; font-weight: 900;" title="@tip">
|
||||||
|
<span style="font-size: 0.95rem;">@icon</span>
|
||||||
|
<span>@stacks</span>
|
||||||
|
<span class="ms-1" style="font-size: 0.75rem;">@label</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public StatusCollection? Statuses { get; set; }
|
||||||
|
|
||||||
|
private (string badgeClass, string icon, string label, string tip) GetStatusDetails(StatusType status, int stacks)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
StatusType.Strength => ("status-strength", "⚔️", "STR", $"Strength: +{stacks} damage to all attacks."),
|
||||||
|
StatusType.Vulnerable => ("status-vulnerable", "🎯", "VULN", $"Vulnerable: Takes 50% more damage ({stacks} turns)."),
|
||||||
|
StatusType.Weak => ("status-weak", "📉", "WEAK", $"Weak: Deals 25% less damage ({stacks} turns)."),
|
||||||
|
StatusType.MemoryLeak => ("status-memoryleak", "⚠️", "LEAK", $"Memory Leak: Takes {stacks} direct damage at end of turn!"),
|
||||||
|
StatusType.JitTier => ("status-strength", "⚡", "JIT", $"JIT: Optimized execution."),
|
||||||
|
_ => ("status-pill", "✨", status.ToString(), $"{status}: {stacks}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace SlaySharp.Web.Models;
|
||||||
|
|
||||||
|
public record FloatingTextItem(
|
||||||
|
string Id,
|
||||||
|
string Text,
|
||||||
|
string CssClass,
|
||||||
|
DateTime CreatedAt
|
||||||
|
);
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
@page "/"
|
||||||
|
@using SlaySharp.Core.Combat
|
||||||
|
@using SlaySharp.Core.Models
|
||||||
|
@using SlaySharp.Core.Catalogs
|
||||||
|
@using SlaySharp.Core.Relics
|
||||||
|
@using SlaySharp.Web.Components
|
||||||
|
|
||||||
|
<PageTitle>SlaySharp - Visual Studio Code Edition</PageTitle>
|
||||||
|
|
||||||
|
@if (_engine == null || !_gameStarted)
|
||||||
|
{
|
||||||
|
<!-- VS Code Welcome / Get Started Dashboard -->
|
||||||
|
<div class="container-fluid h-100 p-4 d-flex flex-column overflow-y-auto font-mono" style="background-color: var(--vscode-bg-editor);">
|
||||||
|
<!-- Welcome Header -->
|
||||||
|
<div class="mb-4 pb-3 border-bottom border-secondary">
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-1">
|
||||||
|
<span style="font-size: 3rem; color: #007acc;">⚔️</span>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-white fw-bolder mb-0" style="font-size: 2.4rem; letter-spacing: -1px;">
|
||||||
|
SlaySharp <span style="color: #61afef; font-size: 1.4rem;">v1.0</span>
|
||||||
|
</h1>
|
||||||
|
<span class="text-muted" style="font-size: 1rem; font-weight: 800;">
|
||||||
|
C# & .NET Roguelike Deckbuilder
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Left Column: Start Debugging Incidents -->
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<h4 class="text-white mb-3 d-flex align-items-center gap-2 fw-bold">
|
||||||
|
<span style="color: #61afef; font-size: 1.5rem;">🐞</span>
|
||||||
|
<span>Start Incident Session</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
<!-- Config 1 -->
|
||||||
|
<div class="card p-4 shadow-sm" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; transition: all 0.2s ease;"
|
||||||
|
@onclick="() => StartEncounter(EncounterType.Standard)">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="text-danger fw-bolder fs-5">💥 [SEV-1] NullReference & Off-By-One</span>
|
||||||
|
<span class="badge bg-danger text-white fs-6 px-3 py-2">Launch (F5) ➔</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white mb-0" style="font-size: 0.92rem; font-weight: 700;">
|
||||||
|
Patch NullReferenceException (45 HP) and Off-By-One Bug (22 HP). Keep SLA at 100%.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Config 2 -->
|
||||||
|
<div class="card p-4 shadow-sm" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; transition: all 0.2s ease;"
|
||||||
|
@onclick="() => StartEncounter(EncounterType.MemoryLeak)">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="text-warning fw-bolder fs-5">👻 [SEV-2] Memory Leak Spectre</span>
|
||||||
|
<span class="badge bg-warning text-dark fs-6 px-3 py-2">Launch (F5) ➔</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white mb-0" style="font-size: 0.92rem; font-weight: 700;">
|
||||||
|
Dispose dangling unmanaged event handlers from Memory Leak Spectre (34 HP).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Config 3 -->
|
||||||
|
<div class="card p-4 shadow-sm" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px; cursor: pointer; transition: all 0.2s ease;"
|
||||||
|
@onclick="() => StartEncounter(EncounterType.Chaos)">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="text-info fw-bolder fs-5">🔥 [SEV-0] Friday 5 PM Outage</span>
|
||||||
|
<span class="badge bg-primary text-white fs-6 px-3 py-2">Launch (F5) ➔</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-white mb-0" style="font-size: 0.92rem; font-weight: 700;">
|
||||||
|
Critical Outage: Face NullReferenceException and Memory Leak Spectre simultaneously.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column: Workspace & Diagnostics Configuration -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<h4 class="text-white mb-3 d-flex align-items-center gap-2 fw-bold">
|
||||||
|
<span style="color: #56b6c2; font-size: 1.5rem;">⚙️</span>
|
||||||
|
<span>Active Profile</span>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="card p-4 mb-3" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px;">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<span style="font-size: 2.2rem;">👨💻</span>
|
||||||
|
<div>
|
||||||
|
<div class="text-info fw-bolder fs-5">Principal .NET Architect</div>
|
||||||
|
<div class="text-white" style="font-size: 0.85rem; font-weight: 800;">3 CPU Cycles / 70 SLA HP</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-muted fw-bold mb-1" style="font-size: 0.78rem;">INSTALLED NUGETS:</div>
|
||||||
|
<div class="d-flex flex-column gap-2 mb-2">
|
||||||
|
<div class="badge bg-dark border border-secondary text-white p-2 d-flex align-items-center justify-content-between" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span>📜 Newtonsoft.Json</span>
|
||||||
|
<span class="text-info">+1 Card T1</span>
|
||||||
|
</div>
|
||||||
|
<div class="badge bg-dark border border-secondary text-white p-2 d-flex align-items-center justify-content-between" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span>⚡ BenchmarkDotNet</span>
|
||||||
|
<span class="text-warning">+1 CPU / 2T</span>
|
||||||
|
</div>
|
||||||
|
<div class="badge bg-dark border border-secondary text-white p-2 d-flex align-items-center justify-content-between" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span>📊 Serilog</span>
|
||||||
|
<span class="text-success">+2 Shield on Attack</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card p-3" style="background-color: #21252b; border: 2px solid #383e4a; border-radius: 12px;">
|
||||||
|
<div class="text-white fw-bolder mb-2" style="font-size: 0.88rem;">CONTROLS</div>
|
||||||
|
<div class="d-flex justify-content-between text-white" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span>Play Card</span>
|
||||||
|
<span class="badge bg-dark border border-secondary text-light">Click Card</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between text-white mt-1" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span>Target Bug</span>
|
||||||
|
<span class="badge bg-dark border border-secondary text-light">Click Bug</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between text-white mt-1" style="font-size: 0.85rem; font-weight: 800;">
|
||||||
|
<span>End Turn</span>
|
||||||
|
<span class="badge bg-success text-white">F5 / Click Button</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<!-- Live VS Code Combat Workspace -->
|
||||||
|
<CombatArena Engine="@_engine" OnRestartRequested="HandleRestartRequested" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private CombatEngine? _engine;
|
||||||
|
private bool _gameStarted = false;
|
||||||
|
private EncounterType _lastEncounterType = EncounterType.Standard;
|
||||||
|
|
||||||
|
private enum EncounterType
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
MemoryLeak,
|
||||||
|
Chaos
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartEncounter(EncounterType type)
|
||||||
|
{
|
||||||
|
_lastEncounterType = type;
|
||||||
|
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||||
|
player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck());
|
||||||
|
|
||||||
|
// Add starter NuGets (Relics)
|
||||||
|
player.Relics.Add(new NewtonsoftJsonRelic());
|
||||||
|
player.Relics.Add(new BenchmarkDotNetRelic());
|
||||||
|
player.Relics.Add(new SerilogRelic());
|
||||||
|
|
||||||
|
var enemies = type switch
|
||||||
|
{
|
||||||
|
EncounterType.Standard => new List<Enemy> { new NullReferenceExceptionEnemy(), new OffByOneBugEnemy() },
|
||||||
|
EncounterType.MemoryLeak => new List<Enemy> { new MemoryLeakSpectreEnemy(), new OffByOneBugEnemy() },
|
||||||
|
EncounterType.Chaos => new List<Enemy> { new NullReferenceExceptionEnemy(), new MemoryLeakSpectreEnemy() },
|
||||||
|
_ => new List<Enemy> { new NullReferenceExceptionEnemy() }
|
||||||
|
};
|
||||||
|
|
||||||
|
_engine = new CombatEngine();
|
||||||
|
_engine.StartCombat(player, enemies);
|
||||||
|
_gameStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleRestartRequested()
|
||||||
|
{
|
||||||
|
_gameStarted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using SlaySharp.Web;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
builder.RootComponents.Add<App>("#app");
|
||||||
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
|
||||||
|
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
|
||||||
|
await builder.Build().RunAsync();
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:2889",
|
||||||
|
"sslPort": 44340
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"applicationUrl": "http://localhost:5160",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"applicationUrl": "https://localhost:7224;http://localhost:5160",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<div class="d-flex flex-column" style="height: 100vh; overflow: hidden; background-color: var(--vscode-bg-editor);">
|
||||||
|
<!-- VS Code Top Title Bar -->
|
||||||
|
<div class="vsc-titlebar font-mono">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span style="color: #007acc; font-weight: 900; font-size: 1rem;">Visual Studio Code</span>
|
||||||
|
<span class="text-muted" style="font-size: 0.72rem;">— SlaySharp [Workspace] — Incident.cs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Command Palette Bar -->
|
||||||
|
<div class="vsc-search-palette font-mono">
|
||||||
|
<span>🔍</span>
|
||||||
|
<span>> SlaySharp: Debug Incident (Press F5 / Play to Execute Turn)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Window Controls -->
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<button class="btn btn-sm btn-dark p-0 px-2 font-mono d-flex align-items-center gap-1 border-secondary"
|
||||||
|
style="font-size: 0.68rem;"
|
||||||
|
@onclick="ToggleSound"
|
||||||
|
title="Audio Synthesizer Toggle">
|
||||||
|
<span>@(_isMuted ? "🔇" : "🔊")</span>
|
||||||
|
<span>@(_isMuted ? "Muted" : "Sound ON")</span>
|
||||||
|
</button>
|
||||||
|
<div class="d-flex gap-2 text-muted" style="font-size: 0.8rem;">
|
||||||
|
<span>─</span>
|
||||||
|
<span>□</span>
|
||||||
|
<span class="text-danger">✕</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS Code Body (Activity Bar + Workspace) -->
|
||||||
|
<div class="vsc-shell flex-grow-1">
|
||||||
|
<!-- Left Activity Bar -->
|
||||||
|
<div class="vsc-activitybar">
|
||||||
|
<div class="d-flex flex-column align-items-center w-100">
|
||||||
|
<div class="activity-icon active" title="Explorer (Ctrl+Shift+E)">
|
||||||
|
<span>📁</span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-icon" title="Search (Ctrl+Shift+F)">
|
||||||
|
<span>🔍</span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-icon" title="Source Control (Ctrl+Shift+G)">
|
||||||
|
<span>🌿</span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-icon" title="Run and Debug (Ctrl+Shift+D)">
|
||||||
|
<span style="color: #3794ff;">🐞</span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-icon" title="Extensions & NuGet Marketplace (Ctrl+Shift+X)">
|
||||||
|
<span>🧩</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column align-items-center w-100">
|
||||||
|
<div class="activity-icon" title="Accounts">
|
||||||
|
<span>👤</span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-icon" title="Settings">
|
||||||
|
<span>⚙️</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Editor Workspace -->
|
||||||
|
<main class="flex-grow-1 d-flex flex-column overflow-hidden" style="background-color: var(--vscode-bg-editor);">
|
||||||
|
@Body
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS Code Bottom Status Bar -->
|
||||||
|
<div class="vsc-statusbar font-mono">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="statusbar-item" title="Git Branch">
|
||||||
|
<span>🌿 main*</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-item" title="Source Control Synchronize">
|
||||||
|
<span>🔄 0↓ 1↑</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-item" title="Roslyn Diagnostics">
|
||||||
|
<span>⛔ 0</span>
|
||||||
|
<span class="ms-1">⚠️ 2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="statusbar-item">
|
||||||
|
<span>Ln 42, Col 18</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-item">
|
||||||
|
<span>Spaces: 4</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-item">
|
||||||
|
<span>UTF-8</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-item">
|
||||||
|
<span>CRLF</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-item" style="background-color: rgba(255,255,255,0.15);">
|
||||||
|
<span style="color: #4ec9b0; font-weight: 700;">{ } C# (Roslyn JIT)</span>
|
||||||
|
</div>
|
||||||
|
<div class="statusbar-item" title="OmniSharp / C# Dev Kit Server">
|
||||||
|
<span>🔥 .NET 8.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _isMuted = false;
|
||||||
|
|
||||||
|
private async Task ToggleSound()
|
||||||
|
{
|
||||||
|
_isMuted = await JS.InvokeAsync<bool>("slayAudio.toggleMute");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.top-row:not(.auth) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.page {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row, article {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="">SlaySharp.Web</a>
|
||||||
|
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||||
|
<nav class="flex-column">
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="bi bi-house-door-fill" aria-hidden="true"></span> Home
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="counter">
|
||||||
|
<span class="bi bi-plus-square-fill" aria-hidden="true"></span> Counter
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="fetchdata">
|
||||||
|
<span class="bi bi-list-nested" aria-hidden="true"></span> Fetch data
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool collapseNavMenu = true;
|
||||||
|
|
||||||
|
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||||
|
|
||||||
|
private void ToggleNavMenu()
|
||||||
|
{
|
||||||
|
collapseNavMenu = !collapseNavMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.navbar-toggler {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
height: 3.5rem;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
width: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
vertical-align: text-top;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:first-of-type {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:last-of-type {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a {
|
||||||
|
color: #d7d7d7;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a.active {
|
||||||
|
background-color: rgba(255,255,255,0.25);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse {
|
||||||
|
/* Never collapse the sidebar for wide screens */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
/* Allow sidebar to scroll for tall menus */
|
||||||
|
height: calc(100vh - 3.5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0-preview.7.23375.9" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0-preview.7.23375.9" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SlaySharp.Core\SlaySharp.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using SlaySharp.Web
|
||||||
|
@using SlaySharp.Web.Shared
|
||||||
|
@using SlaySharp.Web.Components
|
||||||
|
@using SlaySharp.Web.Models
|
||||||
|
@using SlaySharp.Core.Enums
|
||||||
|
@using SlaySharp.Core.Models
|
||||||
|
@using SlaySharp.Core.Combat
|
||||||
|
@using SlaySharp.Core.Catalogs
|
||||||
|
@using SlaySharp.Core.Relics
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
html, body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .btn-link {
|
||||||
|
color: #0071c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||||
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding-top: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.valid.modified:not([type=checkbox]) {
|
||||||
|
outline: 1px solid #26b050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
outline: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary {
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||||
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blazor-error-boundary::after {
|
||||||
|
content: "An error has occurred."
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
margin: 20vh auto 1rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress circle {
|
||||||
|
fill: none;
|
||||||
|
stroke: #e0e0e0;
|
||||||
|
stroke-width: 0.6rem;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress circle:last-child {
|
||||||
|
stroke: #1b6ec2;
|
||||||
|
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||||
|
transition: stroke-dasharray 0.05s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress-text {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-progress-text:after {
|
||||||
|
content: var(--blazor-load-percentage-text, "Loading");
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,724 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,600;0,700;0,800;0,900;1,700&family=Plus+Jakarta+Sans:wght@600;700;800;900&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Exact VS Code Dark+ Palette */
|
||||||
|
--vscode-bg-editor: #181a1f;
|
||||||
|
--vscode-bg-sidebar: #21252b;
|
||||||
|
--vscode-bg-activitybar: #1e2227;
|
||||||
|
--vscode-bg-statusbar: #007acc;
|
||||||
|
--vscode-bg-titlebar: #282c34;
|
||||||
|
--vscode-bg-tab-active: #181a1f;
|
||||||
|
--vscode-bg-tab-inactive: #21252b;
|
||||||
|
--vscode-bg-input: #282c34;
|
||||||
|
|
||||||
|
--vscode-border: #383e4a;
|
||||||
|
--vscode-active-border: #007acc;
|
||||||
|
--vscode-text-main: #abb2bf;
|
||||||
|
--vscode-text-muted: #828997;
|
||||||
|
--vscode-text-bright: #ffffff;
|
||||||
|
|
||||||
|
/* VS Code C# Syntax Colors */
|
||||||
|
--syn-keyword: #61afef;
|
||||||
|
--syn-control: #c678dd;
|
||||||
|
--syn-type: #56b6c2;
|
||||||
|
--syn-string: #98c379;
|
||||||
|
--syn-method: #e5c07b;
|
||||||
|
--syn-var: #e06c75;
|
||||||
|
--syn-comment: #7f848e;
|
||||||
|
--syn-num: #d19a66;
|
||||||
|
|
||||||
|
/* Badges & Status */
|
||||||
|
--vsc-error: #e06c75;
|
||||||
|
--vsc-warning: #e5c07b;
|
||||||
|
--vsc-info: #61afef;
|
||||||
|
--vsc-success: #98c379;
|
||||||
|
|
||||||
|
--card-w: 245px;
|
||||||
|
--card-h: 335px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--vscode-bg-editor);
|
||||||
|
color: var(--vscode-text-bright);
|
||||||
|
font-family: 'Plus Jakarta Sans', 'Segoe UI', -apple-system, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
user-select: none;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre, .font-mono {
|
||||||
|
font-family: 'JetBrains Mono', 'Consolas', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Damage & Shield Numbers */
|
||||||
|
.floating-vfx-container {
|
||||||
|
position: absolute;
|
||||||
|
top: -30px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 200;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-dmg-number {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 2.6rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #ff4757;
|
||||||
|
text-shadow: 0 0 12px rgba(0, 0, 0, 1), 0 0 25px rgba(255, 71, 87, 0.9);
|
||||||
|
animation: float-damage-up 1.3s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: -1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-dmg-number.crit {
|
||||||
|
font-size: 3.2rem;
|
||||||
|
color: #ffa502;
|
||||||
|
text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 35px rgba(255, 165, 2, 1);
|
||||||
|
animation: float-crit-up 1.3s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-dmg-number.pierce {
|
||||||
|
color: #00f0ff;
|
||||||
|
text-shadow: 0 0 15px rgba(0, 0, 0, 1), 0 0 25px rgba(0, 240, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-dmg-number.shield {
|
||||||
|
color: #61afef;
|
||||||
|
text-shadow: 0 0 12px rgba(0, 0, 0, 1), 0 0 25px rgba(97, 175, 239, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-damage-up {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px) scale(0.6);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-12px) scale(1.3);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-28px) scale(1.0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-75px) scale(0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-crit-up {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px) scale(0.5) rotate(-8deg);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-18px) scale(1.5) rotate(4deg);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-34px) scale(1.1) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-90px) scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slower Dramatic Enemy Attack Lunge Animation */
|
||||||
|
.enemy-attacking-lunge {
|
||||||
|
animation: enemy-attack-strike 0.9s cubic-bezier(0.25, 1, 0.5, 1) forwards !important;
|
||||||
|
z-index: 120 !important;
|
||||||
|
border-color: #ff4757 !important;
|
||||||
|
box-shadow: 0 0 45px rgba(255, 71, 87, 1), 0 20px 40px rgba(0, 0, 0, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes enemy-attack-strike {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-20px) scale(1.12);
|
||||||
|
box-shadow: 0 0 50px rgba(255, 71, 87, 1), 0 0 25px rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
55% {
|
||||||
|
transform: translateY(75px) scale(1.24);
|
||||||
|
box-shadow: 0 0 70px rgba(255, 71, 87, 1);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateY(55px) scale(1.16);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.acting-enemy-banner {
|
||||||
|
position: absolute;
|
||||||
|
top: -32px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(90deg, #e06c75, #b31d28);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 0 15px rgba(224, 108, 117, 0.9);
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: pulse-banner 0.8s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-banner {
|
||||||
|
0% { transform: translateX(-50%) scale(0.95); opacity: 0.9; }
|
||||||
|
100% { transform: translateX(-50%) scale(1.05); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hit Impact Screen & Card Shake */
|
||||||
|
.hit-shake {
|
||||||
|
animation: shake-element 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake-element {
|
||||||
|
10%, 90% { transform: translate3d(-4px, 0, 0) scale(0.98); }
|
||||||
|
20%, 80% { transform: translate3d(6px, 0, 0) scale(1.02); }
|
||||||
|
30%, 50%, 70% { transform: translate3d(-7px, 0, 0); }
|
||||||
|
40%, 60% { transform: translate3d(7px, 0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash Impact Overlay */
|
||||||
|
.impact-flash {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: radial-gradient(circle, rgba(255, 71, 87, 0.75) 0%, transparent 80%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 150;
|
||||||
|
animation: flash-fade 0.4s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash-fade {
|
||||||
|
0% { opacity: 1; transform: scale(1.15); }
|
||||||
|
100% { opacity: 0; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Launch Fly Animation */
|
||||||
|
.card-launched {
|
||||||
|
animation: card-launch-swoosh 0.5s ease-out forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes card-launch-swoosh {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2) translateY(-140px);
|
||||||
|
opacity: 0.9;
|
||||||
|
box-shadow: 0 0 35px rgba(0, 122, 204, 0.9);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.2) translateY(-300px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VS Code Title Bar */
|
||||||
|
.vsc-titlebar {
|
||||||
|
height: 38px;
|
||||||
|
background-color: var(--vscode-bg-titlebar);
|
||||||
|
border-bottom: 1px solid var(--vscode-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--vscode-text-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-search-palette {
|
||||||
|
background: var(--vscode-bg-input);
|
||||||
|
border: 1px solid #484f58;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 28px;
|
||||||
|
color: var(--vscode-text-main);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 800;
|
||||||
|
min-width: 360px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VS Code Shell Layout */
|
||||||
|
.vsc-shell {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 38px - 26px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Activity Bar */
|
||||||
|
.vsc-activitybar {
|
||||||
|
width: 52px;
|
||||||
|
background-color: var(--vscode-bg-activitybar);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-right: 1px solid var(--vscode-border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--vscode-text-muted);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-icon:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-icon.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-icon.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 6px;
|
||||||
|
bottom: 6px;
|
||||||
|
width: 3px;
|
||||||
|
background-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VS Code Tabs Bar */
|
||||||
|
.vsc-tabs-bar {
|
||||||
|
height: 38px;
|
||||||
|
background-color: var(--vscode-bg-sidebar);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--vscode-border);
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-tab {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 16px;
|
||||||
|
background-color: var(--vscode-bg-tab-inactive);
|
||||||
|
color: var(--vscode-text-muted);
|
||||||
|
border-right: 1px solid var(--vscode-border);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-tab:hover {
|
||||||
|
color: var(--vscode-text-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-tab.active {
|
||||||
|
background-color: var(--vscode-bg-tab-active);
|
||||||
|
color: var(--vscode-text-bright);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-tab.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2.5px;
|
||||||
|
background-color: var(--vscode-active-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs Bar */
|
||||||
|
.vsc-breadcrumbs {
|
||||||
|
height: 28px;
|
||||||
|
background-color: var(--vscode-bg-editor);
|
||||||
|
padding: 0 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--vscode-text-muted);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Debug Floating Action Toolbar */
|
||||||
|
.vsc-debug-toolbar {
|
||||||
|
background-color: #21252b;
|
||||||
|
border: 1.5px solid #484f58;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-debug-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--vscode-text-bright);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-debug-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vsc-debug-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VS Code Status Bar (Bottom) */
|
||||||
|
.vsc-statusbar {
|
||||||
|
height: 26px;
|
||||||
|
background-color: var(--vscode-bg-statusbar);
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Visual Window with Art Box */
|
||||||
|
.game-card {
|
||||||
|
width: var(--card-w);
|
||||||
|
height: var(--card-h);
|
||||||
|
background-color: #1e2227;
|
||||||
|
border: 2px solid #383e4a;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.6);
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card:hover {
|
||||||
|
transform: translateY(-26px) scale(1.06);
|
||||||
|
z-index: 50;
|
||||||
|
border-color: #61afef;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 122, 204, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card.playable {
|
||||||
|
border-color: #007acc;
|
||||||
|
box-shadow: 0 0 16px rgba(0, 122, 204, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card.selected {
|
||||||
|
transform: translateY(-46px) scale(1.1) !important;
|
||||||
|
border-color: #98c379 !important;
|
||||||
|
box-shadow: 0 0 30px rgba(152, 195, 121, 0.8) !important;
|
||||||
|
z-index: 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card.unaffordable {
|
||||||
|
opacity: 0.45;
|
||||||
|
filter: grayscale(80%);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card VS Code Tab Header */
|
||||||
|
.card-vsc-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-bottom: 1.5px solid #181a1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tab-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-cost-badge {
|
||||||
|
background: #007acc;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 122, 204, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-attack .card-cost-badge { background: #e06c75; box-shadow: 0 0 8px rgba(224, 108, 117, 0.6); }
|
||||||
|
.type-skill .card-cost-badge { background: #61afef; box-shadow: 0 0 8px rgba(97, 175, 239, 0.6); }
|
||||||
|
.type-power .card-cost-badge { background: #c678dd; box-shadow: 0 0 8px rgba(198, 120, 221, 0.6); }
|
||||||
|
|
||||||
|
/* Card Art Box (Visual Illustration) */
|
||||||
|
.card-art-box {
|
||||||
|
height: 80px;
|
||||||
|
background: linear-gradient(135deg, #181a1f 0%, #21252b 100%);
|
||||||
|
border-bottom: 1px solid #383e4a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-art-icon {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.7));
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card:hover .card-art-icon {
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor Gutter and Lines inside Card */
|
||||||
|
.card-editor-viewport {
|
||||||
|
background-color: #181a1f;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 0;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-gutter {
|
||||||
|
width: 26px;
|
||||||
|
color: #5c6370;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 6px;
|
||||||
|
user-select: none;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-code-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: var(--syn-var);
|
||||||
|
word-break: break-word;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-vsc-footer {
|
||||||
|
background-color: #21252b;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-top: 1.5px solid #383e4a;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffffff;
|
||||||
|
min-height: 52px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enemy Bug VS Code Problem Card */
|
||||||
|
.enemy-card {
|
||||||
|
background-color: #21252b;
|
||||||
|
border: 2px solid #383e4a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
width: 255px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enemy-card:hover {
|
||||||
|
border-color: #e06c75;
|
||||||
|
box-shadow: 0 14px 32px rgba(224, 108, 117, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enemy-card.targeted {
|
||||||
|
border-color: #e06c75 !important;
|
||||||
|
box-shadow: 0 0 30px rgba(224, 108, 117, 0.9) !important;
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enemy-avatar-wrap {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(224, 108, 117, 0.2) 0%, transparent 70%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 4.2rem;
|
||||||
|
margin: 6px 0;
|
||||||
|
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-squiggle {
|
||||||
|
text-decoration: underline wavy #e06c75;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intent Forecast Ribbon */
|
||||||
|
.intent-badge {
|
||||||
|
background: #181a1f;
|
||||||
|
border: 1.5px solid #383e4a;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 900;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intent-attack { color: #e06c75; border-color: rgba(224, 108, 117, 0.7); box-shadow: 0 0 15px rgba(224, 108, 117, 0.4); }
|
||||||
|
.intent-defend { color: #61afef; border-color: rgba(97, 175, 239, 0.7); box-shadow: 0 0 15px rgba(97, 175, 239, 0.4); }
|
||||||
|
.intent-debuff { color: #e5c07b; border-color: rgba(229, 192, 123, 0.7); box-shadow: 0 0 15px rgba(229, 192, 123, 0.4); }
|
||||||
|
.intent-buff { color: #98c379; border-color: rgba(152, 195, 121, 0.7); box-shadow: 0 0 15px rgba(152, 195, 121, 0.4); }
|
||||||
|
|
||||||
|
/* Health / SLA Progress Bar */
|
||||||
|
.hp-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 22px;
|
||||||
|
background: #181a1f;
|
||||||
|
border-radius: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
border: 1.5px solid #383e4a;
|
||||||
|
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hp-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #e06c75 0%, #e5c07b 50%, #98c379 100%);
|
||||||
|
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hp-bar-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #ffffff;
|
||||||
|
text-shadow: 0 1px 3px rgba(0,0,0,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shield-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
color: #61afef;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
background: rgba(97, 175, 239, 0.18);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1.5px solid rgba(97, 175, 239, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CPU Core Orb */
|
||||||
|
.cpu-reactor {
|
||||||
|
width: 86px;
|
||||||
|
height: 86px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle at 35% 35%, #007acc, #181a1f);
|
||||||
|
border: 3px solid #61afef;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 25px rgba(0, 122, 204, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar for VS Code look */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #181a1f;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #383e4a;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #484f58;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>SlaySharp - The .NET Deckbuilder</title>
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="css/bootstrap-icons/bootstrap-icons.min.css" />
|
||||||
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
|
<link rel="stylesheet" href="css/game.css" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
<link href="SlaySharp.Web.styles.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<svg class="loading-progress">
|
||||||
|
<circle r="40%" cx="50%" cy="50%" />
|
||||||
|
<circle r="40%" cx="50%" cy="50%" />
|
||||||
|
</svg>
|
||||||
|
<div class="loading-progress-text"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
||||||
|
<script src="js/audio.js"></script>
|
||||||
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// SlaySharp Web Audio Synthesizer
|
||||||
|
window.slayAudio = {
|
||||||
|
ctx: null,
|
||||||
|
muted: false,
|
||||||
|
|
||||||
|
init: function () {
|
||||||
|
if (!this.ctx) {
|
||||||
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (AudioCtx) {
|
||||||
|
this.ctx = new AudioCtx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.ctx && this.ctx.state === 'suspended') {
|
||||||
|
this.ctx.resume();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMute: function () {
|
||||||
|
this.muted = !this.muted;
|
||||||
|
return this.muted;
|
||||||
|
},
|
||||||
|
|
||||||
|
playCardSound: function () {
|
||||||
|
if (this.muted) return;
|
||||||
|
this.init();
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'sine';
|
||||||
|
osc.frequency.setValueAtTime(440, this.ctx.currentTime);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(880, this.ctx.currentTime + 0.12);
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.12);
|
||||||
|
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.ctx.destination);
|
||||||
|
|
||||||
|
osc.start();
|
||||||
|
osc.stop(this.ctx.currentTime + 0.12);
|
||||||
|
},
|
||||||
|
|
||||||
|
playHitSound: function () {
|
||||||
|
if (this.muted) return;
|
||||||
|
this.init();
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'sawtooth';
|
||||||
|
osc.frequency.setValueAtTime(220, this.ctx.currentTime);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(60, this.ctx.currentTime + 0.18);
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.18);
|
||||||
|
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.ctx.destination);
|
||||||
|
|
||||||
|
osc.start();
|
||||||
|
osc.stop(this.ctx.currentTime + 0.18);
|
||||||
|
},
|
||||||
|
|
||||||
|
playShieldSound: function () {
|
||||||
|
if (this.muted) return;
|
||||||
|
this.init();
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.setValueAtTime(320, this.ctx.currentTime);
|
||||||
|
osc.frequency.exponentialRampToValueAtTime(640, this.ctx.currentTime + 0.15);
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
|
||||||
|
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.ctx.destination);
|
||||||
|
|
||||||
|
osc.start();
|
||||||
|
osc.stop(this.ctx.currentTime + 0.15);
|
||||||
|
},
|
||||||
|
|
||||||
|
playVictorySound: function () {
|
||||||
|
if (this.muted) return;
|
||||||
|
this.init();
|
||||||
|
if (!this.ctx) return;
|
||||||
|
|
||||||
|
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
|
||||||
|
notes.forEach((freq, idx) => {
|
||||||
|
const osc = this.ctx.createOscillator();
|
||||||
|
const gain = this.ctx.createGain();
|
||||||
|
osc.type = 'triangle';
|
||||||
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime + idx * 0.12);
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0.15, this.ctx.currentTime + idx * 0.12);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + idx * 0.12 + 0.35);
|
||||||
|
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(this.ctx.destination);
|
||||||
|
|
||||||
|
osc.start(this.ctx.currentTime + idx * 0.12);
|
||||||
|
osc.stop(this.ctx.currentTime + idx * 0.12 + 0.35);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"date": "2022-01-06",
|
||||||
|
"temperatureC": 1,
|
||||||
|
"summary": "Freezing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2022-01-07",
|
||||||
|
"temperatureC": 14,
|
||||||
|
"summary": "Bracing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2022-01-08",
|
||||||
|
"temperatureC": -13,
|
||||||
|
"summary": "Freezing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2022-01-09",
|
||||||
|
"temperatureC": -16,
|
||||||
|
"summary": "Balmy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2022-01-10",
|
||||||
|
"temperatureC": -2,
|
||||||
|
"summary": "Chilly"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using SlaySharp.Core.Catalogs;
|
||||||
|
using SlaySharp.Core.Combat;
|
||||||
|
using SlaySharp.Core.Enums;
|
||||||
|
using SlaySharp.Core.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SlaySharp.Tests;
|
||||||
|
|
||||||
|
public class CombatEngineTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CombatEngine_InitializesDeckAndDraws5Cards()
|
||||||
|
{
|
||||||
|
var player = new Player("Senior Dev", 70, maxEnergy: 3, seed: 42);
|
||||||
|
player.MasterDeck.AddRange(StarterDecks.CreateCsharpArchitectDeck());
|
||||||
|
|
||||||
|
var engine = new CombatEngine();
|
||||||
|
var enemy = new NullReferenceExceptionEnemy();
|
||||||
|
|
||||||
|
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||||
|
|
||||||
|
Assert.Equal(CombatPhase.PlayerTurn, engine.Phase);
|
||||||
|
Assert.Equal(3, player.CurrentEnergy);
|
||||||
|
Assert.Equal(5, player.Hand.Count);
|
||||||
|
Assert.Equal(7, player.DrawPile.Count);
|
||||||
|
Assert.Empty(player.DiscardPile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlayCard_DealsDamage_AndConsumesEnergy()
|
||||||
|
{
|
||||||
|
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||||
|
var card = new ConsoleWriteLineCard(); // Cost 1, dmg 6
|
||||||
|
var enemy = new NullReferenceExceptionEnemy(); // 45 HP
|
||||||
|
|
||||||
|
var engine = new CombatEngine();
|
||||||
|
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||||
|
|
||||||
|
// Force hand
|
||||||
|
player.Hand.Clear();
|
||||||
|
player.Hand.Add(card);
|
||||||
|
player.CurrentEnergy = 3;
|
||||||
|
|
||||||
|
var success = engine.PlayCard(card, enemy);
|
||||||
|
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Equal(2, player.CurrentEnergy);
|
||||||
|
Assert.Equal(39, enemy.CurrentHp);
|
||||||
|
Assert.Contains(card, player.DiscardPile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpanSliceCard_PiercesThroughShield()
|
||||||
|
{
|
||||||
|
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||||
|
var card = new SpanSliceCard(); // 8 piercing dmg
|
||||||
|
var enemy = new NullReferenceExceptionEnemy(); // 45 HP
|
||||||
|
|
||||||
|
var engine = new CombatEngine();
|
||||||
|
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||||
|
|
||||||
|
enemy.GainShield(10);
|
||||||
|
player.Hand.Clear();
|
||||||
|
player.Hand.Add(card);
|
||||||
|
player.CurrentEnergy = 3;
|
||||||
|
|
||||||
|
var success = engine.PlayCard(card, enemy);
|
||||||
|
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Equal(10, enemy.Shield); // Shield remains 10!
|
||||||
|
Assert.Equal(37, enemy.CurrentHp); // 45 - 8 = 37
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StatusEffects_StrengthAndVulnerable_MultiplyDamage()
|
||||||
|
{
|
||||||
|
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||||
|
var enemy = new NullReferenceExceptionEnemy(); // 45 HP
|
||||||
|
var card = new ConsoleWriteLineCard(); // 6 dmg
|
||||||
|
|
||||||
|
var engine = new CombatEngine();
|
||||||
|
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||||
|
|
||||||
|
player.Statuses.Add(StatusType.Strength, 2); // Base 6 + 2 = 8
|
||||||
|
enemy.Statuses.Add(StatusType.Vulnerable, 1); // 8 * 1.5 = 12
|
||||||
|
|
||||||
|
player.Hand.Clear();
|
||||||
|
player.Hand.Add(card);
|
||||||
|
player.CurrentEnergy = 3;
|
||||||
|
|
||||||
|
engine.PlayCard(card, enemy);
|
||||||
|
|
||||||
|
Assert.Equal(33, enemy.CurrentHp); // 45 - 12 = 33
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DiscardPile_Reshuffles_WhenDrawPileEmpty()
|
||||||
|
{
|
||||||
|
var player = new Player("Senior Dev", 70, maxEnergy: 3, seed: 123);
|
||||||
|
// Only 3 cards in master deck
|
||||||
|
player.MasterDeck.Add(new ConsoleWriteLineCard());
|
||||||
|
player.MasterDeck.Add(new TryCatchCard());
|
||||||
|
player.MasterDeck.Add(new LinqWhereCard());
|
||||||
|
|
||||||
|
var engine = new CombatEngine();
|
||||||
|
var enemy = new OffByOneBugEnemy();
|
||||||
|
|
||||||
|
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||||
|
|
||||||
|
// All 3 cards drawn to hand, draw pile empty
|
||||||
|
Assert.Equal(3, player.Hand.Count);
|
||||||
|
Assert.Empty(player.DrawPile);
|
||||||
|
|
||||||
|
// Discard 2 cards
|
||||||
|
player.DiscardPile.Add(player.Hand[0]);
|
||||||
|
player.DiscardPile.Add(player.Hand[1]);
|
||||||
|
player.Hand.RemoveRange(0, 2);
|
||||||
|
|
||||||
|
// Request drawing 2 cards
|
||||||
|
player.DrawCards(2, engine.Log);
|
||||||
|
|
||||||
|
Assert.Equal(3, player.Hand.Count);
|
||||||
|
Assert.Empty(player.DiscardPile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KillingAllEnemies_TriggersVictory()
|
||||||
|
{
|
||||||
|
var player = new Player("Senior Dev", 70, maxEnergy: 3);
|
||||||
|
var card = new ThrowExceptionCard(); // 16 dmg
|
||||||
|
var enemy = new OffByOneBugEnemy(); // 22 HP
|
||||||
|
|
||||||
|
var engine = new CombatEngine();
|
||||||
|
engine.StartCombat(player, new List<Enemy> { enemy });
|
||||||
|
|
||||||
|
enemy.CurrentHp = 10; // low HP
|
||||||
|
player.Hand.Clear();
|
||||||
|
player.Hand.Add(card);
|
||||||
|
player.CurrentEnergy = 2;
|
||||||
|
|
||||||
|
engine.PlayCard(card, enemy);
|
||||||
|
|
||||||
|
Assert.Equal(CombatPhase.Victory, engine.Phase);
|
||||||
|
Assert.Empty(engine.Enemies);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\SlaySharp.Core\SlaySharp.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user