Scripting: allow precise and safe control of peep animations

This commit is contained in:
Aaron van Geffen 2024-05-05 23:09:41 +02:00 committed by GitHub
parent bf74dfba7b
commit 893392d987
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 492 additions and 3 deletions

View File

@ -1,6 +1,7 @@
0.4.12 (in development)
------------------------------------------------------------------------
- Feature: [#21714] [Plugin] Costume assignment is now tailored to each staff type.
- Feature: [#21913] [Plugin] Allow precise and safe control of peep animations.
0.4.11 (2024-05-05)
------------------------------------------------------------------------

View File

@ -2682,6 +2682,34 @@ declare global {
*/
type PeepType = "guest" | "staff";
type GuestAnimation =
"walking" |
"checkTime" |
"watchRide" |
"eatFood" |
"shakeHead" |
"emptyPockets" |
"holdMat" |
"sittingIdle" |
"sittingEatFood" |
"sittingLookAroundLeft" |
"sittingLookAroundRight" |
"hanging" |
"wow" |
"throwUp" |
"jump" |
"drowning" |
"joy" |
"readMap" |
"wave" |
"wave2" |
"takePhoto" |
"clap" |
"disgust" |
"drawPicture" |
"beingWatched" |
"withdrawMoney";
/**
* Represents a guest.
*/
@ -2819,6 +2847,31 @@ declare global {
* Removes all items from the guest's possession.
*/
removeAllItems(): void;
/**
* The animations available to this guest.
*/
readonly availableAnimations: GuestAnimation[];
/**
* Gets an array of sprite ids representing a particular guest animation.
*/
getAnimationSpriteIds(animation: GuestAnimation, rotation: number): number[];
/**
* The animation the guest is currently exhibiting.
*/
animation: GuestAnimation;
/**
* The frame offset in the current animation.
*/
animationOffset: number;
/**
* The total number of frames in the current animation.
*/
readonly animationLength: number;
}
/**
@ -3113,6 +3166,26 @@ declare global {
"sheriff" |
"pirate";
type StaffAnimation =
"walking" |
"watchRide" |
"wave" |
"hanging" |
"staffMower" |
"staffSweep" |
"drowning" |
"staffAnswerCall" |
"staffAnswerCall2" |
"staffCheckBoard" |
"staffFix" |
"staffFix2" |
"staffFixGround" |
"staffFix3" |
"staffWatering" |
"joy" |
"staffEmptyBin" |
"wave2";
/**
* Represents a staff member.
*/
@ -3146,6 +3219,31 @@ declare global {
* Gets the patrol area for the staff member.
*/
readonly patrolArea: PatrolArea;
/**
* The animations available to this staff member.
*/
readonly availableAnimations: StaffAnimation[];
/**
* Gets an array of sprite ids representing a particular staff animation.
*/
getAnimationSpriteIds(animation: StaffAnimation, rotation: number): number[];
/**
* The animation the staff member is currently exhibiting.
*/
animation: StaffAnimation;
/**
* The frame offset in the current animation.
*/
animationOffset: number;
/**
* The total number of frames in the current animation.
*/
readonly animationLength: number;
}
type StaffType = "handyman" | "mechanic" | "security" | "entertainer";

View File

@ -344,9 +344,15 @@ void Peep::UpdateCurrentActionSpriteType()
return;
}
Invalidate();
ActionSpriteType = newActionSpriteType;
UpdateSpriteBoundingBox();
}
void Peep::UpdateSpriteBoundingBox()
{
Invalidate();
const SpriteBounds* spriteBounds = &GetSpriteBounds(SpriteType, ActionSpriteType);
SpriteData.Width = spriteBounds->sprite_width;
SpriteData.HeightMin = spriteBounds->sprite_height_negative;
@ -2813,7 +2819,14 @@ void Peep::Paint(PaintSession& session, int32_t imageDirection) const
// In the following 4 calls to PaintAddImageAsParent/PaintAddImageAsChild, we add 5 (instead of 3) to the
// bound_box_offset_z to make sure peeps are drawn on top of railways
uint32_t baseImageId = (imageDirection >> 3) + GetPeepAnimation(SpriteType, actionSpriteType).base_image + imageOffset * 4;
uint32_t baseImageId = GetPeepAnimation(SpriteType, actionSpriteType).base_image;
// Offset frame onto the base image, using rotation except for the 'picked up' state
if (actionSpriteType != PeepActionSpriteType::Ui)
baseImageId += (imageDirection >> 3) + imageOffset * 4;
else
baseImageId += imageOffset;
auto imageId = ImageId(baseImageId, TshirtColour, TrousersColour);
auto bb = BoundBoxXYZ{ { 0, 0, z + 5 }, { 1, 1, 11 } };

View File

@ -380,6 +380,7 @@ public: // Peep
void SetState(PeepState new_state);
void Remove();
void UpdateCurrentActionSpriteType();
void UpdateSpriteBoundingBox();
void SwitchToSpecialSprite(uint8_t special_sprite_id);
void StateReset();
[[nodiscard]] uint8_t GetNextDirection() const;

View File

@ -47,7 +47,7 @@ namespace OpenRCT2
namespace OpenRCT2::Scripting
{
static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 86;
static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 87;
// Versions marking breaking changes.
static constexpr int32_t API_VERSION_33_PEEP_DEPRECATION = 33;

View File

@ -13,6 +13,7 @@
# include "../../../entity/Guest.h"
# include "../../../localisation/Localisation.h"
# include "../../../peep/PeepAnimationData.h"
namespace OpenRCT2::Scripting
{
@ -144,6 +145,35 @@ namespace OpenRCT2::Scripting
{ "here_we_are", PeepThoughtType::HereWeAre },
});
static const DukEnumMap<PeepActionSpriteType> availableGuestAnimations({
{ "walking", PeepActionSpriteType::None },
{ "checkTime", PeepActionSpriteType::CheckTime },
{ "watchRide", PeepActionSpriteType::WatchRide },
{ "eatFood", PeepActionSpriteType::EatFood },
{ "shakeHead", PeepActionSpriteType::ShakeHead },
{ "emptyPockets", PeepActionSpriteType::EmptyPockets },
{ "holdMat", PeepActionSpriteType::HoldMat },
{ "sittingIdle", PeepActionSpriteType::SittingIdle },
{ "sittingEatFood", PeepActionSpriteType::SittingEatFood },
{ "sittingLookAroundLeft", PeepActionSpriteType::SittingLookAroundLeft },
{ "sittingLookAroundRight", PeepActionSpriteType::SittingLookAroundRight },
{ "hanging", PeepActionSpriteType::Ui },
{ "wow", PeepActionSpriteType::Wow },
{ "throwUp", PeepActionSpriteType::ThrowUp },
{ "jump", PeepActionSpriteType::Jump },
{ "drowning", PeepActionSpriteType::Drowning },
{ "joy", PeepActionSpriteType::Joy },
{ "readMap", PeepActionSpriteType::ReadMap },
{ "wave", PeepActionSpriteType::Wave },
{ "wave2", PeepActionSpriteType::Wave2 },
{ "takePhoto", PeepActionSpriteType::TakePhoto },
{ "clap", PeepActionSpriteType::Clap },
{ "disgust", PeepActionSpriteType::Disgust },
{ "drawPicture", PeepActionSpriteType::DrawPicture },
{ "beingWatched", PeepActionSpriteType::BeingWatched },
{ "withdrawMoney", PeepActionSpriteType::WithdrawMoney },
});
ScGuest::ScGuest(EntityId id)
: ScPeep(id)
{
@ -174,6 +204,11 @@ namespace OpenRCT2::Scripting
dukglue_register_property(ctx, &ScGuest::lostCountdown_get, &ScGuest::lostCountdown_set, "lostCountdown");
dukglue_register_property(ctx, &ScGuest::thoughts_get, nullptr, "thoughts");
dukglue_register_property(ctx, &ScGuest::items_get, nullptr, "items");
dukglue_register_property(ctx, &ScGuest::availableAnimations_get, nullptr, "availableAnimations");
dukglue_register_property(ctx, &ScGuest::animation_get, &ScGuest::animation_set, "animation");
dukglue_register_property(ctx, &ScGuest::animationOffset_get, &ScGuest::animationOffset_set, "animationOffset");
dukglue_register_property(ctx, &ScGuest::animationLength_get, nullptr, "animationLength");
dukglue_register_method(ctx, &ScGuest::getAnimationSpriteIds, "getAnimationSpriteIds");
dukglue_register_method(ctx, &ScGuest::has_item, "hasItem");
dukglue_register_method(ctx, &ScGuest::give_item, "giveItem");
dukglue_register_method(ctx, &ScGuest::remove_item, "removeItem");
@ -791,6 +826,131 @@ namespace OpenRCT2::Scripting
}
}
std::vector<std::string> ScGuest::availableAnimations_get() const
{
std::vector<std::string> availableAnimations{};
for (auto& animation : availableGuestAnimations)
{
availableAnimations.push_back(std::string(animation.first));
}
return availableAnimations;
}
std::vector<uint32_t> ScGuest::getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const
{
std::vector<uint32_t> spriteIds{};
auto animationType = availableGuestAnimations.TryGet(groupKey);
if (animationType == std::nullopt)
{
return spriteIds;
}
auto peep = GetPeep();
if (peep != nullptr)
{
auto& animationGroup = GetPeepAnimation(peep->SpriteType, *animationType);
for (auto frameOffset : animationGroup.frame_offsets)
{
auto imageId = animationGroup.base_image;
if (animationType != PeepActionSpriteType::Ui)
imageId += rotation + frameOffset * 4;
else
imageId += frameOffset;
spriteIds.push_back(imageId);
}
}
return spriteIds;
}
std::string ScGuest::animation_get() const
{
auto* peep = GetGuest();
if (peep == nullptr)
{
return nullptr;
}
std::string_view action = availableGuestAnimations[peep->ActionSpriteType];
// Special consideration for sitting peeps
// TODO: something funky going on in the state machine
if (peep->ActionSpriteType == PeepActionSpriteType::None && peep->State == PeepState::Sitting)
action = availableGuestAnimations[PeepActionSpriteType::SittingIdle];
return std::string(action);
}
void ScGuest::animation_set(std::string groupKey)
{
ThrowIfGameStateNotMutable();
auto newType = availableGuestAnimations.TryGet(groupKey);
if (newType == std::nullopt)
{
throw DukException() << "Invalid animation for this guest (" << groupKey << ")";
}
auto* peep = GetGuest();
peep->ActionSpriteType = peep->NextActionSpriteType = *newType;
auto offset = 0;
if (peep->IsActionWalking())
peep->WalkingFrameNum = offset;
else
peep->ActionFrame = offset;
auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType);
peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset];
peep->UpdateSpriteBoundingBox();
}
uint8_t ScGuest::animationOffset_get() const
{
auto* peep = GetGuest();
if (peep == nullptr)
{
return 0;
}
if (peep->IsActionWalking())
return peep->WalkingFrameNum;
else
return peep->ActionFrame;
}
void ScGuest::animationOffset_set(uint8_t offset)
{
ThrowIfGameStateNotMutable();
auto* peep = GetGuest();
auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType);
auto length = animationGroup.frame_offsets.size();
offset %= length;
if (peep->IsActionWalking())
peep->WalkingFrameNum = offset;
else
peep->ActionFrame = offset;
peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset];
peep->UpdateSpriteBoundingBox();
}
uint8_t ScGuest::animationLength_get() const
{
auto* peep = GetGuest();
if (peep == nullptr)
{
return 0;
}
auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType);
return static_cast<uint8_t>(animationGroup.frame_offsets.size());
}
ScThought::ScThought(PeepThought backing)
: _backing(backing)
{

View File

@ -14,6 +14,8 @@
# include "../../../entity/Guest.h"
# include "ScPeep.hpp"
enum class PeepActionSpriteType : uint8_t;
namespace OpenRCT2::Scripting
{
static const DukEnumMap<ShopItem> ShopItemMap({
@ -172,6 +174,14 @@ namespace OpenRCT2::Scripting
void give_item(const DukValue& item) const;
void remove_item(const DukValue& item) const;
void remove_all_items() const;
std::vector<std::string> availableAnimations_get() const;
std::vector<uint32_t> getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const;
std::string animation_get() const;
void animation_set(std::string groupKey);
uint8_t animationOffset_get() const;
void animationOffset_set(uint8_t offset);
uint8_t animationLength_get() const;
};
} // namespace OpenRCT2::Scripting

View File

@ -13,9 +13,51 @@
# include "../../../entity/PatrolArea.h"
# include "../../../entity/Staff.h"
# include "../../../peep/PeepAnimationData.h"
namespace OpenRCT2::Scripting
{
static const DukEnumMap<PeepActionSpriteType> availableHandymanAnimations({
{ "walking", PeepActionSpriteType::None },
{ "watchRide", PeepActionSpriteType::WatchRide },
{ "hanging", PeepActionSpriteType::Ui },
{ "staffMower", PeepActionSpriteType::StaffMower },
{ "staffSweep", PeepActionSpriteType::StaffSweep },
{ "drowning", PeepActionSpriteType::Drowning },
{ "staffWatering", PeepActionSpriteType::StaffWatering },
{ "staffEmptyBin", PeepActionSpriteType::StaffEmptyBin },
});
static const DukEnumMap<PeepActionSpriteType> availableMechanicAnimations({
{ "walking", PeepActionSpriteType::None },
{ "watchRide", PeepActionSpriteType::WatchRide },
{ "hanging", PeepActionSpriteType::Ui },
{ "staffAnswerCall", PeepActionSpriteType::StaffAnswerCall },
{ "staffAnswerCall2", PeepActionSpriteType::StaffAnswerCall2 },
{ "staffCheckBoard", PeepActionSpriteType::StaffCheckboard },
{ "staffFix", PeepActionSpriteType::StaffFix },
{ "staffFix2", PeepActionSpriteType::StaffFix2 },
{ "staffFixGround", PeepActionSpriteType::StaffFixGround },
{ "staffFix3", PeepActionSpriteType::StaffFix3 },
});
static const DukEnumMap<PeepActionSpriteType> availableSecurityAnimations({
{ "walking", PeepActionSpriteType::None },
{ "watchRide", PeepActionSpriteType::WatchRide },
{ "hanging", PeepActionSpriteType::Ui },
{ "drowning", PeepActionSpriteType::Drowning },
});
static const DukEnumMap<PeepActionSpriteType> availableEntertainerAnimations({
{ "walking", PeepActionSpriteType::None },
{ "watchRide", PeepActionSpriteType::WatchRide },
{ "wave", PeepActionSpriteType::EatFood }, // NB: this not a typo
{ "hanging", PeepActionSpriteType::Ui },
{ "drowning", PeepActionSpriteType::Drowning },
{ "joy", PeepActionSpriteType::Joy },
{ "wave2", PeepActionSpriteType::Wave2 },
});
ScStaff::ScStaff(EntityId Id)
: ScPeep(Id)
{
@ -30,6 +72,11 @@ namespace OpenRCT2::Scripting
dukglue_register_property(ctx, &ScStaff::costume_get, &ScStaff::costume_set, "costume");
dukglue_register_property(ctx, &ScStaff::patrolArea_get, nullptr, "patrolArea");
dukglue_register_property(ctx, &ScStaff::orders_get, &ScStaff::orders_set, "orders");
dukglue_register_property(ctx, &ScStaff::availableAnimations_get, nullptr, "availableAnimations");
dukglue_register_property(ctx, &ScStaff::animation_get, &ScStaff::animation_set, "animation");
dukglue_register_property(ctx, &ScStaff::animationOffset_get, &ScStaff::animationOffset_set, "animationOffset");
dukglue_register_property(ctx, &ScStaff::animationLength_get, nullptr, "animationLength");
dukglue_register_method(ctx, &ScStaff::getAnimationSpriteIds, "getAnimationSpriteIds");
}
Staff* ScStaff::GetStaff() const
@ -243,6 +290,153 @@ namespace OpenRCT2::Scripting
}
}
const DukEnumMap<PeepActionSpriteType>& ScStaff::animationsByStaffType(StaffType staffType) const
{
switch (staffType)
{
case StaffType::Handyman:
return availableHandymanAnimations;
case StaffType::Mechanic:
return availableMechanicAnimations;
case StaffType::Security:
return availableSecurityAnimations;
case StaffType::Entertainer:
default:
return availableEntertainerAnimations;
}
}
std::vector<std::string> ScStaff::availableAnimations_get() const
{
std::vector<std::string> availableAnimations{};
auto* peep = GetStaff();
if (peep != nullptr)
{
for (auto& animation : animationsByStaffType(peep->AssignedStaffType))
{
availableAnimations.push_back(std::string(animation.first));
}
}
return availableAnimations;
}
std::vector<uint32_t> ScStaff::getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const
{
std::vector<uint32_t> spriteIds{};
auto* peep = GetStaff();
if (peep == nullptr)
{
return spriteIds;
}
auto& animationGroups = animationsByStaffType(peep->AssignedStaffType);
auto animationType = animationGroups.TryGet(groupKey);
if (animationType == std::nullopt)
{
return spriteIds;
}
auto& animationGroup = GetPeepAnimation(peep->SpriteType, *animationType);
for (auto frameOffset : animationGroup.frame_offsets)
{
auto imageId = animationGroup.base_image;
if (animationType != PeepActionSpriteType::Ui)
imageId += rotation + frameOffset * 4;
else
imageId += frameOffset;
spriteIds.push_back(imageId);
}
return spriteIds;
}
std::string ScStaff::animation_get() const
{
auto* peep = GetStaff();
if (peep == nullptr)
{
return nullptr;
}
auto& animationGroups = animationsByStaffType(peep->AssignedStaffType);
std::string_view action = animationGroups[peep->ActionSpriteType];
return std::string(action);
}
void ScStaff::animation_set(std::string groupKey)
{
ThrowIfGameStateNotMutable();
auto* peep = GetStaff();
auto& animationGroups = animationsByStaffType(peep->AssignedStaffType);
auto newType = animationGroups.TryGet(groupKey);
if (newType == std::nullopt)
{
throw DukException() << "Invalid animation for this staff member (" << groupKey << ")";
}
peep->ActionSpriteType = peep->NextActionSpriteType = *newType;
auto offset = 0;
if (peep->IsActionWalking())
peep->WalkingFrameNum = offset;
else
peep->ActionFrame = offset;
auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType);
peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset];
peep->UpdateSpriteBoundingBox();
}
uint8_t ScStaff::animationOffset_get() const
{
auto* peep = GetStaff();
if (peep == nullptr)
{
return 0;
}
if (peep->IsActionWalking())
return peep->WalkingFrameNum;
else
return peep->ActionFrame;
}
void ScStaff::animationOffset_set(uint8_t offset)
{
ThrowIfGameStateNotMutable();
auto* peep = GetStaff();
auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType);
auto length = animationGroup.frame_offsets.size();
offset %= length;
if (peep->IsActionWalking())
peep->WalkingFrameNum = offset;
else
peep->ActionFrame = offset;
peep->ActionSpriteImageOffset = animationGroup.frame_offsets[offset];
peep->UpdateSpriteBoundingBox();
}
uint8_t ScStaff::animationLength_get() const
{
auto* peep = GetStaff();
if (peep == nullptr)
{
return 0;
}
auto& animationGroup = GetPeepAnimation(peep->SpriteType, peep->ActionSpriteType);
return static_cast<uint8_t>(animationGroup.frame_offsets.size());
}
ScPatrolArea::ScPatrolArea(EntityId id)
: _staffId(id)
{

View File

@ -15,6 +15,9 @@
# include <memory>
enum class PeepActionSpriteType : uint8_t;
enum class StaffType : uint8_t;
namespace OpenRCT2::Scripting
{
class ScPatrolArea
@ -64,6 +67,15 @@ namespace OpenRCT2::Scripting
uint8_t orders_get() const;
void orders_set(uint8_t value);
const DukEnumMap<PeepActionSpriteType>& animationsByStaffType(StaffType staffType) const;
std::vector<uint32_t> getAnimationSpriteIds(std::string groupKey, uint8_t rotation) const;
std::vector<std::string> availableAnimations_get() const;
std::string animation_get() const;
void animation_set(std::string groupKey);
uint8_t animationOffset_get() const;
void animationOffset_set(uint8_t offset);
uint8_t animationLength_get() const;
};
} // namespace OpenRCT2::Scripting