Browse Source

Merge branch 'dev' of github.com:eesast/THUAI6 into dev

tags/0.1.0
Shawqeem 2 years ago
parent
commit
863c77ee84
38 changed files with 766 additions and 285 deletions
  1. +24
    -0
      .github/workflows/build.yml
  2. +47
    -1
      CAPI/cpp/API/include/API.h
  3. +7
    -0
      CAPI/cpp/API/include/logic.h
  4. +78
    -0
      CAPI/cpp/API/src/API.cpp
  5. +82
    -0
      CAPI/cpp/API/src/DebugAPI.cpp
  6. +36
    -0
      CAPI/cpp/API/src/logic.cpp
  7. +5
    -0
      README.md
  8. +5
    -7
      logic/ClientTest/Program.cs
  9. +2
    -0
      logic/GameClass/GameObj/Bullet/BombedBullet.cs
  10. +12
    -12
      logic/GameClass/GameObj/Bullet/Bullet.Ghost.cs
  11. +8
    -7
      logic/GameClass/GameObj/Bullet/Bullet.cs
  12. +4
    -5
      logic/GameClass/GameObj/Character/Character.Skill.cs
  13. +61
    -28
      logic/GameClass/GameObj/Character/Character.cs
  14. +10
    -84
      logic/GameClass/GameObj/GameObj.cs
  15. +1
    -0
      logic/GameClass/GameObj/Map/Doorway.cs
  16. +1
    -0
      logic/GameClass/GameObj/Map/EmergencyExit.cs
  17. +1
    -0
      logic/GameClass/GameObj/Map/Generator.cs
  18. +7
    -6
      logic/GameClass/GameObj/Map/Map.cs
  19. +1
    -0
      logic/GameClass/GameObj/Map/Wall.cs
  20. +103
    -0
      logic/GameClass/GameObj/Moveable.cs
  21. +1
    -1
      logic/GameClass/GameObj/ObjOfCharacter.cs
  22. +1
    -0
      logic/GameClass/GameObj/OutOfBoundBlock.cs
  23. +1
    -0
      logic/GameClass/GameObj/PickedProp.cs
  24. +8
    -13
      logic/GameClass/GameObj/Prop.cs
  25. +32
    -12
      logic/GameEngine/MoveEngine.cs
  26. +3
    -2
      logic/Gaming/ActionManager.cs
  27. +10
    -8
      logic/Gaming/AttackManager.cs
  28. +107
    -38
      logic/Gaming/Game.cs
  29. +10
    -9
      logic/Gaming/PropManager.cs
  30. +2
    -2
      logic/Preparation/Interface/IGameObj.cs
  31. +2
    -0
      logic/Preparation/Interface/IMap.cs
  32. +4
    -2
      logic/Preparation/Interface/IMoveable.cs
  33. +16
    -1
      logic/Preparation/Interface/IOccupation.cs
  34. +7
    -0
      logic/Preparation/Utility/GameData.cs
  35. +6
    -2
      logic/Server/CopyInfo.cs
  36. +48
    -31
      logic/Server/GameServer.cs
  37. +1
    -1
      logic/Server/Program.cs
  38. +12
    -13
      logic/规则Logic.md

+ 24
- 0
.github/workflows/build.yml View File

@@ -0,0 +1,24 @@
name: build
on: [push, pull_request]
jobs:
dotnet-build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
- name: Build Logic
run: dotnet build "./logic/logic.sln" -c Release
- name: Build Installer
run: dotnet build "./installer/installer.sln" -c Release
- name: Build Launcher
run: dotnet build "./launcher/launcher.sln" -c Release
- name: Build Playback
run: dotnet build "./playback/playback.sln" -c Release

+ 47
- 1
CAPI/cpp/API/include/API.h View File

@@ -61,6 +61,13 @@ public:
virtual bool StartTreatMate() = 0;
virtual bool StartRescueMate() = 0;

virtual bool OpenDoor() = 0;
virtual bool CloseDoor() = 0;
virtual bool SkipWindow() = 0;
virtual bool StartOpenGate() = 0;
virtual bool StartOpenChest() = 0;
virtual bool EndAllAction() = 0;

// ITrickerAPI使用的部分
virtual bool Attack(double angle) = 0;

@@ -84,6 +91,14 @@ public:
virtual std::future<bool> PickProp(THUAI6::PropType prop) = 0;
virtual std::future<bool> UseProp(THUAI6::PropType prop) = 0;
virtual std::future<bool> UseSkill(int32_t skillID) = 0;
virtual std::future<bool> Attack(double angleInRadian) = 0;

virtual std::future<bool> OpenDoor() = 0;
virtual std::future<bool> CloseDoor() = 0;
virtual std::future<bool> SkipWindow() = 0;
virtual std::future<bool> StartOpenGate() = 0;
virtual std::future<bool> StartOpenChest() = 0;
virtual std::future<bool> EndAllAction() = 0;

// 发送信息、接受信息,注意收消息时无消息则返回nullopt
virtual std::future<bool> SendMessage(int64_t, std::string) = 0;
@@ -149,7 +164,6 @@ class ITrickerAPI : public IAPI
public:
/*****捣蛋鬼阵营的特定函数*****/

virtual std::future<bool> Attack(double angleInRadian) = 0;
[[nodiscard]] virtual std::shared_ptr<const THUAI6::Tricker> GetSelfInfo() const = 0;
};

@@ -190,6 +204,15 @@ public:
std::future<bool> UseProp(THUAI6::PropType prop) override;
std::future<bool> UseSkill(int32_t skillID) override;

std::future<bool> Attack(double angleInRadian) override;

std::future<bool> OpenDoor() override;
std::future<bool> CloseDoor() override;
std::future<bool> SkipWindow() override;
std::future<bool> StartOpenGate() override;
std::future<bool> StartOpenChest() override;
std::future<bool> EndAllAction() override;

std::future<bool> SendMessage(int64_t, std::string) override;
[[nodiscard]] std::future<bool> HaveMessage() override;
[[nodiscard]] std::future<std::optional<std::pair<int64_t, std::string>>> GetMessage() override;
@@ -256,6 +279,13 @@ public:
std::future<bool> UseProp(THUAI6::PropType prop) override;
std::future<bool> UseSkill(int32_t skillID) override;

std::future<bool> OpenDoor() override;
std::future<bool> CloseDoor() override;
std::future<bool> SkipWindow() override;
std::future<bool> StartOpenGate() override;
std::future<bool> StartOpenChest() override;
std::future<bool> EndAllAction() override;

std::future<bool> SendMessage(int64_t, std::string) override;
[[nodiscard]] std::future<bool> HaveMessage() override;
[[nodiscard]] std::future<std::optional<std::pair<int64_t, std::string>>> GetMessage() override;
@@ -312,6 +342,15 @@ public:
std::future<bool> UseProp(THUAI6::PropType prop) override;
std::future<bool> UseSkill(int32_t skillID) override;

std::future<bool> Attack(double angleInRadian) override;

std::future<bool> OpenDoor() override;
std::future<bool> CloseDoor() override;
std::future<bool> SkipWindow() override;
std::future<bool> StartOpenGate() override;
std::future<bool> StartOpenChest() override;
std::future<bool> EndAllAction() override;

std::future<bool> SendMessage(int64_t, std::string) override;
[[nodiscard]] std::future<bool> HaveMessage() override;
[[nodiscard]] std::future<std::optional<std::pair<int64_t, std::string>>> GetMessage() override;
@@ -365,6 +404,13 @@ public:
std::future<bool> UseProp(THUAI6::PropType prop) override;
std::future<bool> UseSkill(int32_t skillID) override;

std::future<bool> OpenDoor() override;
std::future<bool> CloseDoor() override;
std::future<bool> SkipWindow() override;
std::future<bool> StartOpenGate() override;
std::future<bool> StartOpenChest() override;
std::future<bool> EndAllAction() override;

std::future<bool> SendMessage(int64_t, std::string) override;
[[nodiscard]] std::future<bool> HaveMessage() override;
[[nodiscard]] std::future<std::optional<std::pair<int64_t, std::string>>> GetMessage() override;


+ 7
- 0
CAPI/cpp/API/include/logic.h View File

@@ -118,6 +118,13 @@ private:

bool Attack(double angle) override;

bool OpenDoor() override;
bool CloseDoor() override;
bool SkipWindow() override;
bool StartOpenGate() override;
bool StartOpenChest() override;
bool EndAllAction() override;

bool WaitThread() override;

int GetCounter() const override;


+ 78
- 0
CAPI/cpp/API/src/API.cpp View File

@@ -101,6 +101,78 @@ std::future<bool> TrickerAPI::UseSkill(int32_t skillID)
{ return logic.UseSkill(skillID); });
}

std::future<bool> StudentAPI::OpenDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.OpenDoor(); });
}

std::future<bool> TrickerAPI::OpenDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.OpenDoor(); });
}

std::future<bool> StudentAPI::CloseDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.CloseDoor(); });
}

std::future<bool> TrickerAPI::CloseDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.CloseDoor(); });
}

std::future<bool> StudentAPI::SkipWindow()
{
return std::async(std::launch::async, [&]()
{ return logic.SkipWindow(); });
}

std::future<bool> TrickerAPI::SkipWindow()
{
return std::async(std::launch::async, [&]()
{ return logic.SkipWindow(); });
}

