diff --git a/SECURITY.md b/SECURITY.md index 9a90b4038..1c85363a4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,4 @@ # Reporting a Vulnerability Contact a @Mod Developer on [Discord](https://discord.gg/c-rpg) or send an -email to [hello@c-rpg.eu](mailto:hello@c-rpg.eu). +email to [hello@namidaka.fr](mailto:hello@namidaka.fr). diff --git a/deploy/group_vars/all.yml b/deploy/group_vars/all.yml index 0f1b0e830..1b907de96 100644 --- a/deploy/group_vars/all.yml +++ b/deploy/group_vars/all.yml @@ -15,28 +15,24 @@ steam_api_key: !vault | epic_games_client_secret: !vault | $ANSIBLE_VAULT;1.1;AES256 - 30633266323035383864653436643932396433663739313636613531313732623064643462636161 - 6330386538663935316566343730343130663662396130610a643363666138316337356164303234 - 64343039343232346562363162653934626562376631316563613565396163356264356636313663 - 6236383639633361380a373535306563326538643638376463363630323930326235643837373165 - 35303537386133376264663038626164373164373666383866633332333163643835373734306532 - 6333353234666531373062623732323437393337633037303435 - + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 microsoft_client_secret: !vault | $ANSIBLE_VAULT;1.1;AES256 - 65336566656238663362376330353964333330303266663633616635326333363437653539333737 - 3439333666343931353632636334666466323431373964350a663864376464326137323932313666 - 34373462353133626337353134333064346538326636336632393266306339616131353835303133 - 6631663465353164350a633961323336356338373835613662376263333533613839313332306231 - 61626232356330653561626130313136393361373464353863663635353830396135643537303030 - 3563386630386336353465376130643462383132343063383131 - + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 crpg_game_server_api_secret: !vault | $ANSIBLE_VAULT;1.1;AES256 - 32326433613332653435346566636265643466643030643234383735333663383433663836656334 - 6263343930313465663436303339343262633130653864340a306439393363616435613732323437 - 31643762313537653230353636346363383430393765653933303665336339616264316138663038 - 3435316531373164640a616437393663666161653465303461306334623362343034386637356162 - 62353136616336613035623733333161333235366361643733336534343564643065 + 37326237663864613932666364383863373430303930353934326662313034306137306432303531 + 3830373066613633333165356230626163613233383937300a613736646331303631333536643163 + 37363938643363346530346166653733396264313036343238636536653837646564326132353936 + 3836613332616234360a383330306238386639653539333732663439353438306437643636313539 + 63383937363866363633653933623336613564616339303832303637633065393233 -crpg_api_domain_name: api.c-rpg.eu +crpg_api_domain_name: api.namidaka.fr diff --git a/deploy/group_vars/centralservers.yml b/deploy/group_vars/centralservers.yml index 0622125a0..e79a76cfb 100644 --- a/deploy/group_vars/centralservers.yml +++ b/deploy/group_vars/centralservers.yml @@ -2,11 +2,11 @@ ansible_sudo_pass: !vault | $ANSIBLE_VAULT;1.1;AES256 - 66323536646461616430383436323736313532653037363330346537336431666432366361653438 - 3332333437643932353834383338336363616635306362610a623032656162623466653239613437 - 39393333333334613531333930336562323637643135303338363365653837643634366131646163 - 6464396461366162300a383766366539633839393539346439653662313334363565333731353236 - 32393961366339366233393731383165373039346638313661323864353130363835 + 32326165616133323332366631633136663436323037633536643730363333313234356538303437 + 3036326266636130353664326263313165333036313034330a306537376130376636323062303863 + 30666637363636626236393832333163653162666337343137326339666232663434626532343539 + 3733376466396562650a633061383938306533316235366163356239356666656166396334663137 + 3439 crpg_reverse_proxy_service_name: crpg-reverse-proxy @@ -18,18 +18,18 @@ crpg_api_log_file: "/var/log/crpg/{{ crpg_api_service_name }}.log" crpg_ui_path: "/var/www/{{ crpg_domain_name }}" -crpg_domain_name: c-rpg.eu +crpg_domain_name: namidaka.fr crpg_db_service_name: crpg-db crpg_db: crpg crpg_db_user: crpg crpg_db_user_password: !vault | $ANSIBLE_VAULT;1.1;AES256 - 38303737393930623462303066366563363435616265376637656538383336373931333532326362 - 3364653166383839383735346266336661613566373265650a613637626666656635653134623165 - 38626562646536376361343732396539373764663132663262333566326339383239653065356138 - 3434356465313665390a643036346635343562646136653063646166366264396664313530326334 - 3163 + 36666130613663333435323864656237653566383531326463383433343831363038303732656131 + 3662643863663838396534643766616262626365346563650a623136393037343630313063653561 + 31363665626230626535323437623461323037656631303564323864643265616461613963333263 + 6338623462376663340a313138616563353562313738643862343533396563646431363639616563 + 32666435623364313032643564306565646131346430336134613032393033336436 datadog_db_user: dd-agent # Same username as Datadog agent so ident authentication can be used @@ -40,21 +40,19 @@ datadog_opentracing_version: 1.3.5 datadog_api_key: !vault | $ANSIBLE_VAULT;1.1;AES256 - 63336331303239353839636366306365663262326338343831616564396133386631616539386636 - 6436303036373834386662363836623266336536376365330a653532333535666436353230623332 - 34386236323063306233383730343034333632643532396532353962653335393863653139653336 - 3730653536623865660a356565313035373337313030613966646164623736623937626464373966 - 66663735636134326334333164343430616634623761326163316434306530363833626236633430 - 6537393033613533636438646634643235666337383030336130 + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 datadog_application_key: !vault | $ANSIBLE_VAULT;1.1;AES256 - 33323830303038316239393336613633303062613765303737343364663533393462396439353865 - 6232646534353762356632656436653863383661643961620a366337396531313032636665646331 - 65373062643737353431656238623264613239326364623939623566393631373035386364383363 - 6330363838333033390a366637373134626562373132333938613630653036313738636531666337 - 65346338633837646361386362393363336461383065346338353530623461643930326632326565 - 3239616561353364363634616233333934633865633262393137 + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 datadog_checks: nginx: @@ -192,21 +190,23 @@ patreon_access_token: !vault | afdian_access_token: !vault | $ANSIBLE_VAULT;1.1;AES256 - 39343663386135383239343237633366373636376337643731376638306231376163626238643838 - 3861323337343066623666373761643837313838313763340a333364353066303366643331626232 - 32616234373235663231393462636664396263623361646564616431663961616465383035383734 - 6665353666386261620a383036633833303030623837366330333938613265373563663639316639 - 62623365356162663637343239306532623761326334636132613339323736323062666332623766 - 6237373533363461343439303630356438376337343765386232 + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 github_access_token: !vault | $ANSIBLE_VAULT;1.1;AES256 - 37396232626466353962303235373030326335363866303362643234616461663830646530363565 - 3038346164373634623838313365313639376133313331390a663064393837323239326662393635 - 63373237626336616634313132343231303161633131653461393366303762393737653139613234 - 6166363334326462320a346637383135343666653636326439386138303536396562353232663439 - 63356232643334373836376131656334626661383363376132626461663130376233343038326138 - 32373966363261303935383465323834386131626138326235623363623661323430666238623730 - 62376330336330653737353036346237306539623233333731363463313531336433623338373332 - 63353162313034363131376130366565646565613533626631383337663834626331396235626131 - 3263 + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 +afdian_access_token: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 \ No newline at end of file diff --git a/deploy/group_vars/gameservers.yml b/deploy/group_vars/gameservers.yml index 5c95733b9..3e218d08d 100644 --- a/deploy/group_vars/gameservers.yml +++ b/deploy/group_vars/gameservers.yml @@ -7,3 +7,11 @@ bannerlord_server_bin_path: "{{ bannerlord_server_path }}/bin/Linux64_Shipping_S crpg_module_path: "{{ bannerlord_server_path }}/Modules/cRPG" crpg_game_server_id: "{{ ansible_hostname | regex_replace('^crpg(\\d\\d)$', '\\1') }}" + +steam_user_name: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 61353865653665623739633563633933626564623130343662643336363134343166313135663863 + 6565313738343862346631353435373432313062646330350a343836326233353239663739336331 + 34393762366338333333343635316562633162653932663138306432376431643363323762633430 + 6465316635666365610a336564363433646161363632646665363236383936636134613164323462 + 6434 diff --git a/deploy/host_vars/crpg02.c-rpg.eu.yml b/deploy/host_vars/crpg02.c-rpg.eu.yml deleted file mode 100644 index 8803f559a..000000000 --- a/deploy/host_vars/crpg02.c-rpg.eu.yml +++ /dev/null @@ -1,41 +0,0 @@ ---- - -crpg_region: na -crpg_happy_hours: "19:30-23:30,America/Chicago" - -crpg_game_server_instances: - - name: a - port: 7210 - game_type: battle - - name: b - port: 7211 - game_type: battle - - name: c - port: 7212 - game_type: duel - - name: d - port: 7213 - game_type: skirmish - - name: e - port: 7214 - game_type: skirmish - - name: f - port: 7215 - game_type: skirmish - -ansible_sudo_pass: !vault | - $ANSIBLE_VAULT;1.1;AES256 - 62333333613761653062663061326461396431626235356262653734346265333031336238666264 - 6166643630396662373962613963373965386635656662620a343432633436616266616131316532 - 61616536336566363066303233306638646266623538393834346564353566326638336334663835 - 3838666464313333370a306435626662656163323831383738623832353362336439376137376661 - 63356532326236333130393766616661313539353633633866633732363065336136 - -datadog_api_key: !vault | - $ANSIBLE_VAULT;1.1;AES256 - 66383861316562376630646164613537323232633532383337313635323463396562383335646231 - 3938313162343134653331363665666334613737666466660a616663363638373664306663323465 - 36353231653932353366316461623864386265653164383739333666616534643761313365363464 - 3239333432643236640a396338393331313637653533343435313032386365373931306663386339 - 30636139363632643135353562303034373539396364323332363765366437613164633635323062 - 3966626430373534636463613933303037373331326133366134 diff --git a/deploy/hosts.ini b/deploy/hosts.ini index c69222af6..34acf5b61 100644 --- a/deploy/hosts.ini +++ b/deploy/hosts.ini @@ -1,5 +1,5 @@ [centralservers] -c-rpg.eu +namidaka.fr [gameservers] -crpg03.c-rpg.eu +crpg03.namidaka.fr diff --git a/deploy/roles/geoip/vars/main.yml b/deploy/roles/geoip/vars/main.yml index 81c7ab0df..3b6cd1843 100644 --- a/deploy/roles/geoip/vars/main.yml +++ b/deploy/roles/geoip/vars/main.yml @@ -4,8 +4,9 @@ geoip_path: /usr/share/geoip maxmind_license_key: !vault | $ANSIBLE_VAULT;1.1;AES256 - 62613238363266343034336663373161383734336665303730663639373362306166623734303231 - 3636373937376336363763396635323562393861653939610a313565366163313731303562623430 - 63363564366133303330376462333361353164363731633261313931326162613238343635636266 - 3861643032326339660a646463363936333862303361626133333039303737306462366430316331 - 31373738653833333463373465333337646236393233623563336133616532396638 + 34633832353166333731333634653537373538613431313135333839353633323235653434326131 + 6165653032383033333839326365623731363663663765610a396265623834623331386438623330 + 62373939383566356565393763373637663962363531346164306164663363343435623037303935 + 3365333665343831660a383333353734653030383035613834353737623639336163616462306662 + 38643836316462336435316466653333326361343833643865393061656466353963623364356638 + 3664376239346334303934353962666161373731313965656632 diff --git a/deploy/roles/nginx/templates/nginx-api.c-rpg.eu.j2 b/deploy/roles/nginx/templates/nginx-api.namidaka.fr.j2 similarity index 100% rename from deploy/roles/nginx/templates/nginx-api.c-rpg.eu.j2 rename to deploy/roles/nginx/templates/nginx-api.namidaka.fr.j2 diff --git a/deploy/roles/nginx/templates/nginx-c-rpg.eu.j2 b/deploy/roles/nginx/templates/nginx-namidaka.fr.j2 similarity index 100% rename from deploy/roles/nginx/templates/nginx-c-rpg.eu.j2 rename to deploy/roles/nginx/templates/nginx-namidaka.fr.j2 diff --git a/src/Application/Captains/Commands/GetUserCaptainCommand.cs b/src/Application/Captains/Commands/GetUserCaptainCommand.cs new file mode 100644 index 000000000..e04207957 --- /dev/null +++ b/src/Application/Captains/Commands/GetUserCaptainCommand.cs @@ -0,0 +1,75 @@ +using AutoMapper; +using Crpg.Application.Captains.Models; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Domain.Entities.Captains; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Captains.Commands; + +/// +/// Get or create a user with its character. +/// +public record GetUserCaptainCommand : IMediatorRequest +{ + public int UserId { get; init; } + public class Validator : AbstractValidator + { + public Validator() + { + } + } + + internal class Handler : IMediatorRequestHandler + { + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task> Handle(GetUserCaptainCommand req, CancellationToken cancellationToken) + { + var user = await _db.Users + .Where(u => u.Id == req.UserId) + .Include(u => u.Captain) + .FirstOrDefaultAsync(cancellationToken); + if (user == null) + { + return new(CommonErrors.UserNotFound(req.UserId)); + } + + if (user.Captain == null) + { + user.Captain = CreateCaptain(req.UserId); + _db.Captains.Add(user.Captain); + + await _db.SaveChangesAsync(cancellationToken); + } + + var gameUser = _mapper.Map(user.Captain); + return new(gameUser); + } + + private Captain CreateCaptain(int userId) + { + Captain captain = new() + { + UserId = userId, + Formations = new List() + { + new() { Number = 1, Weight = 33 }, + new() { Number = 2, Weight = 33 }, + new() { Number = 3, Weight = 33 }, + }, + }; + + return captain; + } + } +} diff --git a/src/Application/Captains/Commands/UpdateFormationCharacterCommand.cs b/src/Application/Captains/Commands/UpdateFormationCharacterCommand.cs new file mode 100644 index 000000000..431a22105 --- /dev/null +++ b/src/Application/Captains/Commands/UpdateFormationCharacterCommand.cs @@ -0,0 +1,67 @@ +using AutoMapper; +using Crpg.Application.Captains.Models; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Captains.Commands; + +public record UpdateFormationCharacterCommand : IMediatorRequest +{ + public int? CharacterId { get; init; } + public int UserId { get; init; } + public int Number { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private readonly ICrpgDbContext _db; + + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task> Handle(UpdateFormationCharacterCommand req, CancellationToken cancellationToken) + { + var captain = await _db.Captains + .Where(c => c.UserId == req.UserId) + .Select(c => new + { + Formation = c.Formations.FirstOrDefault(f => f.Number == req.Number), + }) + .FirstOrDefaultAsync(cancellationToken); + + if (captain?.Formation == null) + { + return new(CommonErrors.CaptainFormationNotFound(req.Number, req.UserId)); + } + + if (req.CharacterId == null) { + captain.Formation.CharacterId = null; + await _db.SaveChangesAsync(cancellationToken); + return new(_mapper.Map(captain.Formation)); + } + + var character = await _db.Characters + .FirstOrDefaultAsync(c => c.UserId == req.UserId && c.Id == req.CharacterId.Value, cancellationToken); + if (character == null) + { + return new(CommonErrors.CharacterNotFound(req.CharacterId.Value, req.UserId)); + } + + if (character.ForTournament) + { + return new(CommonErrors.CharacterForTournament(req.CharacterId.Value)); + } + + captain.Formation.CharacterId = req.CharacterId.Value; + + await _db.SaveChangesAsync(cancellationToken); + return new(_mapper.Map(captain.Formation)); + } + } +} diff --git a/src/Application/Captains/Commands/UpdateFormationWeightCommand.cs b/src/Application/Captains/Commands/UpdateFormationWeightCommand.cs new file mode 100644 index 000000000..27e2e94b3 --- /dev/null +++ b/src/Application/Captains/Commands/UpdateFormationWeightCommand.cs @@ -0,0 +1,53 @@ +using AutoMapper; +using Crpg.Application.Captains.Models; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Captains.Commands; + +public record UpdateFormationWeightCommand : IMediatorRequest +{ + public int UserId { get; init; } + public int Weight { get; init; } + public int Number { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task> Handle(UpdateFormationWeightCommand req, CancellationToken cancellationToken) + { + if (req.Weight < -1 || req.Weight > 100) + { + return new(CommonErrors.CaptainFormationWeightNotInBounds(req.Number, req.UserId, req.Weight)); + } + + var captain = await _db.Captains + .Where(c => c.UserId == req.UserId) + .Select(c => new + { + Formation = c.Formations.FirstOrDefault(f => f.Number == req.Number), + }) + .FirstOrDefaultAsync(cancellationToken); + + if (captain?.Formation == null) + { + return new(CommonErrors.CaptainFormationNotFound(req.Number, req.UserId)); + } + + captain.Formation.Weight = req.Weight; + + await _db.SaveChangesAsync(cancellationToken); + return new(_mapper.Map(captain.Formation)); + } + } +} diff --git a/src/Application/Captains/Models/CaptainFormationViewModel.cs b/src/Application/Captains/Models/CaptainFormationViewModel.cs new file mode 100644 index 000000000..0e4edda5c --- /dev/null +++ b/src/Application/Captains/Models/CaptainFormationViewModel.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using Crpg.Application.Characters.Models; +using Crpg.Application.Common.Mappings; +using Crpg.Domain.Entities.Captains; +using Crpg.Domain.Entities.Characters; + +namespace Crpg.Application.Captains.Models; + +public record CaptainFormationViewModel : IMapFrom +{ + public int Number { get; set; } + public int? CharacterId { get; set; } + public GameCharacterViewModel? Character { get; set; } + public int Weight { get; set; } + +} diff --git a/src/Application/Captains/Models/CaptainViewModel.cs b/src/Application/Captains/Models/CaptainViewModel.cs new file mode 100644 index 000000000..03657b956 --- /dev/null +++ b/src/Application/Captains/Models/CaptainViewModel.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Crpg.Application.Common.Mappings; +using Crpg.Application.Users.Models; +using Crpg.Domain.Entities.Captains; + +namespace Crpg.Application.Captains.Models; + +public record CaptainViewModel : IMapFrom +{ + public IList Formations { get; set; } = new List(); + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Formations, opt => opt.MapFrom(src => src.Formations.OrderBy(f => f.Id))); + } +} diff --git a/src/Application/Captains/Queries/GetUserCaptainFormationQuery.cs b/src/Application/Captains/Queries/GetUserCaptainFormationQuery.cs new file mode 100644 index 000000000..ab85c3a12 --- /dev/null +++ b/src/Application/Captains/Queries/GetUserCaptainFormationQuery.cs @@ -0,0 +1,41 @@ +using AutoMapper; +using Crpg.Application.Captains.Models; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Captains.Queries; + +public record GetUserCaptainFormationQuery : IMediatorRequest +{ + public int UserId { get; init; } + public int Number { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task> Handle(GetUserCaptainFormationQuery req, CancellationToken cancellationToken) + { + var captain = await _db.Captains + .Where(c => c.UserId == req.UserId) + .Select(c => new + { + Formation = c.Formations.FirstOrDefault(f => f.Number == req.Number), + }) + .FirstOrDefaultAsync(cancellationToken); + + return captain == null + ? new(CommonErrors.CaptainFormationNotFound(req.Number, req.UserId)) + : new(_mapper.Map(captain.Formation)); + } + } +} diff --git a/src/Application/Captains/Queries/GetUserCaptainFormationsQuery.cs b/src/Application/Captains/Queries/GetUserCaptainFormationsQuery.cs new file mode 100644 index 000000000..7df1a4a55 --- /dev/null +++ b/src/Application/Captains/Queries/GetUserCaptainFormationsQuery.cs @@ -0,0 +1,41 @@ +using AutoMapper; +using Crpg.Application.Captains.Models; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace Crpg.Application.Captains.Queries; + +public record GetUserCaptainFormationsQuery : IMediatorRequest> +{ + public int UserId { get; init; } + + internal class Handler : IMediatorRequestHandler> + { + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task>> Handle(GetUserCaptainFormationsQuery req, CancellationToken cancellationToken) + { + var captain = await _db.Captains + .Where(c => c.UserId == req.UserId) + .Select(c => new + { + c.Formations, + }) + .FirstOrDefaultAsync(cancellationToken); + + return captain == null + ? new(CommonErrors.CaptainNotFound(req.UserId)) + : new(_mapper.Map>(captain.Formations)); + } + } +} diff --git a/src/Application/Captains/Queries/GetUserCaptainQuery.cs b/src/Application/Captains/Queries/GetUserCaptainQuery.cs new file mode 100644 index 000000000..ba018a765 --- /dev/null +++ b/src/Application/Captains/Queries/GetUserCaptainQuery.cs @@ -0,0 +1,37 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Crpg.Application.Captains.Models; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Captains.Queries; + +public record GetUserCaptainQuery : IMediatorRequest +{ + public int UserId { get; init; } + + internal class Handler : IMediatorRequestHandler + { + private readonly ICrpgDbContext _db; + private readonly IMapper _mapper; + + public Handler(ICrpgDbContext db, IMapper mapper) + { + _db = db; + _mapper = mapper; + } + + public async Task> Handle(GetUserCaptainQuery req, CancellationToken cancellationToken) + { + var captain = await _db.Captains + .Where(c => c.UserId == req.UserId) + .ProjectTo(_mapper.ConfigurationProvider) + .FirstOrDefaultAsync(cancellationToken); + return captain == null + ? new(CommonErrors.CaptainNotFound(req.UserId)) + : new(captain); + } + } +} diff --git a/src/Application/Common/Interfaces/ICrpgDbContext.cs b/src/Application/Common/Interfaces/ICrpgDbContext.cs index 6c50bbeb3..57b97cd0c 100644 --- a/src/Application/Common/Interfaces/ICrpgDbContext.cs +++ b/src/Application/Common/Interfaces/ICrpgDbContext.cs @@ -1,5 +1,6 @@ using Crpg.Domain.Entities.ActivityLogs; using Crpg.Domain.Entities.Battles; +using Crpg.Domain.Entities.Captains; using Crpg.Domain.Entities.Characters; using Crpg.Domain.Entities.Clans; using Crpg.Domain.Entities.GameServers; @@ -29,6 +30,7 @@ public interface ICrpgDbContext DbSet ClanArmoryItems { get; } DbSet ClanArmoryBorrowedItems { get; } DbSet ClanInvitations { get; } + DbSet Captains { get; } DbSet Parties { get; } DbSet Settlements { get; } DbSet SettlementItems { get; } diff --git a/src/Application/Common/Results/CommonErrors.cs b/src/Application/Common/Results/CommonErrors.cs index 6afcb7e6b..68ca3c88b 100644 --- a/src/Application/Common/Results/CommonErrors.cs +++ b/src/Application/Common/Results/CommonErrors.cs @@ -37,6 +37,24 @@ internal static class CommonErrors Detail = $"Battle with id '{battleId}' is too far to perform the requested action", }; + public static Error CaptainFormationNotFound(int captainFormationId, int userId) => new(ErrorType.NotFound, ErrorCode.CaptainFormationNotFound) + { + Title = "Formation was not found", + Detail = $"Formation with id '{captainFormationId}' for user with id '{userId}' was not found", + }; + + public static Error CaptainFormationWeightNotInBounds(int captainFormationId, int userId, int weight) => new(ErrorType.NotFound, ErrorCode.CaptainFormationWeightNotInBounds) + { + Title = "Weight was invalid", + Detail = $"Formation with id '{captainFormationId}' for user with id '{userId}' with weight '{weight}' was outside of bounds.", + }; + + public static Error CaptainNotFound(int userId) => new(ErrorType.NotFound, ErrorCode.CaptainNotFound) + { + Title = "Captain was not found", + Detail = $"Captain for user with id '{userId}' was not found", + }; + public static Error CharacterForTournament(int characterId) => new(ErrorType.Validation, ErrorCode.CharacterForTournament) { Title = "Character is for tournament", diff --git a/src/Application/Common/Results/ErrorCode.cs b/src/Application/Common/Results/ErrorCode.cs index 36ecb291d..cea79b774 100644 --- a/src/Application/Common/Results/ErrorCode.cs +++ b/src/Application/Common/Results/ErrorCode.cs @@ -10,6 +10,9 @@ public enum ErrorCode BattleInvalidPhase, BattleNotFound, BattleTooFar, + CaptainFormationNotFound, + CaptainFormationWeightNotInBounds, + CaptainNotFound, CharacterForTournament, CharacterForTournamentNotFound, CharacterGenerationRequirement, diff --git a/src/Application/Games/Commands/GetGameUserCommand.cs b/src/Application/Games/Commands/GetGameUserCommand.cs index 18c21fce6..50f8e7de9 100644 --- a/src/Application/Games/Commands/GetGameUserCommand.cs +++ b/src/Application/Games/Commands/GetGameUserCommand.cs @@ -1,10 +1,13 @@ -using AutoMapper; +using System.Threading; +using AutoMapper; +using Crpg.Application.Captains.Models; using Crpg.Application.Common.Interfaces; using Crpg.Application.Common.Mediator; using Crpg.Application.Common.Results; using Crpg.Application.Common.Services; using Crpg.Application.Games.Models; using Crpg.Domain.Entities; +using Crpg.Domain.Entities.Captains; using Crpg.Domain.Entities.Characters; using Crpg.Domain.Entities.Items; using Crpg.Domain.Entities.Limitations; @@ -158,6 +161,8 @@ public async Task> Handle(GetGameUserCommand req, Canc var user = await _db.Users .Include(u => u.ActiveCharacter) .Include(u => u.ClanMembership) + .Include(u => u.Captain) + .ThenInclude(c => c!.Formations) .FirstOrDefaultAsync(u => u.Platform == req.Platform && u.PlatformUserId == req.PlatformUserId, cancellationToken); @@ -246,6 +251,30 @@ await _db.Entry(user.ActiveCharacter) user.ActiveCharacter.Statistics = new List { statistic }; } + if (user.Captain == null) + { + user.Captain = CreateCaptain(user.Id); + + _db.Captains.Add(user.Captain); + await _db.SaveChangesAsync(cancellationToken); + } + else + { + // Iterate over the formations to load each Character's EquippedItems separately +/* foreach (var formation in user.Captain.Formations) + { + if (formation.Character != null) + { + var equippedItems = await _db.EquippedItems + .Where(ei => ei.CharacterId == formation.Character.Id) + .Include(ei => ei.UserItem) + .ToListAsync(cancellationToken); + + formation.Character.EquippedItems = equippedItems; + } + }*/ + } + var gameUser = _mapper.Map(user); gameUser.Restrictions = gameUser.Restrictions .Where(r => _dateTime.UtcNow < r.CreatedAt + r.Duration) @@ -371,5 +400,23 @@ private async Task> GiveUserRandomItemSet(User user, (string return equippedItems; } + + private Captain CreateCaptain(int userId) + { + Captain captain = new() + { + UserId = userId, + Formations = new List() + { + new() { Number = 1, Weight = 33 }, + new() { Number = 2, Weight = 33 }, + new() { Number = 3, Weight = 33 }, + }, + }; + + + + return captain; + } } } diff --git a/src/Application/Games/Commands/UpdateGameUsersCommand.cs b/src/Application/Games/Commands/UpdateGameUsersCommand.cs index ba022a98b..4d80c5826 100644 --- a/src/Application/Games/Commands/UpdateGameUsersCommand.cs +++ b/src/Application/Games/Commands/UpdateGameUsersCommand.cs @@ -112,13 +112,27 @@ private async Task> LoadCharacters(IList u.CharacterId).ToArray(); var charactersById = await _db.Characters .Include(c => c.User!.ClanMembership) + .Include(c => c.User!.Captain!.Formations) + .ThenInclude(f => f!.Character) + .Where(c => characterIds.Contains(c.Id)) .ToDictionaryAsync(c => c.Id, cancellationToken); - // Load items in a separate query to avoid cartesian explosion. The items will be automatically set - // to their respective character. + int[] primaryCharacterIds = charactersById.Keys.ToArray(); + + // Extract character IDs from formations + int[] formationCharacterIds = charactersById.Values + .Where(c => c.User!.Captain != null) + .SelectMany(c => c.User!.Captain!.Formations) + .Where(f => f.CharacterId.HasValue) + .Select(f => f.CharacterId!.Value) + .Distinct() + .ToArray(); + + int[] allCharacterIds = primaryCharacterIds.Concat(formationCharacterIds).Distinct().ToArray(); + await _db.EquippedItems - .Where(ei => characterIds.Contains(ei.CharacterId)) + .Where(ei => allCharacterIds.Contains(ei.CharacterId)) .Include(ei => ei.UserItem) .LoadAsync(cancellationToken); diff --git a/src/Application/Games/Models/GameUserViewModel.cs b/src/Application/Games/Models/GameUserViewModel.cs index ca9876dfe..2ffda1f2f 100644 --- a/src/Application/Games/Models/GameUserViewModel.cs +++ b/src/Application/Games/Models/GameUserViewModel.cs @@ -1,9 +1,11 @@ using AutoMapper; using Crpg.Application.Characters.Models; using Crpg.Application.Clans.Models; +using Crpg.Application.Captains.Models; using Crpg.Application.Common.Mappings; using Crpg.Application.Restrictions.Models; using Crpg.Domain.Entities; +using Crpg.Domain.Entities.Captains; using Crpg.Domain.Entities.Users; namespace Crpg.Application.Games.Models; @@ -23,6 +25,7 @@ public record GameUserViewModel : IMapFrom public GameCharacterViewModel Character { get; init; } = default!; public IList Restrictions { get; set; } = Array.Empty(); public GameClanMemberViewModel? ClanMembership { get; set; } + public CaptainViewModel? Captain { get; set; } = default!; public void Mapping(Profile profile) { diff --git a/src/Application/System/Commands/SeedDataCommand.cs b/src/Application/System/Commands/SeedDataCommand.cs index 44bf45a25..402b7bb16 100644 --- a/src/Application/System/Commands/SeedDataCommand.cs +++ b/src/Application/System/Commands/SeedDataCommand.cs @@ -6,6 +6,7 @@ using Crpg.Domain.Entities; using Crpg.Domain.Entities.ActivityLogs; using Crpg.Domain.Entities.Battles; +using Crpg.Domain.Entities.Captains; using Crpg.Domain.Entities.Characters; using Crpg.Domain.Entities.Clans; using Crpg.Domain.Entities.Items; @@ -941,7 +942,8 @@ private async Task AddDevelopmentData() Character[] newCharacters = { takeoCharacter0, takeoCharacter1, takeoCharacter2, namidakaCharacter0, orleCharacter0, orleCharacter1, orleCharacter2, droobCharacter0, - falcomCharacter0, victorhh888Character0, sellkaCharacter0, krogCharacter0, kadseCharacter0, noobAmphetamine0, baronCyborg0, + droobCharacter1, droobCharacter2, falcomCharacter0, victorhh888Character0, sellkaCharacter0, krogCharacter0, kadseCharacter0, + noobAmphetamine0, baronCyborg0, }; var existingCharacters = await _db.Characters.ToDictionaryAsync(c => c.Name); diff --git a/src/Domain/Entities/Captains/Captain.cs b/src/Domain/Entities/Captains/Captain.cs new file mode 100644 index 000000000..52a8d8555 --- /dev/null +++ b/src/Domain/Entities/Captains/Captain.cs @@ -0,0 +1,15 @@ +using Crpg.Domain.Common; +using Crpg.Domain.Entities.Users; + +namespace Crpg.Domain.Entities.Captains; + +/// +/// Represents a cRPG captain. +/// +public class Captain : AuditableEntity +{ + public int Id { get; set; } + public int UserId { get; set; } + public IList Formations { get; set; } = new List(); + public User User { get; set; } = default!; +} diff --git a/src/Domain/Entities/Captains/CaptainFormation.cs b/src/Domain/Entities/Captains/CaptainFormation.cs new file mode 100644 index 000000000..5cb48364c --- /dev/null +++ b/src/Domain/Entities/Captains/CaptainFormation.cs @@ -0,0 +1,29 @@ +using Crpg.Domain.Common; +using Crpg.Domain.Entities.Characters; + +namespace Crpg.Domain.Entities.Captains; + +/// +/// Represents a cRPG captain formation. +/// +public class CaptainFormation : AuditableEntity +{ + public int Id { get; set; } + /// + /// The number indentifies the formation slot of the captain. + /// + public int Number { get; set; } + public int CaptainId { get; set; } + /// + /// The characterId of the troops to spawn in a formation. + /// + public int? CharacterId { get; set; } + + /// + /// The weight is compared to other formations to determine the composition of an army. + /// + public int Weight { get; set; } + + public Captain Captain { get; set; } = default!; + public Character? Character { get; set; } +} diff --git a/src/Domain/Entities/GameServers/GameMode.cs b/src/Domain/Entities/GameServers/GameMode.cs index 20b3a25ca..1bdd38825 100644 --- a/src/Domain/Entities/GameServers/GameMode.cs +++ b/src/Domain/Entities/GameServers/GameMode.cs @@ -10,6 +10,7 @@ public enum GameMode CRPGSkirmish, CRPGTeamDeathmatch, CRPGUnknownGameMode, + CRPGCaptain, } public enum GameModeAlias @@ -20,5 +21,6 @@ public enum GameModeAlias D, E, F, + G, Z, } diff --git a/src/Domain/Entities/Users/User.cs b/src/Domain/Entities/Users/User.cs index da2a4ed3f..42e0b74c7 100644 --- a/src/Domain/Entities/Users/User.cs +++ b/src/Domain/Entities/Users/User.cs @@ -1,4 +1,5 @@ using Crpg.Domain.Common; +using Crpg.Domain.Entities.Captains; using Crpg.Domain.Entities.Characters; using Crpg.Domain.Entities.Clans; using Crpg.Domain.Entities.Items; @@ -52,4 +53,5 @@ public class User : AuditableEntity public IList Restrictions { get; set; } = new List(); public ClanMember? ClanMembership { get; set; } public Party? Party { get; set; } + public Captain? Captain { get; set; } = default!; } diff --git a/src/Module.Server/Api/Models/Captains/CrpgCaptain.cs b/src/Module.Server/Api/Models/Captains/CrpgCaptain.cs new file mode 100644 index 000000000..ca35877f6 --- /dev/null +++ b/src/Module.Server/Api/Models/Captains/CrpgCaptain.cs @@ -0,0 +1,7 @@ +namespace Crpg.Module.Api.Models.Captains; + +internal class CrpgCaptain +{ + public int Id { get; set; } + public IList Formations { get; set; } = new List(); +} diff --git a/src/Module.Server/Api/Models/Captains/CrpgCaptainFormation.cs b/src/Module.Server/Api/Models/Captains/CrpgCaptainFormation.cs new file mode 100644 index 000000000..11301637c --- /dev/null +++ b/src/Module.Server/Api/Models/Captains/CrpgCaptainFormation.cs @@ -0,0 +1,10 @@ +using Crpg.Module.Api.Models.Characters; + +namespace Crpg.Module.Api.Models.Captains; +internal class CrpgCaptainFormation +{ + public int Id { get; set; } + public int Number { get; set; } + public CrpgCharacter? Character { get; set; } = default!; + public int Weight { get; set; } +} diff --git a/src/Module.Server/Api/Models/Users/CrpgUser.cs b/src/Module.Server/Api/Models/Users/CrpgUser.cs index 59d92db86..5d78bd15b 100644 --- a/src/Module.Server/Api/Models/Users/CrpgUser.cs +++ b/src/Module.Server/Api/Models/Users/CrpgUser.cs @@ -1,4 +1,5 @@ -using Crpg.Module.Api.Models.Characters; +using Crpg.Module.Api.Models.Captains; +using Crpg.Module.Api.Models.Characters; using Crpg.Module.Api.Models.Clans; using Crpg.Module.Api.Models.Restrictions; @@ -18,4 +19,5 @@ internal class CrpgUser public CrpgCharacter Character { get; set; } = default!; public IList Restrictions { get; set; } = Array.Empty(); public CrpgClanMember? ClanMembership { get; set; } + public CrpgCaptain Captain { get; set; } = default!; } diff --git a/src/Module.Server/Common/CrpgCharacterBuilder.cs b/src/Module.Server/Common/CrpgCharacterBuilder.cs index 1986fbd92..f60c680ac 100644 --- a/src/Module.Server/Common/CrpgCharacterBuilder.cs +++ b/src/Module.Server/Common/CrpgCharacterBuilder.cs @@ -62,6 +62,18 @@ public static Equipment CreateCharacterEquipment(IList equippe return equipment; } + public static Equipment CreateBotCharacterEquipment(IList equippedItems) + { + Equipment equipment = new(); + foreach (var equippedItem in equippedItems) + { + var index = ItemSlotToIndex[equippedItem.Slot]; + AddBotEquipment(equipment, index, equippedItem.UserItem.ItemId); + } + + return equipment; + } + public static void AssignArmorsToTroopOrigin(CrpgBattleAgentOrigin origin, List items) { if (items == null) @@ -115,4 +127,33 @@ private static void AddEquipment(Equipment equipments, EquipmentIndex idx, strin EquipmentElement equipmentElement = new(itemObject); equipments.AddEquipmentToSlotWithoutAgent(idx, equipmentElement); } + + private static void AddBotEquipment(Equipment equipments, EquipmentIndex idx, string itemId) + { + var itemObject = MBObjectManager.Instance.GetObject(itemId); + if (itemObject == null) + { + Debug.Print($"Cannot equip unknown item '{itemId}'"); + return; + } + + if (!Equipment.IsItemFitsToSlot(idx, itemObject)) + { + Debug.Print($"Cannot equip item '{itemId} on slot {idx}"); + return; + } + + if (itemObject.ItemType == ItemObject.ItemTypeEnum.Bow) + { + itemObject = MBObjectManager.Instance.GetObject(itemId.Replace("crpg", "dtv")); + if (itemObject == null) + { + Debug.Print($"Cannot find appropriate bot item for '{itemId}'"); + return; + } + } + + EquipmentElement equipmentElement = new(itemObject); + equipments.AddEquipmentToSlotWithoutAgent(idx, equipmentElement); + } } diff --git a/src/Module.Server/Common/CrpgServerConfiguration.cs b/src/Module.Server/Common/CrpgServerConfiguration.cs index 412fcb982..6678383ec 100644 --- a/src/Module.Server/Common/CrpgServerConfiguration.cs +++ b/src/Module.Server/Common/CrpgServerConfiguration.cs @@ -28,6 +28,7 @@ public static void Init() public static float TeamBalancerClanGroupSizePenalty { get; private set; } = 0f; public static float ServerExperienceMultiplier { get; private set; } = 1.0f; public static int RewardTick { get; private set; } = 60; + public static int CaptainTotalBotCount { get; private set; } = 40; public static bool TeamBalanceOnce { get; private set; } public static bool FrozenBots { get; private set; } = false; public static Tuple? HappyHours { get; private set; } @@ -81,6 +82,23 @@ private static void SetRewardTick(string? rewardTickStr) Debug.Print($"Set reward tick to {rewardTick}"); } + [UsedImplicitly] + [ConsoleCommandMethod("crpg_captain_bot_count", "Sets the total number of bots to spawn in Captain gamemode.")] + private static void SetCaptainBotCount(string? botCountStr) + { + if (botCountStr == null + || !int.TryParse(botCountStr, out int botCount) + || botCount < 0 + || botCount > 1000) + { + Debug.Print($"Invalid bot count: {botCountStr}"); + return; + } + + CaptainTotalBotCount = botCount; + Debug.Print($"Set total captain bot count to {botCount}"); + } + [UsedImplicitly] [ConsoleCommandMethod("crpg_team_balance_once", "Sets if the team balancer should balance only after warmup.")] private static void SetTeamBalanceOnce(string? teamBalanceOnceStr) diff --git a/src/Module.Server/Common/CrpgSpawningBehaviorBase.cs b/src/Module.Server/Common/CrpgSpawningBehaviorBase.cs index b9cd07c5d..ef61f8a15 100644 --- a/src/Module.Server/Common/CrpgSpawningBehaviorBase.cs +++ b/src/Module.Server/Common/CrpgSpawningBehaviorBase.cs @@ -1,6 +1,8 @@ -using System.ComponentModel; +using Crpg.Module.Api.Models.Captains; using Crpg.Module.Api.Models.Characters; using Crpg.Module.Api.Models.Users; +using Microsoft.VisualBasic; +using NetworkMessages.FromServer; using TaleWorlds.Core; using TaleWorlds.Library; using TaleWorlds.MountAndBlade; @@ -31,7 +33,9 @@ internal abstract class CrpgSpawningBehaviorBase : SpawningBehaviorBase WeaponClass.ThrowingAxe, WeaponClass.ThrowingKnife, }; + private MultiplayerGameType gameMode = MultiplayerGameType.Battle; + public virtual MultiplayerGameType GameMode { get => gameMode; protected set => gameMode = value; } public CrpgSpawningBehaviorBase(CrpgConstants constants) { _constants = constants; @@ -42,6 +46,12 @@ public override bool AllowEarlyAgentVisualsDespawning(MissionPeer missionPeer) return false; } + public override void Initialize(SpawnComponent spawnComponent) + { + base.Initialize(spawnComponent); + base.OnAllAgentsFromPeerSpawnedFromVisuals += OnAllAgentsFromPeerSpawnedFromVisuals; + } + public override void RequestStartSpawnSession() { base.RequestStartSpawnSession(); @@ -62,7 +72,7 @@ protected override void SpawnAgents() { BasicCultureObject cultureTeam1 = MBObjectManager.Instance.GetObject(MultiplayerOptions.OptionType.CultureTeam1.GetStrValue()); BasicCultureObject cultureTeam2 = MBObjectManager.Instance.GetObject(MultiplayerOptions.OptionType.CultureTeam2.GetStrValue()); - + int p = 1; foreach (NetworkCommunicator networkPeer in GameNetwork.NetworkPeers) { MissionPeer missionPeer = networkPeer.GetComponent(); @@ -81,7 +91,7 @@ protected override void SpawnAgents() } BasicCultureObject teamCulture = missionPeer.Team == Mission.AttackerTeam ? cultureTeam1 : cultureTeam2; - var peerClass = MBObjectManager.Instance.GetObject("crpg_class_division"); + var peerClass = MBObjectManager.Instance.GetObject($"crpg_class_division_{p}"); // var character = CreateCharacter(crpgPeer.User.Character, _constants); var characterSkills = CrpgCharacterBuilder.CreateCharacterSkills(crpgPeer.User!.Character.Characteristics); var characterXml = peerClass.HeroCharacter; @@ -95,8 +105,6 @@ protected override void SpawnAgents() Expecting that a character always exist in xmls */ - // - bool hasMount = characterEquipment[EquipmentIndex.Horse].Item != null; bool firstSpawn = missionPeer.SpawnCountThisRound == 0; @@ -106,6 +114,19 @@ Expecting that a character always exist in xmls initialDirection.RotateCCW(MBRandom.RandomFloatRanged(-MathF.PI / 3f, MathF.PI / 3f)); var troopOrigin = new CrpgBattleAgentOrigin(characterXml, characterSkills); CrpgCharacterBuilder.AssignArmorsToTroopOrigin(troopOrigin, crpgPeer.User.Character.EquippedItems.ToList()); + Formation formation = missionPeer.ControlledFormation; + if (formation == null) + { + formation = missionPeer.Team.FormationsIncludingEmpty.First((Formation x) => x.PlayerOwner == null && x.CountOfUnits == 0); + formation.ContainsAgentVisuals = true; + if (string.IsNullOrEmpty(formation.BannerCode)) + { + formation.BannerCode = crpgPeer.Clan?.BannerKey ?? missionPeer.Peer.BannerCode; + } + } + + missionPeer.ControlledFormation = formation; + missionPeer.HasSpawnedAgentVisuals = true; AgentBuildData agentBuildData = new AgentBuildData(characterXml) .MissionPeer(missionPeer) .Equipment(characterEquipment) @@ -118,7 +139,8 @@ Expecting that a character always exist in xmls // Note that what is sent here doesn't matter since it's ignored by the client. .BodyProperties(characterXml.GetBodyPropertiesMin()) .InitialPosition(in spawnFrame.origin) - .InitialDirection(in initialDirection); + .InitialDirection(in initialDirection) + .Formation(formation); if (crpgPeer.Clan != null) { @@ -150,8 +172,55 @@ Expecting that a character always exist in xmls agent.WieldInitialWeapons(); } - missionPeer.HasSpawnedAgentVisuals = true; + if (IsRoundInProgress() && gameMode == MultiplayerGameType.Captain) + { + var peers = GameNetwork.NetworkPeers; + var teamRelevantPeers = + peers.Where(p => IsNetworkPeerRelevant(p) && p.GetComponent().Team == missionPeer.Team).ToList(); + float sumOfTeamEquipment = _teamSumOfEquipment[missionPeer.Team]; + float peerSumOfEquipment = ComputeEquipmentValue(crpgPeer); + int peerNumberOfBots = 0; + if (teamRelevantPeers.Count - 1 < 1) + { + peerNumberOfBots = _teamNumberOfBots[missionPeer.Team] - 1; + } + else + { + peerNumberOfBots = (int)(_teamNumberOfBots[missionPeer.Team] * (1 - peerSumOfEquipment / sumOfTeamEquipment) / + (float)(teamRelevantPeers.Count - 1)); + } + + Dictionary formationBotWeight = new(); + var captainFormations = crpgPeer.User.Captain.Formations.Where(f => f.Character != null); + if (captainFormations.Any()) + { + double totalWeight = captainFormations.Sum(cf => cf.Weight); + + foreach (CrpgCaptainFormation captainFormation in captainFormations) + { + double proportion = (double)(captainFormation.Weight / totalWeight); + formationBotWeight.Add(captainFormation.Number, proportion); + } + } + else + { + for (int i = 0; i < peerNumberOfBots; i++) + { + SpawnBotAgent(peerClass.StringId, agent.Team, missionPeer, p); + } + } + + foreach (KeyValuePair captainFormation in formationBotWeight) + { + for (int i = 0; i < (int)(captainFormation.Value * peerNumberOfBots); i++) + { + SpawnBotAgent(peerClass.StringId, agent.Team, missionPeer, p, captainFormation.Key); + } + } + + } + p++; // AgentVisualSpawnComponent.RemoveAgentVisuals(missionPeer, sync: true); } } @@ -165,7 +234,7 @@ private CrpgCharacterObject CreateCharacter(CrpgCharacter character, CrpgConstan return characterObject; } - protected Agent SpawnBotAgent(string classDivisionId, Team team) + protected Agent SpawnBotAgent(string classDivisionId, Team team, MissionPeer? peer = null, int peerId = 0, int formationId = 0) { var teamCulture = team.Side == BattleSideEnum.Attacker ? MBObjectManager.Instance.GetObject(MultiplayerOptions.OptionType.CultureTeam1.GetStrValue()) @@ -195,6 +264,52 @@ protected Agent SpawnBotAgent(string classDivisionId, Team team) ? teamCulture.Color2 : teamCulture.ClothAlternativeColor2); + if (peer != null) + { + var crpgPeer = peer.GetComponent(); + if (crpgPeer != null && crpgPeer?.User != null) + { + var formation = GetFormationFromPeer(peer, formationId); + + var formationCharacter = formation?.Character ?? crpgPeer.User.Character; + string characterClass = formation != null ? $"crpg_class_division_{peerId}_{formation.Number}" : $"crpg_class_division_{peerId}"; + botClass = MultiplayerClassDivisions + .GetMPHeroClasses() + .First(h => h.StringId == characterClass); + character = botClass.HeroCharacter; + agentBuildData = new AgentBuildData(character) + .Equipment(character.AllEquipments[MBRandom.RandomInt(character.AllEquipments.Count)]) + .TroopOrigin(new BasicBattleAgentOrigin(character)) + .EquipmentSeed(MissionLobbyComponent.GetRandomFaceSeedForCharacter(character)) + .Team(team) + .VisualsIndex(0) + .InitialPosition(in spawnFrame.origin) + .InitialDirection(in initialDirection) + .IsFemale(character.IsFemale) + .ClothingColor1( + team.Side == BattleSideEnum.Attacker ? teamCulture.Color : teamCulture.ClothAlternativeColor) + .ClothingColor2(team.Side == BattleSideEnum.Attacker + ? teamCulture.Color2 + : teamCulture.ClothAlternativeColor2); + var characterEquipment = CrpgCharacterBuilder.CreateBotCharacterEquipment(formationCharacter.EquippedItems); + var peerClass = MBObjectManager.Instance.GetObject(characterClass); + var characterSkills = CrpgCharacterBuilder.CreateCharacterSkills(formationCharacter.Characteristics); + var characterXml = peerClass.HeroCharacter; + var troopOrigin = new CrpgBattleAgentOrigin(characterXml, characterSkills); + agentBuildData.OwningMissionPeer(peer); + agentBuildData.Formation(peer.ControlledFormation); + agentBuildData.Equipment(characterEquipment); + agentBuildData.TroopOrigin(troopOrigin); + agentBuildData.Banner(new Banner(peer.Peer.BannerCode)); + + if (crpgPeer.Clan != null) + { + agentBuildData.ClothingColor1(crpgPeer.Clan.PrimaryColor); + agentBuildData.ClothingColor2(crpgPeer.Clan.SecondaryColor); + } + } + } + var bodyProperties = BodyProperties.GetRandomBodyProperties( character.Race, character.IsFemale, @@ -287,4 +402,44 @@ private void ResetSpawnTeams() } } } + + private new void OnAllAgentsFromPeerSpawnedFromVisuals(MissionPeer peer) + { + if (peer.ControlledFormation != null) + { + peer.ControlledFormation.OnFormationDispersed(); + peer.ControlledFormation.SetMovementOrder(MovementOrder.MovementOrderFollow(peer.ControlledAgent)); + NetworkCommunicator networkPeer = peer.GetNetworkPeer(); + if (peer.BotsUnderControlAlive != 0 || peer.BotsUnderControlTotal != 0) + { + GameNetwork.BeginBroadcastModuleEvent(); + GameNetwork.WriteMessage(new BotsControlledChange(networkPeer, peer.BotsUnderControlAlive, peer.BotsUnderControlTotal)); + GameNetwork.EndBroadcastModuleEvent(GameNetwork.EventBroadcastFlags.None, null); + Mission.GetMissionBehavior().OnBotsControlledChanged(peer, peer.BotsUnderControlAlive, peer.BotsUnderControlTotal); + } + + if (peer.Team == Mission.AttackerTeam) + { + Mission.NumOfFormationsSpawnedTeamOne++; + } + else + { + Mission.NumOfFormationsSpawnedTeamTwo++; + } + + GameNetwork.BeginBroadcastModuleEvent(); + GameNetwork.WriteMessage(new SetSpawnedFormationCount(Mission.NumOfFormationsSpawnedTeamOne, Mission.NumOfFormationsSpawnedTeamTwo)); + GameNetwork.EndBroadcastModuleEvent(GameNetwork.EventBroadcastFlags.None, null); + } + } + private CrpgCaptainFormation? GetFormationFromPeer(MissionPeer peer, int formationId) + { + var crpgPeer = peer.GetComponent(); + if (crpgPeer != null && crpgPeer.User != null) + { + return crpgPeer.User.Captain.Formations.Where(f => f.Number == formationId).FirstOrDefault(); + } + + return null; + } } diff --git a/src/Module.Server/Common/Models/CrpgAgentStatCalculateModel.cs b/src/Module.Server/Common/Models/CrpgAgentStatCalculateModel.cs index 824a825f3..11b0288d8 100644 --- a/src/Module.Server/Common/Models/CrpgAgentStatCalculateModel.cs +++ b/src/Module.Server/Common/Models/CrpgAgentStatCalculateModel.cs @@ -258,12 +258,12 @@ private void UpdateMountAgentStats(Agent agent, AgentDrivenProperties props) // if you're dividing by (str -3) private void UpdateHumanAgentStats(Agent agent, AgentDrivenProperties props) { - // Dirty hack, part of the work-around to have skills without spawning custom characters. This hack should be // be performed in InitializeHumanAgentStats but the MissionPeer is null there. if (GameNetwork.IsClientOrReplay) // Server-side the hacky AgentOrigin is directly passed to the AgentBuildData. { var crpgUser = agent.MissionPeer?.GetComponent()?.User; + var agentCharacterId = agent.Character.StringId; if (crpgUser != null && agent.Origin is not CrpgBattleAgentOrigin) { var characteristics = crpgUser.Character.Characteristics; @@ -271,6 +271,41 @@ private void UpdateHumanAgentStats(Agent agent, AgentDrivenProperties props) agent.Origin = new CrpgBattleAgentOrigin(agent.Origin?.Troop, mbSkills); InitializeAgentStats(agent, agent.SpawnEquipment, props, null!); } + else if (agentCharacterId.StartsWith("crpg_character")) + { + string[] parts = agentCharacterId.Split('_'); // Split the agentCharacterId to get the base ID and the formation ID + string baseId = string.Join("_", parts.Take(3)); // This reassembles the base ID (e.g., "crpg_character_1") + int? formationId = parts.Length > 3 ? int.Parse(parts[3]) : default(int?); // Extracts the formation ID + + var crpgNetworkPeers = GameNetwork.NetworkPeers.Where(p => + p.GetComponent() != null); + var ownerNetworkPeer = + crpgNetworkPeers.FirstOrDefault(p => p.ControlledAgent?.Character.StringId.Contains(baseId) ?? false); + if (ownerNetworkPeer?.GetComponent()?.User != null && agent.Origin is not CrpgBattleAgentOrigin) + { + var formation = formationId != null ? ownerNetworkPeer.GetComponent().User!.Captain?.Formations? + .FirstOrDefault(f => f?.Number == formationId) : null; + + if (formation != null) + { + var characteristics = formation.Character!.Characteristics; + + var mbSkills = CrpgCharacterBuilder.CreateCharacterSkills(characteristics); + agent.Origin = new CrpgBattleAgentOrigin(agent.Origin?.Troop, mbSkills); + InitializeAgentStats(agent, agent.SpawnEquipment, props, null!); + } + else + { + var characteristics = ownerNetworkPeer.GetComponent().User!.Character.Characteristics; + + var mbSkills = CrpgCharacterBuilder.CreateCharacterSkills(characteristics); + agent.Origin = new CrpgBattleAgentOrigin(agent.Origin?.Troop, mbSkills); + InitializeAgentStats(agent, agent.SpawnEquipment, props, null!); + } + + } + } + } MissionEquipment equipment = agent.Equipment; diff --git a/src/Module.Server/Common/Network/UpdateCrpgUser.cs b/src/Module.Server/Common/Network/UpdateCrpgUser.cs index 8a7296e07..24ab05862 100644 --- a/src/Module.Server/Common/Network/UpdateCrpgUser.cs +++ b/src/Module.Server/Common/Network/UpdateCrpgUser.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using Crpg.Module.Api.Models.Captains; using Crpg.Module.Api.Models.Characters; using Crpg.Module.Api.Models.Clans; using Crpg.Module.Api.Models.Items; @@ -50,6 +51,7 @@ private void WriteUserToPacket(CrpgUser user) { WriteCharacterToPacket(writer, user.Character); WriteClanMemberToPacket(writer, user.ClanMembership); + WriteCaptainToPacket(writer, user.Captain); } WriteByteArrayToPacket(stream.ToArray(), 0, (int)stream.Length); @@ -57,7 +59,7 @@ private void WriteUserToPacket(CrpgUser user) private CrpgUser ReadUserFromPacket(ref bool bufferReadValid) { - byte[] buffer = new byte[1024]; + byte[] buffer = new byte[10240]; int bufferLength = ReadByteArrayFromPacket(buffer, 0, buffer.Length, ref bufferReadValid); using MemoryStream stream = new(buffer, 0, bufferLength); @@ -66,15 +68,22 @@ private CrpgUser ReadUserFromPacket(ref bool bufferReadValid) var character = ReadCharacterFromPacket(reader); var clanMember = ReadClanMemberFromPacket(reader); + var captain = ReadCaptainFromPacket(reader); return new CrpgUser { Character = character, ClanMembership = clanMember, + Captain = captain, }; } - private void WriteCharacterToPacket(BinaryWriter writer, CrpgCharacter character) + private void WriteCharacterToPacket(BinaryWriter writer, CrpgCharacter? character) { + if (character == null) + { + return; + } + writer.Write(character.Generation); writer.Write(character.Level); writer.Write(character.Experience); @@ -209,4 +218,54 @@ private void WriteClanMemberToPacket(BinaryWriter writer, CrpgClanMember? clanMe int clanId = reader.ReadInt32(); return clanId != -1 ? new CrpgClanMember { ClanId = clanId } : null; } + + private void WriteCaptainToPacket(BinaryWriter writer, CrpgCaptain captain) + { + writer.Write(captain.Formations.Count); + foreach (CrpgCaptainFormation formation in captain.Formations) + { + WriteFormationToPacket(writer, formation); + } + } + + private CrpgCaptain ReadCaptainFromPacket(BinaryReader reader) + { + var formations = ReadFormationFromPacket(reader); + return new CrpgCaptain + { + Formations = formations, + }; + } + + private void WriteFormationToPacket(BinaryWriter writer, CrpgCaptainFormation formation) + { + writer.Write(formation.Number); + writer.Write(formation.Character != null); + if (formation.Character != null) + { + WriteCharacterToPacket(writer, formation.Character); + } + } + + private IList ReadFormationFromPacket(BinaryReader reader) + { + + int formationLength = reader.ReadInt32(); + + List formations = new(formationLength); + for (int i = 0; i < formationLength; i += 1) + { + int number = reader.ReadInt32(); + bool doesCharacterExist = reader.ReadBoolean(); + var character = doesCharacterExist ? ReadCharacterFromPacket(reader) : null; + + formations.Add(new CrpgCaptainFormation + { + Number = number, + Character = character, + }); + } + + return formations; + } } diff --git a/src/Module.Server/Common/TeamSelect/CrpgTeamSelectServerComponent.cs b/src/Module.Server/Common/TeamSelect/CrpgTeamSelectServerComponent.cs index 53626e63c..4ed2a70df 100644 --- a/src/Module.Server/Common/TeamSelect/CrpgTeamSelectServerComponent.cs +++ b/src/Module.Server/Common/TeamSelect/CrpgTeamSelectServerComponent.cs @@ -26,6 +26,7 @@ internal class CrpgTeamSelectServerComponent : MultiplayerTeamSelectComponent private readonly MultiplayerRoundController? _roundController; private readonly MatchBalancer _balancer; private readonly PeriodStatsHelper _periodStatsHelper; + private readonly MultiplayerGameType _gameType; /// /// Players waiting to be assigned to a team when the cRPG balancer is enabled. @@ -34,7 +35,7 @@ internal class CrpgTeamSelectServerComponent : MultiplayerTeamSelectComponent private readonly Dictionary _playerTeamsBeforeJoiningSpectator; - public CrpgTeamSelectServerComponent(MultiplayerWarmupComponent warmupComponent, MultiplayerRoundController? roundController) + public CrpgTeamSelectServerComponent(MultiplayerWarmupComponent warmupComponent, MultiplayerRoundController? roundController, MultiplayerGameType gameType) { _warmupComponent = warmupComponent; _roundController = roundController; @@ -42,6 +43,7 @@ public CrpgTeamSelectServerComponent(MultiplayerWarmupComponent warmupComponent, _periodStatsHelper = new PeriodStatsHelper(); _playersWaitingForTeam = new HashSet(); _playerTeamsBeforeJoiningSpectator = new Dictionary(); + _gameType = gameType; } public override void OnBehaviorInitialize() @@ -106,13 +108,13 @@ private bool HandleTeamChange(NetworkCommunicator peer, TeamChange message) else { var missionPeer = peer.GetComponent(); - if (missionPeer is { Team: null }) + if (missionPeer is { Team: null } && _gameType != MultiplayerGameType.Captain) { // If the player just connected to the server, auto-assign their team so they have a chance // to play the round. AutoAssignTeam(peer); } - else if (_playerTeamsBeforeJoiningSpectator.TryGetValue(peer.VirtualPlayer.Id, out var teamBeforeSpectator)) + else if (_playerTeamsBeforeJoiningSpectator.TryGetValue(peer.VirtualPlayer.Id, out var teamBeforeSpectator))// && _gameType != MultiplayerGameType.Captain) { ChangeTeamServer(peer, teamBeforeSpectator); _playerTeamsBeforeJoiningSpectator.Remove(peer.VirtualPlayer.Id); diff --git a/src/Module.Server/CrpgSubModule.cs b/src/Module.Server/CrpgSubModule.cs index ad07fe747..f0b02cbaf 100644 --- a/src/Module.Server/CrpgSubModule.cs +++ b/src/Module.Server/CrpgSubModule.cs @@ -62,8 +62,9 @@ protected override void OnSubModuleLoad() { base.OnSubModuleLoad(); _constants = LoadCrpgConstants(); - TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgBattleGameMode(_constants, isSkirmish: true)); - TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgBattleGameMode(_constants, isSkirmish: false)); + TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgBattleGameMode(_constants, MultiplayerGameType.Battle)); + TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgBattleGameMode(_constants, MultiplayerGameType.Skirmish)); + TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgBattleGameMode(_constants, MultiplayerGameType.Captain)); TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgConquestGameMode(_constants)); TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgSiegeGameMode(_constants)); TaleWorlds.MountAndBlade.Module.CurrentModule.AddMultiplayerGameMode(new CrpgTeamDeathmatchGameMode(_constants)); diff --git a/src/Module.Server/MapRotation/ds_config_crpg_battle.txt b/src/Module.Server/MapRotation/ds_config_crpg_battle.txt index c620d0e04..a9f2a57b0 100644 --- a/src/Module.Server/MapRotation/ds_config_crpg_battle.txt +++ b/src/Module.Server/MapRotation/ds_config_crpg_battle.txt @@ -1,7 +1,5 @@ -ServerName cRPG - Test Battle - c-rpg.eu 55 lol -GameType cRPGBattle -GamePassword yoyo -WelcomeMessage yoyoyooooo +ServerName cRPG - Test Captain's Mode - c-rpg.eu +GameType cRPGCaptain AllowPollsToKickPlayers True AllowPollsToChangeMaps True disable_culture_voting @@ -16,15 +14,17 @@ FriendlyFireDamageMeleeSelfPercent 0 FriendlyFireDamageRangedFriendPercent 50 FriendlyFireDamageRangedSelfPercent 0 MinNumberOfPlayersForMatchStart 0 -NumberOfBotsTeam1 20 -NumberOfBotsTeam2 20 +NumberOfBotsTeam1 0 +NumberOfBotsTeam2 0 +NumberOfBotsPerFormation 10 RoundTotal 10 MapTimeLimit 8 -RoundTimeLimit 60 +RoundTimeLimit 600 WarmupTimeLimit 1 crpg_happy_hours 19:00-23:00,Central European Standard Time start_game set_automated_battle_count -1 enable_automated_battle_switching -crpg_frozen_bots True +crpg_frozen_bots False crpg_apply_harmony_patches +crpg_captain_bot_count 40 diff --git a/src/Module.Server/MapRotation/ds_config_crpg_battle_maps.txt b/src/Module.Server/MapRotation/ds_config_crpg_battle_maps.txt index f345ce18a..7ec4238f9 100644 --- a/src/Module.Server/MapRotation/ds_config_crpg_battle_maps.txt +++ b/src/Module.Server/MapRotation/ds_config_crpg_battle_maps.txt @@ -1,19 +1,2 @@ -crpg_battle_skolderby -mp_battle_map_002 -crpg_battle_tolstabeach -crpg_battle_drakenburg -crpg_battle_frosthaven -crpg_battle_thearena -crpg_battle_snowytown -crpg_battle_hadiiqa -crpg_battle_killington -mp_skirmish_map_003_skinc -crpg_battle_whitebridge -crpg_battle_sandpit -crpg_battle_lighthouse -crpg_battle_orionarena -crpg_battle_sanctuary -crpg_battle_verloren -crpg_battle_hexenbrennen -crpg_battle_icelake -crpg_battle_persia +crpg_battle_howlingmeadow +crpg_battle_howlingmeadow diff --git a/src/Module.Server/Modes/Battle/CrpgBattleClient.cs b/src/Module.Server/Modes/Battle/CrpgBattleClient.cs index c2b489cde..52c18f178 100644 --- a/src/Module.Server/Modes/Battle/CrpgBattleClient.cs +++ b/src/Module.Server/Modes/Battle/CrpgBattleClient.cs @@ -1,10 +1,12 @@ -using NetworkMessages.FromServer; +using System.Reflection; +using NetworkMessages.FromServer; using TaleWorlds.Core; using TaleWorlds.Engine; using TaleWorlds.Library; using TaleWorlds.Localization; using TaleWorlds.MountAndBlade; using TaleWorlds.MountAndBlade.MissionRepresentatives; +using TaleWorlds.MountAndBlade.Network.Messages; using TaleWorlds.MountAndBlade.Objects; using static TaleWorlds.MountAndBlade.MissionLobbyComponent; using MathF = TaleWorlds.Library.MathF; @@ -17,7 +19,7 @@ internal class CrpgBattleClient : MissionMultiplayerGameModeBaseClient, ICommand private const int BattleFlagUnlockTime = 45; private const int SkirmishFlagsRemovalTime = 120; - private readonly bool _isSkirmish; + private readonly MultiplayerGameType _gameType; private FlagCapturePoint[] _flags = Array.Empty(); private Team?[] _flagOwners = Array.Empty(); private bool _notifiedForFlagRemoval; @@ -30,17 +32,14 @@ internal class CrpgBattleClient : MissionMultiplayerGameModeBaseClient, ICommand public event Action? OnFlagNumberChangedEvent; public event Action? OnCapturePointOwnerChangedEvent; - public CrpgBattleClient(bool isSkirmish) + public CrpgBattleClient(MultiplayerGameType gameType) { - _isSkirmish = isSkirmish; + _gameType = gameType; } - public override bool IsGameModeUsingGold => false; public override bool IsGameModeTactical => _flags.Length != 0; public override bool IsGameModeUsingRoundCountdown => true; - public override MultiplayerGameType GameType => _isSkirmish - ? MultiplayerGameType.Skirmish - : MultiplayerGameType.Battle; + public override MultiplayerGameType GameType => _gameType; public override bool IsGameModeUsingCasualGold => false; public IEnumerable AllCapturePoints => _flags; public bool AreMoralesIndependent => false; @@ -49,6 +48,9 @@ public CrpgBattleClient(bool isSkirmish) public override void OnBehaviorInitialize() { + typeof(TaleWorlds.MountAndBlade.CompressionMission) + .GetField(nameof(TaleWorlds.MountAndBlade.CompressionMission.AgentOffsetCompressionInfo), BindingFlags.Public | BindingFlags.Static)? + .SetValue(null, new CompressionInfo.Integer(0, 16)); base.OnBehaviorInitialize(); RoundComponent.OnPreparationEnded += OnPreparationEnded; MissionNetworkComponent.OnMyClientSynchronized += OnMyClientSynchronized; @@ -203,9 +205,11 @@ protected override void AddRemoveMessageHandlers( base.AddRemoveMessageHandlers(registerer); if (GameNetwork.IsClientOrReplay) { + registerer.Register(HandleServerEventBotsControlledChangeEvent); registerer.Register(OnMoraleChange); registerer.Register(OnCapturePoint); - if (_isSkirmish) + registerer.Register(HandleServerEventFormationWipedMessage); + if (_gameType == MultiplayerGameType.Skirmish) { registerer.Register(OnFlagsRemovedSkirmish); } @@ -243,7 +247,7 @@ protected override int GetWarningTimer() private void NotifyForFlagManipulation() { - if (!_isSkirmish) + if (!(_gameType == MultiplayerGameType.Skirmish)) { TextObject textObject = new("{=nbOZ9BNQ}A flag will spawn in {TIMER} seconds.", new Dictionary { ["TIMER"] = 30 }); @@ -326,4 +330,17 @@ private void ResetFlags() _flags = Mission.Current.MissionObjects.FindAllWithType().ToArray(); _flagOwners = new Team[_flags.Length]; } + private void HandleServerEventBotsControlledChangeEvent(GameNetworkMessage baseMessage) + { + BotsControlledChange botsControlledChange = (BotsControlledChange)baseMessage; + MissionPeer component = botsControlledChange.Peer.GetComponent(); + this.OnBotsControlledChanged(component, botsControlledChange.AliveCount, botsControlledChange.TotalCount); + } + public void OnBotsControlledChanged(MissionPeer missionPeer, int botAliveCount, int botTotalCount) + { + missionPeer.BotsUnderControlAlive = botAliveCount; + } + private void HandleServerEventFormationWipedMessage(GameNetworkMessage baseMessage) + { + } } diff --git a/src/Module.Server/Modes/Battle/CrpgBattleGameMode.cs b/src/Module.Server/Modes/Battle/CrpgBattleGameMode.cs index e5edd38d2..4307c703a 100644 --- a/src/Module.Server/Modes/Battle/CrpgBattleGameMode.cs +++ b/src/Module.Server/Modes/Battle/CrpgBattleGameMode.cs @@ -2,6 +2,7 @@ using Crpg.Module.Common.Commander; using Crpg.Module.Common.HotConstants; using Crpg.Module.Common.TeamSelect; +using Crpg.Module.Modes.Captain; using Crpg.Module.Modes.Skirmish; using Crpg.Module.Modes.Warmup; using Crpg.Module.Notifications; @@ -18,7 +19,8 @@ using Crpg.Module.Api; using Crpg.Module.Common.ChatCommands; #else - +using TaleWorlds.MountAndBlade.Multiplayer.GauntletUI.Mission; +using TaleWorlds.MountAndBlade.View.MissionViews.Order; using Crpg.Module.GUI; using Crpg.Module.GUI.Commander; using Crpg.Module.GUI.EndOfRound; @@ -38,16 +40,20 @@ internal class CrpgBattleGameMode : MissionBasedMultiplayerGameMode { private const string BattleGameName = "cRPGBattle"; private const string SkirmishGameName = "cRPGSkirmish"; - + private const string CaptainGameName = "cRPGCaptain"; private static CrpgConstants _constants = default!; // Static so it's accessible from the views. - - private readonly bool _isSkirmish; - - public CrpgBattleGameMode(CrpgConstants constants, bool isSkirmish) - : base(isSkirmish ? SkirmishGameName : BattleGameName) + private MultiplayerGameType _gameType; + public CrpgBattleGameMode(CrpgConstants constants, MultiplayerGameType gameType) + : base(gameType switch + { + MultiplayerGameType.Battle => BattleGameName, + MultiplayerGameType.Skirmish => SkirmishGameName, + MultiplayerGameType.Captain => CaptainGameName, + _ => throw new ArgumentException(message: "Invalid game type", paramName: nameof(gameType)), + }) { + _gameType = gameType; _constants = constants; - _isSkirmish = isSkirmish; } #if CRPG_CLIENT @@ -57,6 +63,8 @@ public CrpgBattleGameMode(CrpgConstants constants, bool isSkirmish) public static MissionView[] OpenCrpgBattle(Mission mission) => OpenCrpgBattleOrSkirmish(mission); [ViewMethod(SkirmishGameName)] public static MissionView[] OpenCrpgSkirmish(Mission mission) => OpenCrpgBattleOrSkirmish(mission); + [ViewMethod(CaptainGameName)] + public static MissionView[] OpenCrpgCaptain(Mission mission) => OpenCrpgBattleOrSkirmish(mission); [ViewMethod("")] // All static instances in ViewCreatorModule classes are expected to have a ViewMethod attribute. private static MissionView[] OpenCrpgBattleOrSkirmish(Mission mission) @@ -70,6 +78,8 @@ private static MissionView[] OpenCrpgBattleOrSkirmish(Mission mission) MultiplayerViewCreator.CreateMultiplayerFactionBanVoteUIHandler(), ViewCreator.CreateMissionAgentStatusUIHandler(mission), ViewCreator.CreateMissionMainAgentEquipmentController(mission), // Pick/drop items. + new MissionGauntletMultiplayerOrderUIHandler(), + new OrderTroopPlacer(), new CrpgMissionGauntletMainAgentCheerControllerView(), crpgEscapeMenu, ViewCreator.CreateMissionAgentLabelUIHandler(mission), @@ -96,6 +106,7 @@ private static MissionView[] OpenCrpgBattleOrSkirmish(Mission mission) } #endif + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "")] public override void StartMultiplayerGame(string scene) { // Inherits the MultiplayerGameNotificationsComponent component. @@ -109,16 +120,21 @@ public override void StartMultiplayerGame(string scene) MultiplayerRoundController roundController = new(); // starts/stops round, ends match CrpgWarmupComponent warmupComponent = new(_constants, notificationsComponent, () => - (new FlagDominationSpawnFrameBehavior(), _isSkirmish - ? new CrpgSkirmishSpawningBehavior(_constants, roundController) - : new CrpgBattleSpawningBehavior(_constants, roundController))); - CrpgTeamSelectServerComponent teamSelectComponent = new(warmupComponent, roundController); + (new FlagDominationSpawnFrameBehavior(), + _gameType switch + { + MultiplayerGameType.Battle => new CrpgBattleSpawningBehavior(_constants, roundController, _gameType), + MultiplayerGameType.Skirmish => new CrpgSkirmishSpawningBehavior(_constants, roundController), + MultiplayerGameType.Captain => new CrpgCaptainSpawningBehavior(_constants, roundController, _gameType), + _ => throw new ArgumentException(message: "Invalid game type", paramName: nameof(_gameType)), + })); + CrpgTeamSelectServerComponent teamSelectComponent = new(warmupComponent, roundController, _gameType); CrpgRewardServer rewardServer = new(crpgClient, _constants, warmupComponent, enableTeamHitCompensations: true, enableRating: true); #else CrpgWarmupComponent warmupComponent = new(_constants, notificationsComponent, null); CrpgTeamSelectClientComponent teamSelectComponent = new(); #endif - CrpgBattleClient battleClient = new(_isSkirmish); + CrpgBattleClient battleClient = new(_gameType); MissionState.OpenNew( Name, @@ -143,7 +159,13 @@ public override void StartMultiplayerGame(string scene) new MultiplayerPollComponent(), // poll logic to kick player, ban player, change game new CrpgCommanderPollComponent(), new MissionOptionsComponent(), - new CrpgScoreboardComponent(_isSkirmish ? new CrpgSkirmishScoreboardData() : new CrpgBattleScoreboardData()), + new CrpgScoreboardComponent(_gameType switch + { + MultiplayerGameType.Battle => new CrpgBattleScoreboardData(), + MultiplayerGameType.Skirmish => new CrpgSkirmishScoreboardData(), + MultiplayerGameType.Captain => new CrpgBattleScoreboardData(), + _ => throw new ArgumentException(message: "Invalid game type", paramName: nameof(_gameType)), + }), new MissionAgentPanicHandler(), new EquipmentControllerLeaveLogic(), new MultiplayerPreloadHelper(), @@ -153,11 +175,18 @@ public override void StartMultiplayerGame(string scene) #if CRPG_SERVER roundController, - new CrpgBattleServer(battleClient, _isSkirmish, rewardServer), + new CrpgBattleServer(battleClient, _gameType, rewardServer), rewardServer, // SpawnFrameBehaviour: where to spawn, SpawningBehaviour: when to spawn new SpawnComponent(new BattleSpawnFrameBehavior(), - _isSkirmish ? new CrpgSkirmishSpawningBehavior(_constants, roundController) : new CrpgBattleSpawningBehavior(_constants, roundController)), + _gameType switch + { + MultiplayerGameType.Battle => new CrpgBattleSpawningBehavior(_constants, roundController, _gameType), + MultiplayerGameType.Skirmish => new CrpgSkirmishSpawningBehavior(_constants, roundController), + MultiplayerGameType.Captain => new CrpgCaptainSpawningBehavior(_constants, roundController, _gameType), + _ => throw new ArgumentException(message: "Invalid game type", paramName: nameof(_gameType)), + }), + new AgentHumanAILogic(), // bot intelligence new MultiplayerAdminComponent(), // admin UI to kick player or restart game new CrpgUserManagerServer(crpgClient, _constants), diff --git a/src/Module.Server/Modes/Battle/CrpgBattleServer.cs b/src/Module.Server/Modes/Battle/CrpgBattleServer.cs index 6c7446daf..bb9d054ec 100644 --- a/src/Module.Server/Modes/Battle/CrpgBattleServer.cs +++ b/src/Module.Server/Modes/Battle/CrpgBattleServer.cs @@ -1,4 +1,6 @@ -using Crpg.Module.Modes.Battle.FlagSystems; +using System.Reflection; +using Crpg.Module.Modes.Battle.FlagSystems; +using Crpg.Module.Modes.Captain; using Crpg.Module.Modes.Skirmish; using Crpg.Module.Rewards; using NetworkMessages.FromServer; @@ -20,7 +22,7 @@ internal class CrpgBattleServer : MissionMultiplayerGameModeBase private const float SkirmishMoraleGainMultiplierLastFlag = 2f; private readonly CrpgBattleClient _battleClient; - private readonly bool _isSkirmish; + private readonly MultiplayerGameType _gametype; private readonly CrpgRewardServer _rewardServer; private AbstractFlagSystem _flagSystem = default!; @@ -35,22 +37,26 @@ internal class CrpgBattleServer : MissionMultiplayerGameModeBase public override bool AllowCustomPlayerBanners() => false; public override bool UseRoundController() => true; - public CrpgBattleServer(CrpgBattleClient battleClient, bool isSkirmish, + public CrpgBattleServer(CrpgBattleClient battleClient, MultiplayerGameType gametype, CrpgRewardServer rewardServer) { _battleClient = battleClient; - _isSkirmish = isSkirmish; + _gametype = gametype; _rewardServer = rewardServer; + typeof(TaleWorlds.MountAndBlade.CompressionMission) + .GetField(nameof(TaleWorlds.MountAndBlade.CompressionMission.AgentOffsetCompressionInfo), BindingFlags.Public | BindingFlags.Static)? + .SetValue(null, new CompressionInfo.Integer(0, 16)); } public override MultiplayerGameType GetMissionType() { - return MultiplayerGameType.Battle; + return _gametype; } public override void AfterStart() { base.AfterStart(); + MissionPeer.OnPreTeamChanged += this.OnPreTeamChanged; RoundController.OnPreRoundEnding += OnPreRoundEnding; AddTeams(); @@ -58,7 +64,7 @@ public override void AfterStart() public override void OnAgentRemoved(Agent affectedAgent, Agent affectorAgent, AgentState agentState, KillingBlow blow) { - if (_isSkirmish || !affectedAgent.IsHuman) + if (_gametype == MultiplayerGameType.Skirmish || !affectedAgent.IsHuman) { return; } @@ -66,12 +72,21 @@ public override void OnAgentRemoved(Agent affectedAgent, Agent affectorAgent, Ag ((CrpgBattleFlagSystem)_flagSystem).CheckForDeadPlayerFlagSpawnThreshold(_attackersSpawned, _defendersSpawned); } + protected override void HandleEarlyPlayerDisconnect(NetworkCommunicator networkPeer) + { + if (this.RoundController.IsRoundInProgress && MultiplayerOptions.OptionType.NumberOfBotsPerFormation.GetIntValue(MultiplayerOptions.MultiplayerOptionsAccessMode.CurrentMapOptions) > 0) + { + this.MakePlayerFormationCharge(networkPeer); + } + } + public override void OnBehaviorInitialize() { base.OnBehaviorInitialize(); - _flagSystem = _isSkirmish - ? new CrpgSkirmishFlagSystem(Mission, NotificationsComponent, _battleClient) - : new CrpgBattleFlagSystem(Mission, NotificationsComponent, _battleClient); + _flagSystem = _gametype switch { + MultiplayerGameType.Skirmish => new CrpgSkirmishFlagSystem(Mission, NotificationsComponent, _battleClient), + _ => new CrpgBattleFlagSystem(Mission, NotificationsComponent, _battleClient), + }; _flagSystem.ResetFlags(); _morale = 0f; @@ -81,6 +96,7 @@ public override void OnBehaviorInitialize() public override void OnRemoveBehavior() { RoundController.OnPreRoundEnding -= OnPreRoundEnding; + MissionPeer.OnPreTeamChanged -= this.OnPreTeamChanged; base.OnRemoveBehavior(); } @@ -115,12 +131,12 @@ public override void OnMissionTick(float dt) base.OnMissionTick(dt); if (MissionLobbyComponent.CurrentMultiplayerState != MissionLobbyComponent.MultiplayerGameState.Playing || !RoundController.IsRoundInProgress - || !CanGameModeSystemsTickThisFrame - || _flagSystem.HasNoFlags()) // Protection against scene with no flags. + || !CanGameModeSystemsTickThisFrame) { return; } + if (SpawnComponent.SpawningBehavior is CrpgBattleSpawningBehavior s && s.SpawnDelayEnded() && !_hasSpawnDelayEnded) { _hasSpawnDelayEnded = true; @@ -128,6 +144,14 @@ public override void OnMissionTick(float dt) _defendersSpawned = Mission.DefenderTeam.ActiveAgents.Count; } + CheckForPlayersSpawningAsBots(); + + + if (_flagSystem.HasNoFlags()) // Protection against scene with no flags. + { + return; + } + _flagSystem.CheckForManipulationOfFlags(); CheckMorales(); _flagSystem.TickFlags(); @@ -162,9 +186,14 @@ public override bool CheckForRoundEnd() return false; } + if (SpawnComponent.SpawningBehavior is CrpgCaptainSpawningBehavior c && !c.SpawnDelayEnded()) + { + return false; + } + bool defenderTeamDepleted = Mission.DefenderTeam.ActiveAgents.Count == 0; bool attackerTeamDepleted = Mission.AttackerTeam.ActiveAgents.Count == 0; - if (!_isSkirmish) + if (!(_gametype == MultiplayerGameType.Skirmish)) { return defenderTeamDepleted || attackerTeamDepleted; } @@ -238,9 +267,20 @@ private void AddTeams() BasicCultureObject cultureTeam1 = MBObjectManager.Instance.GetObject(MultiplayerOptions.OptionType.CultureTeam1.GetStrValue()); Banner bannerTeam1 = new(cultureTeam1.BannerKey, cultureTeam1.BackgroundColor1, cultureTeam1.ForegroundColor1); Mission.Teams.Add(BattleSideEnum.Attacker, cultureTeam1.BackgroundColor1, cultureTeam1.ForegroundColor1, bannerTeam1, false, true); + for (int i = 0; i < 42; i++) + { + Formation f = new(Mission.Current.Teams.Attacker, 1); + Mission.Teams.Attacker.FormationsIncludingEmpty.Add(f); + } + BasicCultureObject cultureTeam2 = MBObjectManager.Instance.GetObject(MultiplayerOptions.OptionType.CultureTeam2.GetStrValue()); Banner bannerTeam2 = new(cultureTeam2.BannerKey, cultureTeam2.BackgroundColor2, cultureTeam2.ForegroundColor2); Mission.Teams.Add(BattleSideEnum.Defender, cultureTeam2.BackgroundColor2, cultureTeam2.ForegroundColor2, bannerTeam2, false, true); + for (int i = 0; i < 42; i++) + { + Formation f = new(Mission.Current.Teams.Defender, 1); + Mission.Teams.Defender.FormationsIncludingEmpty.Add(f); + } } private void CheckMorales() @@ -271,8 +311,8 @@ private float GetMoraleGain() return 0f; } - float moraleGainOnTick = _isSkirmish ? SkirmishMoraleGainOnTick : BattleMoraleGainOnTick; - float moraleGainMultiplierLastFlag = _isSkirmish ? SkirmishMoraleGainMultiplierLastFlag : BattleMoraleGainMultiplierLastFlag; + float moraleGainOnTick = _gametype == MultiplayerGameType.Skirmish ? SkirmishMoraleGainOnTick : BattleMoraleGainOnTick; + float moraleGainMultiplierLastFlag = _gametype == MultiplayerGameType.Skirmish ? SkirmishMoraleGainMultiplierLastFlag : BattleMoraleGainMultiplierLastFlag; float moraleMultiplier = moraleGainOnTick * Math.Abs(teamFlagsDelta); float moraleGain = teamFlagsDelta <= 0 @@ -386,4 +426,71 @@ private void CheerForRoundEnd(CaptureTheFlagCaptureResultEnum roundResult) missionBehavior.SetTimersOfVictoryReactionsOnBattleEnd(BattleSideEnum.Defender); } } + + private void CheckForPlayersSpawningAsBots() + { + foreach (NetworkCommunicator networkCommunicator in GameNetwork.NetworkPeers) + { + if (networkCommunicator.IsSynchronized) + { + MissionPeer component = networkCommunicator.GetComponent(); + if (component != null && component.ControlledAgent == null && component.Team != null && component.ControlledFormation != null && component.SpawnCountThisRound > 0) + { + if (!component.HasSpawnTimerExpired && component.SpawnTimer.Check(base.Mission.CurrentTime)) + { + component.HasSpawnTimerExpired = true; + } + + if (component.HasSpawnTimerExpired) + { + if (component.ControlledFormation.HasUnitsWithCondition((Agent agent) => agent.IsActive() && agent.IsAIControlled)) + { + Agent? newAgent = null; + Agent followingAgent = component.FollowedAgent; + if (followingAgent != null && followingAgent.IsActive() && followingAgent.IsAIControlled && component.ControlledFormation.HasUnitsWithCondition((Agent agent) => agent == followingAgent)) + { + newAgent = followingAgent; + } + else + { + float maxHealth = 0f; + component.ControlledFormation.ApplyActionOnEachUnit(delegate (Agent agent) + { + if (agent.Health > maxHealth) + { + maxHealth = agent.Health; + newAgent = agent; + } + }, null); + } + + Mission.Current.ReplaceBotWithPlayer(newAgent, component); + component.WantsToSpawnAsBot = false; + component.HasSpawnTimerExpired = false; + } + } + } + } + } + } + + private void MakePlayerFormationCharge(NetworkCommunicator peer) + { + if (peer.IsSynchronized) + { + MissionPeer component = peer.GetComponent(); + if (component.ControlledFormation != null) + { + component.ControlledFormation.SetMovementOrder(MovementOrder.MovementOrderCharge); + } + } + } + + private void OnPreTeamChanged(NetworkCommunicator peer, Team currentTeam, Team newTeam) + { + if (peer.IsSynchronized && peer.GetComponent().ControlledAgent != null) + { + this.MakePlayerFormationCharge(peer); + } + } } diff --git a/src/Module.Server/Modes/Battle/CrpgBattleSpawningBehavior.cs b/src/Module.Server/Modes/Battle/CrpgBattleSpawningBehavior.cs index 23b958acd..65631b127 100644 --- a/src/Module.Server/Modes/Battle/CrpgBattleSpawningBehavior.cs +++ b/src/Module.Server/Modes/Battle/CrpgBattleSpawningBehavior.cs @@ -1,4 +1,5 @@ using Crpg.Module.Common; +using Crpg.Module.Modes.Warmup; using Crpg.Module.Notifications; using TaleWorlds.Core; using TaleWorlds.MountAndBlade; @@ -14,12 +15,12 @@ internal class CrpgBattleSpawningBehavior : CrpgSpawningBehaviorBase private MissionTimer? _spawnTimer; private MissionTimer? _cavalrySpawnDelayTimer; private bool _botsSpawned; - - public CrpgBattleSpawningBehavior(CrpgConstants constants, MultiplayerRoundController roundController) + public CrpgBattleSpawningBehavior(CrpgConstants constants, MultiplayerRoundController roundController, MultiplayerGameType gameType) : base(constants) { _roundController = roundController; _notifiedPlayersAboutSpawnRestriction = new HashSet(); + GameMode = gameType; } public override void Initialize(SpawnComponent spawnComponent) @@ -60,7 +61,6 @@ public override void OnTick(float dt) public override void RequestStartSpawnSession() { base.RequestStartSpawnSession(); - _botsSpawned = false; _spawnTimer = new MissionTimer(TotalSpawnDuration); // Limit spawning for 30 seconds. _cavalrySpawnDelayTimer = new MissionTimer(GetCavalrySpawnDelay()); // Cav will spawn X seconds later. _notifiedPlayersAboutSpawnRestriction.Clear(); diff --git a/src/Module.Server/Modes/Captain/CrpgCaptainSpawningBehavior.cs b/src/Module.Server/Modes/Captain/CrpgCaptainSpawningBehavior.cs new file mode 100644 index 000000000..6df9d3633 --- /dev/null +++ b/src/Module.Server/Modes/Captain/CrpgCaptainSpawningBehavior.cs @@ -0,0 +1,412 @@ +using Crpg.Module.Api.Models.Captains; +using Crpg.Module.Api.Models.Users; +using Crpg.Module.Common; +using Crpg.Module.Modes.Warmup; +using Crpg.Module.Notifications; +using TaleWorlds.Core; +using TaleWorlds.MountAndBlade; +using TaleWorlds.ObjectSystem; +using TaleWorlds.PlayerServices; + +namespace Crpg.Module.Modes.Captain; + +internal class CrpgCaptainSpawningBehavior : CrpgSpawningBehaviorBase +{ + private const float TotalSpawnDuration = 30f; + private readonly MultiplayerRoundController _roundController; + private readonly HashSet _notifiedPlayersAboutSpawnRestriction; + private MissionTimer? _spawnTimer; + private MissionTimer? _cavalrySpawnDelayTimer; + private bool _botsSpawned = false; + + private readonly Dictionary _teamSumOfEquipment = new(); + private readonly Dictionary _teamAverageEquipment = new(); + private readonly Dictionary _teamNumberOfBots = new(); + + private readonly int _totalNumberOfBots; + private bool _isSinglePlayer; + + public CrpgCaptainSpawningBehavior(CrpgConstants constants, MultiplayerRoundController roundController, MultiplayerGameType gameType) + : base(constants) + { + _roundController = roundController; + _notifiedPlayersAboutSpawnRestriction = new HashSet(); + GameMode = gameType; +#if CRPG_SERVER + _totalNumberOfBots = CrpgServerConfiguration.CaptainTotalBotCount; +#endif + } + + public override void Initialize(SpawnComponent spawnComponent) + { + base.Initialize(spawnComponent); + _roundController.OnPreparationEnded += RequestStartSpawnSession; + _roundController.OnRoundEnding += RequestStopSpawnSession; + } + + public override void Clear() + { + base.Clear(); + _roundController.OnPreparationEnded -= RequestStartSpawnSession; + _roundController.OnRoundEnding -= RequestStopSpawnSession; + } + + public override void OnTick(float dt) + { + if (!IsSpawningEnabled || !IsRoundInProgress()) + { + return; + } + + if (_spawnTimer!.Check()) + { + return; + } + + if (!_botsSpawned) + { + SpawnCaptainBots(); + _botsSpawned = true; + } + + SpawnAgents(); + } + + public override void RequestStartSpawnSession() + { + _isSinglePlayer = IsSinglePlayer(); + + foreach (Team team in Mission.Current.Teams) + { + _teamSumOfEquipment[team] = ComputeTeamSumOfEquipmentValue(team); + _teamAverageEquipment[team] = ComputeTeamAverageUnitValue(team, _teamSumOfEquipment[team]); + } + + if (!_isSinglePlayer) + { + foreach (Team team in Mission.Current.Teams) + { + var peers = GameNetwork.NetworkPeers; + var teamRelevantPeers = + peers.Where(p => IsNetworkPeerRelevant(p) && p.GetComponent().Team == team).ToList(); + + float numerator = _totalNumberOfBots * _teamAverageEquipment.Where(kvp => kvp.Key != team).Sum(kvp => kvp.Value); + float denominator = (Mission.Current.Teams.Count - 2) * _teamAverageEquipment.Sum(kvp => kvp.Value); // -2 because we also remove spectator + _teamNumberOfBots[team] = (int)(numerator / denominator) - teamRelevantPeers.Count; + } + } + else + { + foreach (Team team in Mission.Current.Teams) + { + if (team.Side != BattleSideEnum.None) + { + _teamNumberOfBots[team] = _totalNumberOfBots / (Mission.Current.Teams.Count - 1); + } + } + } + + base.RequestStartSpawnSession(); + _botsSpawned = false; + _spawnTimer = new MissionTimer(TotalSpawnDuration); // Limit spawning for 30 seconds. + _cavalrySpawnDelayTimer = new MissionTimer(GetCavalrySpawnDelay()); // Cav will spawn X seconds later. + _notifiedPlayersAboutSpawnRestriction.Clear(); + } + + public bool SpawnDelayEnded() + { + return _cavalrySpawnDelayTimer != null && _cavalrySpawnDelayTimer!.Check(); + } + + protected override bool IsRoundInProgress() + { + return _roundController.IsRoundInProgress; + } + + protected override bool IsPlayerAllowedToSpawn(NetworkCommunicator networkPeer) + { + var crpgPeer = networkPeer.GetComponent(); + var missionPeer = networkPeer.GetComponent(); + if (crpgPeer?.User == null + || crpgPeer.LastSpawnInfo != null + || missionPeer == null) + { + return false; + } + + var characterEquipment = CrpgCharacterBuilder.CreateCharacterEquipment(crpgPeer.User.Character.EquippedItems); + if (!DoesEquipmentContainWeapon(characterEquipment)) // Disallow spawning without weapons. + { + if (_notifiedPlayersAboutSpawnRestriction.Add(networkPeer.VirtualPlayer.Id)) + { + GameNetwork.BeginModuleEventAsServer(networkPeer); + GameNetwork.WriteMessage(new CrpgNotificationId + { + Type = CrpgNotificationType.Announcement, + TextId = "str_kick_reason", + TextVariation = "no_weapon", + SoundEvent = string.Empty, + }); + GameNetwork.EndModuleEventAsServer(); + } + + return false; + } + + bool hasMount = characterEquipment[EquipmentIndex.Horse].Item != null; + // Disallow spawning cavalry before the cav spawn delay ended. + if (hasMount && _cavalrySpawnDelayTimer != null && !_cavalrySpawnDelayTimer.Check()) + { + if (_notifiedPlayersAboutSpawnRestriction.Add(networkPeer.VirtualPlayer.Id)) + { + GameNetwork.BeginModuleEventAsServer(networkPeer); + GameNetwork.WriteMessage(new CrpgNotificationId + { + Type = CrpgNotificationType.Notification, + TextId = "str_notification", + TextVariation = "cavalry_spawn_delay", + SoundEvent = string.Empty, + Variables = { ["SECONDS"] = ((int)_cavalrySpawnDelayTimer.GetTimerDuration()).ToString() }, + }); + GameNetwork.EndModuleEventAsServer(); + } + + return false; + } + + return true; + } + + protected override void OnPeerSpawned(Agent agent) + { + MissionPeer? missionPeer = agent.MissionPeer; + + if (missionPeer == null) + { + return; + } + + CrpgPeer? crpgPeer = missionPeer.Peer.GetComponent(); + + if (crpgPeer == null) + { + return; + } + + if (agent.MissionPeer.ControlledFormation != null) + { + agent.Team.AssignPlayerAsSergeantOfFormation(agent.MissionPeer, agent.MissionPeer.ControlledFormation.FormationIndex); + } + + base.OnPeerSpawned(agent); + agent.MissionPeer.SpawnCountThisRound += 1; + + if (IsRoundInProgress()) + { + int p = int.Parse(agent.Character.StringId.Split('_').Last()); + var peers = GameNetwork.NetworkPeers; + var teamRelevantPeers = + peers.Where(p => IsNetworkPeerRelevant(p) && p.GetComponent().Team == missionPeer.Team).ToList(); + float sumOfTeamEquipment = _teamSumOfEquipment[missionPeer.Team]; + float peerSumOfEquipment = ComputeEquipmentValue(crpgPeer); + int peerNumberOfBots = 0; + if (teamRelevantPeers.Count - 1 < 1) + { + peerNumberOfBots = _teamNumberOfBots[missionPeer.Team] - 1; + } + else + { + peerNumberOfBots = (int)(_teamNumberOfBots[missionPeer.Team] * (1 - peerSumOfEquipment / sumOfTeamEquipment) / + (float)(teamRelevantPeers.Count - 1)); + } + + Dictionary formationBotWeight = new(); + var captainFormations = crpgPeer.User!.Captain.Formations.Where(f => f.Character != null); + if (captainFormations.Any()) + { + double totalWeight = captainFormations.Sum(cf => cf.Weight); + + foreach (CrpgCaptainFormation captainFormation in captainFormations) + { + double proportion = (double)(captainFormation.Weight / totalWeight); + formationBotWeight.Add(captainFormation.Number, proportion); + } + } + else + { + for (int i = 0; i < peerNumberOfBots; i++) + { + SpawnBotAgent($"crpg_class_division_{p}", agent.Team, missionPeer, p); + } + } + + foreach (KeyValuePair captainFormation in formationBotWeight) + { + for (int i = 0; i < (int)(captainFormation.Value * peerNumberOfBots); i++) + { + SpawnBotAgent($"crpg_class_division_{p}", agent.Team, missionPeer, p, captainFormation.Key); + } + } + } + + } + + /// + /// Cav spawn delay values + /// 10 => 7sec + /// 30 => 9sec + /// 60 => 13sec + /// 90 => 17sec + /// 120 => 22sec + /// 150 => 26sec + /// 165+ => 28sec. + /// + private int GetCavalrySpawnDelay() + { + int currentPlayers = Math.Max(GetCurrentPlayerCount(), 1); + return Math.Min(28, 5 + currentPlayers / 7); + } + + private int GetCurrentPlayerCount() + { + int counter = 0; + foreach (NetworkCommunicator networkPeer in GameNetwork.NetworkPeers) + { + var missionPeer = networkPeer.GetComponent(); + if (!networkPeer.IsSynchronized + || missionPeer == null + || missionPeer.Team == null + || missionPeer.Team.Side == BattleSideEnum.None) + { + continue; + } + + counter++; + } + + return counter; + } + + private int ComputeTeamSumOfEquipmentValue(Team team) + { + var peers = GameNetwork.NetworkPeers; + var teamRelevantPeers = + peers.Where(p => IsNetworkPeerRelevant(p) && p.GetComponent().Team == team).ToList(); + int valueToReturn = teamRelevantPeers.Sum(p => ComputeEquipmentValue(p.GetComponent())); + return (int)Math.Max(valueToReturn, 1); + } + + private int ComputeTeamAverageUnitValue(Team team, int teamSumOfEquipment) + { + var peers = GameNetwork.NetworkPeers; + var teamRelevantPeers = + peers.Where(p => IsNetworkPeerRelevant(p) && p.GetComponent().Team == team).ToList(); + if (teamRelevantPeers.Count < 2) + { + return teamRelevantPeers.Sum(p => ComputeEquipmentValue(p.GetComponent())); + } + + double sumOfEachSquared = teamRelevantPeers.Sum(p => Math.Pow(ComputeEquipmentValue(p.GetComponent()), 2f)); + int valueToReturn = (int)((teamSumOfEquipment - sumOfEachSquared / teamSumOfEquipment) / (float)(teamRelevantPeers.Count - 1)); + return (int)Math.Max(valueToReturn, 1); + } + + private int ComputeEquipmentValue(CrpgPeer peer) + { + int totalValue = 0; + + Dictionary formationBotWeight = new(); + var captainFormations = peer?.User?.Captain.Formations.Where(f => f.Character != null); + if (captainFormations.Any()) + { + double totalWeight = captainFormations.Sum(cf => cf.Weight); + + foreach (CrpgCaptainFormation captainFormation in captainFormations!) + { + double proportion = (double)(captainFormation.Weight / totalWeight); + formationBotWeight.Add(captainFormation.Number, proportion); + + int characterValue = captainFormation.Character!.EquippedItems + .Select(i => MBObjectManager.Instance.GetObject(i.UserItem.ItemId)) + .Where(io => io != null) + .Sum(io => io.Value); + + totalValue += (int)(characterValue * formationBotWeight[captainFormation.Number]); + } + } + else + { + totalValue = peer?.User?.Character.EquippedItems.Select(i => MBObjectManager.Instance.GetObject(i.UserItem.ItemId)).Sum(io => io.Value) ?? 0; + } + + return totalValue + 10000; // protection against naked + } + + private int ComputeSingleEquipmentValue(ItemObject item) + { + return 0; + } + + private bool IsNetworkPeerRelevant(NetworkCommunicator networkPeer) + { + MissionPeer missionPeer = networkPeer.GetComponent(); + CrpgPeer crpgPeer = networkPeer.GetComponent(); + bool isRelevant = !(!networkPeer.IsSynchronized + || missionPeer == null + || missionPeer.Team == null + || missionPeer.Team == Mission.SpectatorTeam + || crpgPeer == null + || crpgPeer.UserLoading + || crpgPeer.User == null); + return isRelevant; + } + + private bool IsSinglePlayer() + { + bool isATeamEmpty = false; + + foreach (Team team in Mission.Current.Teams) + { + if (team.Side != BattleSideEnum.None) + { + var peers = GameNetwork.NetworkPeers; + var teamRelevantPeers = + peers.Where(p => IsNetworkPeerRelevant(p) && p.GetComponent().Team == team).ToList(); + + if (teamRelevantPeers.Count < 1) + { + isATeamEmpty = true; + } + } + } + + return isATeamEmpty; + } + + private void SpawnCaptainBots() + { + if (IsRoundInProgress()) + { + foreach (Team team in Mission.Current.Teams) + { + if (team.Side != BattleSideEnum.None) + { + var peers = GameNetwork.NetworkPeers; + var teamRelevantPeers = + peers.Where(p => IsNetworkPeerRelevant(p) && p.GetComponent().Team == team).ToList(); + + if (teamRelevantPeers.Count == 0) + { + for (int i = 0; i < _teamNumberOfBots[team]; i++) + { + MultiplayerClassDivisions.MPHeroClass botClass = MultiplayerClassDivisions + .GetMPHeroClasses() + .GetRandomElementWithPredicate(x => x.StringId.StartsWith("crpg_bot_")); + SpawnBotAgent(botClass.StringId, team); + } + } + } + } + } + } +} diff --git a/src/Module.Server/Modes/Conquest/CrpgConquestGameMode.cs b/src/Module.Server/Modes/Conquest/CrpgConquestGameMode.cs index 015bf3c28..df8e72d3f 100644 --- a/src/Module.Server/Modes/Conquest/CrpgConquestGameMode.cs +++ b/src/Module.Server/Modes/Conquest/CrpgConquestGameMode.cs @@ -91,7 +91,7 @@ public override void StartMultiplayerGame(string scene) ChatBox chatBox = Game.Current.GetGameHandler(); CrpgWarmupComponent warmupComponent = new(_constants, notificationsComponent, () => (new SiegeSpawnFrameBehavior(), new CrpgSiegeSpawningBehavior(_constants))); - CrpgTeamSelectServerComponent teamSelectComponent = new(warmupComponent, null); + CrpgTeamSelectServerComponent teamSelectComponent = new(warmupComponent, null, MultiplayerGameType.Siege); CrpgRewardServer rewardServer = new(crpgClient, _constants, warmupComponent, enableTeamHitCompensations: false, enableRating: false); #else CrpgWarmupComponent warmupComponent = new(_constants, notificationsComponent, null); diff --git a/src/Module.Server/Modes/Dtv/CrpgDtvGameMode.cs b/src/Module.Server/Modes/Dtv/CrpgDtvGameMode.cs index 2537e746e..69c164c12 100644 --- a/src/Module.Server/Modes/Dtv/CrpgDtvGameMode.cs +++ b/src/Module.Server/Modes/Dtv/CrpgDtvGameMode.cs @@ -95,7 +95,7 @@ public override void StartMultiplayerGame(string scene) CrpgWarmupComponent warmupComponent = new(_constants, notificationsComponent, () => (new FlagDominationSpawnFrameBehavior(), new CrpgDtvSpawningBehavior(_constants))); - CrpgTeamSelectServerComponent teamSelectComponent = new(warmupComponent, null); + CrpgTeamSelectServerComponent teamSelectComponent = new(warmupComponent, null, MultiplayerGameType.Siege); CrpgRewardServer rewardServer = new(crpgClient, _constants, warmupComponent, enableTeamHitCompensations: true, enableRating: false, enableLowPopulationUpkeep: true); CrpgDtvSpawningBehavior spawnBehaviour = new(_constants); #else diff --git a/src/Module.Server/ModuleData/crpg_captain_characters.xml b/src/Module.Server/ModuleData/crpg_captain_characters.xml new file mode 100644 index 000000000..d7aef07fa --- /dev/null +++ b/src/Module.Server/ModuleData/crpg_captain_characters.xml @@ -0,0 +1,1104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Module.Server/ModuleData/crpg_captain_class_divisions.xml b/src/Module.Server/ModuleData/crpg_captain_class_divisions.xml new file mode 100644 index 000000000..65c13b165 --- /dev/null +++ b/src/Module.Server/ModuleData/crpg_captain_class_divisions.xml @@ -0,0 +1,5064 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Module.Server/ModuleData/multiplayer_strings.xml b/src/Module.Server/ModuleData/multiplayer_strings.xml index e077ea5c0..0f2f9262f 100644 --- a/src/Module.Server/ModuleData/multiplayer_strings.xml +++ b/src/Module.Server/ModuleData/multiplayer_strings.xml @@ -4,7 +4,10 @@ - + + + + diff --git a/src/Module.Server/Properties/launchSettings.json b/src/Module.Server/Properties/launchSettings.json index 3e7d15ee1..00559c318 100644 --- a/src/Module.Server/Properties/launchSettings.json +++ b/src/Module.Server/Properties/launchSettings.json @@ -72,4 +72,4 @@ } }, "$schema": "http://json.schemastore.org/launchsettings.json" -} +} \ No newline at end of file diff --git a/src/Module.Server/SubModule.xml b/src/Module.Server/SubModule.xml index 71c89c05c..1c679505c 100644 --- a/src/Module.Server/SubModule.xml +++ b/src/Module.Server/SubModule.xml @@ -65,6 +65,12 @@ + + + + + + diff --git a/src/Persistence/Configurations/CaptainConfiguration.cs b/src/Persistence/Configurations/CaptainConfiguration.cs new file mode 100644 index 000000000..05ee1a509 --- /dev/null +++ b/src/Persistence/Configurations/CaptainConfiguration.cs @@ -0,0 +1,18 @@ +using Crpg.Domain.Entities.Captains; +using Crpg.Domain.Entities.Users; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Crpg.Persistence.Configurations; + +public class CaptainConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(c => c.Id); + + builder.HasMany(c => c.Formations) + .WithOne(f => f.Captain) + .HasForeignKey(f => f.CaptainId); + } +} diff --git a/src/Persistence/Configurations/CaptainFormationConfiguration.cs b/src/Persistence/Configurations/CaptainFormationConfiguration.cs new file mode 100644 index 000000000..f7938bcc0 --- /dev/null +++ b/src/Persistence/Configurations/CaptainFormationConfiguration.cs @@ -0,0 +1,20 @@ +using Crpg.Domain.Entities.Captains; +using Crpg.Domain.Entities.Users; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Crpg.Persistence.Configurations; + +public class CaptainFormationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(f => f.Id); + + builder.Property(f => f.Weight) + .IsRequired(); + builder.HasOne(f => f.Character) + .WithMany() + .HasForeignKey(f => f.CharacterId); + } +} diff --git a/src/Persistence/Configurations/UserConfiguration.cs b/src/Persistence/Configurations/UserConfiguration.cs index 4bb87f2f8..518877511 100644 --- a/src/Persistence/Configurations/UserConfiguration.cs +++ b/src/Persistence/Configurations/UserConfiguration.cs @@ -1,4 +1,5 @@ using Crpg.Domain.Entities.Users; +using Crpg.Domain.Entities.Captains; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Persistence/CrpgDbContext.cs b/src/Persistence/CrpgDbContext.cs index d8b4fb242..66f98a91a 100644 --- a/src/Persistence/CrpgDbContext.cs +++ b/src/Persistence/CrpgDbContext.cs @@ -4,6 +4,7 @@ using Crpg.Domain.Entities; using Crpg.Domain.Entities.ActivityLogs; using Crpg.Domain.Entities.Battles; +using Crpg.Domain.Entities.Captains; using Crpg.Domain.Entities.Characters; using Crpg.Domain.Entities.Clans; using Crpg.Domain.Entities.GameServers; @@ -79,6 +80,7 @@ public CrpgDbContext( public DbSet ClanArmoryItems { get; set; } = default!; public DbSet ClanArmoryBorrowedItems { get; set; } = default!; public DbSet ClanInvitations { get; set; } = default!; + public DbSet Captains { get; set; } = default!; public DbSet Parties { get; set; } = default!; public DbSet Settlements { get; set; } = default!; public DbSet SettlementItems { get; set; } = default!; diff --git a/src/Persistence/Migrations/20240307120749_Captain.Designer.cs b/src/Persistence/Migrations/20240307120749_Captain.Designer.cs new file mode 100644 index 000000000..c22fa341d --- /dev/null +++ b/src/Persistence/Migrations/20240307120749_Captain.Designer.cs @@ -0,0 +1,2301 @@ +// +using System; +using Crpg.Domain.Entities; +using Crpg.Domain.Entities.ActivityLogs; +using Crpg.Domain.Entities.Battles; +using Crpg.Domain.Entities.Characters; +using Crpg.Domain.Entities.Clans; +using Crpg.Domain.Entities.Items; +using Crpg.Domain.Entities.Parties; +using Crpg.Domain.Entities.Restrictions; +using Crpg.Domain.Entities.Settlements; +using Crpg.Domain.Entities.Users; +using Crpg.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Crpg.Persistence.Migrations +{ + [DbContext(typeof(CrpgDbContext))] + [Migration("20240307120749_Captain")] + partial class Captain + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "activity_log_type", new[] { "user_created", "user_deleted", "user_renamed", "user_rewarded", "item_bought", "item_sold", "item_broke", "item_reforged", "item_repaired", "item_upgraded", "character_created", "character_deleted", "character_rating_reset", "character_respecialized", "character_retired", "character_rewarded", "character_earned", "server_joined", "chat_message_sent", "team_hit", "clan_armory_add_item", "clan_armory_remove_item", "clan_armory_return_item", "clan_armory_borrow_item" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_fighter_application_status", new[] { "pending", "declined", "accepted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_mercenary_application_status", new[] { "pending", "declined", "accepted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_phase", new[] { "preparation", "hiring", "scheduled", "live", "end" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_side", new[] { "attacker", "defender" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "character_class", new[] { "peasant", "infantry", "shock_infantry", "skirmisher", "crossbowman", "archer", "cavalry", "mounted_archer" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_invitation_status", new[] { "pending", "declined", "accepted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_invitation_type", new[] { "request", "offer" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_member_role", new[] { "member", "officer", "leader" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "culture", new[] { "neutral", "aserai", "battania", "empire", "khuzait", "looters", "sturgia", "vlandia" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "damage_type", new[] { "undefined", "cut", "pierce", "blunt" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_slot", new[] { "head", "shoulder", "body", "hand", "leg", "mount_harness", "mount", "weapon0", "weapon1", "weapon2", "weapon3", "weapon_extra" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_type", new[] { "undefined", "head_armor", "shoulder_armor", "body_armor", "hand_armor", "leg_armor", "mount_harness", "mount", "shield", "bow", "crossbow", "one_handed_weapon", "two_handed_weapon", "polearm", "thrown", "arrows", "bolts", "pistol", "musket", "bullets", "banner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "party_status", new[] { "idle", "idle_in_settlement", "recruiting_in_settlement", "moving_to_point", "following_party", "moving_to_settlement", "moving_to_attack_party", "moving_to_attack_settlement", "in_battle" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "platform", new[] { "steam", "epic_games", "microsoft" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "region", new[] { "eu", "na", "as", "oc" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "restriction_type", new[] { "all", "join", "chat" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role", new[] { "user", "moderator", "game_admin", "admin" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "settlement_type", new[] { "village", "castle", "town" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "weapon_class", new[] { "undefined", "dagger", "one_handed_sword", "two_handed_sword", "one_handed_axe", "two_handed_axe", "mace", "pick", "two_handed_mace", "one_handed_polearm", "two_handed_polearm", "low_grip_polearm", "arrow", "bolt", "cartridge", "bow", "crossbow", "stone", "boulder", "throwing_axe", "throwing_knife", "javelin", "pistol", "musket", "small_shield", "large_shield", "banner" }); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Type") + .HasColumnType("activity_log_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_activity_logs"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_activity_logs_user_id"); + + b.HasIndex("CreatedAt", "UserId") + .HasDatabaseName("ix_activity_logs_created_at_user_id"); + + b.ToTable("activity_logs", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLogMetadata", b => + { + b.Property("ActivityLogId") + .HasColumnType("integer") + .HasColumnName("activity_log_id"); + + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("ActivityLogId", "Key") + .HasName("pk_activity_log_metadata"); + + b.ToTable("activity_log_metadata", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.Battle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Phase") + .HasColumnType("battle_phase") + .HasColumnName("phase"); + + b.Property("Position") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("position"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("ScheduledFor") + .HasColumnType("timestamp with time zone") + .HasColumnName("scheduled_for"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_battles"); + + b.ToTable("battles", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("Commander") + .HasColumnType("boolean") + .HasColumnName("commander"); + + b.Property("MercenarySlots") + .HasColumnType("integer") + .HasColumnName("mercenary_slots"); + + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("SettlementId") + .HasColumnType("integer") + .HasColumnName("settlement_id"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.HasKey("Id") + .HasName("pk_battle_fighters"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_fighters_battle_id"); + + b.HasIndex("PartyId") + .HasDatabaseName("ix_battle_fighters_party_id"); + + b.HasIndex("SettlementId") + .HasDatabaseName("ix_battle_fighters_settlement_id"); + + b.ToTable("battle_fighters", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighterApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.Property("Status") + .HasColumnType("battle_fighter_application_status") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_battle_fighter_applications"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_fighter_applications_battle_id"); + + b.HasIndex("PartyId") + .HasDatabaseName("ix_battle_fighter_applications_party_id"); + + b.ToTable("battle_fighter_applications", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleMercenary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApplicationId") + .HasColumnType("integer") + .HasColumnName("application_id"); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("CaptainFighterId") + .HasColumnType("integer") + .HasColumnName("captain_fighter_id"); + + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.HasKey("Id") + .HasName("pk_battle_mercenaries"); + + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_battle_mercenaries_application_id"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_mercenaries_battle_id"); + + b.HasIndex("CaptainFighterId") + .HasDatabaseName("ix_battle_mercenaries_captain_fighter_id"); + + b.HasIndex("CharacterId") + .HasDatabaseName("ix_battle_mercenaries_character_id"); + + b.ToTable("battle_mercenaries", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleMercenaryApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.Property("Status") + .HasColumnType("battle_mercenary_application_status") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Wage") + .HasColumnType("integer") + .HasColumnName("wage"); + + b.HasKey("Id") + .HasName("pk_battle_mercenary_applications"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_mercenary_applications_battle_id"); + + b.HasIndex("CharacterId") + .HasDatabaseName("ix_battle_mercenary_applications_character_id"); + + b.ToTable("battle_mercenary_applications", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Captains.Captain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_captains"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ix_captains_user_id"); + + b.ToTable("captains", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Captains.CaptainFormation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CaptainId") + .HasColumnType("integer") + .HasColumnName("captain_id"); + + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Number") + .HasColumnType("integer") + .HasColumnName("number"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Weight") + .HasColumnType("integer") + .HasColumnName("weight"); + + b.HasKey("Id") + .HasName("pk_captain_formation"); + + b.HasIndex("CaptainId") + .HasDatabaseName("ix_captain_formation_captain_id"); + + b.HasIndex("CharacterId") + .HasDatabaseName("ix_captain_formation_character_id"); + + b.ToTable("captain_formation", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Class") + .HasColumnType("character_class") + .HasColumnName("class"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Experience") + .HasColumnType("integer") + .HasColumnName("experience"); + + b.Property("ForTournament") + .HasColumnType("boolean") + .HasColumnName("for_tournament"); + + b.Property("Generation") + .HasColumnType("integer") + .HasColumnName("generation"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_characters"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_characters_user_id"); + + b.ToTable("characters", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.Clan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArmoryTimeout") + .HasColumnType("interval") + .HasColumnName("armory_timeout"); + + b.Property("BannerKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("banner_key"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Discord") + .HasColumnType("text") + .HasColumnName("discord"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PrimaryColor") + .HasColumnType("bigint") + .HasColumnName("primary_color"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("SecondaryColor") + .HasColumnType("bigint") + .HasColumnName("secondary_color"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_clans"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_clans_name"); + + b.HasIndex("Tag") + .IsUnique() + .HasDatabaseName("ix_clans_tag"); + + b.ToTable("clans", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", b => + { + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.Property("BorrowerClanId") + .HasColumnType("integer") + .HasColumnName("borrower_clan_id"); + + b.Property("BorrowerUserId") + .HasColumnType("integer") + .HasColumnName("borrower_user_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("UserItemId") + .HasName("pk_clan_armory_borrowed_items"); + + b.HasIndex("BorrowerClanId") + .HasDatabaseName("ix_clan_armory_borrowed_items_borrower_clan_id"); + + b.HasIndex("BorrowerUserId") + .HasDatabaseName("ix_clan_armory_borrowed_items_borrower_user_id"); + + b.ToTable("clan_armory_borrowed_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClanId") + .HasColumnType("integer") + .HasColumnName("clan_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("InviteeId") + .HasColumnType("integer") + .HasColumnName("invitee_id"); + + b.Property("InviterId") + .HasColumnType("integer") + .HasColumnName("inviter_id"); + + b.Property("Status") + .HasColumnType("clan_invitation_status") + .HasColumnName("status"); + + b.Property("Type") + .HasColumnType("clan_invitation_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_clan_invitations"); + + b.HasIndex("ClanId") + .HasDatabaseName("ix_clan_invitations_clan_id"); + + b.HasIndex("InviteeId") + .HasDatabaseName("ix_clan_invitations_invitee_id"); + + b.HasIndex("InviterId") + .HasDatabaseName("ix_clan_invitations_inviter_id"); + + b.ToTable("clan_invitations", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanMember", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("ClanId") + .HasColumnType("integer") + .HasColumnName("clan_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Role") + .HasColumnType("clan_member_role") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("UserId") + .HasName("pk_clan_members"); + + b.HasIndex("ClanId") + .HasDatabaseName("ix_clan_members_clan_id"); + + b.ToTable("clan_members", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ClanArmoryItem", b => + { + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("LenderClanId") + .HasColumnType("integer") + .HasColumnName("lender_clan_id"); + + b.Property("LenderUserId") + .HasColumnType("integer") + .HasColumnName("lender_user_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("UserItemId") + .HasName("pk_clan_armory_items"); + + b.HasIndex("LenderClanId") + .HasDatabaseName("ix_clan_armory_items_lender_clan_id"); + + b.HasIndex("LenderUserId") + .HasDatabaseName("ix_clan_armory_items_lender_user_id"); + + b.ToTable("clan_armory_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.EquippedItem", b => + { + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("Slot") + .HasColumnType("item_slot") + .HasColumnName("slot"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.HasKey("CharacterId", "Slot") + .HasName("pk_equipped_items"); + + b.HasIndex("UserItemId") + .HasDatabaseName("ix_equipped_items_user_item_id"); + + b.ToTable("equipped_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.Item", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("BaseId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("base_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Culture") + .HasColumnType("culture") + .HasColumnName("culture"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Price") + .HasColumnType("integer") + .HasColumnName("price"); + + b.Property("Rank") + .HasColumnType("integer") + .HasColumnName("rank"); + + b.Property("Requirement") + .HasColumnType("integer") + .HasColumnName("requirement"); + + b.Property("Tier") + .HasColumnType("real") + .HasColumnName("tier"); + + b.Property("Type") + .HasColumnType("item_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Weight") + .HasColumnType("real") + .HasColumnName("weight"); + + b.HasKey("Id") + .HasName("pk_items"); + + b.ToTable("items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsBroken") + .HasColumnType("boolean") + .HasColumnName("is_broken"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_items"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_user_items_item_id"); + + b.HasIndex("UserId", "ItemId") + .IsUnique() + .HasDatabaseName("ix_user_items_user_id_item_id"); + + b.ToTable("user_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Limitations.CharacterLimitations", b => + { + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("LastRespecializeAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_respecialize_at"); + + b.HasKey("CharacterId") + .HasName("pk_character_limitations"); + + b.ToTable("character_limitations", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.Party", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Gold") + .HasColumnType("integer") + .HasColumnName("gold"); + + b.Property("Position") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("position"); + + b.Property("Status") + .HasColumnType("party_status") + .HasColumnName("status"); + + b.Property("TargetedPartyId") + .HasColumnType("integer") + .HasColumnName("targeted_party_id"); + + b.Property("TargetedSettlementId") + .HasColumnType("integer") + .HasColumnName("targeted_settlement_id"); + + b.Property("Troops") + .HasColumnType("real") + .HasColumnName("troops"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Waypoints") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("waypoints"); + + b.HasKey("Id") + .HasName("pk_parties"); + + b.HasIndex("TargetedPartyId") + .HasDatabaseName("ix_parties_targeted_party_id"); + + b.HasIndex("TargetedSettlementId") + .HasDatabaseName("ix_parties_targeted_settlement_id"); + + b.ToTable("parties", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.PartyItem", b => + { + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("ItemId") + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("PartyId", "ItemId") + .HasName("pk_party_items"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_party_items_item_id"); + + b.ToTable("party_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Restrictions.Restriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Duration") + .HasColumnType("interval") + .HasColumnName("duration"); + + b.Property("PublicReason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("public_reason"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RestrictedByUserId") + .HasColumnType("integer") + .HasColumnName("restricted_by_user_id"); + + b.Property("RestrictedUserId") + .HasColumnType("integer") + .HasColumnName("restricted_user_id"); + + b.Property("Type") + .HasColumnType("restriction_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_restrictions"); + + b.HasIndex("RestrictedByUserId") + .HasDatabaseName("ix_restrictions_restricted_by_user_id"); + + b.HasIndex("RestrictedUserId") + .HasDatabaseName("ix_restrictions_restricted_user_id"); + + b.ToTable("restrictions", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.Settlement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Culture") + .HasColumnType("culture") + .HasColumnName("culture"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Position") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("position"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("Scene") + .IsRequired() + .HasColumnType("text") + .HasColumnName("scene"); + + b.Property("Troops") + .HasColumnType("integer") + .HasColumnName("troops"); + + b.Property("Type") + .HasColumnType("settlement_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_settlements"); + + b.HasIndex("OwnerId") + .HasDatabaseName("ix_settlements_owner_id"); + + b.HasIndex("Region", "Name") + .IsUnique() + .HasDatabaseName("ix_settlements_region_name"); + + b.ToTable("settlements", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.SettlementItem", b => + { + b.Property("SettlementId") + .HasColumnType("integer") + .HasColumnName("settlement_id"); + + b.Property("ItemId") + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("SettlementId", "ItemId") + .HasName("pk_settlement_items"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_settlement_items_item_id"); + + b.ToTable("settlement_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveCharacterId") + .HasColumnType("integer") + .HasColumnName("active_character_id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExperienceMultiplier") + .HasColumnType("real") + .HasColumnName("experience_multiplier"); + + b.Property("Gold") + .HasColumnType("integer") + .HasColumnName("gold"); + + b.Property("HeirloomPoints") + .HasColumnType("integer") + .HasColumnName("heirloom_points"); + + b.Property("IsDonor") + .HasColumnType("boolean") + .HasColumnName("is_donor"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Platform") + .HasColumnType("platform") + .HasColumnName("platform"); + + b.Property("PlatformUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("platform_user_id"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("Role") + .HasColumnType("role") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("ActiveCharacterId") + .IsUnique() + .HasDatabaseName("ix_users_active_character_id"); + + b.HasIndex("Platform", "PlatformUserId") + .IsUnique() + .HasDatabaseName("ix_users_platform_platform_user_id"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLog", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_activity_logs_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLogMetadata", b => + { + b.HasOne("Crpg.Domain.Entities.ActivityLogs.ActivityLog", null) + .WithMany("Metadata") + .HasForeignKey("ActivityLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_activity_log_metadata_activity_logs_activity_log_id"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighter", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("Fighters") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_fighters_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany() + .HasForeignKey("PartyId") + .HasConstraintName("fk_battle_fighters_parties_party_id"); + + b.HasOne("Crpg.Domain.Entities.Settlements.Settlement", "Settlement") + .WithMany() + .HasForeignKey("SettlementId") + .HasConstraintName("fk_battle_fighters_settlements_settlement_id"); + + b.Navigation("Battle"); + + b.Navigation("Party"); + + b.Navigation("Settlement"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighterApplication", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("FighterApplications") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_fighter_applications_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany() + .HasForeignKey("PartyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_fighter_applications_parties_party_id"); + + b.Navigation("Battle"); + + b.Navigation("Party"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleMercenary", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.BattleMercenaryApplication", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenaries_battle_mercenary_applications_applicatio"); + + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("Mercenaries") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenaries_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Battles.BattleFighter", "CaptainFighter") + .WithMany() + .HasForeignKey("CaptainFighterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenaries_battle_fighters_captain_fighter_id"); + + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenaries_characters_character_id"); + + b.Navigation("Application"); + + b.Navigation("Battle"); + + b.Navigation("CaptainFighter"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleMercenaryApplication", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("MercenaryApplications") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenary_applications_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenary_applications_characters_character_id"); + + b.Navigation("Battle"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Captains.Captain", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithOne("Captain") + .HasForeignKey("Crpg.Domain.Entities.Captains.Captain", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_captains_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Captains.CaptainFormation", b => + { + b.HasOne("Crpg.Domain.Entities.Captains.Captain", "Captain") + .WithMany("Formations") + .HasForeignKey("CaptainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_captain_formation_captains_captain_id"); + + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .HasConstraintName("fk_captain_formation_characters_character_id"); + + b.Navigation("Captain"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany("Characters") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_characters_users_user_id"); + + b.OwnsOne("Crpg.Domain.Entities.Characters.CharacterCharacteristics", "Characteristics", b1 => + { + b1.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.HasKey("CharacterId"); + + b1.ToTable("characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId") + .HasConstraintName("fk_characters_characters_id"); + + b1.OwnsOne("Crpg.Domain.Entities.Characters.CharacterAttributes", "Attributes", b2 => + { + b2.Property("CharacterCharacteristicsCharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("Agility") + .HasColumnType("integer") + .HasColumnName("agility"); + + b2.Property("Points") + .HasColumnType("integer") + .HasColumnName("attribute_points"); + + b2.Property("Strength") + .HasColumnType("integer") + .HasColumnName("strength"); + + b2.HasKey("CharacterCharacteristicsCharacterId"); + + b2.ToTable("characters"); + + b2.WithOwner() + .HasForeignKey("CharacterCharacteristicsCharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b1.OwnsOne("Crpg.Domain.Entities.Characters.CharacterSkills", "Skills", b2 => + { + b2.Property("CharacterCharacteristicsCharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("Athletics") + .HasColumnType("integer") + .HasColumnName("athletics"); + + b2.Property("IronFlesh") + .HasColumnType("integer") + .HasColumnName("iron_flesh"); + + b2.Property("MountedArchery") + .HasColumnType("integer") + .HasColumnName("mounted_archery"); + + b2.Property("Points") + .HasColumnType("integer") + .HasColumnName("skill_points"); + + b2.Property("PowerDraw") + .HasColumnType("integer") + .HasColumnName("power_draw"); + + b2.Property("PowerStrike") + .HasColumnType("integer") + .HasColumnName("power_strike"); + + b2.Property("PowerThrow") + .HasColumnType("integer") + .HasColumnName("power_throw"); + + b2.Property("Riding") + .HasColumnType("integer") + .HasColumnName("riding"); + + b2.Property("Shield") + .HasColumnType("integer") + .HasColumnName("shield"); + + b2.Property("WeaponMaster") + .HasColumnType("integer") + .HasColumnName("weapon_master"); + + b2.HasKey("CharacterCharacteristicsCharacterId"); + + b2.ToTable("characters"); + + b2.WithOwner() + .HasForeignKey("CharacterCharacteristicsCharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b1.OwnsOne("Crpg.Domain.Entities.Characters.CharacterWeaponProficiencies", "WeaponProficiencies", b2 => + { + b2.Property("CharacterCharacteristicsCharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("Bow") + .HasColumnType("integer") + .HasColumnName("bow"); + + b2.Property("Crossbow") + .HasColumnType("integer") + .HasColumnName("crossbow"); + + b2.Property("OneHanded") + .HasColumnType("integer") + .HasColumnName("one_handed"); + + b2.Property("Points") + .HasColumnType("integer") + .HasColumnName("weapon_proficiency_points"); + + b2.Property("Polearm") + .HasColumnType("integer") + .HasColumnName("polearm"); + + b2.Property("Throwing") + .HasColumnType("integer") + .HasColumnName("throwing"); + + b2.Property("TwoHanded") + .HasColumnType("integer") + .HasColumnName("two_handed"); + + b2.HasKey("CharacterCharacteristicsCharacterId"); + + b2.ToTable("characters"); + + b2.WithOwner() + .HasForeignKey("CharacterCharacteristicsCharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b1.Navigation("Attributes") + .IsRequired(); + + b1.Navigation("Skills") + .IsRequired(); + + b1.Navigation("WeaponProficiencies") + .IsRequired(); + }); + + b.OwnsOne("Crpg.Domain.Entities.Characters.CharacterRating", "Rating", b1 => + { + b1.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("CompetitiveValue") + .HasColumnType("real") + .HasColumnName("competitive_rating"); + + b1.Property("Deviation") + .HasColumnType("real") + .HasColumnName("rating_deviation"); + + b1.Property("Value") + .HasColumnType("real") + .HasColumnName("rating"); + + b1.Property("Volatility") + .HasColumnType("real") + .HasColumnName("rating_volatility"); + + b1.HasKey("CharacterId"); + + b1.ToTable("characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Characters.CharacterStatistics", "Statistics", b1 => + { + b1.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("Assists") + .HasColumnType("integer") + .HasColumnName("assists"); + + b1.Property("Deaths") + .HasColumnType("integer") + .HasColumnName("deaths"); + + b1.Property("Kills") + .HasColumnType("integer") + .HasColumnName("kills"); + + b1.Property("PlayTime") + .HasColumnType("interval") + .HasColumnName("play_time"); + + b1.HasKey("CharacterId"); + + b1.ToTable("characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b.Navigation("Characteristics") + .IsRequired(); + + b.Navigation("Rating") + .IsRequired(); + + b.Navigation("Statistics") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("ArmoryBorrowedItems") + .HasForeignKey("BorrowerClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_clans_borrower_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Clans.ClanMember", "Borrower") + .WithMany("ArmoryBorrowedItems") + .HasForeignKey("BorrowerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_clan_members_borrower_user_id"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithOne("ClanArmoryBorrowedItem") + .HasForeignKey("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", "UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_user_items_user_item_id"); + + b.HasOne("Crpg.Domain.Entities.Items.ClanArmoryItem", "ArmoryItem") + .WithOne("BorrowedItem") + .HasForeignKey("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", "UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_clan_armory_items_user_item_id"); + + b.Navigation("ArmoryItem"); + + b.Navigation("Borrower"); + + b.Navigation("Clan"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanInvitation", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("Invitations") + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_invitations_clans_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "Invitee") + .WithMany() + .HasForeignKey("InviteeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_invitations_users_invitee_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "Inviter") + .WithMany() + .HasForeignKey("InviterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_invitations_users_inviter_id"); + + b.Navigation("Clan"); + + b.Navigation("Invitee"); + + b.Navigation("Inviter"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanMember", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("Members") + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_members_clans_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithOne("ClanMembership") + .HasForeignKey("Crpg.Domain.Entities.Clans.ClanMember", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_members_users_user_id"); + + b.Navigation("Clan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ClanArmoryItem", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("ArmoryItems") + .HasForeignKey("LenderClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_items_clans_lender_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Clans.ClanMember", "Lender") + .WithMany("ArmoryItems") + .HasForeignKey("LenderUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_items_clan_members_lender_user_id"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithOne("ClanArmoryItem") + .HasForeignKey("Crpg.Domain.Entities.Items.ClanArmoryItem", "UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_items_user_items_user_item_id"); + + b.Navigation("Clan"); + + b.Navigation("Lender"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.EquippedItem", b => + { + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany("EquippedItems") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_equipped_items_characters_character_id"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithMany("EquippedItems") + .HasForeignKey("UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_equipped_items_user_items_user_item_id"); + + b.Navigation("Character"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.Item", b => + { + b.OwnsOne("Crpg.Domain.Entities.Items.ItemWeaponComponent", "PrimaryWeapon", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("Accuracy") + .HasColumnType("integer") + .HasColumnName("primary_accuracy"); + + b1.Property("Balance") + .HasColumnType("real") + .HasColumnName("primary_balance"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("primary_body_armor"); + + b1.Property("Class") + .HasColumnType("weapon_class") + .HasColumnName("primary_class"); + + b1.Property("Flags") + .HasColumnType("bigint") + .HasColumnName("primary_flags"); + + b1.Property("Handling") + .HasColumnType("integer") + .HasColumnName("primary_handling"); + + b1.Property("ItemUsage") + .IsRequired() + .HasColumnType("text") + .HasColumnName("primary_weapon_item_usage"); + + b1.Property("Length") + .HasColumnType("integer") + .HasColumnName("primary_length"); + + b1.Property("MissileSpeed") + .HasColumnType("integer") + .HasColumnName("primary_missile_speed"); + + b1.Property("StackAmount") + .HasColumnType("integer") + .HasColumnName("primary_stack_amount"); + + b1.Property("SwingDamage") + .HasColumnType("integer") + .HasColumnName("primary_swing_damage"); + + b1.Property("SwingDamageType") + .HasColumnType("damage_type") + .HasColumnName("primary_swing_damage_type"); + + b1.Property("SwingSpeed") + .HasColumnType("integer") + .HasColumnName("primary_swing_speed"); + + b1.Property("ThrustDamage") + .HasColumnType("integer") + .HasColumnName("primary_thrust_damage"); + + b1.Property("ThrustDamageType") + .HasColumnType("damage_type") + .HasColumnName("primary_thrust_damage_type"); + + b1.Property("ThrustSpeed") + .HasColumnType("integer") + .HasColumnName("primary_thrust_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemWeaponComponent", "SecondaryWeapon", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("Accuracy") + .HasColumnType("integer") + .HasColumnName("secondary_accuracy"); + + b1.Property("Balance") + .HasColumnType("real") + .HasColumnName("secondary_balance"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("secondary_body_armor"); + + b1.Property("Class") + .HasColumnType("weapon_class") + .HasColumnName("secondary_class"); + + b1.Property("Flags") + .HasColumnType("bigint") + .HasColumnName("secondary_flags"); + + b1.Property("Handling") + .HasColumnType("integer") + .HasColumnName("secondary_handling"); + + b1.Property("ItemUsage") + .IsRequired() + .HasColumnType("text") + .HasColumnName("secondary_weapon_item_usage"); + + b1.Property("Length") + .HasColumnType("integer") + .HasColumnName("secondary_length"); + + b1.Property("MissileSpeed") + .HasColumnType("integer") + .HasColumnName("secondary_missile_speed"); + + b1.Property("StackAmount") + .HasColumnType("integer") + .HasColumnName("secondary_stack_amount"); + + b1.Property("SwingDamage") + .HasColumnType("integer") + .HasColumnName("secondary_swing_damage"); + + b1.Property("SwingDamageType") + .HasColumnType("damage_type") + .HasColumnName("secondary_swing_damage_type"); + + b1.Property("SwingSpeed") + .HasColumnType("integer") + .HasColumnName("secondary_swing_speed"); + + b1.Property("ThrustDamage") + .HasColumnType("integer") + .HasColumnName("secondary_thrust_damage"); + + b1.Property("ThrustDamageType") + .HasColumnType("damage_type") + .HasColumnName("secondary_thrust_damage_type"); + + b1.Property("ThrustSpeed") + .HasColumnType("integer") + .HasColumnName("secondary_thrust_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemWeaponComponent", "TertiaryWeapon", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("Accuracy") + .HasColumnType("integer") + .HasColumnName("tertiary_accuracy"); + + b1.Property("Balance") + .HasColumnType("real") + .HasColumnName("tertiary_balance"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("tertiary_body_armor"); + + b1.Property("Class") + .HasColumnType("weapon_class") + .HasColumnName("tertiary_class"); + + b1.Property("Flags") + .HasColumnType("bigint") + .HasColumnName("tertiary_flags"); + + b1.Property("Handling") + .HasColumnType("integer") + .HasColumnName("tertiary_handling"); + + b1.Property("ItemUsage") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tertiary_weapon_item_usage"); + + b1.Property("Length") + .HasColumnType("integer") + .HasColumnName("tertiary_length"); + + b1.Property("MissileSpeed") + .HasColumnType("integer") + .HasColumnName("tertiary_missile_speed"); + + b1.Property("StackAmount") + .HasColumnType("integer") + .HasColumnName("tertiary_stack_amount"); + + b1.Property("SwingDamage") + .HasColumnType("integer") + .HasColumnName("tertiary_swing_damage"); + + b1.Property("SwingDamageType") + .HasColumnType("damage_type") + .HasColumnName("tertiary_swing_damage_type"); + + b1.Property("SwingSpeed") + .HasColumnType("integer") + .HasColumnName("tertiary_swing_speed"); + + b1.Property("ThrustDamage") + .HasColumnType("integer") + .HasColumnName("tertiary_thrust_damage"); + + b1.Property("ThrustDamageType") + .HasColumnType("damage_type") + .HasColumnName("tertiary_thrust_damage_type"); + + b1.Property("ThrustSpeed") + .HasColumnType("integer") + .HasColumnName("tertiary_thrust_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemArmorComponent", "Armor", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("ArmArmor") + .HasColumnType("integer") + .HasColumnName("armor_arm"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("armor_body"); + + b1.Property("FamilyType") + .HasColumnType("integer") + .HasColumnName("armor_family_type"); + + b1.Property("HeadArmor") + .HasColumnType("integer") + .HasColumnName("armor_head"); + + b1.Property("LegArmor") + .HasColumnType("integer") + .HasColumnName("armor_leg"); + + b1.Property("MaterialType") + .HasColumnType("integer") + .HasColumnName("armor_material_type"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemMountComponent", "Mount", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("BodyLength") + .HasColumnType("integer") + .HasColumnName("mount_body_length"); + + b1.Property("ChargeDamage") + .HasColumnType("integer") + .HasColumnName("mount_charge_damage"); + + b1.Property("FamilyType") + .HasColumnType("integer") + .HasColumnName("mount_family_type"); + + b1.Property("HitPoints") + .HasColumnType("integer") + .HasColumnName("mount_hit_points"); + + b1.Property("Maneuver") + .HasColumnType("integer") + .HasColumnName("mount_maneuver"); + + b1.Property("Speed") + .HasColumnType("integer") + .HasColumnName("mount_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.Navigation("Armor"); + + b.Navigation("Mount"); + + b.Navigation("PrimaryWeapon"); + + b.Navigation("SecondaryWeapon"); + + b.Navigation("TertiaryWeapon"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItem", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", "Item") + .WithMany("UserItems") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_items_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany("Items") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_items_users_user_id"); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Limitations.CharacterLimitations", b => + { + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithOne("Limitations") + .HasForeignKey("Crpg.Domain.Entities.Limitations.CharacterLimitations", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_character_limitations_characters_character_id"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.Party", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithOne("Party") + .HasForeignKey("Crpg.Domain.Entities.Parties.Party", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_parties_users_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "TargetedParty") + .WithMany() + .HasForeignKey("TargetedPartyId") + .HasConstraintName("fk_parties_parties_targeted_party_id"); + + b.HasOne("Crpg.Domain.Entities.Settlements.Settlement", "TargetedSettlement") + .WithMany() + .HasForeignKey("TargetedSettlementId") + .HasConstraintName("fk_parties_settlements_targeted_settlement_id"); + + b.Navigation("TargetedParty"); + + b.Navigation("TargetedSettlement"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.PartyItem", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_party_items_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany("Items") + .HasForeignKey("PartyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_party_items_parties_party_id"); + + b.Navigation("Item"); + + b.Navigation("Party"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Restrictions.Restriction", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "RestrictedByUser") + .WithMany() + .HasForeignKey("RestrictedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_restrictions_users_restricted_by_user_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "RestrictedUser") + .WithMany("Restrictions") + .HasForeignKey("RestrictedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_restrictions_users_restricted_user_id"); + + b.Navigation("RestrictedByUser"); + + b.Navigation("RestrictedUser"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.Settlement", b => + { + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Owner") + .WithMany("OwnedSettlements") + .HasForeignKey("OwnerId") + .HasConstraintName("fk_settlements_parties_owner_id"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.SettlementItem", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_settlement_items_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Settlements.Settlement", "Settlement") + .WithMany("Items") + .HasForeignKey("SettlementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_settlement_items_settlements_settlement_id"); + + b.Navigation("Item"); + + b.Navigation("Settlement"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => + { + b.HasOne("Crpg.Domain.Entities.Characters.Character", "ActiveCharacter") + .WithOne() + .HasForeignKey("Crpg.Domain.Entities.Users.User", "ActiveCharacterId") + .HasConstraintName("fk_users_characters_active_character_id"); + + b.Navigation("ActiveCharacter"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLog", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.Battle", b => + { + b.Navigation("FighterApplications"); + + b.Navigation("Fighters"); + + b.Navigation("Mercenaries"); + + b.Navigation("MercenaryApplications"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Captains.Captain", b => + { + b.Navigation("Formations"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => + { + b.Navigation("EquippedItems"); + + b.Navigation("Limitations"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.Clan", b => + { + b.Navigation("ArmoryBorrowedItems"); + + b.Navigation("ArmoryItems"); + + b.Navigation("Invitations"); + + b.Navigation("Members"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanMember", b => + { + b.Navigation("ArmoryBorrowedItems"); + + b.Navigation("ArmoryItems"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ClanArmoryItem", b => + { + b.Navigation("BorrowedItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.Item", b => + { + b.Navigation("UserItems"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItem", b => + { + b.Navigation("ClanArmoryBorrowedItem"); + + b.Navigation("ClanArmoryItem"); + + b.Navigation("EquippedItems"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.Party", b => + { + b.Navigation("Items"); + + b.Navigation("OwnedSettlements"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.Settlement", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => + { + b.Navigation("Captain"); + + b.Navigation("Characters"); + + b.Navigation("ClanMembership"); + + b.Navigation("Items"); + + b.Navigation("Party"); + + b.Navigation("Restrictions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/Migrations/20240307120749_Captain.cs b/src/Persistence/Migrations/20240307120749_Captain.cs new file mode 100644 index 000000000..df3b5cb2c --- /dev/null +++ b/src/Persistence/Migrations/20240307120749_Captain.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Crpg.Persistence.Migrations +{ + /// + public partial class Captain : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "captains", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "integer", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_captains", x => x.id); + table.ForeignKey( + name: "fk_captains_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "captain_formation", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + number = table.Column(type: "integer", nullable: false), + captain_id = table.Column(type: "integer", nullable: false), + character_id = table.Column(type: "integer", nullable: true), + weight = table.Column(type: "integer", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_captain_formation", x => x.id); + table.ForeignKey( + name: "fk_captain_formation_captains_captain_id", + column: x => x.captain_id, + principalTable: "captains", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_captain_formation_characters_character_id", + column: x => x.character_id, + principalTable: "characters", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_captain_formation_captain_id", + table: "captain_formation", + column: "captain_id"); + + migrationBuilder.CreateIndex( + name: "ix_captain_formation_character_id", + table: "captain_formation", + column: "character_id"); + + migrationBuilder.CreateIndex( + name: "ix_captains_user_id", + table: "captains", + column: "user_id", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "captain_formation"); + + migrationBuilder.DropTable( + name: "captains"); + } + } +} diff --git a/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs b/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs index d4e5b2d09..c7ba4118a 100644 --- a/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs +++ b/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs @@ -351,6 +351,82 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("battle_mercenary_applications", (string)null); }); + modelBuilder.Entity("Crpg.Domain.Entities.Captains.Captain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_captains"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ix_captains_user_id"); + + b.ToTable("captains", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Captains.CaptainFormation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CaptainId") + .HasColumnType("integer") + .HasColumnName("captain_id"); + + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Number") + .HasColumnType("integer") + .HasColumnName("number"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Weight") + .HasColumnType("integer") + .HasColumnName("weight"); + + b.HasKey("Id") + .HasName("pk_captain_formation"); + + b.HasIndex("CaptainId") + .HasDatabaseName("ix_captain_formation_captain_id"); + + b.HasIndex("CharacterId") + .HasDatabaseName("ix_captain_formation_character_id"); + + b.ToTable("captain_formation", (string)null); + }); + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => { b.Property("Id") @@ -1298,6 +1374,37 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Character"); }); + modelBuilder.Entity("Crpg.Domain.Entities.Captains.Captain", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithOne("Captain") + .HasForeignKey("Crpg.Domain.Entities.Captains.Captain", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_captains_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Captains.CaptainFormation", b => + { + b.HasOne("Crpg.Domain.Entities.Captains.Captain", "Captain") + .WithMany("Formations") + .HasForeignKey("CaptainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_captain_formation_captains_captain_id"); + + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .HasConstraintName("fk_captain_formation_characters_character_id"); + + b.Navigation("Captain"); + + b.Navigation("Character"); + }); + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => { b.HasOne("Crpg.Domain.Entities.Users.User", "User") @@ -2189,6 +2296,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("MercenaryApplications"); }); + modelBuilder.Entity("Crpg.Domain.Entities.Captains.Captain", b => + { + b.Navigation("Formations"); + }); + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => { b.Navigation("EquippedItems"); @@ -2249,6 +2361,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => { + b.Navigation("Captain"); + b.Navigation("Characters"); b.Navigation("ClanMembership"); diff --git a/src/WebApi/Controllers/UsersController.cs b/src/WebApi/Controllers/UsersController.cs index 133101315..eceb710b6 100644 --- a/src/WebApi/Controllers/UsersController.cs +++ b/src/WebApi/Controllers/UsersController.cs @@ -1,5 +1,8 @@ using System.Net; using Crpg.Application.ActivityLogs.Models; +using Crpg.Application.Captains.Commands; +using Crpg.Application.Captains.Models; +using Crpg.Application.Captains.Queries; using Crpg.Application.Characters.Commands; using Crpg.Application.Characters.Models; using Crpg.Application.Characters.Queries; @@ -11,7 +14,6 @@ using Crpg.Application.Items.Queries; using Crpg.Application.Limitations.Models; using Crpg.Application.Limitations.Queries; -using Crpg.Application.Parties.Commands; using Crpg.Application.Restrictions.Models; using Crpg.Application.Restrictions.Queries; using Crpg.Application.Users.Commands; @@ -200,6 +202,54 @@ public Task>> RewardUser([FromRoute] int id, public Task DeleteUser() => ResultToActionAsync(Mediator.Send(new DeleteUserCommand { UserId = CurrentUser.User!.Id })); + /// + /// Gets the current user's captain. + /// + /// Ok. + /// Captain not found. + [HttpGet("self/captain/")] + public Task>> GetUserCaptain() => + ResultToActionAsync(Mediator.Send(new GetUserCaptainCommand + { UserId = CurrentUser.User!.Id })); + + /// + /// Gets the current user's formations. + /// + /// Ok. + /// Captain not found. + [HttpGet("self/captain/formations")] + public Task>>> GetUserCaptainFormations() => + ResultToActionAsync(Mediator.Send(new GetUserCaptainFormationsQuery + { UserId = CurrentUser.User!.Id })); + + /// + /// Assigns a character to the selected formation. + /// + /// Formation id. + /// Ok. + /// Captain not found. + [HttpPut("self/captain/{id}/assign/character")] + public Task>> AssignFormationCharacter([FromRoute] int id, + [FromBody] UpdateFormationCharacterCommand req) + { + req = req with { CharacterId = req.CharacterId, Number = id, UserId = CurrentUser.User!.Id }; + return ResultToActionAsync(Mediator.Send(req)); + } + + /// + /// Assigns a character to the selected formation. + /// + /// Formation id. + /// Ok. + /// Captain not found. + [HttpPut("self/captain/{id}/assign/weight")] + public Task>> AssignFormationWeight([FromRoute] int id, + [FromBody] UpdateFormationWeightCommand req) + { + req = req with { Number = id, UserId = CurrentUser.User!.Id, Weight = req.Weight }; + return ResultToActionAsync(Mediator.Send(req)); + } + /// /// Gets the specified current user's character. /// diff --git a/src/WebUI/index.html b/src/WebUI/index.html index 4eccbe140..345b8e17e 100644 --- a/src/WebUI/index.html +++ b/src/WebUI/index.html @@ -14,8 +14,8 @@ { "@context": "https://schema.org", "@type": "Organization", - "url": "https://c-rpg.eu", - "logo": "https://c-rpg.eu/crpg.png" + "url": "https://namidaka.fr", + "logo": "https://namidaka.fr/crpg.png" } cRPG: Multiplayer Mod for Mount & Blade Bannerlord diff --git a/src/WebUI/locales/cn.yml b/src/WebUI/locales/cn.yml index 56f3994a7..2607200d6 100644 --- a/src/WebUI/locales/cn.yml +++ b/src/WebUI/locales/cn.yml @@ -74,6 +74,7 @@ nav: Characters: 角色 Shop: 商店 Clans: 氏族 + Captain: 队长 Moderator: 管理员 Builder: 生成器 Leaderboard: 排行榜 @@ -191,6 +192,7 @@ game-mode: CRPGSiege: 围困 CRPGTeamDeathmatch: 团队死亡竞赛 CRPGSkirmish: 小规模战斗 + CRPGCaptain: 队长 clanBalancingExplanation: '

你不在任何宗族中

@@ -1332,6 +1334,19 @@ item: filter: all: 一应俱全 +captain: + title: 船长编队 + formation: + title: 编队 {id} + update: + notify: + success: 编队更新成功 + character: + title: 选定的部队 + remove: 从编队中移除 + weight: + title: 编队重量 + leaderboard: title: 排行榜 table: diff --git a/src/WebUI/locales/en.yml b/src/WebUI/locales/en.yml index f8cc43548..dee8c6a54 100644 --- a/src/WebUI/locales/en.yml +++ b/src/WebUI/locales/en.yml @@ -125,6 +125,7 @@ nav: Characters: Characters Shop: Shop Clans: Clans + Captain: Captain Moderator: Moderator Builder: Builder Leaderboard: Leaderboard @@ -270,6 +271,7 @@ game-mode: CRPGTeamDeathmatch: Team Deathmatch CRPGSkirmish: Skirmish + CRPGCaptain: Captain clanBalancingExplanation: "

You're not in any clan

@@ -318,6 +320,7 @@ character: empty: title: You don't have a character yet desc: To create a character, simply join a cRPG server + name: Select Character create: title: New character @@ -348,6 +351,7 @@ character: Archer: Archer Cavalry: Cavalry MountedArcher: Mounted archer + NotFound: The character class could not be found statistics: generation: @@ -1455,6 +1459,45 @@ activityLog: ClanArmoryBorrowItem: 'Item {userItemId} was borrowed from clan {clanId} armory' CharacterEarned: 'The character {characterId} earned {experience} experience and {gold} gold in the {gameMode} game mode' +captain: + title: Captain Formations + formation: + title: Formation {id} + update: + notify: + success: Formations updated successfully + character: + title: Selected Troop + remove: Remove from Formation + weight: + title: Formation Weight + +captain: + title: Captain Formations + formation: + title: Formation {id} + update: + notify: + success: Formations updated successfully + character: + title: Selected Troop + remove: Remove from Formation + weight: + title: Formation Weight + +captain: + title: Captain Formations + formation: + title: Formation {id} + update: + notify: + success: Formations updated successfully + character: + title: Selected Troop + remove: Remove from Formation + weight: + title: Formation Weight + leaderboard: title: Leaderboard table: diff --git a/src/WebUI/locales/ru.yml b/src/WebUI/locales/ru.yml index fdf740767..4b93a6fce 100644 --- a/src/WebUI/locales/ru.yml +++ b/src/WebUI/locales/ru.yml @@ -124,6 +124,7 @@ nav: Characters: Персонаж Shop: Магазин Clans: Кланы + Captain: капитан Moderator: Модератор Builder: Калькулятор Leaderboard: Лидеры @@ -240,6 +241,7 @@ game-mode: CRPGSiege: Осада CRPGTeamDeathmatch: Командный смертельный бой CRPGSkirmish: Скирмиш + CRPGCaptain: капитан clanBalancingExplanation: '