std::future<bool> StudentAPI::StartOpenGate()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenGate(); });
}

std::future<bool> TrickerAPI::StartOpenGate()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenGate(); });
}

std::future<bool> StudentAPI::StartOpenChest()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenChest(); });
}

std::future<bool> TrickerAPI::StartOpenChest()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenChest(); });
}

std::future<bool> StudentAPI::EndAllAction()
{
return std::async(std::launch::async, [&]()
{ return logic.EndAllAction(); });
}

std::future<bool> TrickerAPI::EndAllAction()
{
return std::async(std::launch::async, [&]()
{ return logic.EndAllAction(); });
}

std::future<bool> StudentAPI::SendMessage(int64_t toID, std::string message)
{
return std::async(std::launch::async, [&]()
@@ -252,6 +324,12 @@ std::future<bool> TrickerAPI::Attack(double angleInRadian)
{ return logic.Attack(angleInRadian); });
}

std::future<bool> StudentAPI::Attack(double angleInRadian)
{
return std::async(std::launch::async, [&]()
{ return logic.Attack(angleInRadian); });
}

std::shared_ptr<const THUAI6::Tricker> TrickerAPI::GetSelfInfo() const
{
return logic.TrickerGetSelfInfo();


+ 82
- 0
CAPI/cpp/API/src/DebugAPI.cpp View File

@@ -206,6 +206,78 @@ std::future<bool> TrickerDebugAPI::UseSkill(int32_t skillID)
return result; });
}

std::future<bool> StudentDebugAPI::OpenDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.OpenDoor(); });
}

std::future<bool> TrickerDebugAPI::OpenDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.OpenDoor(); });
}

std::future<bool> StudentDebugAPI::CloseDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.CloseDoor(); });
}

std::future<bool> TrickerDebugAPI::CloseDoor()
{
return std::async(std::launch::async, [&]()
{ return logic.CloseDoor(); });
}

std::future<bool> StudentDebugAPI::SkipWindow()
{
return std::async(std::launch::async, [&]()
{ return logic.SkipWindow(); });
}

std::future<bool> TrickerDebugAPI::SkipWindow()
{
return std::async(std::launch::async, [&]()
{ return logic.SkipWindow(); });
}

std::future<bool> StudentDebugAPI::StartOpenGate()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenGate(); });
}

std::future<bool> TrickerDebugAPI::StartOpenGate()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenGate(); });
}

std::future<bool> StudentDebugAPI::StartOpenChest()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenChest(); });
}

std::future<bool> TrickerDebugAPI::StartOpenChest()
{
return std::async(std::launch::async, [&]()
{ return logic.StartOpenChest(); });
}

std::future<bool> StudentDebugAPI::EndAllAction()
{
return std::async(std::launch::async, [&]()
{ return logic.EndAllAction(); });
}

std::future<bool> TrickerDebugAPI::EndAllAction()
{
return std::async(std::launch::async, [&]()
{ return logic.EndAllAction(); });
}

std::future<bool> StudentDebugAPI::SendMessage(int64_t toID, std::string message)
{
logger->info("SendMessage: toID = {}, message = {}, called at {}ms", toID, message, Time::TimeSinceStart(startPoint));
@@ -403,6 +475,16 @@ std::future<bool> TrickerDebugAPI::Attack(double angleInRadian)
return result; });
}

std::future<bool> StudentDebugAPI::Attack(double angleInRadian)
{
logger->info("Attack: angleInRadian = {}, called at {}ms", angleInRadian, Time::TimeSinceStart(startPoint));
return std::async(std::launch::async, [=]()
{ auto result = logic.Attack(angleInRadian);
if (!result)
logger->warn("Attack: failed at {}ms", Time::TimeSinceStart(startPoint));
return result; });
}

std::shared_ptr<const THUAI6::Tricker> TrickerDebugAPI::GetSelfInfo() const
{
return logic.TrickerGetSelfInfo();


+ 36
- 0
CAPI/cpp/API/src/logic.cpp View File

@@ -147,6 +147,42 @@ bool Logic::Attack(double angle)
return pComm->Attack(angle, playerID);
}

bool Logic::OpenDoor()
{
logger->debug("Called OpenDoor");
return pComm->OpenDoor(playerID);
}

bool Logic::CloseDoor()
{
logger->debug("Called CloseDoor");
return pComm->CloseDoor(playerID);
}

bool Logic::SkipWindow()
{
logger->debug("Called SkipWindow");
return pComm->SkipWindow(playerID);
}

bool Logic::StartOpenGate()
{
logger->debug("Called StartOpenGate");
return pComm->StartOpenGate(playerID);
}

bool Logic::StartOpenChest()
{
logger->debug("Called StartOpenChest");
return pComm->StartOpenChest(playerID);
}

bool Logic::EndAllAction()
{
logger->debug("Called EndAllAction");
return pComm->EndAllAction(playerID);
}