Вы не в клане

@@ -1392,6 +1394,19 @@ item: filter: all: Все +captain: + title: Формации Капитана + formation: + title: Формация {id} + update: + notify: + success: Формации успешно обновлены + character: + title: Выбранный Отряд + remove: Удалить из Формации + weight: + title: Вес Формации + leaderboard: title: Таблица лидеров table: diff --git a/src/WebUI/public/.well-known/security.txt b/src/WebUI/public/.well-known/security.txt index dda4e6742..36a902f0a 100644 --- a/src/WebUI/public/.well-known/security.txt +++ b/src/WebUI/public/.well-known/security.txt @@ -1,3 +1,3 @@ -Contact: mailto:hello@c-rpg.eu +Contact: mailto:hello@namidaka.fr Preferred-Languages: en, fr Policy: https://raw.githubusercontent.com/verdie-g/crpg/master/SECURITY.md diff --git a/src/WebUI/public/robots.txt b/src/WebUI/public/robots.txt index 661d13843..5591095b2 100644 --- a/src/WebUI/public/robots.txt +++ b/src/WebUI/public/robots.txt @@ -1 +1 @@ -Sitemap: https://c-rpg.eu/sitemap.xml +Sitemap: https://namidaka.fr/sitemap.xml diff --git a/src/WebUI/public/sitemap.xml b/src/WebUI/public/sitemap.xml index 3eb6f0b2a..27e646982 100644 --- a/src/WebUI/public/sitemap.xml +++ b/src/WebUI/public/sitemap.xml @@ -1,6 +1,6 @@ - https://c-rpg.eu + https://namidaka.fr weekly diff --git a/src/WebUI/src/assets/themes/oruga-tailwind/icons/game-mode/captain.ts b/src/WebUI/src/assets/themes/oruga-tailwind/icons/game-mode/captain.ts new file mode 100644 index 000000000..77e996aac --- /dev/null +++ b/src/WebUI/src/assets/themes/oruga-tailwind/icons/game-mode/captain.ts @@ -0,0 +1,12 @@ +export default { + prefix: 'crpg', + iconName: 'game-mode-captain', + icon: [ + 32, + 32, + [], + 'e001', + 'M26.663 1.1014c-.553 0-.9907.4372-.9907.9901 0 .5536.4378.9901.9907.9901s.9907-.4366.9907-.9901c0-.553-.4378-.9901-.9907-.9901zm-4.6253 1.2569c-9.2333.0461-12.5626 12.5257-20.9047 5.5446.1872 2.1197 5.0935 4.5158 5.0935 4.5158-1.4993 1.152-2.0033 1.4227-4.1466 1.4688 5.9956 4.9939 19.5085-5.7485 23.5232 2.3098l-.1325-2.2118-2.615-2.6784 2.4595.0346-.0346-.5875-2.9203-1.8547 2.7878-.4147-.0403-.6394-2.88-1.5725 2.759-.5069-.0346-.6157L21.9456 3.8396l2.9088-.337-.0461-.8006c-.985-.2419-1.9008-.3485-2.7706-.3439zm4.0781 1.7263 1.1635 24.4218 1.0771-.0518-1.1635-24.3648c-.3629.0887-.7315.0864-1.0771-.0058zM20.736 16.871l-2.5114 4.032 1.2269 1.44L18.5472 28.3968h1.0886l.8755-5.8925 1.6128-1.1635c-.4666-1.4918-.9274-2.9837-1.3882-4.4698zm-9.0259 1.031-1.6589 4.0205 1.1808 1.1635.1267 5.3107h1.0771l-.1267-5.3568 1.129-1.1462-1.728-3.9917zm-9.11.288-.9446 3.5654 1.0166.7603L3.5274 28.3968h1.087L3.7376 22.3661l.7615-1.0138-1.8967-3.1622zm4.9513.4262-1.6243 3.312.8698.9792-.3859 5.4893H7.488l.3917-5.4893.9158-.8179-1.2442-3.4733zM15.4368 20.4768l-1.1232 3.9398 1.1174.8755.3341 3.1046h1.0886l-.3398-3.1277.9677-1.2442c-.6854-1.1808-1.3651-2.3616-2.0448-3.5482zm8.2714.0058-1.0886 3.9514 1.1693.8928.3571 3.0701H25.2288l-.3686-3.1795.9216-1.2038z', + ], + }; + \ No newline at end of file diff --git a/src/WebUI/src/components/app/MainNavigation.vue b/src/WebUI/src/components/app/MainNavigation.vue index ca0995dfc..4547c64fc 100644 --- a/src/WebUI/src/components/app/MainNavigation.vue +++ b/src/WebUI/src/components/app/MainNavigation.vue @@ -94,6 +94,14 @@ const userStore = useUserStore(); + + {{ $t('nav.main.Captain') }} + + +import { ref, watch } from 'vue'; +import { useUserStore } from '@/stores/user'; +import { getCharacterItems, computeOverallPrice } from '@/services/characters-service'; +import type { CaptainFormation } from '@/models/captain' +import type { Character } from '@/models/character'; + +const props = withDefaults( + defineProps<{ + formation: CaptainFormation | null, + }>(), + { + formation: null, + } +); + +const formationCharacter = ref(null); +const userStore = useUserStore(); + +if (userStore.characters.length === 0) { + await userStore.fetchCharacters(); +} + +const route = useRoute('Captain'); + +const emit = defineEmits<{ + (e: 'update:formation', formation: CaptainFormation): void, +}>(); + +const setFormationCharacter = async (characterId: number | null) => { + emit('update:formation', { characterId, number: props.formation!.number, weight: props.formation!.weight}); +} + +const onWeightChanged = (newWeight: number) => { + emit('update:formation', { characterId: props.formation!.characterId ?? null, number: props.formation!.number, weight: newWeight }); +} + +watch( + () => props.formation?.characterId, + (newCharacterId) => { + formationCharacter.value = userStore.characters.find(c => c.id === newCharacterId) || null; + }, + { + immediate: true, + } +); + + + + diff --git a/src/WebUI/src/components/character/CharacterMedia.vue b/src/WebUI/src/components/character/CharacterMedia.vue index b9e478fc4..8fe36993d 100644 --- a/src/WebUI/src/components/character/CharacterMedia.vue +++ b/src/WebUI/src/components/character/CharacterMedia.vue @@ -1,27 +1,33 @@ -