bool Logic::WaitThread()
{
Update();


+ 5
- 0
README.md View File

@@ -1,10 +1,15 @@
# THUAI6

清华大学第六届人工智能挑战赛电子系赛道(原电子系第 24 届队式程序设计大赛 teamstyle24)

Gitee 镜像地址:[THUAI6: Gitee Mirror](https://gitee.com/eesast/THUAI6)

GitLink 镜像地址:[THUAI6: GitLink Mirror](https://www.gitlink.org.cn/EESAST/THUAI6)

项目主页:[THUAI6 Project Home Page](https://eesast.github.io/THUAI6)

关于本届及历届清华大学人工智能挑战赛与队式程序设计大赛的更多内容参见:[THUAI6 Github Wiki](https://github.com/eesast/THUAI6/wiki)

## 赛题背景

待定


+ 5
- 7
logic/ClientTest/Program.cs View File

@@ -12,12 +12,10 @@ namespace ClientTest
var client = new AvailableService.AvailableServiceClient(channel);
PlayerMsg playerInfo = new();
playerInfo.PlayerId = 0;
playerInfo.PlayerType = PlayerType.StudentPlayer;
playerInfo.StudentType = StudentType.NullStudentType;
var call = client.AddPlayer(playerInfo);
MoveMsg moveMsg = new();
moveMsg.PlayerId = 0;
moveMsg.PlayerType = PlayerType.StudentPlayer;
moveMsg.TimeInMilliseconds = 100;
moveMsg.Angle = 0;
while (true)
@@ -29,15 +27,15 @@ namespace ClientTest

while (await call.ResponseStream.MoveNext())
{
Console.WriteLine("hi");
var currentGameInfo = call.ResponseStream.Current;
if (playerInfo.PlayerType == PlayerType.StudentPlayer)
for (int i = 0; i < currentGameInfo.ObjMessage.Count; i++)
{
for (int i = 0; i < currentGameInfo.ObjMessage.Count; i++)
{
//Console.WriteLine($"Human is at ({currentGameInfo.StudentMessage[i].X}, {currentGameInfo.StudentMessage[i].Y})");
}
if (currentGameInfo.ObjMessage[i].MessageOfObjCase == MessageOfObj.MessageOfObjOneofCase.StudentMessage)
Console.WriteLine($"Human is at ({currentGameInfo.ObjMessage[i].StudentMessage.X}, {currentGameInfo.ObjMessage[i].StudentMessage.Y})");
}
}

}
}
}

+ 2
- 0
logic/GameClass/GameObj/Bullet/BombedBullet.cs View File

@@ -9,9 +9,11 @@ namespace GameClass.GameObj
public override bool IsRigid => false;
public long MappingID { get; }
public Bullet bulletHasBombed;

public BombedBullet(Bullet bullet) :
base(bullet.Position, bullet.Radius, GameObjType.BombedBullet)
{
this.place = bullet.Place;
this.bulletHasBombed = bullet;
this.MappingID = bullet.ID;
this.FacingDirection = bullet.FacingDirection;


+ 12
- 12
logic/GameClass/GameObj/Bullet/Bullet.Ghost.cs View File

@@ -6,8 +6,8 @@ namespace GameClass.GameObj
{
internal sealed class CommonAttackOfGhost : Bullet
{
public CommonAttackOfGhost(Character player, int radius = GameData.bulletRadius) :
base(player, radius)
public CommonAttackOfGhost(Character player, PlaceType placeType, XY pos, int radius = GameData.bulletRadius) :
base(player, radius, placeType, pos)
{
}
public override double BulletBombRange => 0;
@@ -28,8 +28,8 @@ namespace GameClass.GameObj
}
internal sealed class FlyingKnife : Bullet
{
public FlyingKnife(Character player, int radius = GameData.bulletRadius) :
base(player, radius)
public FlyingKnife(Character player, PlaceType placeType, XY pos, int radius = GameData.bulletRadius) :
base(player, radius, placeType, pos)
{
}
public override double BulletBombRange => 0;
@@ -51,8 +51,8 @@ namespace GameClass.GameObj

internal sealed class AtomBomb : Bullet
{
public AtomBomb(Character player, int radius = GameData.bulletRadius) :
base(player, radius)
public AtomBomb(Character player, PlaceType placeType, XY pos, int radius = GameData.bulletRadius) :
base(player, radius, placeType, pos)
{
}
public override double BulletBombRange => GameData.basicBulletBombRange / 3 * 7;
@@ -75,8 +75,8 @@ namespace GameClass.GameObj

internal sealed class OrdinaryBullet : Bullet // 1倍攻击范围,1倍攻击力,一倍速
{
public OrdinaryBullet(Character player, int radius = GameData.bulletRadius) :
base(player, radius)
public OrdinaryBullet(Character player, PlaceType placeType, XY pos, int radius = GameData.bulletRadius) :
base(player, radius, placeType, pos)
{
}
public override double BulletBombRange => GameData.basicBulletBombRange / 6 * 5;
@@ -98,8 +98,8 @@ namespace GameClass.GameObj

internal sealed class FastBullet : Bullet // 1倍攻击范围,0.2倍攻击力,2倍速
{
public FastBullet(Character player, int radius = GameData.bulletRadius) :
base(player, radius)
public FastBullet(Character player, PlaceType placeType, XY pos, int radius = GameData.bulletRadius) :
base(player, radius, placeType, pos)
{
}
public override double BulletBombRange => GameData.basicBulletBombRange / 4 * 2;
@@ -122,8 +122,8 @@ namespace GameClass.GameObj

internal sealed class LineBullet : Bullet // 直线爆炸,宽度1格,长度为2倍爆炸范围
{
public LineBullet(Character player, int radius = GameData.bulletRadius) :
base(player, radius)
public LineBullet(Character player, PlaceType placeType, XY pos, int radius = GameData.bulletRadius) :
base(player, radius, placeType, pos)
{
}
public override double BulletBombRange => GameData.basicBulletBombRange / 3 * 4;


+ 8
- 7
logic/GameClass/GameObj/Bullet/Bullet.cs View File

@@ -37,9 +37,10 @@ namespace GameClass.GameObj
return true;
return false;
}
public Bullet(Character player, int radius) :
base(player.Position, radius, GameObjType.Bullet)
public Bullet(Character player, int radius, PlaceType placeType, XY Position) :
base(Position, radius, GameObjType.Bullet)
{
this.place = placeType;
this.CanMove = true;
this.moveSpeed = this.Speed;
this.hasSpear = player.HasSpear;
@@ -52,22 +53,22 @@ namespace GameClass.GameObj

public static class BulletFactory
{
public static Bullet? GetBullet(Character character)
public static Bullet? GetBullet(Character character, PlaceType place, XY pos)
{
Bullet? newBullet = null;
switch (character.BulletOfPlayer)
{
case BulletType.AtomBomb:
newBullet = new AtomBomb(character);
newBullet = new AtomBomb(character, place, pos);
break;
case BulletType.LineBullet:
newBullet = new LineBullet(character);
newBullet = new LineBullet(character, place, pos);
break;
case BulletType.FastBullet:
newBullet = new FastBullet(character);
newBullet = new FastBullet(character, place, pos);
break;
case BulletType.OrdinaryBullet:
newBullet = new OrdinaryBullet(character);
newBullet = new OrdinaryBullet(character, place, pos);
break;
default:
break;


logic/GameClass/GameObj/Character/Character.SkillManager.cs → logic/GameClass/GameObj/Character/Character.Skill.cs View File

@@ -53,16 +53,13 @@ namespace GameClass.GameObj

public bool IsGhost()
{
return this.CharacterType switch
{
CharacterType.Assassin => true,
_ => false,
};
return GameData.IsGhost(CharacterType);
}

protected Character(XY initPos, int initRadius, CharacterType characterType) :
base(initPos, initRadius, GameObjType.Character)
{
this.place = PlaceType.Null;
this.CanMove = true;
this.score = 0;
this.propInventory = null;
@@ -88,6 +85,8 @@ namespace GameClass.GameObj
this.bulletNum = maxBulletNum;
this.bulletOfPlayer = Occupation.InitBullet;
this.OriBulletOfPlayer = Occupation.InitBullet;
this.concealment = Occupation.Concealment;
this.alertnessRadius = Occupation.AlertnessRadius;
this.characterType = characterType;

foreach (var activeSkill in this.Occupation.ListOfIActiveSkill)

+ 61
- 28
logic/GameClass/GameObj/Character/Character.cs View File

@@ -8,7 +8,7 @@ using System.Threading;

namespace GameClass.GameObj
{
public partial class Character : GameObj, ICharacter // 负责人LHR摆烂终了
public partial class Character : Moveable, ICharacter // 负责人LHR摆烂终了
{
private readonly object beAttackedLock = new();

@@ -175,24 +175,56 @@ namespace GameClass.GameObj
}
}

private Dictionary<BgmType, double> bgmDictionary = new();
public Dictionary<BgmType, double> BgmDictionary
{
get => bgmDictionary;
set
{
lock (gameObjLock)
{
bgmDictionary = value;
}
}
}

private int alertnessRadius;
public int AlertnessRadius
{
get => alertnessRadius;
set
{
lock (gameObjLock)
{
alertnessRadius = value;
}
}
}

private double concealment;
public double Concealment
{
get => concealment;
set
{
lock (gameObjLock)
{
concealment = value;
}
}
}

/// <summary>
/// 进行一次远程攻击
/// 进行一次攻击
/// </summary>
/// <param name="posOffset">子弹初始位置偏差值</param>
/// <returns>攻击操作发出的子弹</returns>
public Bullet? RemoteAttack(XY posOffset)
public Bullet? Attack(XY pos, PlaceType place)
{
if (TrySubBulletNum())
return ProduceOneBullet(this.Position + posOffset);
return BulletFactory.GetBullet(this, place, pos);
else
return null;
}
protected Bullet? ProduceOneBullet(XY initPos)
{
var newBullet = BulletFactory.GetBullet(this);
newBullet?.SetPosition(initPos);
return newBullet;
}

/// <summary>
/// 尝试将子弹数量减1
@@ -452,24 +484,24 @@ namespace GameClass.GameObj
}
}
#endregion
public override void Reset() // 要加锁吗?
{
lock (gameObjLock)
{
// _ = AddDeathCount();
base.Reset();
this.MoveSpeed = OrgMoveSpeed;
HP = MaxHp;
PropInventory = null;
BulletOfPlayer = OriBulletOfPlayer;
lock (gameObjLock)
bulletNum = maxBulletNum;
/* public override void Reset() // 要加锁吗?
{
lock (gameObjLock)
{
// _ = AddDeathCount();
base.Reset();
this.MoveSpeed = OrgMoveSpeed;
HP = MaxHp;
PropInventory = null;
BulletOfPlayer = OriBulletOfPlayer;
lock (gameObjLock)
bulletNum = maxBulletNum;

buffManager.ClearAll();
IsInvisible = false;
this.Vampire = this.OriVampire;
}
}
buffManager.ClearAll();
IsInvisible = false;
this.Vampire = this.OriVampire;
}
}*/
public void Die(PlayerStateType playerStateType)
{
lock (gameObjLock)
@@ -478,6 +510,7 @@ namespace GameClass.GameObj
CanMove = false;
IsResetting = true;
Position = GameData.PosWhoDie;
place = PlaceType.Grass;
}
}



+ 10
- 84
logic/GameClass/GameObj/GameObj.cs View File

@@ -7,13 +7,10 @@ namespace GameClass.GameObj
/// <summary>
/// 一切游戏元素的总基类,与THUAI4不同,继承IMoveable接口(出于一切物体其实都是可运动的指导思想)——LHR
/// </summary>
public abstract class GameObj : IMoveable
public abstract class GameObj : IGameObj
{
protected readonly object gameObjLock = new();
/// <summary>
/// 可移动物体专用锁
/// </summary>
public object MoveLock => gameObjLock;
public object GameLock => gameObjLock;

protected readonly XY birthPos;

@@ -38,7 +35,9 @@ namespace GameClass.GameObj
}
}
}
public abstract bool IsRigid { get; }

protected PlaceType place;
public PlaceType Place { get => place; }

private XY facingDirection = new(1, 0);
public XY FacingDirection
@@ -50,6 +49,9 @@ namespace GameClass.GameObj
facingDirection = value;
}
}

public abstract bool IsRigid { get; }

public abstract ShapeType Shape { get; }

private bool canMove;
@@ -65,19 +67,6 @@ namespace GameClass.GameObj
}
}

private bool isMoving;
public bool IsMoving
{
get => isMoving;
set
{
lock (gameObjLock)
{
isMoving = value;
}
}
}

private bool isResetting;
public bool IsResetting
{
@@ -90,77 +79,14 @@ namespace GameClass.GameObj
}
}
}
public bool IsAvailable => !IsMoving && CanMove && !IsResetting; // 是否能接收指令
public int Radius { get; }

protected int moveSpeed;
/// <summary>
/// 移动速度
/// </summary>
public int MoveSpeed
{
get => moveSpeed;
set
{
lock (gameObjLock)
{
moveSpeed = value;
}
}
}
/// <summary>
/// 原初移动速度
/// </summary>
public int OrgMoveSpeed { get; protected set; }
public int Radius { get; }

// 移动,改变坐标
public long Move(XY moveVec)
{
lock (gameObjLock)
{
FacingDirection = moveVec;
this.Position += moveVec;
}
return (long)(moveVec * moveVec);
}
/// <summary>
/// 设置位置
/// </summary>
/// <param name="newpos">新位置</param>
public void SetPosition(XY newpos)
{
Position = newpos;
}
/// <summary>
/// 设置移动速度
/// </summary>
/// <param name="newMoveSpeed">新速度</param>
public void SetMoveSpeed(int newMoveSpeed)
{
MoveSpeed = newMoveSpeed;
}
/// <summary>
/// 复活时数据重置
/// </summary>
public virtual void Reset()
{
lock (gameObjLock)
{
facingDirection = new XY(1, 0);
isMoving = false;
canMove = false;
isResetting = true;
this.position = birthPos;
}
}
/// <summary>
/// 为了使IgnoreCollide多态化并使GameObj能不报错地继承IMoveable
/// 在xfgg点播下设计了这个抽象辅助方法,在具体类中实现
/// </summary>
/// <returns> 依具体类及该方法参数而定,默认为false </returns>
protected virtual bool IgnoreCollideExecutor(IGameObj targetObj) => false;

bool IMoveable.IgnoreCollide(IGameObj targetObj) => IgnoreCollideExecutor(targetObj);
bool IGameObj.IgnoreCollide(IGameObj targetObj) => IgnoreCollideExecutor(targetObj);
public GameObj(XY initPos, int initRadius, GameObjType initType)
{
this.Position = this.birthPos = initPos;


+ 1
- 0
logic/GameClass/GameObj/Map/Doorway.cs View File

@@ -11,6 +11,7 @@ namespace GameClass.GameObj
public Doorway(XY initPos) :
base(initPos, GameData.numOfPosGridPerCell / 2, GameObjType.Doorway)
{
this.place = PlaceType.Doorway;
this.CanMove = false;
}
public override bool IsRigid => true;


+ 1
- 0
logic/GameClass/GameObj/Map/EmergencyExit.cs View File

@@ -11,6 +11,7 @@ namespace GameClass.GameObj
public EmergencyExit(XY initPos) :
base(initPos, GameData.numOfPosGridPerCell / 2, GameObjType.EmergencyExit)
{
this.place = PlaceType.EmergencyExit;
this.CanMove = false;
}
public override bool IsRigid => true;


+ 1
- 0
logic/GameClass/GameObj/Map/Generator.cs View File

@@ -10,6 +10,7 @@ namespace GameClass.GameObj
public Generator(XY initPos) :
base(initPos, GameData.numOfPosGridPerCell / 2, GameObjType.Generator)
{
this.place = PlaceType.Generator;
this.CanMove = false;
}
public override bool IsRigid => true;


+ 7
- 6
logic/GameClass/GameObj/Map/Map.cs View File

@@ -18,12 +18,13 @@ namespace GameClass.GameObj
private Dictionary<GameObjType, ReaderWriterLockSlim> gameObjLockDict;
public Dictionary<GameObjType, ReaderWriterLockSlim> GameObjLockDict => gameObjLockDict;

public readonly uint[,] ProtoGameMap;
public PlaceType GetPlaceType(GameObj obj)
public readonly uint[,] protoGameMap;
public uint[,] ProtoGameMap => protoGameMap;
public PlaceType GetPlaceType(IGameObj obj)
{
try
{
return (PlaceType)ProtoGameMap[obj.Position.x / GameData.numOfPosGridPerCell, obj.Position.y / GameData.numOfPosGridPerCell];
return (PlaceType)protoGameMap[obj.Position.x / GameData.numOfPosGridPerCell, obj.Position.y / GameData.numOfPosGridPerCell];
}
catch
{
@@ -35,7 +36,7 @@ namespace GameClass.GameObj
{
try
{
return (PlaceType)ProtoGameMap[pos.x / GameData.numOfPosGridPerCell, pos.y / GameData.numOfPosGridPerCell];
return (PlaceType)protoGameMap[pos.x / GameData.numOfPosGridPerCell, pos.y / GameData.numOfPosGridPerCell];
}
catch
{
@@ -120,8 +121,8 @@ namespace GameClass.GameObj
}
}

ProtoGameMap = new uint[mapResource.GetLength(0), mapResource.GetLength(1)];
Array.Copy(mapResource, ProtoGameMap, mapResource.Length);
protoGameMap = new uint[mapResource.GetLength(0), mapResource.GetLength(1)];
Array.Copy(mapResource, protoGameMap, mapResource.Length);

birthPointList = new Dictionary<uint, XY>(GameData.numOfBirthPoint);



+ 1
- 0
logic/GameClass/GameObj/Map/Wall.cs View File

@@ -10,6 +10,7 @@ namespace GameClass.GameObj
public Wall(XY initPos) :
base(initPos, GameData.numOfPosGridPerCell / 2, GameObjType.Wall)
{
this.place = PlaceType.Wall;
this.CanMove = false;
}
public override bool IsRigid => true;


+ 103
- 0
logic/GameClass/GameObj/Moveable.cs View File

@@ -0,0 +1,103 @@
using Preparation.Interface;
using Preparation.Utility;
using System.Threading;

namespace GameClass.GameObj
{
/// <summary>
/// 一切游戏元素的总基类,与THUAI4不同,继承IMoveable接口(出于一切物体其实都是可运动的指导思想)——LHR
/// </summary>
public abstract class Moveable : GameObj, IMoveable
{
protected readonly object moveObjLock = new();
public object MoveLock => moveObjLock;

private bool isMoving;
public bool IsMoving
{
get => isMoving;
set
{
lock (gameObjLock)
{
isMoving = value;
}
}
}

public bool IsAvailable => !IsMoving && CanMove && !IsResetting; // 是否能接收指令

protected int moveSpeed;
/// <summary>
/// 移动速度
/// </summary>
public int MoveSpeed
{
get => moveSpeed;
set
{
lock (gameObjLock)
{
moveSpeed = value;
}
}
}
/// <summary>
/// 原初移动速度
/// </summary>
public int OrgMoveSpeed { get; protected set; }

// 移动,改变坐标
public long MovingSetPos(XY moveVec, PlaceType place)
{
lock (gameObjLock)
{
FacingDirection = moveVec;
this.Position += moveVec;
this.place = place;
}
return moveVec * moveVec;
}

public void ReSetPos(XY pos, PlaceType place)
{
lock (gameObjLock)
{
this.Position = pos;
this.place = place;
}
}

/// <summary>
/// 设置移动速度
/// </summary>
/// <param name="newMoveSpeed">新速度</param>
public void SetMoveSpeed(int newMoveSpeed)
{
MoveSpeed = newMoveSpeed;
}
/* /// <summary>
/// 复活时数据重置
/// </summary>
public virtual void Reset(PlaceType place)
{
lock (gameObjLock)
{
this.FacingDirection = new XY(1, 0);
isMoving = false;
CanMove = false;
IsResetting = true;
this.Position = birthPos;
this.Place= place;
}
}*/
/// <summary>
/// 为了使IgnoreCollide多态化并使GameObj能不报错地继承IMoveable
/// 在xfgg点播下设计了这个抽象辅助方法,在具体类中实现
/// </summary>
/// <returns> 依具体类及该方法参数而定,默认为false </returns>
public Moveable(XY initPos, int initRadius, GameObjType initType) : base(initPos, initRadius, initType)
{
}
}
}

+ 1
- 1
logic/GameClass/GameObj/ObjOfCharacter.cs View File

@@ -6,7 +6,7 @@ namespace GameClass.GameObj
/// <summary>
/// 所有物,具有主人(Parent)(特定玩家)属性的对象
/// </summary>
public abstract class ObjOfCharacter : GameObj, IObjOfCharacter
public abstract class ObjOfCharacter : Moveable, IObjOfCharacter
{
private ICharacter? parent = null; // 主人
public ICharacter? Parent


+ 1
- 0
logic/GameClass/GameObj/OutOfBoundBlock.cs View File

@@ -11,6 +11,7 @@ namespace GameClass.GameObj
public OutOfBoundBlock(XY initPos) :
base(initPos, int.MaxValue, GameObjType.OutOfBoundBlock)
{
this.place = PlaceType.Wall;
this.CanMove = false;
}



+ 1
- 0
logic/GameClass/GameObj/PickedProp.cs View File

@@ -17,6 +17,7 @@ namespace GameClass.GameObj
public PickedProp(Prop prop) :
base(prop.Position, prop.Radius, GameObjType.PickedProp)
{
this.place = prop.Place;
this.PropHasPicked = prop;
this.MappingID = prop.ID;
}


+ 8
- 13
logic/GameClass/GameObj/Prop.cs View File

@@ -21,16 +21,13 @@ namespace GameClass.GameObj

public abstract PropType GetPropType();

public Prop(XY initPos, int radius = GameData.PropRadius) :
public Prop(XY initPos, PlaceType place, int radius = GameData.PropRadius) :
base(initPos, radius, GameObjType.Prop)
{
this.place = place;
this.CanMove = false;
this.moveSpeed = GameData.PropMoveSpeed;
}
public void SetNewPos(XY pos)
{
this.Position = pos;
}
}


@@ -48,8 +45,8 @@ namespace GameClass.GameObj
/// </summary>
public sealed class AddSpeed : Prop
{
public AddSpeed(XY initPos) :
base(initPos)
public AddSpeed(XY initPos, PlaceType placeType) :
base(initPos, placeType)
{
}
public override PropType GetPropType() => PropType.addSpeed;
@@ -59,8 +56,8 @@ namespace GameClass.GameObj
/// </summary>
public sealed class AddLIFE : Prop
{
public AddLIFE(XY initPos) :
base(initPos)
public AddLIFE(XY initPos, PlaceType placeType) :
base(initPos, placeType)
{
}
public override PropType GetPropType() => PropType.addLIFE;
@@ -70,8 +67,7 @@ namespace GameClass.GameObj
/// </summary>
public sealed class Shield : Prop
{
public Shield(XY initPos) :
base(initPos)
public Shield(XY initPos, PlaceType placeType) : base(initPos, placeType)
{
}
public override PropType GetPropType() => PropType.Shield;
@@ -81,8 +77,7 @@ namespace GameClass.GameObj
/// </summary>
public sealed class Spear : Prop
{
public Spear(XY initPos) :
base(initPos)
public Spear(XY initPos, PlaceType placeType) : base(initPos, placeType)
{
}
public override PropType GetPropType() => PropType.Spear;


+ 32
- 12
logic/GameEngine/MoveEngine.cs View File

@@ -20,6 +20,18 @@ namespace GameEngine

private readonly ITimer gameTimer;
private readonly Action<IMoveable> EndMove;
public readonly uint[,] ProtoGameMap;
public PlaceType GetPlaceType(XY Position)
{
try
{
return (PlaceType)ProtoGameMap[Position.x / GameData.numOfPosGridPerCell, Position.y / GameData.numOfPosGridPerCell];
}
catch
{
return PlaceType.Null;
}
}
private readonly CollisionChecker collisionChecker;
private readonly Func<IMoveable, IGameObj, XY, AfterCollision> OnCollision;
/// <summary>
@@ -34,6 +46,7 @@ namespace GameEngine
Action<IMoveable> EndMove
)
{
this.ProtoGameMap = gameMap.ProtoGameMap;
this.gameTimer = gameMap.Timer;
this.EndMove = EndMove;
this.OnCollision = OnCollision;
@@ -50,9 +63,11 @@ namespace GameEngine

/*由于四周是墙,所以人物永远不可能与越界方块碰撞*/
XY nextPos = obj.Position + moveVec;
double maxLen = collisionChecker.FindMax(obj, nextPos, moveVec);
maxLen = Math.Min(maxLen, obj.MoveSpeed / GameData.numOfStepPerSecond);
_ = obj.Move(new XY(moveVec.Angle(), maxLen));
//double maxLen
_ = collisionChecker.FindMax(obj, nextPos, moveVec);
//maxLen = Math.Min(maxLen, obj.MoveSpeed / GameData.numOfStepPerSecond);

obj.MovingSetPos(moveVec, GetPlaceType(nextPos));
}

public void MoveObj(IMoveable obj, int moveTime, double direction)
@@ -69,7 +84,8 @@ namespace GameEngine
obj.IsMoving = true;

double moveVecLength = 0.0;
double deltaLen = moveVecLength - Math.Sqrt(obj.Move(new XY(direction, moveVecLength))); // 转向,并用deltaLen存储行走的误差
XY res = new XY(direction, moveVecLength);
double deltaLen = moveVecLength - Math.Sqrt(obj.MovingSetPos(res, GetPlaceType(obj.Position + res))); // 转向,并用deltaLen存储行走的误差
IGameObj? collisionObj = null;
bool isDestroyed = false;
new FrameRateTaskExecutor<int>(
@@ -77,17 +93,18 @@ namespace GameEngine
() =>
{
moveVecLength = obj.MoveSpeed / GameData.numOfStepPerSecond;
XY res = new XY(direction, moveVecLength);

// 越界情况处理:如果越界,则与越界方块碰撞
bool flag; // 循环标志
do
{
flag = false;
collisionObj = collisionChecker.CheckCollision(obj, new XY(direction, moveVecLength));
collisionObj = collisionChecker.CheckCollision(obj, res);
if (collisionObj == null)
break;

switch (OnCollision(obj, collisionObj, new XY(direction, moveVecLength)))
switch (OnCollision(obj, collisionObj, res))
{
case AfterCollision.ContinueCheck:
flag = true;
@@ -97,13 +114,14 @@ namespace GameEngine
isDestroyed = true;
return false;
case AfterCollision.MoveMax:
MoveMax(obj, new XY(direction, moveVecLength));
MoveMax(obj, res);
moveVecLength = 0;
res = new XY(direction, moveVecLength);
break;
}
} while (flag);

deltaLen += moveVecLength - Math.Sqrt(obj.Move(new XY(direction, moveVecLength)));
deltaLen += moveVecLength - Math.Sqrt(obj.MovingSetPos(res, GetPlaceType(obj.Position + res)));

return true;
},
@@ -118,13 +136,14 @@ namespace GameEngine
if (!isDestroyed)
{
moveVecLength = deltaLen + leftTime * obj.MoveSpeed / GameData.numOfPosGridPerCell;
if ((collisionObj = collisionChecker.CheckCollision(obj, new XY(direction, moveVecLength))) == null)
XY res = new XY(direction, moveVecLength);
if ((collisionObj = collisionChecker.CheckCollision(obj, res)) == null)
{
obj.Move(new XY(direction, moveVecLength));
obj.MovingSetPos(res, GetPlaceType(obj.Position + res));
}
else
{
switch (OnCollision(obj, collisionObj, new XY(direction, moveVecLength)))
switch (OnCollision(obj, collisionObj, res))
{
case AfterCollision.ContinueCheck:
flag = true;
@@ -134,8 +153,9 @@ namespace GameEngine
isDestroyed = true;
break;
case AfterCollision.MoveMax:
MoveMax(obj, new XY(direction, moveVecLength));
MoveMax(obj, res);
moveVecLength = 0;
res = new XY(direction, moveVecLength);
break;
}
}


+ 3
- 2
logic/Gaming/ActionManager.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using GameClass.GameObj;
@@ -38,7 +39,7 @@ namespace Gaming

public bool Fix(Student player)// 自动检查有无发电机可修
{
if (player.PlayerState != PlayerStateType.Null || player.IsGhost())
if (player.IsGhost() || (player.PlayerState != PlayerStateType.Null && player.PlayerState != PlayerStateType.IsMoving))
return false;
Generator? generatorForFix = null;

@@ -72,7 +73,7 @@ namespace Gaming
loopCondition: () => player.PlayerState == PlayerStateType.IsFixing && gameMap.Timer.IsGaming && generatorForFix.DegreeOfFRepair < GameData.degreeOfFixedGenerator && GameData.ApproachToInteract(player.Position, generatorForFix.Position),
loopToDo: () =>
{
return !generatorForFix.Repair(player.FixSpeed * GameData.frameDuration);
generatorForFix.Repair(player.FixSpeed * GameData.frameDuration);
},
timeInterval: GameData.frameDuration,
finallyReturn: () => 0


+ 10
- 8
logic/Gaming/AttackManager.cs View File

@@ -38,7 +38,7 @@ namespace Gaming
);
}

public void BeAddictedToGame(Student player)
private void BeAddictedToGame(Student player)
{
new Thread
(() =>
@@ -68,11 +68,10 @@ namespace Gaming
{ IsBackground = true }.Start();
}

public void Die(Character player)
private void Die(Character player)
{

player.CanMove = false;
player.IsResetting = true;
player.Die(PlayerStateType.IsDeceased);
// gameMap.GameObjLockDict[GameObjType.Character].EnterWriteLock();
// try
//{
@@ -87,7 +86,8 @@ namespace Gaming
if (player.PropInventory != null) // 若角色原来有道具,则原始道具掉落在原地
{
dropProp = player.PropInventory;
dropProp.SetNewPos(GameData.GetCellCenterPos(player.Position.x / GameData.numOfPosGridPerCell, player.Position.y / GameData.numOfPosGridPerCell));
XY res = GameData.GetCellCenterPos(player.Position.x / GameData.numOfPosGridPerCell, player.Position.y / GameData.numOfPosGridPerCell);
dropProp.ReSetPos(res, gameMap.GetPlaceType(res));
}
gameMap.GameObjLockDict[GameObjType.Prop].EnterWriteLock();
try
@@ -298,12 +298,14 @@ namespace Gaming
if (player.PlayerState != PlayerStateType.Null || player.PlayerState != PlayerStateType.IsMoving)
return false;

Bullet? bullet = player.RemoteAttack(
new XY // 子弹紧贴人物生成。
XY res = new XY // 子弹紧贴人物生成。
(
(int)((player.Radius + BulletFactory.BulletRadius(player.BulletOfPlayer)) * Math.Cos(angle)),
(int)((player.Radius + BulletFactory.BulletRadius(player.BulletOfPlayer)) * Math.Sin(angle))
)
);

Bullet? bullet = player.Attack(
res, gameMap.GetPlaceType(res)
);
if (bullet.CastTime > 0)
{


+ 107
- 38
logic/Gaming/Game.cs View File

@@ -5,6 +5,7 @@ using Preparation.Utility;
using Timothy.FrameRateTask;
using Preparation.Interface;
using GameClass.GameObj;
using System.Numerics;

namespace Gaming
{
@@ -41,21 +42,14 @@ namespace Gaming
// Console.WriteLine($"x,y: {pos.x},{pos.y}");

Character newPlayer = (GameData.IsGhost(playerInitInfo.characterType)) ? new Ghost(pos, GameData.characterRadius, playerInitInfo.characterType) : new Student(pos, GameData.characterRadius, playerInitInfo.characterType);
gameMap.GameObjLockDict[GameObjType.Character].EnterWriteLock();
try
{
gameMap.GameObjDict[GameObjType.Character].Add(newPlayer);
}
finally
{
gameMap.GameObjLockDict[GameObjType.Character].ExitWriteLock();
}
gameMap.Add(newPlayer);

// Console.WriteLine($"GameObjDict[GameObjType.Character] length:{gameMap.GameObjDict[GameObjType.Character].Count}");
teamList[(int)playerInitInfo.teamID].AddPlayer(newPlayer);
newPlayer.TeamID = playerInitInfo.teamID;
newPlayer.PlayerID = playerInitInfo.playerID;
new Thread //人物装弹
#region 人物装弹
new Thread
(
() =>
{
@@ -63,19 +57,16 @@ namespace Gaming
Thread.Sleep(newPlayer.CD);
long lastTime = Environment.TickCount64;
new FrameRateTaskExecutor<int>(
loopCondition: () => gameMap.Timer.IsGaming,
loopCondition: () => gameMap.Timer.IsGaming && !newPlayer.IsResetting,
loopToDo: () =>
{
if (!newPlayer.IsResetting)
long nowTime = Environment.TickCount64;
if (newPlayer.BulletNum == newPlayer.MaxBulletNum)
lastTime = nowTime;
if (nowTime - lastTime >= newPlayer.CD)
{
long nowTime = Environment.TickCount64;
if (newPlayer.BulletNum == newPlayer.MaxBulletNum)
lastTime = nowTime;
if (nowTime - lastTime >= newPlayer.CD)
{
_ = newPlayer.TryAddBulletNum();
lastTime = nowTime;
}
_ = newPlayer.TryAddBulletNum();
lastTime = nowTime;
}
},
timeInterval: GameData.checkInterval,
@@ -93,6 +84,91 @@ namespace Gaming
}
)
{ IsBackground = true }.Start();
#endregion
#region BGM更新
new Thread
(
() =>
{
while (!gameMap.Timer.IsGaming)
Thread.Sleep((int)GameData.checkInterval);
new FrameRateTaskExecutor<int>(
loopCondition: () => gameMap.Timer.IsGaming && !newPlayer.IsResetting,
loopToDo: () =>
{
gameMap.GameObjLockDict[GameObjType.Character].EnterReadLock();
try
{
if (newPlayer.IsGhost())
{
double bgmVolume = 0;
foreach (Character person in gameMap.GameObjDict[GameObjType.Character])
{
if (!person.IsGhost() && XY.Distance(newPlayer.Position, person.Position) <= (newPlayer.AlertnessRadius / person.Concealment))
{
if ((double)newPlayer.AlertnessRadius / XY.Distance(newPlayer.Position, person.Position) > bgmVolume)
bgmVolume = newPlayer.AlertnessRadius / XY.Distance(newPlayer.Position, person.Position);
}
}
if (bgmVolume > 0)
newPlayer.BgmDictionary.Add(BgmType.StudentIsApproaching, bgmVolume);
}
else
{
foreach (Character person in gameMap.GameObjDict[GameObjType.Character])
{
if (person.IsGhost())
{
if (XY.Distance(newPlayer.Position, person.Position) <= (newPlayer.AlertnessRadius / person.Concealment))
newPlayer.BgmDictionary.Add(BgmType.GhostIsComing, (double)newPlayer.AlertnessRadius / XY.Distance(newPlayer.Position, person.Position));
break;
}
}
}
}
finally
{
gameMap.GameObjLockDict[GameObjType.Character].ExitReadLock();
}

gameMap.GameObjLockDict[GameObjType.Generator].EnterReadLock();
try
{
double bgmVolume = 0;
foreach (Generator generator in gameMap.GameObjDict[GameObjType.Generator])
{
if (XY.Distance(newPlayer.Position, generator.Position) <= newPlayer.AlertnessRadius)
{
if ((double)newPlayer.AlertnessRadius * generator.DegreeOfFRepair / GameData.degreeOfFixedGenerator / XY.Distance(newPlayer.Position, generator.Position) > bgmVolume)
bgmVolume = (double)newPlayer.AlertnessRadius * generator.DegreeOfFRepair / GameData.degreeOfFixedGenerator / XY.Distance(newPlayer.Position, generator.Position);
}
}
if (bgmVolume > 0)
newPlayer.BgmDictionary.Add(BgmType.StudentIsApproaching, bgmVolume);
}


finally
{
gameMap.GameObjLockDict[GameObjType.Generator].ExitReadLock();
}
},
timeInterval: GameData.checkInterval,
finallyReturn: () => 0
)
{
AllowTimeExceed = true/*,
MaxTolerantTimeExceedCount = 5,
TimeExceedAction = exceedTooMuch =>
{
if (exceedTooMuch) Console.WriteLine("The computer runs too slow that it cannot check the color below the player in time!");
}*/
}
.Start();
}
)
{ IsBackground = true }.Start();
#endregion

return newPlayer.ID;
}
@@ -100,28 +176,21 @@ namespace Gaming
{
if (gameMap.Timer.IsGaming)
return false;
gameMap.GameObjLockDict[GameObjType.Character].EnterReadLock();
try
{
foreach (Character player in gameMap.GameObjDict[GameObjType.Character])
{
player.CanMove = true;

player.AddShield(GameData.shieldTimeAtBirth);
}
}
finally
{
gameMap.GameObjLockDict[GameObjType.Character].ExitReadLock();
}

propManager.StartProducing();

// 开始游戏
if (!gameMap.Timer.StartGame(milliSeconds))
return false;
new Thread
(
() =>
{
if (!gameMap.Timer.StartGame(milliSeconds))
return;

EndGame(); // 游戏结束时要做的事
EndGame(); // 游戏结束时要做的事
}
)
{ IsBackground = true }.Start();

return true;
}


+ 10
- 9
logic/Gaming/PropManager.cs View File

@@ -110,7 +110,8 @@ namespace Gaming
if (player.PropInventory != null) // 若角色原来有道具,则原始道具掉落在原地
{
dropProp = player.PropInventory;
dropProp.SetNewPos(GameData.GetCellCenterPos(player.Position.x / GameData.numOfPosGridPerCell, player.Position.y / GameData.numOfPosGridPerCell));
XY res = GameData.GetCellCenterPos(player.Position.x / GameData.numOfPosGridPerCell, player.Position.y / GameData.numOfPosGridPerCell);
dropProp.ReSetPos(res, gameMap.GetPlaceType(res));
}
player.PropInventory = pickProp;
gameMap.GameObjLockDict[GameObjType.Prop].EnterWriteLock();
@@ -151,7 +152,7 @@ namespace Gaming
return;

prop.CanMove = true;
prop.SetNewPos(player.Position);
prop.ReSetPos(player.Position, gameMap.GetPlaceType(player.Position));
gameMap.GameObjLockDict[GameObjType.Prop].EnterWriteLock();
try
{
@@ -187,16 +188,16 @@ namespace Gaming
switch (r.Next(0, 4))
{
case 0:
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new AddLIFE(randPos));
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new AddLIFE(randPos, gameMap.GetPlaceType(randPos)));
break;
case 1:
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new AddSpeed(randPos));
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new AddSpeed(randPos, gameMap.GetPlaceType(randPos)));
break;
case 2:
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new Shield(randPos));
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new Shield(randPos, gameMap.GetPlaceType(randPos)));
break;
case 3:
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new Spear(randPos));
gameMap.GameObjDict[GameObjType.Prop].Add((Preparation.Interface.IGameObj)new Spear(randPos, gameMap.GetPlaceType(randPos)));
break;
default:
break;
@@ -230,11 +231,11 @@ namespace Gaming
}
);
availableCellForGenerateProp = new List<XY>();
for (int i = 0; i < gameMap.ProtoGameMap.GetLength(0); i++)
for (int i = 0; i < gameMap.protoGameMap.GetLength(0); i++)
{
for (int j = 0; j < gameMap.ProtoGameMap.GetLength(1); j++)
for (int j = 0; j < gameMap.protoGameMap.GetLength(1); j++)
{
if (gameMap.ProtoGameMap[i, j] == (int)PlaceType.Null)
if (gameMap.protoGameMap[i, j] == (int)PlaceType.Null)
{
availableCellForGenerateProp.Add(GameData.GetCellCenterPos(i, j));
}


+ 2
- 2
logic/Preparation/Interface/IGameObj.cs View File

@@ -7,13 +7,13 @@ namespace Preparation.Interface
public GameObjType Type { get; }
public long ID { get; }
public XY Position { get; } // if Square, Pos equals the center
public PlaceType Place { get; }
public XY FacingDirection { get; }
public bool IsRigid { get; }
public ShapeType Shape { get; }
public bool CanMove { get; set; }
public bool IsMoving { get; set; }
public bool IsResetting { get; set; } // reviving
public bool IsAvailable { get; }
public int Radius { get; } // if Square, Radius equals half length of one side
protected bool IgnoreCollide(IGameObj targetObj); // 忽略碰撞,在具体类中实现
}
}

+ 2
- 0
logic/Preparation/Interface/IMap.cs View File

@@ -12,6 +12,8 @@ namespace Preparation.Interface
Dictionary<GameObjType, IList<IGameObj>> GameObjDict { get; }
Dictionary<GameObjType, ReaderWriterLockSlim> GameObjLockDict { get; }

public uint[,] ProtoGameMap { get; }
public PlaceType GetPlaceType(IGameObj obj);
public bool IsOutOfBound(IGameObj obj);
public IOutOfBound GetOutOfBound(XY pos); // 返回新建的一个OutOfBound对象
}


+ 4
- 2
logic/Preparation/Interface/IMoveable.cs View File

@@ -7,8 +7,10 @@ namespace Preparation.Interface
{
object MoveLock { get; }
public int MoveSpeed { get; }
public long Move(XY moveVec);
protected bool IgnoreCollide(IGameObj targetObj); // 忽略碰撞,在具体类中实现
public bool IsMoving { get; set; }
public bool IsAvailable { get; }
public long MovingSetPos(XY moveVec, PlaceType place);
public void ReSetPos(XY pos, PlaceType place);
public bool WillCollideWith(IGameObj? targetObj, XY nextPos) // 检查下一位置是否会和目标物碰撞
{
if (targetObj == null)


+ 16
- 1
logic/Preparation/Interface/IOccupation.cs View File

@@ -12,6 +12,8 @@ namespace Preparation.Interface
public int MaxBulletNum { get; }
public List<ActiveSkillType> ListOfIActiveSkill { get; }
public List<PassiveSkillType> ListOfIPassiveSkill { get; }
public double Concealment { get; }
public int AlertnessRadius { get; }
}

public interface IGhost : IOccupation
@@ -41,6 +43,12 @@ namespace Preparation.Interface

public List<ActiveSkillType> ListOfIActiveSkill => new(new ActiveSkillType[] { ActiveSkillType.BecomeInvisible, ActiveSkillType.UseKnife });
public List<PassiveSkillType> ListOfIPassiveSkill => new(new PassiveSkillType[] { });

public double concealment = GameData.basicConcealment * 1.5;
public double Concealment => concealment;

public int alertnessRadius = (int)(GameData.basicAlertnessRadius * 1.3);
public int AlertnessRadius => alertnessRadius;
}
public class Athlete : IStudent
{
@@ -61,6 +69,13 @@ namespace Preparation.Interface
public List<ActiveSkillType> ListOfIActiveSkill => new(new ActiveSkillType[] { ActiveSkillType.BeginToCharge });
public List<PassiveSkillType> ListOfIPassiveSkill => new(new PassiveSkillType[] { });

public int FixSpeed => GameData.basicFixSpeed / 10 * 6;
public const int fixSpeed = GameData.basicFixSpeed / 10 * 6;
public int FixSpeed => fixSpeed;

public const double concealment = GameData.basicConcealment * 0.9;
public double Concealment => concealment;

public const int alertnessRadius = (int)(GameData.basicAlertnessRadius * 0.9);
public int AlertnessRadius => alertnessRadius;
}
}

+ 7
- 0
logic/Preparation/Utility/GameData.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Reflection.Metadata.Ecma335;
using System.Threading;

namespace Preparation.Utility
{
@@ -75,12 +76,15 @@ namespace Preparation.Utility
public const int basicMoveSpeed = 3800; // 基本移动速度,单位:s-1
public const int basicBulletMoveSpeed = 5400; // 基本子弹移动速度,单位:s-1
public const int characterMaxSpeed = 12000; // 最大速度
public const double basicConcealment = 1.0;
public const int basicAlertnessRadius = 30700;
public const int addScoreWhenKillOneLevelPlayer = 30; // 击杀一级角色获得的加分
public const int commonSkillCD = 30000; // 普通技能标准冷却时间
public const int commonSkillTime = 10000; // 普通技能标准持续时间
public const int bulletRadius = 200; // 默认子弹半径
public const int reviveTime = 30000; // 复活时间
public const int shieldTimeAtBirth = 3000; // 复活时的护盾时间

public static XY PosWhoDie = new XY(1, 1);

public static bool IsGhost(CharacterType characterType)
@@ -101,9 +105,12 @@ namespace Preparation.Utility
public const long GemProduceTime = 10000;
public const long PropProduceTime = 10000;
public const int PropDuration = 10000;
#endregion

#region 物体相关
public const int degreeOfFixedGenerator = 10300000;
#endregion

#region 游戏帧相关
public const long checkInterval = 50; // 检查位置标志、补充子弹的帧时长
#endregion


+ 6
- 2
logic/Server/CopyInfo.cs View File

@@ -32,6 +32,7 @@ namespace Server
{
MessageOfObj msg = new MessageOfObj();
if (player.IsGhost()) return null;
msg.StudentMessage = new();

msg.StudentMessage.X = player.Position.x;
msg.StudentMessage.Y = player.Position.y;
@@ -42,7 +43,7 @@ namespace Server
msg.StudentMessage.TimeUntilSkillAvailable.Add(keyValue.Value);
//msg.StudentMessage.StudentType; // 下面写
msg.StudentMessage.Guid = player.ID;
msg.StudentMessage.State = StudentState.NullStatus;
// msg.StudentMessage.State = player.PlayerState;
msg.StudentMessage.FailTime = 0;
msg.StudentMessage.EmoTime = 0;
msg.StudentMessage.PlayerId = 0;
@@ -114,6 +115,7 @@ namespace Server
{
MessageOfObj msg = new MessageOfObj();
if (!player.IsGhost()) return null;
msg.TrickerMessage = new();

msg.TrickerMessage.X = player.Position.x;
msg.TrickerMessage.Y = player.Position.y;
@@ -212,6 +214,7 @@ namespace Server
private static MessageOfObj Bullet(Bullet bullet)
{
MessageOfObj msg = new MessageOfObj();
msg.BulletMessage = new();
msg.BulletMessage.X = bullet.Position.x;
msg.BulletMessage.Y = bullet.Position.y;
//msg.BulletMessage.FacingDirection = bullet.FacingDirection; // XY转double?
@@ -263,6 +266,7 @@ namespace Server
private static MessageOfObj Prop(Prop prop)
{
MessageOfObj msg = new MessageOfObj();
msg.PropMessage = new();
//msg.PropMessage.Type = PropType.NullPropType; 下面写
msg.PropMessage.X = prop.Position.x;
msg.PropMessage.Y = prop.Position.y;
@@ -326,7 +330,7 @@ namespace Server
private static MessageOfObj BombedBullet(BombedBullet bombedBullet)
{
MessageOfObj msg = new MessageOfObj();
msg.BombedBulletMessage = new();
msg.BombedBulletMessage.X = bombedBullet.bulletHasBombed.Position.x;
msg.BombedBulletMessage.Y = bombedBullet.bulletHasBombed.Position.y;
//msg.BombedBulletMessage.FacingDirection = bombedBullet.FacingDirection; XY类型转double?


+ 48
- 31
logic/Server/GameServer.cs View File

@@ -32,7 +32,7 @@ namespace Server
protected readonly ArgumentOptions options;
protected readonly Game game;
private uint spectatorMinPlayerID = 2022;
private List<Tuple<PlayerType, uint>> spectatorList = new List<Tuple<PlayerType, uint>>();
private List<uint> spectatorList = new List<uint>();
public int TeamCount => options.TeamCount;
protected long[,] communicationToGameID; // 通信用的ID映射到游戏内的ID,[i,j]表示team:i,player:j的id。
private readonly object messageToAllClientsLock = new();
@@ -82,8 +82,8 @@ namespace Server
}
public void StartGame()
{
bool gameState = game.StartGame((int)options.GameTimeInSecond * 1000);
var waitHandle = new SemaphoreSlim(gameState == true ? 1 : 0); // 注意修改
game.StartGame((int)options.GameTimeInSecond * 1000);
Thread.Sleep(1);
new Thread(() =>
{
bool flag = true;
@@ -109,13 +109,6 @@ namespace Server
).Start();
})
{ IsBackground = true }.Start();
new Thread(() =>
{
waitHandle.Wait();
this.endGameSem.Release();
})
{ IsBackground = true }.Start();

}
public void WaitForEnd()
{
@@ -136,9 +129,11 @@ namespace Server
public void ReportGame(GameState gameState, bool requiredGaming = true)
{
var gameObjList = game.GetGameObj();
currentGameInfo = new();
lock (messageToAllClientsLock)
{
//currentGameInfo.MapMessage = (Messa(game.GameMap));
if (gameState == GameState.GameStart) currentGameInfo.MapMessage = MapMsg(game.GameMap.ProtoGameMap);
switch (gameState)
{
case GameState.GameRunning:
@@ -149,6 +144,7 @@ namespace Server
currentGameInfo.ObjMessage.Add(CopyInfo.Auto(gameObj));
}
currentGameInfo.GameState = gameState;
currentGameInfo.AllMessage = new(); // 还没写
mwr?.WriteOne(currentGameInfo);
break;
default:
@@ -167,25 +163,40 @@ namespace Server
}
}

private int PlayerTypeToTeamID(PlayerType playerType)
private int PlayerIDToTeamID(long playerID)
{
if (playerType == PlayerType.StudentPlayer) return 0;
if (playerType == PlayerType.TrickerPlayer) return 1;
if (0 <= playerID && playerID < options.PlayerCountPerTeam) return 0;
if (playerID == options.PlayerCountPerTeam) return 1;
return -1;
}
private uint GetBirthPointIdx(PlayerType playerType, long playerID) // 获取出生点位置
private uint GetBirthPointIdx(long playerID) // 获取出生点位置
{
return (uint)((PlayerTypeToTeamID(playerType) * options.PlayerCountPerTeam) + playerID + 1);
return (uint)(playerID + 1);
}
private bool ValidPlayerTypeAndPlayerID(PlayerType playerType, long playerID)
private bool ValidPlayerID(long playerID)
{
if (playerType == PlayerType.StudentPlayer && 0 <= playerID && playerID < options.PlayerCountPerTeam)
return true; // 人数待修改
if (playerType == PlayerType.TrickerPlayer && 0 <= playerID && playerID < options.PlayerCountPerTeam)
if (0 <= playerID && playerID < options.PlayerCountPerTeam + 1)
return true;
return false;
}
private MessageOfMap MapMsg(Map map)

private Protobuf.PlaceType IntToPlaceType(uint n)
{
switch (n)
{
case 0: return Protobuf.PlaceType.Land;
case 6: return Protobuf.PlaceType.Wall;
case 7: return Protobuf.PlaceType.Grass;
case 8: return Protobuf.PlaceType.Classroom;
case 9: return Protobuf.PlaceType.Gate;
case 10: return Protobuf.PlaceType.HiddenGate;
case 11: return Protobuf.PlaceType.Window;
case 12: return Protobuf.PlaceType.Door;
case 13: return Protobuf.PlaceType.Chest;
default: return Protobuf.PlaceType.NullPlaceType;
}
}
private MessageOfMap MapMsg(uint[,] map)
{
MessageOfMap msgOfMap = new MessageOfMap();
for (int i = 0; i < GameData.rows; i++)
@@ -193,7 +204,7 @@ namespace Server
msgOfMap.Row.Add(new MessageOfMap.Types.Row());
for (int j = 0; j < GameData.cols; j++)
{
//msgOfMap.Row[i].Col.Add((int)map.ProtoGameMap[i, j]); int转placetype
msgOfMap.Row[i].Col.Add(IntToPlaceType(map[i, j]));
}
}
return msgOfMap;
@@ -219,10 +230,10 @@ namespace Server
public override async Task AddPlayer(PlayerMsg request, IServerStreamWriter<MessageToClient> responseStream, ServerCallContext context)
{
Console.WriteLine($"AddPlayer: {request.PlayerId}");
if (request.PlayerId >= spectatorMinPlayerID && request.PlayerType == PlayerType.NullPlayerType)
if (request.PlayerId >= spectatorMinPlayerID)
{
// 观战模式
Tuple<PlayerType, uint> tp = new Tuple<PlayerType, uint>(request.PlayerType, (uint)request.PlayerId);
uint tp = (uint)request.PlayerId;
if (!spectatorList.Contains(tp))
{
spectatorList.Add(tp);
@@ -233,7 +244,7 @@ namespace Server

if (game.GameMap.Timer.IsGaming)
return;
if (!ValidPlayerTypeAndPlayerID(request.PlayerType, request.PlayerId)) //玩家id是否正确
if (!ValidPlayerID(request.PlayerId)) //玩家id是否正确
return;
//if (communicationToGameID[PlayerTypeToTeamID(request.PlayerType), request.PlayerId] != GameObj.invalidID) //是否已经添加了该玩家
//return;
@@ -242,15 +253,15 @@ namespace Server

lock (addPlayerLock)
{
Game.PlayerInitInfo playerInitInfo = new(GetBirthPointIdx(request.PlayerType, request.PlayerId), PlayerTypeToTeamID(request.PlayerType), request.PlayerId, characterType);
Game.PlayerInitInfo playerInitInfo = new(GetBirthPointIdx(request.PlayerId), PlayerIDToTeamID(request.PlayerId), request.PlayerId, characterType);
long newPlayerID = game.AddPlayer(playerInitInfo);
if (newPlayerID == GameObj.invalidID)
return;
communicationToGameID[PlayerTypeToTeamID(request.PlayerType), request.PlayerId] = newPlayerID;
communicationToGameID[PlayerIDToTeamID(request.PlayerId), request.PlayerId] = newPlayerID;
// 内容待修改
var temp = (new SemaphoreSlim(0, 1), new SemaphoreSlim(0, 1));
bool start = false;
Console.WriteLine($"PlayerType: {request.PlayerType} Id: {request.PlayerId} joins.");
Console.WriteLine($"Id: {request.PlayerId} joins.");
lock (semaDict)
{
semaDict.Add(request.PlayerId, temp);
@@ -270,14 +281,13 @@ namespace Server
if (currentGameInfo != null)
{
await responseStream.WriteAsync(currentGameInfo);
Console.WriteLine("Send!");
//Console.WriteLine("Send!");
}
semaDict[request.PlayerId].Item2.Release();
} while (game.GameMap.Timer.IsGaming);
}

public override Task<BoolRes> Attack(AttackMsg request, ServerCallContext context)

{
game.Attack(request.PlayerId, request.Angle);
BoolRes boolRes = new();
@@ -292,8 +302,10 @@ namespace Server

public override Task<MoveRes> Move(MoveMsg request, ServerCallContext context)
{
#if DEBUG
Console.WriteLine($"Move ID: {request.PlayerId}, TimeInMilliseconds: {request.TimeInMilliseconds}");
var gameID = communicationToGameID[PlayerTypeToTeamID(request.PlayerType), request.PlayerId];
#endif
var gameID = communicationToGameID[PlayerIDToTeamID(request.PlayerId), request.PlayerId];
game.MovePlayer(gameID, (int)request.TimeInMilliseconds, request.Angle);
// 之后game.MovePlayer可能改为bool类型
MoveRes moveRes = new();
@@ -336,7 +348,12 @@ namespace Server
}
public override Task<BoolRes> StartLearning(IDMsg request, ServerCallContext context)
{
return base.StartLearning(request, context);
#if DEBUG
Console.WriteLine($"StartLearning ID: {request.PlayerId}");
#endif
BoolRes boolRes = new();
boolRes.ActSuccess = game.Fix(request.PlayerId);
return Task.FromResult(boolRes);
}

public GameServer(ArgumentOptions options)


+ 1
- 1
logic/Server/Program.cs View File

@@ -34,7 +34,7 @@ namespace Server
Grpc.Core.Server server = new Grpc.Core.Server(new[] { new ChannelOption(ChannelOptions.SoReuseport, 0) })
{
Services = { AvailableService.BindService(gameServer) },
Ports = { new ServerPort("127.0.0.1", 8888, ServerCredentials.Insecure) }
Ports = { new ServerPort("0.0.0.0", 8888, ServerCredentials.Insecure) }
};
server.Start();



+ 12
- 13
logic/规则Logic.md View File

@@ -1,7 +1,7 @@
# 规则Logic

## 说明
- 版本V1.003
- 版本V2.0
- 该规则直接服务于Sever,并非选手版本
- *斜体表示Logic底层尚未(完全)实现*
- []表示待决定
@@ -23,13 +23,11 @@
- 底层实现中的属性,不代表界面全部都需要展示,也可能需要额外展示信息
- 只展示外部需要的属性,部分属性被省略

### Bgm
- *double bgmVolume*
- *BgmType bgmType*
对于枚举类BgmType
1. *不详的感觉:监管者进入(求生者的警戒半径/监管者的隐蔽度)时,求生者收到;监管者距离求生者越近,Bgm音量越大。bgmVolume=(警戒半径/二者距离)*
2. *期待搞事的感觉:求生者进入(监管者的警戒半径/求生者的隐蔽度)时,监管者收到;监管者距离求生者越近,Bgm音量2.越大。bgmVolume=(警戒半径/二者距离)*
3. *修理电机的声音: 警戒半径内有电机正在被修理时,全员收到;bgmVolume=(警戒半径/二者距离)*电机修理程度/10300000*
### BgmType
- 枚举类BgmType
1. 不详的感觉:监管者进入(求生者的警戒半径/监管者的隐蔽度)时,求生者收到;监管者距离求生者越近,Bgm音量越大。bgmVolume=(警戒半径/二者距离)
2. 期待搞事的感觉:求生者进入(监管者的警戒半径/求生者的隐蔽度)时,监管者收到;监管者距离求生者越近,Bgm音量越大。bgmVolume=(警戒半径/可被发觉的最近的求生者距离)
3. 修理电机的声音: 警戒半径内有电机正在被修理时收到;bgmVolume=(警戒半径*电机修理程度/二者距离)/10300000
~~~csharp
public enum BgmType
{
@@ -58,6 +56,7 @@

### 物体
- 位置
- 位置地形
- ID
- 类型
- 面向角度
@@ -95,20 +94,20 @@
IsClimbingThroughWindows = 15,
}
~~~
- *Bgm(数组)*
- *Bgm(字典)*
- 得分
- ~~回血率/原始回血率~~
- 当前子弹类型
- 原始子弹类型
- 持有道具*(最多三个)(数组)*
- 持有道具 *(最多三个)(列表)*
- 是否隐身
- 队伍ID
- 玩家ID
- 当前Buff
- 职业类型
- 拥有的被动技能(数组)
- 拥有的主动技能(数组)
- 各个主动技能CD(数组)
- 拥有的被动技能(列表)
- 拥有的主动技能(列表)
- 各个主动技能CD(字典)
- *警戒半径*
- *double 隐蔽度*
- *翻墙速度*


Loading…
Cancel
Save