Docs, is-named bool functions and MSVC utf-8

This commit is contained in:
shweet committed 2026-11-12 21:48:04 -05:00
1 parent 1a0db1abbd
commit 1cf4d1ea85
18 files changed
+443 -586

No files matched your search

+4
View File
@@ -13,6 +13,10 @@ if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_BUILD_TYPE Release) set(CMAKE_BUILD_TYPE Release)
endif() endif()
if(MSVC)
add_compile_options(/utf-8)
endif()
# Release builds: -O2, link-time optimization, unused code and data dropped, and the symbols stripped. # Release builds: -O2, link-time optimization, unused code and data dropped, and the symbols stripped.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux") if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
+284 -482
View File
@@ -4,526 +4,328 @@ Want to add characters or modify existing ones or mod Shweet's Sim in general or
# Animation Format # Animation Format
Shweet's Sim uses a semi-proprietary format called ".anm2" for tweened character animations, sourced from a spritesheet. This file format comes from the game _The Binding of Isaac: Rebirth_ (a game I mod on my own time) for animations. Shweet's Sim uses ".anm2" files for tweened character animations, sourced from spritesheets. The format comes from the game _The Binding of Isaac: Rebirth_ (a game I mod on my own time), but the game reads the JSON flavor of it saved by my own animation editor, [Anm2Ed](https://github.com/ShweetsStuff/anm2ed). In Anm2Ed, turn on Save as JSON (Settings → Configure → Serialization), or convert an existing file with `anm2ed convert file.anm2 --format json`. Keep the `.anm2` extension. Isaac's own editor can't open these.
You can either use my own animation editor for that game and this one (recommended), [Anm2Ed](https://github.com/ShweetsStuff/anm2ed), or if you have that game on Steam, you can find that game's own proprietary animation editor in that game's Steam folder and then in tools/IsaacAnimationEditor. I can't guarantee the stability or perfect efficacy of either, but Anm2Ed has crash mitigations and autosave (but of course, save often, for whatever you do). Anm2Ed has "Regions", which let you set spritesheet regions (areas in the spritesheet that can be repurposed) to quickly reference in animations; previously, one would have to manually create a perfectly spaced spritesheet and then manually set values; this is no longer the case, so using "Regions" is recommended.
Anm2Ed has slightly more extensions to the base format; in particular, something called "Regions", which allows you to set spritesheet regions (areas in the spritesheet that can be repurposed) to quickly reference in animations; previously, one would have to manually create a perfectly spaced spritesheet and then manually set values; this is no longer the case, so using "Regions" is recommended. For the leanest files, in Anm2Ed, go to Settings -> Configure -> File -> Compatibility, and set it to "Anm2Ed Limited".
My process for making characters is that they're typically comprised of many "cells" (small graphics). I make these cells their own file, and then in the editor, I drag them onto "Spritesheets" and then right click -> merge them, with the "Make Spritesheet Regions" option enabled. Then, right click -> Pack on a spritesheet to optimize it all nice for one big spritesheet atlas with everything in one place. It won't produce a very comprehensible spritesheet, but it works just fine (not my problem). Make sure to save the spritesheets you're using once you're done. You can sort regions around by clicking, holding and moving them around. My process for making characters is that they're typically comprised of many "cells" (small graphics). I make these cells their own file, and then in the editor, I drag them onto "Spritesheets" and then right click -> merge them, with the "Make Spritesheet Regions" option enabled. Then, right click -> Pack on a spritesheet to optimize it all nice for one big spritesheet atlas with everything in one place. It won't produce a very comprehensible spritesheet, but it works just fine (not my problem). Make sure to save the spritesheets you're using once you're done. You can sort regions around by clicking, holding and moving them around.
If you'd like a quick primer on how to use either program, check either of [these](https://www.youtube.com/watch?v=qU_GMo2l7NY) [videos](https://www.youtube.com/watch?v=QiXVM6Gwzlw) out. Much of the information is interchangeable between the programs, though both are a little out of date on their current version. If you'd like a quick primer on how to use the editor, check either of [these](https://www.youtube.com/watch?v=qU_GMo2l7NY) [videos](https://www.youtube.com/watch?v=QiXVM6Gwzlw) out. They're a little out of date, but much of it still applies.
Stage-specific character animations are controlled by `AppendStringForAnimations` on each stage in `character.xml`. The base stage uses no suffix. Later stages append that stage's configured suffix. The game plays spritesheets, regions, layers, nulls, layer groups, events, trigger sounds, and the position, crop, pivot, scale, rotation, tint, color offset, visibility and easing of keyframes. It doesn't play skew, depth, blur, custom shaders or animation layers. Animation groups (the editor's folders) are fine; the game just looks animations up by name, so keep those unique.
Stage-specific character animations are controlled by `animationSuffix` on each stage in `character.json`. The base stage uses no suffix. Later stages append that stage's suffix.
Example: Example:
- stage 1: `Look` - stage 1: `Look`
- stage 2 with `AppendStringForAnimations="Stage2"`: `LookStage2` - stage 2 with `"animationSuffix": "Stage2"`: `LookStage2`
- stage 5 with `AppendStringForAnimations="Stage5"`: `LookStage5` - stage 5 with `"animationSuffix": "Stage5"`: `LookStage5`
Animation references in XML should still use just the base name like `Look`, `Neutral`, or `Eat`. The game appends the current stage's suffix automatically. Animation names in the JSON files should still use just the base name like `Look`, `Neutral`, or `Eat`. The game appends the current stage's suffix automatically.
Know that despite Anm2Ed supporting sounds for animations, don't expect these to work in context based on names; use the .xml sound attributes when applicable. You can add sounds for fluff, though. Know that despite Anm2Ed supporting sounds for animations, don't expect these to work in context based on names; use the JSON sound lists when applicable. You can add sounds for fluff, though (trigger sounds play).
# Resources # Resources
There's two folders inside resources; "characters" and "font". There's two folders inside resources; "characters" and "font".
"font" is just a font containing the font used in the selection screen. That's literally it. This technically isn't needed; Dear ImGui has its own font that will be used if this font is missing. "font" is just a font containing the font used in the selection screen. That's literally it.
EVERYTHING ELSE is stored inside bespoke character archives (.zips) in the "characters" folder. This has all data associated with characters. Think of characters more as tailored game experiences rather than literally being just the characters. Not only is there the character graphics, but backgrounds, items, parameters, etc. These are intensely customizable to suit whatever experience you'd like (within the confines of the engine, of course). EVERYTHING ELSE is stored inside character folders in the "characters" folder; one folder per character, named however you like (the folder name is also the name of its save file). This has all data associated with characters. Think of characters more as tailored game experiences rather than literally being just the characters. Not only is there the character graphics, but backgrounds, items, parameters, etc. These are intensely customizable to suit whatever experience you'd like (within the confines of the engine, of course).
Shweet's Sim uses a collection of [XML](https://en.wikipedia.org/wiki/XML) files to parse data; make sure to brush up on the format. A character archive can contain these files in its root: Shweet's Sim uses [JSON](https://en.wikipedia.org/wiki/JSON) files for its data; make sure to brush up on the format. A character folder holds these files:
- areas.xml - character.json
- character.xml - items.json
- cursor.xml - dialogue.json
- dialogue.xml
- items.xml
- menu.xml
- strings.xml
The game currently expects `character.xml`. The rest are optional in the sense that the loader will warn and continue if some are missing, but most real characters will want nearly all of them. `character.json` is required; a folder without one isn't listed. The other two are optional, but most real characters will want both.
Every path in these files is relative to the character's folder (e.g. `"sfx/blip.ogg"` or `"gfx/items/apple.png"`). Paths inside an .anm2 are relative to the .anm2. Anything left out takes its default, so only write what you need. Wherever sounds are listed, one is picked at random each time.
If you're making your own character, your best bet would probably be just to copy the Snivy character and edit it based on your needs, just as a helpful start. If you're making your own character, your best bet would probably be just to copy the Snivy character and edit it based on your needs, just as a helpful start.
# Character # character.json
## areas.xml
"Areas" refers to the backgrounds of the game. I'd planned for the game to dynamically switch areas based on stage (in case you want a character to become a blob that grows bigger than a city or whatever) but this isn't implemented. I'd just have one entry in here for the game's background.
### Areas
#### TextureRootPath (path)
Working folder/directory of textures the areas will use.
#### Anm2RootPath (path)
Working folder/directory of anm2 files the areas will use. If omitted, TextureRootPath is used.
#### WorldBoundsNull (string)
The area ANM2 null used to set world bounds. Defaults to `Bounds`.
#### ItemSpawnNull (string)
The area ANM2 null used to choose random item spawn positions. Items can spawn at any point inside this null rect. If omitted or invalid, WorldBoundsNull is used.
#### Area
#### Texture (path)
The file path of the area's texture (background).
#### Anm2 (path)
The file path of the area's animated background. If this is set, it is used instead of Texture.
#### Gravity (float)
Gravity the area has; applies to items' velocities per tick.
#### Friction (float)
Friction of the area; applies to items' velocities when grounded or hitting walls.
## character.xml
This is the main character file where much of the functionality is stored. This is the main character file where much of the functionality is stored.
### Character ```json
#### Name (string) {
Name of the character; will appear in dialogue, stats, etc. "name": "Snivy",
#### TextureRootPath (path) "anm2": "gfx/character/snivy.anm2",
Working folder/directory of where used textures will be contained within. "weight": 8.1,
#### SoundRootPath (path) "stages": [{"weight": 10, "dialoguePool": "Stage2", "animationSuffix": "Stage2"}],
Working folder/directory of where used sounds will be contained within. "animations": {"start": "Appear", "idle": "Neutral", "idleFull": "NeutralFull", "stageUp": "StageUp"},
#### Render (path) "sounds": {"digest": ["sfx/digest1.ogg"], "gurgle": ["sfx/gurgle1.ogg", "sfx/gurgle2.ogg"]}
Texture for the character's "render" (i.e., a typical full-body display), will show in the Select screen. }
#### Portrait (path) ```
Texture for the character's "portrait" (i.e., a cropped profile view), will show in the Select screen.
#### Anm2 (path)
Character's "anm2" file (uses TextureRootPath); should have all the character's animations.
#### Description (string)
A general description of the character; will show in the Select screen.
#### Credits (string)
Credits text shown in the Select screen's Credits tab.
#### WeightKilograms (double)
The character's starting weight, in kilograms.
#### WeightMaxKilograms (double)
Maximum weight the character can reach.
#### CapacityCalories (double)
The character's starting capacity, in calories.
#### CapacityMinCalories (double)
The character's minimum capacity, in calories.
#### CapacityMaxCalories (double)
Maximum base capacity.
#### CapacityOverstuffedMultiplier (double)
Multiplier used when computing the effective full threshold from current base capacity.
#### CapacityIfOverStuffedOnDigestBonus (double, multiplier)
Additional capacity gained on digest when the character was over base capacity.
#### CaloriesToKilogram (double)
Determines how many calories become a kilogram (1 cal -> X kg).
#### DigestionRatePerSecond (double, percent/second)
The base digestion rate, in percent per second.
#### DigestionRatePerSecondMin (double, percent/second)
The minimum digestion rate for the character, in percent per second.
#### DigestionRatePerSecondMax (double, percent/second)
The maximum digestion rate for the character, in percent per second.
#### DigestionTimerMax (int, ticks)
When digesting, the digestion bar will count down, and then when it hits 0, the current calories will be digested. This determines how long this takes, in ticks.
#### BlinkChance (double, percent)
Chance each tick to trigger a blink override.
#### GurgleChance (double, percent)
Determines how often the character will gurgle (see the Gurgle sounds later) per tick.
#### GurgleChanceFromCapacityMultiplier (double)
Per the character's capacity, multiplies the character's gurgle chance based on the percent of capacity filled (based on max capacity). Higher capacity = higher gurgle chance, using this number at maximum.
#### DialoguePool (string)
Determines the character's base dialogue options (for the "How are you feeling?" option in Chat). This is effectively "Stage 1"'s dialogue; each stage should have its own dialogue pool (see later). Also see dialogue.xml for how "Pools" work.
#### ColorR, ColorG, ColorB (float, 0-1)
UI accent color for the character.
### AlternateSpritesheet
Determines the alternate spritesheet of the character, if applicable (in Pokemon terms, the "shiny").
#### Texture (path)
The alternate spritesheet texture; uses TextureRootPath.
#### Sound (path)
The sound that will play when the spritesheet is set to alternate (either on new game, or with an item that has IsToggleSpritesheet; see items.xml); uses SoundRootPath.
#### ID (int)
ID of spritesheet that the alternate spritesheet will replace in the character's .anm2.
#### ChanceOnNewGame (float, percent)
Chance of rolling for the alternate spritesheet on starting a new game, in percent.
### Stages
A "stage" represents each visual change of the character as the weight increases. By default, there's always one stage; adding more here will add additional stages.
#### ThresholdKilograms (float)
The weight threshold to reach this stage, in kilograms.
#### AreaID (int)
Reserved for background switching. Parsed, but not currently used by gameplay.
#### DialoguePool (string)
Determines the stage's dialogue options (for the "How are you feeling?" option in Chat). Also see dialogue.xml for how "Pools" work.
#### AppendStringForAnimations (string)
Suffix appended to base animation names while this stage is active. Usually `Stage2`, `Stage3`, etc.
### Animations
The character's animations. Know that a lot of character animations are typically easily played/activated through Dialogue; this is just for animations that aren't reliant on that system. Know that _these animations should just be the base name_; all animations are expected to have a stage number after them, so don't include the number for these.
### Start
The animation name that will first play when starting a new game; used for like a character's introduction.
### Idle
The idle animation that the character will regularly return to.
### IdleFull
When over capacity, this idle animation will be used instead.
### StageUp
When going to a new stage after digestion and going over a stage's threshold, this animation will play.
#### Animation
The name of the animation to be used for each of these.
### AnimationOverrides
"AnimationOverrides" are the term I use for when some animations will be tweaked by the game engine for effect (blinking and talking being the ones used). There are two elements; "Talk" and "Blink" for this. Typically, how this is handled is that each animation with have an invisible blinking/talking layer (just the graphic that blinks), which will change when called for. Again, review the .anm2 format for what a "layer" is. All that's sourced is just the spritesheet crop in these cases; so don't worry about how the blink/talk layers are set up.
### Talk / Blink
#### LayerSource (string)
The layer in the animation which will be sourced for the override.
#### LayerDestination (string)
The layer in the animation which the source will be applied to.
### EatAreas
An "eat area" is where the food should be dragged to. I'm pretty much expecting this to only be a mouth for most characters but hey maybe you want the character to also eat through their butt or something. Again, not something that's well-developed at the moment.
### EatArea
#### Null (string)
The null area in the animation where food will be checked for. Again, review the .anm2 format.
#### Animation (string)
The animation that will play when food is dragged to it.
#### EventToActivate (string)
The event that will be checked for, detrermining the time the food is actually eaten/chewed. Again, review the .anm2 format.
### ExpandAreas
The areas which will expand based on capacity percent; usually a stomach. This adds additional scale onto the layer of an animation. This also can scale a null as well.
ExpandAreas are presently hardcoded to scale with both capacity and weight at a 50/50 ratio; this could be changed in the future for custom ratios.
#### ExpandArea
#### Layer (string)
The layer of the animation that will expand.
#### Null (string)
The null of the animation that will expand.
#### ScaleMultiplierMaximum (float, percent)
The scale that will be added at maximum. Know that scale is a percent; a {100, 100} scale is the default and is the normal scale of an animation.
### InteractAreaTypes
Default behavior for each interaction type. These entries should not have `Null`; they define shared cursor animations, sounds, particles, and bonuses for concrete `InteractAreas`.
### InteractArea
#### Type (string)
The interaction type name. Concrete interact areas with the same `Type` inherit unspecified values from this definition.
#### GlyphFromIconFont (string)
The icon-font glyph used for this interaction in the tools bar.
#### IsHold (bool)
If true, the interaction is held rather than clicked once.
#### AnimationCursorHover (string)
The animation the cursor will play when hovering over the interact area.
#### AnimationCursorActive (string)
The animation the cursor will play when holding down click over the interact area.
#### DigestionBonusOnHover (float)
Digestion bonus applied repeatedly while hovering with a held interaction.
#### DigestionBonusOnClick (float)
Digestion bonus applied when a click-based interaction lands.
#### TimeTicks (float)
Used by click effects such as smacks. If omitted, the engine uses a short default duration.
#### TimeToActivateNewDialogueTicks (int)
Ticks that must pass without this interact area being interacted with before it can start a new dialogue.
#### ScaleEffectAmplitude (float)
Visual scale effect amount for click interactions.
#### ScaleEffectCycles (float)
How many oscillations the scale effect performs.
### InteractAreas
The concrete areas on a character which can be interacted with; usually for belly rubs, kisses, etc.
### InteractArea
#### Type (string)
The interaction type to use. This should match one of the entries in `InteractAreaTypes`.
#### Null (string)
The null in which the interact area can be triggered. Again, review the .anm2 format.
#### Layer (string; optional)
Layer used for scale-effect interactions such as smacks.
#### DialoguePool (string)
The dialogue pool which will be drawn from when activating an interact area (see dialogue.xml).
#### DialoguePoolFull (string)
Dialogue pool used when the character is over capacity.
### Sound
An interact area can play multiple sounds when interacting; add additional Sound elements to achieve this. Don't worry about repeatedly-loaded sounds; the game will cache them beforehand for efficiency.
#### Path (path)
The path of the sound being used.
### Particle
An interact area can trigger multiple particle effects.
#### Type (string)
The particle type from `particles.xml`.
### Actions
Sounds that play in character-level contexts. Each action can contain multiple `Sound` elements.
### Digest
Sounds that will play when the character begins digesting (digestion bar full).
### Gurgle
Ambient sounds that will play randomly based on capacity; see GurgleChance and GurgleChanceFromCapacityMultiplier above.
### Sound
#### Path (path)
The path of the sound for the specific action.
## cursor.xml | Key | Type | Meaning |
Determines the cursor appearance and behavior. |---|---|---|
### TextureRootPath (path) | `name` | string | Name of the character; will appear in dialogue, stats, etc. Defaults to the folder name. |
Working folder/directory of textures the cursor will use. | `description` | string | A general description of the character; will show in the Select screen. |
#### SoundRootPath (path) | `credits` | string | Credits text shown in the Select screen's Credits tab. |
Working folder/directory of sounds the cursor will use. | `render` | path | Texture for the character's "render" (i.e., a typical full-body display), will show in the Select screen. |
#### Anm2 (path) | `portrait` | path | Texture for the character's "portrait" (i.e., a cropped profile view), will show in the Select screen. |
The anm2 file the cursor will use (depends on TextureRootPath). | `anm2` | path | The character's anm2 file; should have all the character's animations. |
### Animations | `color` | [r, g, b], 0-1 | UI accent color for the character. |
### Idle | `weight` | float, kg | The character's starting weight. |
The cursor's default idle animation. | `weightMax` | float, kg | Maximum weight the character can reach. |
### Hover | `capacity` | float, kcal | The character's starting capacity. |
The cursor's hover animation, when hovering over an item. | `capacityMin`, `capacityMax` | float, kcal | The range capacity stays within. |
### Grab | `overstuffedMultiplier` | float | The character can eat past capacity up to capacity times this. |
The cursor's grab animation, when grabbing an item. | `overstuffedDigestBonus` | float | Additional capacity gained on digest when the character was over capacity (scaled by how far over). |
### Pan | `caloriesToKilogram` | float | How many kilograms each calorie digested becomes. |
The cursor's pan animation, when panning with middle mouse button. | `digestionRate` | float, %/second | The base digestion rate. |
### Zoom | `digestionRateMin`, `digestionRateMax` | float, %/second | The range the digestion rate stays within. |
The cursor's zoom animation, when zooming in/out with the mouse wheel. | `digestionTime` | int, ticks | When digesting, the digestion bar will count down, and then when it hits 0, the current calories will be digested. This determines how long this takes. |
### Return | `blinkChance` | float, % | Chance each tick to blink. |
The cursor's return animation, when holding right click on a spawned item. | `gurgleChance` | float | How often the character will gurgle (see `sounds`) per tick, scaled by how full they are. |
#### Animation (string) | `gurgleCapacityMultiplier` | float | Multiplies the gurgle chance at full capacity. Higher capacity = higher gurgle chance. |
The animation the cursor will use in these contexts. | `dialoguePool` | string | The character's base dialogue pool for "How are you feeling?". This is effectively "Stage 1"'s dialogue; each stage should have its own dialogue pool. |
### Actions
Know that multiple sounds can be defined for each action by adding `Sound` children.
### Grab
The sound that will play when grabbing an item.
### Release
The sound that will play when releasing an item.
### Throw
The sound that will play when throwing an item (releasing when dragging quickly).
### Sound
#### Path (path)
The path of the sound for these respective contexts.
## dialogue.xml ### alternate
The collection of character dialogue; likely the biggest file, depending on your needs. The alternate spritesheet of the character, if applicable (in Pokemon terms, the "shiny").
### Dialogue
#### SoundRootPath (path)
Working folder/directory of where dialogue sounds will be contained within.
#### DelayTicks (int)
How many update ticks pass before the next dialogue character is revealed.
#### BlipDelayTicks (int)
How many displayed dialogue characters elapse between blip sounds. `3` means every third displayed character.
### TextPlayback
Groups the dialogue text reveal settings.
### Blip
Dialogue blip sound. One is played periodically while text reveals. Add one or more `Sound` children.
### Sound
#### Path (path)
The path to the sound that will play, based on SoundRootPath.
### DelayCharacters
Groups character-specific delay rules.
### DelayCharacter
#### Character (string)
The character that should add an extra delay while text reveals.
#### DelayTicks (int)
Extra update ticks to pause after this character.
#### IsDisableTalk (bool)
If true, the talking override pauses during this character's delay.
### Entries
### Entry
Each bit of dialogue is referred to as an "entry".
#### Label (string)
Name for the entry. Other entries will rely on this for dialogue chains.
#### Next (string)
The ID of the entry that will follow after this one. If no Next entry, the text will not continue. Make sure to connect these.
#### Text (string)
The actual dialogue content of the entry. Should support Unicode, provided the font contains the characters.
#### Animation (string)
A character animation that will play at the beginning of the dialogue. This may be expanded into the future to allow animations to play dynamically per dialogue index. Again, make sure it's just the base name for the animation, no stage numbers.
### Effects
Dialogue effects can be added as child elements of an `Entry`. `Small` and `Big` are fixed text scale effects. `Silent` suppresses blips and character talking for its range. `Oscillation`, `Shake`, and `Screenshake` use engine defaults for their strength/timing values; do not write `Amplitude`, `Frequency`, `Period`, `Magnitude`, or `TimeTicks` for those effects. `Scene` hides windows and items and disables interaction until the active dialogue chain reaches and finishes its terminal entry. If another `Scene` appears while one is active, it is ignored. `Scale` and `Rainbow` are no longer supported.
### Choice
Dialogue can be branching; simply added "Choice" elements into the Entry element.
#### Next (string)
The dialogue the choice will lead to.
#### Text (string)
The text of the choice; will appear as a series of buttons in the text window.
### Pools
A "pool" of dialogue refers to a collection of entries that can be randomly picked in some contexts.
### Pool
#### Label (string)
The name of the dialogue pool; can be referenced elsewhere.
### Entry
#### Label (string)
The name of the entry; to be added to the pool.
### Hooks | Key | Type | Meaning |
Groups dialogue hooks. Hooks connect gameplay events to either a specific entry or a pool. |---|---|---|
### Hook | `texture` | path | The alternate spritesheet texture. |
#### Type (string) | `sound` | path | Plays when the spritesheet is set to alternate (either on new game, or with an item that has `isToggleSpritesheet`). |
The gameplay hook name. Current hook names include `Start`, `End`, `Help`, `StageUp`, `Random`, `Feed`, `FeedFull`, `Eat`, `EatFull`, `Full`, `Throw`, `LowCapacity`, `Digest`, `FoodTaken`, and `FoodTakenFull`. | `spritesheet` | int | The spritesheet of the character's anm2 it replaces. Defaults to 0. |
#### Entry (string) | `chance` | float, % | Chance of rolling the alternate spritesheet when starting a new game. |
The entry to play for this hook.
#### Pool (string)
The pool to draw from for this hook.
#### Value (int)
Optional hook-specific value. `StageUp` uses the displayed stage number. For a matching hook type, value-specific hooks override general hooks, then entry hooks override pool hooks.
## items.xml ### stages
Has all items and their parameters, alongside additional behavior. A "stage" represents each visual change of the character as the weight increases. By default, there's always one stage; each entry in this list adds another.
### ItemSchema | Key | Type | Meaning |
#### TextureRootPath (path) |---|---|---|
Working folder/directory of where used textures will be contained within. | `weight` | float, kg | The weight threshold to reach this stage. |
#### SoundRootPath (path) | `dialoguePool` | string | The stage's dialogue pool for "How are you feeling?". |
Working folder/directory of where used sounds will be contained within. | `animationSuffix` | string | Suffix appended to base animation names while this stage is active. Usually `Stage2`, `Stage3`, etc. |
#### BaseAnm2 (path) | `area` | int | Switches to this area (from `areas.list`) from this stage on. |
The base anm2 file that items will use. Items can technically have their own .anm2 and animate accordingly, but I haven't tested this much.
#### BaseSpawnDelayTicks (int)
Base cooldown before another item can be spawned.
#### ItemSpawnLimit (int)
Maximum number of spawned world items at once.
#### SpawnDelayStageMultiplier (float)
Multiplies the spawn delay once per character stage. Values below `1.0` make item spawning faster as stage increases.
### Actions
The various item actions. Each action can contain multiple `Sound` elements to randomly play from a selection.
### Bounce
The sound that will play when an item is bounced (hitting a floor/wall/ceiling with velocity).
### Dispose
The sound that will play when an item is disposed of.
### Summon
The sound that will play when an item is spawned.
### NewItem
The sound that will play when an item is discovered in the Catalog for the first time.
### Return
The sound that will play when an item is returned.
### Sound
#### Path (path)
The path to the sound that will play, based on SoundRootPath.
### Categories
The kinds of items present. Will appear in Catalog tooltips.
### Category
#### Name (string)
The name of the category.
#### UseMode (string)
Controls how items in this category are used after spawning. `Edible` marks food that can be eaten. `Tool` marks non-food tools.
### Flavors
Item flavors (for food). Honestly have no meaning but flavor text at the moment but whatever.
### Flavor
#### Name (string)
Name of the flavor.
### Rarities
Different rarities of item. Shows in the Catalog and each can have their own drop chance.
### Rarity
#### Name (string)
Name of the rarity.
#### Chance (float, percent)
Relative rarity weight for item systems.
#### Sound (path)
Sound that will play when an item of this rarity is awarded.
#### IsHiddenInCatalog (bool)
If true, this rarity is marked as hidden in the Catalog. Undiscovered items still appear as unknown silhouettes.
### Items
#### TextureRootPath (path)
Working folder/directory of where used item textures will be contained within.
### Item
An individual item entry.
#### Name (string)
The name of the item.
#### Texture (path)
The texture the item uses; uses TextureRootPath.
#### Description (string)
Flavor text for the item.
#### Category (string)
The category the item uses; as defined in Categories.
#### Rarity (string)
The rarity the item uses; as defined in Rarities.
#### Anm2 (path; optional)
The custom anm2 the item uses, if needed.
#### Flavor (string; optional)
The flavor the item uses; as defined in Flavors.
#### Calories (float; optional)
The amount of calories in an item has (if food).
#### CapacityBonus (float; optional)
Capacity increase granted as the item is eaten.
#### DigestionBonus (float, percent; optional)
The additional digestion rate in percent the item will give if eaten.
#### Gravity (float; optional)
The item's gravity; will use the default gravity if not available.
#### ColorR, ColorG, ColorB (float, 0-1; optional)
Optional item UI color. Missing components default to `0` if any color component is provided.
#### IsToggleSpritesheet (bool; optional)
When used as a tool, will toggle the character's spritesheet (in Pokemon terms, toggling normal/shiny palettes).
## strings.xml ### animations
Character-specific text strings. The character's animations. Know that a lot of character animations are typically easily played through dialogue; this is just for animations that aren't reliant on that system. These are just the base names.
See [`src/resource/xml/strings.hpp`](src/resource/xml/strings.hpp) for how strings map to in-game displays.
## menu.xml | Key | Meaning |
|---|---|
| `start` | Plays first when starting a new game; used for like a character's introduction. |
| `idle` | The idle animation that the character will regularly return to. |
| `idleFull` | When over capacity, this idle animation will be used instead. |
| `stageUp` | When going to a new stage after digestion goes over a stage's threshold, this animation will play. |
### talk, blink
"talk" and "blink" are animations tweaked by the game for effect. Typically, each animation will have an invisible blinking/talking layer (just the graphic that blinks), which will change when called for. All that's sourced is just the spritesheet crop in these cases; so don't worry about how the blink/talk layers are set up.
| Key | Meaning |
|---|---|
| `source` | The layer in the animation which will be sourced. |
| `destination` | The layer in the animation which the source will be applied to. |
### eatAreas
An "eat area" is where the food should be dragged to. I'm pretty much expecting this to only be a mouth for most characters but hey maybe you want the character to also eat through their butt or something.
| Key | Meaning |
|---|---|
| `null` | The null in the animation where food will be checked for. |
| `animation` | The animation that will play when food is dragged to it. |
| `event` | The event that will be checked for, determining the time the food is actually eaten/chewed. |
### expandAreas
The areas which will expand based on how full the character is and their progress to the next stage (50/50); usually a stomach. This adds scale onto a layer and a null of every animation.
| Key | Meaning |
|---|---|
| `layer` | The layer that will expand. |
| `null` | The null that will expand. |
| `scaleMax` | The scale (in percent) that will be added at maximum. Know that a scale of 100 is the normal scale of an animation. |
### interactTypes, interactAreas
`interactTypes` is an object of interaction types by name (each gets a button in the tools bar), giving each type's defaults. `interactAreas` is a list of the concrete areas on a character which can be interacted with; usually for belly rubs, kisses, etc. Each area names its `type` and takes all of that type's settings, changing whichever it gives itself.
| Key | Type | Meaning |
|---|---|---|
| `type` | string | (Areas only) The interaction type to use. |
| `null` | string | (Areas) The null in which the interact area can be triggered. |
| `layer` | string | The layer that the scale effect (e.g. smacks) applies to. |
| `glyph` | string | The icon-font glyph used for this type in the tools bar. |
| `isHold` | bool | If true, the interaction is held rather than clicked once. |
| `cursorHover` | string | The animation the cursor will play when hovering over the interact area. |
| `cursorActive` | string | The animation the cursor will play when interacting. |
| `animation` | string | A character animation to play on interacting, when the area has no dialogue pool. |
| `idleOnEnd` | string | The idle animation the character uses after interacting (e.g. turning around). |
| `dialoguePool` | string | The dialogue pool which will be drawn from when interacting. |
| `dialoguePoolFull` | string | Dialogue pool used when the character is over capacity. |
| `dialogueCooldown` | int, ticks | Ticks before interacting can start new dialogue again. |
| `digestionClick` | float, % | Digestion added each click. |
| `digestionHover` | float, % | Digestion added each tick the cursor moves while held. |
| `scaleAmplitude` | float, % | Size of the scale effect on clicking. |
| `scaleCycles` | float | How many oscillations the scale effect performs. Defaults to 1. |
| `scaleTime` | float, ticks | How long the scale effect lasts. Defaults to 20. |
| `sounds` | paths | Sounds played when interacting. |
| `particles` | strings | Particles (see `particles`) spawned when interacting. |
### sounds
| Key | Meaning |
|---|---|
| `digest` | Plays when the character begins digesting (digestion bar full). |
| `gurgle` | Ambient sounds that play randomly based on capacity; see `gurgleChance`. |
### areas
"Areas" refers to the backgrounds of the game. Stages can switch between them (see `stages`).
| Key | Type | Meaning |
|---|---|---|
| `anm2` | path | The anm2 the areas use. |
| `boundsNull` | string | The null setting the world's bounds. Defaults to `Bounds`. |
| `spawnNull` | string | The null items spawn at random points within. If omitted or missing, `boundsNull` is used. |
| `list` | list | The areas. Each can set its own `anm2`, its `gravity` (applied to items' velocities per tick), `friction` (applied when items are grounded or hit walls) and `airResistance`. |
### cursor
Determines the cursor appearance and behavior. Without one, the system cursor is used.
| Key | Meaning |
|---|---|
| `anm2` | The anm2 file the cursor will use. |
| `animations` | `idle`, `click`, `hover` (over an item), `grab` (an item), `pan` (with middle mouse), `zoom` (with the mouse wheel) and `dispose` (right clicking an item). |
| `sounds` | `grab` (grabbing an item), `release` (letting go) and `throw` (letting go while dragging quickly). |
| `particles` | `grab`: particles spawned when grabbing an item. |
### menu
Determines menu and general UI appearance and behavior. Determines menu and general UI appearance and behavior.
### Menu | Key | Meaning |
#### SoundRootPath (path) |---|---|
Working folder/directory of where used sounds will be contained within. | `font` | The font the menu will use. |
#### FontRootPath (path) | `iconsFont` | The font holding the tools bar's glyphs. |
Working folder/directory of where used fonts will be contained within. | `rounding` | The rounding of corners the UI will use. |
#### Font (path) | `homeGlyph` | The glyph of the tools bar's Home button. |
The font the menu will use, based on FontRootPath. | `sounds` | `open` and `close` (a sliding panel), `hover` and `select` (a widget), and `cheats` (after entering a special code to activate cheats). |
#### ButtonRounding (float)
The rounding of corners the UI will use. ### particles
### Actions An object of particles by name, each with the `anm2` and `animation` it plays once where spawned.
Menu sound actions. Each action can contain one or more `Sound` elements.
### Open ### strings
Will play when opening a sliding menu panel. Replaces UI text, by name. See [`src/strings.h`](src/strings.h) for every name and its default, e.g. `"strings": {"MenuTabInventory": "Pantry"}`.
### Close
Will play when closing a sliding menu panel. # items.json
### Hover
Will play when hovering over a widget in a menu. Has all items and their parameters, alongside additional behavior.
### Select
Will play when clicking on a widget in a menu. ```json
### CheatsActivated {
Sound that will play after entering a special code to activate cheats. "anm2": "gfx/items/item.anm2",
### Sound "spawnDelay": 600,
#### Path (path) "categories": {"Food": "edible", "Tool": "tool"},
The sound that will play, based on SoundRootPath. "rarities": {"Common": {"weight": 1, "sound": "sfx/common.ogg"}},
"items": [
{"name": "Apple", "texture": "gfx/items/apple.png", "category": "Food", "rarity": "Common", "calories": 95}
]
}
```
| Key | Type | Meaning |
|---|---|---|
| `anm2` | path | The base anm2 file that items will use. |
| `spritesheet` | int | The spritesheet of the anm2 that an item's `texture` replaces. Defaults to 0. |
| `spawnDelay` | int, ticks | Base cooldown before another item can be spawned. |
| `spawnDelayMultiplier` | float | Multiplies the spawn delay once per character stage. Values below `1.0` make item spawning faster as stage increases. |
| `spawnLimit` | int | Maximum number of items in the world at once. |
| `sounds` | | `bounce` (an item hitting a floor/wall/ceiling), `dispose` (an item disposed of), `summon` (an item spawned) and `newItem` (an item found for the first time). |
| `particles` | | `dispose` and `summon`. |
| `categories` | object | The kinds of items, by name; will appear in Catalog tooltips. Each says how its items are used: `edible` (food that can be eaten) or `tool`. |
| `rarities` | object | Rarities, by name. Each has a `weight` (relative chance of spawning) and a `sound` that plays when one spawns. |
### items
| Key | Type | Meaning |
|---|---|---|
| `name` | string | The name of the item. Saves keep items by name. |
| `description` | string | Flavor text for the item. |
| `texture` | path | The texture the item uses. |
| `anm2` | path | The item's own anm2, if needed. |
| `category` | string | The item's category. |
| `rarity` | string | The item's rarity. |
| `flavor` | string | Flavor text (for food); honestly has no meaning but whatever. |
| `calories` | float | The amount of calories the item has (if food). |
| `capacityBonus` | float | Capacity increase granted as the item is eaten. |
| `digestionBonus` | float, %/second | The digestion rate added as the item is eaten. |
| `gravity` | float | The item's gravity; will use the area's if not given. |
| `color` | [r, g, b], 0-1 | Optional UI color for the item. |
| `isToggleSpritesheet` | bool | When used as a tool on the character, will toggle the character's spritesheet (in Pokemon terms, toggling normal/shiny palettes). |
# dialogue.json
The collection of character dialogue; likely the biggest file, depending on your needs.
```json
{
"delay": 2,
"blipDelay": 2,
"blip": ["sfx/blip.ogg"],
"pauses": {".": {"delay": 5}},
"hooks": {"Start": "Start", "Eat": "Eat"},
"pools": {"Eat": ["Eat1", "Eat2"]},
"entries": {
"Start": {
"text": "Hey there, human!",
"animation": "Smug",
"choices": [{"text": "Hi!", "next": "Start2"}],
"effects": [{"type": "shake", "start": 4, "end": 9}]
}
}
}
```
| Key | Type | Meaning |
|---|---|---|
| `delay` | int, ticks | How many ticks pass before the next dialogue character is revealed. |
| `blipDelay` | int | How many displayed dialogue characters pass between blip sounds. `3` means every third displayed character. |
| `blip` | paths | Dialogue blip sounds, played periodically while text reveals. |
| `pauses` | object | Extra `delay` (in ticks) after particular characters. With `isSilent`, talking and blips also pause during it. |
| `hooks` | object | Connects gameplay events to a pool (or else an entry) by name. The hook names are `Start`, `End`, `Help`, `StageUp`, `Random`, `Feed`, `FeedFull`, `Eat`, `EatFull`, `Full`, `Throw`, `LowCapacity`, `Digest`, `FoodTaken` and `FoodTakenFull`. `Random` and `Help` add the "Let's chat!" and "Help" buttons. |
| `pools` | object | A "pool" of dialogue is a list of entry labels, one picked at random when used. |
| `entries` | object | Each bit of dialogue is referred to as an "entry", by its label. |
### entries
| Key | Type | Meaning |
|---|---|---|
| `text` | string | The actual dialogue content of the entry. Supports Unicode, provided the font contains the characters. An entry without text just plays its animation. |
| `animation` | string | A character animation that will play at the beginning of the dialogue. Again, just the base name for the animation, no stage numbers. |
| `next` | string | The label of the entry that will follow this one. If no next entry, the text will not continue. Make sure to connect these. |
| `choices` | list | Dialogue can be branching; each choice has its `text` (shown as a button) and the `next` entry it leads to. |
| `effects` | list | Effects over the text, each with a `type`, and a `start` and `end` (the characters it covers, from `start` up to `end`). |
### effects
| Type | Meaning |
|---|---|
| `color` | Colors the text, by `color` ([r, g, b], 0-1). |
| `big`, `small` | Fixed text scales. |
| `oscillation`, `shake` | Moves the text, with engine defaults. |
| `silent` | Suppresses blips and character talking for its range. |
| `screenshake` | Shakes the screen when its `start` comes up. |
| `sound` | Plays its `sound` (a path) when its `start` comes up. |
| `animation` | Plays its `animation` when its `start` comes up. |
| `delay` | Changes the reveal speed to its `delay` (in ticks) from its `start` on. |
| `scene` | Hides windows and items and disables interaction until the dialogue chain reaches and finishes its last entry. If another scene appears while one is active, it is ignored. |
# Saves # Saves
Outside of resources, Shweet's Sim also has a few files it writes outside of the game. You will find these in %AppData%/shweets-sim on Windows and ~/.local/share/shweets-sim on Linux. Outside of resources, Shweet's Sim also has a few files it writes outside of the game. You will find these in %AppData%/shweets-sim on Windows and ~/.local/share/shweets-sim on Linux. Each is a plain text file of `key=value` lines.
## settings.xml ## settings.txt
Stores general game settings and configuration; beyond invididual characters. Stores general game settings and configuration; beyond individual characters.
### MeasurementSystem ("Imperial", "Metric")
Determines measurement system (kg/g or lb/oz).
### Volume (int, percent)
Master volume.
### IsAudioMuted (bool)
If true, all audio is muted.
### ColorR, ColorG, ColorB (float, 0-1)
Red/Green/Blue components of menu color.
### WindowX, WindowY (int)
Window position.
### WindowW/H (int)
Window size.
## *.save | Key | Meaning |
Saves are per-character; will be named "[blank].save", stored in "saves" folder, from the name of the character's archive. |---|---|
| `measurement` | `0` for metric (kg/g), `1` for imperial (lb/oz). |
| `volume` | Master volume, in percent. |
| `mute` | `1` if all audio is muted. |
| `color` | Red, green and blue (0-1) of the menu color. |
| `character_color` | `1` to use the character's color while playing. |
| `width`, `height` | Window size. |
| `last_character` | The character being played at exit, which the game continues on start. |
| `console` | `1` enables the developer console (the grave key). |
### Save ## saves/<character>.txt
#### IsPostgame (bool) A character's progress: their weight, calories, capacity, digestion, totals and playtime, the camera, and the items found (`discovered=` lines) and lying in the world (`item=` lines, as position, velocity, rotation and name).
Determines if the game has been completed (character's max stage has been reached); if true, will enable cheats.
#### IsAlternateSpritesheet (bool)
Determines if the character's spritesheet is using the alternate version (in Pokemon, would be the "shiny" version
### Character
#### WeightKilograms (float)
Character's current weight, in kilograms.
#### Calories (float)
Character's current consumed calories.
#### CapacityCalories (float)
Character's capacity, in calories. Remember, max capacity effectively is capacity * CapacityOverstuffedMultiplier (from character.xml)
#### DigestionRatePerSecond (float, percent/second)
Character's digestion rate in percent per second.
#### IsDigesting (bool)
Determines if character is currently digesting (when the bar is going down)
#### DigestionProgress (float, percent)
Character's digestion progress. Digestion max is always 100%.
#### DigestionTimer (int)
When digestion bar is going down, this is the remaining time to 0 (in ticks)
#### TotalCaloriesConsumed (float)
Total calories consumed by the character, per save file.
#### TotalItemsSpawned (int)
How many items have been spawned, per save file.
#### TotalItemsConsumed (int)
How many items have been completely consumed by the character, per save file.
### Items
World items currently spawned outside the Catalog.
#### ID (int)
Item ID from `items.xml`.
#### PositionX, PositionY (float)
World position.
#### VelocityX, VelocityY (float)
World velocity.
### Discovered
Catalog item IDs that have been discovered.
#### ID (int)
Item ID from `items.xml`.
#### Rotation (float)
Current rotation.
# Conclusion
Hopefully this'll give you the resources you need to start making your own characters. If you need any help with this guide, or with clarification on anything, I'm available. Additionally, the game is [licensed as free software](https://github.com/ShweetsStuff/shweets-sim), meaning if you're stuck the code should give you a clue (though I apologize for the lack of comments. Self-documenting code though, am I right? :^) )
+40 -7
View File
@@ -11,22 +11,55 @@ This application was developed with the assistance of a large language model.
## Build ## Build
After cloning and enter the repository's directory, make sure to initialize the submodules: After cloning and entering the repository's directory, make sure to initialize the submodules:
```git submodule update --init --recursive``` ```git submodule update --init --recursive```
### Windows Everything (SDL, Dear ImGui, simdjson, stb_vorbis) is built statically from the submodules. The game's resources are in `resources/` and get linked next to the executable (copied, where links aren't allowed). Release is the default build; add `-DCMAKE_BUILD_TYPE=Debug` to the first command for a debug one.
Visual Studio is recommended for build.
### Linux ### Linux
Needs the X11/Wayland and audio headers SDL builds against.
``` ```
mkdir build mkdir build
cd build cd build
cmake .. cmake ..
make make
``` ```
If using VSCode, several tasks are available to quickly run and build. ### Windows
### Emscripten With Visual Studio or MinGW:
Run the VSCode "web" task. Make sure you have the Emscripten SDK accessible at the EMSDK path variable.
```
cmake -B build
cmake --build build --config Release
```
### Web
Needs the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html).
```
emcmake cmake -B build-web
cmake --build build-web
```
This makes `index.html`, `.js`, `.wasm` and `.data`. Serve all four.
## Saves
Settings are kept in `settings.txt`, and each character's progress in `saves/<character>.txt`, as plain `key=value` lines. They're in `~/.local/share/shweets-sim` on Linux, `%AppData%/shweets-sim` on Windows, and the browser's storage on the web.
## Developer Console
Add `console=1` to `settings.txt`, then press the grave key (`` ` ``) in game. `help` lists the commands.
## Modding
See [MODDING.md](MODDING.md).
## License
See [LICENSE](LICENSE).
+10 -10
View File
@@ -121,11 +121,12 @@ static void animations_parse(object list, std::vector<Anm2Animation>& animations
} }
} }
bool Anm2::load(const std::string& path, Renderer& renderer, Audio& audio) void Anm2::load(const std::string& path, Renderer& renderer, Audio& audio)
{ {
Json json; Json json;
if (!json.load(path)) json.load(path);
return false; if (!json.isValid)
return;
object root = json.root; object root = json.root;
std::string folder = path.substr(0, path.find_last_of('/') + 1); std::string folder = path.substr(0, path.find_last_of('/') + 1);
fps = json_float(json_object(root, "info"), "fps", fps); fps = json_float(json_object(root, "info"), "fps", fps);
@@ -174,7 +175,6 @@ bool Anm2::load(const std::string& path, Renderer& renderer, Audio& audio)
animations_parse(json_object(root, "animations"), animations, sheetRegions, layerSheets); animations_parse(json_object(root, "animations"), animations, sheetRegions, layerSheets);
defaultAnimation = std::max(0, animation_get(json_string(root, "defaultAnimation"))); defaultAnimation = std::max(0, animation_get(json_string(root, "defaultAnimation")));
return !animations.empty();
} }
static int name_get(const std::vector<std::string>& names, const std::string& name) static int name_get(const std::vector<std::string>& names, const std::string& name)
@@ -394,12 +394,12 @@ static Affine root_affine_get(const Anm2Frame& frame, Vec2 position = {})
} }
// The transform, tint and color offset a group (and its parents) give its items, or false if any is hidden. // The transform, tint and color offset a group (and its parents) give its items, or false if any is hidden.
bool Actor::group_get(Anm2Animation& current, int group, float frameTime, Affine& model, Color& tint, Color& offset) bool Actor::is_group_visible(Anm2Animation& current, int group, float frameTime, Affine& model, Color& tint, Color& offset)
{ {
if (group < 0 || group >= (int)current.groups.size()) if (group < 0 || group >= (int)current.groups.size())
return true; return true;
Anm2Group& entry = current.groups[group]; Anm2Group& entry = current.groups[group];
if (!group_get(current, entry.parent, frameTime, model, tint, offset) || !entry.isVisible) if (!is_group_visible(current, entry.parent, frameTime, model, tint, offset) || !entry.isVisible)
return false; return false;
if (entry.root.empty()) if (entry.root.empty())
return true; return true;
@@ -427,7 +427,7 @@ void Actor::render(Renderer& renderer, Affine view)
Affine model = rootModel; Affine model = rootModel;
Color tint = root.tint; Color tint = root.tint;
Color offset = root.offset; Color offset = root.offset;
if (!layer.isVisible || !group_get(*current, layer.group, time, model, tint, offset)) if (!layer.isVisible || !is_group_visible(*current, layer.group, time, model, tint, offset))
continue; continue;
Anm2Frame frame = frame_get(layer.frames, time, TARGET_LAYER, layer.id); Anm2Frame frame = frame_get(layer.frames, time, TARGET_LAYER, layer.id);
int sheet = layer.id >= 0 && layer.id < (int)anm2->layerSheets.size() ? anm2->layerSheets[layer.id] : -1; int sheet = layer.id >= 0 && layer.id < (int)anm2->layerSheets.size() ? anm2->layerSheets[layer.id] : -1;
@@ -448,7 +448,7 @@ void Actor::render(Renderer& renderer, Affine view)
Color tint; Color tint;
Color offset; Color offset;
Anm2Frame frame = frame_get(null.frames, time, TARGET_NULL, null.id); Anm2Frame frame = frame_get(null.frames, time, TARGET_NULL, null.id);
if (null.isVisible && frame.isVisible && group_get(*current, null.group, time, model, tint, offset)) if (null.isVisible && frame.isVisible && is_group_visible(*current, null.group, time, model, tint, offset))
renderer.outline_draw(model * null_affine_get(frame), frame.scale.x, frame.scale.y, NULL_COLOR); renderer.outline_draw(model * null_affine_get(frame), frame.scale.x, frame.scale.y, NULL_COLOR);
} }
} }
@@ -468,7 +468,7 @@ Rect Actor::null_rect_get(int null, int index, float frameTime)
Color tint; Color tint;
Color offset; Color offset;
Anm2Frame frame = frame_get(item.frames, frameTime, TARGET_NULL, null); Anm2Frame frame = frame_get(item.frames, frameTime, TARGET_NULL, null);
if (!frame.isVisible || !group_get(*current, item.group, frameTime, model, tint, offset)) if (!frame.isVisible || !is_group_visible(*current, item.group, frameTime, model, tint, offset))
break; break;
return affine_bounds(model * null_affine_get(frame), frame.scale.x, frame.scale.y); return affine_bounds(model * null_affine_get(frame), frame.scale.x, frame.scale.y);
} }
@@ -492,7 +492,7 @@ Rect Actor::bounds_get()
Color offset; Color offset;
Anm2Frame frame = frame_get(layer.frames, (float)frameTime, TARGET_LAYER, layer.id); Anm2Frame frame = frame_get(layer.frames, (float)frameTime, TARGET_LAYER, layer.id);
if (!layer.isVisible || !frame.isVisible || frame.size.x <= 0 || if (!layer.isVisible || !frame.isVisible || frame.size.x <= 0 ||
!group_get(*current, layer.group, (float)frameTime, model, tint, offset)) !is_group_visible(*current, layer.group, (float)frameTime, model, tint, offset))
continue; continue;
model = model * affine_get(frame.position, frame.rotation, frame.scale / PERCENT, frame.pivot); model = model * affine_get(frame.position, frame.rotation, frame.scale / PERCENT, frame.pivot);
bounds = rect_merge(bounds, affine_bounds(model, frame.size.x, frame.size.y)); bounds = rect_merge(bounds, affine_bounds(model, frame.size.x, frame.size.y));
+2 -2
View File
@@ -85,7 +85,7 @@ public:
float fps = 30; float fps = 30;
int defaultAnimation = -1; int defaultAnimation = -1;
bool load(const std::string& path, Renderer& renderer, Audio& audio); void load(const std::string& path, Renderer& renderer, Audio& audio);
int animation_get(const std::string& name) const; int animation_get(const std::string& name) const;
int event_get(const std::string& name) const; int event_get(const std::string& name) const;
int layer_get(const std::string& name) const; int layer_get(const std::string& name) const;
@@ -178,5 +178,5 @@ public:
private: private:
void request_apply(const Request& request); void request_apply(const Request& request);
bool group_get(Anm2Animation& animation, int group, float frameTime, Affine& model, Color& tint, Color& offset); bool is_group_visible(Anm2Animation& animation, int group, float frameTime, Affine& model, Color& tint, Color& offset);
}; };
+16 -9
View File
@@ -51,11 +51,12 @@ Entry* Dialogue::hook_get(const std::string& hook)
return entry ? entry : entry_get(found->second); return entry ? entry : entry_get(found->second);
} }
bool Character::preview_load(const std::string& path, Renderer& renderer) void Character::preview_load(const std::string& path, Renderer& renderer)
{ {
Json json; Json json;
if (!json.load(path + "character.json")) json.load(path + "character.json");
return false; if (!(isValid = json.isValid))
return;
object root = json.root; object root = json.root;
folder = path; folder = path;
key = path.substr(0, path.size() - 1); key = path.substr(0, path.size() - 1);
@@ -75,7 +76,6 @@ bool Character::preview_load(const std::string& path, Renderer& renderer)
stages.push_back({json_string(fields, "animationSuffix"), json_string(fields, "dialoguePool"), stages.push_back({json_string(fields, "animationSuffix"), json_string(fields, "dialoguePool"),
json_float(fields, "weight"), json_int(fields, "area", -1)}); json_float(fields, "weight"), json_int(fields, "area", -1)});
} }
return true;
} }
Anm2* Character::anm2_get(const std::string& path, Renderer& renderer, Audio& audio) Anm2* Character::anm2_get(const std::string& path, Renderer& renderer, Audio& audio)
@@ -86,7 +86,8 @@ Anm2* Character::anm2_get(const std::string& path, Renderer& renderer, Audio& au
if (found != anm2s.end()) if (found != anm2s.end())
return found->second.animations.empty() ? nullptr : &found->second; return found->second.animations.empty() ? nullptr : &found->second;
Anm2& loaded = anm2s[path]; Anm2& loaded = anm2s[path];
return loaded.load(folder + path, renderer, audio) ? &loaded : nullptr; loaded.load(folder + path, renderer, audio);
return loaded.animations.empty() ? nullptr : &loaded;
} }
// Sets an interact area's fields from source, keeping what it doesn't name (its type's settings). // Sets an interact area's fields from source, keeping what it doesn't name (its type's settings).
@@ -397,17 +398,20 @@ void Character::load(Renderer& renderer, Audio& audio)
return; return;
isLoaded = true; isLoaded = true;
Json json; Json json;
if (json.load(folder + "character.json")) json.load(folder + "character.json");
if (json.isValid)
{ {
character_parse(json.root, *this, renderer, audio); character_parse(json.root, *this, renderer, audio);
sounds_parse(json.root, FILE_CHARACTER, *this, audio); sounds_parse(json.root, FILE_CHARACTER, *this, audio);
} }
if (json.load(folder + "items.json")) json.load(folder + "items.json");
if (json.isValid)
{ {
items_parse(json.root, *this, renderer, audio); items_parse(json.root, *this, renderer, audio);
sounds_parse(json.root, FILE_ITEMS, *this, audio); sounds_parse(json.root, FILE_ITEMS, *this, audio);
} }
if (json.load(folder + "dialogue.json")) json.load(folder + "dialogue.json");
if (json.isValid)
dialogue_parse(json.root, *this, audio); dialogue_parse(json.root, *this, audio);
} }
@@ -450,7 +454,10 @@ std::vector<Character> characters_load(Renderer& renderer)
std::vector<Character> characters; std::vector<Character> characters;
characters.reserve(sorted.size()); characters.reserve(sorted.size());
for (const std::string& name : sorted) for (const std::string& name : sorted)
if (!characters.emplace_back().preview_load(folder + name + "/", renderer)) {
characters.emplace_back().preview_load(folder + name + "/", renderer);
if (!characters.back().isValid)
characters.pop_back(); characters.pop_back();
}
return characters; return characters;
} }
+2 -1
View File
@@ -280,8 +280,9 @@ public:
int talkDestination = -1; int talkDestination = -1;
int talkSource = -1; int talkSource = -1;
bool isLoaded = false; bool isLoaded = false;
bool isValid = false;
bool preview_load(const std::string& path, Renderer& renderer); void preview_load(const std::string& path, Renderer& renderer);
void load(Renderer& renderer, Audio& audio); void load(Renderer& renderer, Audio& audio);
Anm2* anm2_get(const std::string& path, Renderer& renderer, Audio& audio); Anm2* anm2_get(const std::string& path, Renderer& renderer, Audio& audio);
Actor item_actor_get(int item); Actor item_actor_get(int item);
+11 -5
View File
@@ -49,7 +49,7 @@ static int history_callback(ImGuiInputTextCallbackData* data)
} }
// A toggle's new state: what its argument says, or the opposite of now without one. // A toggle's new state: what its argument says, or the opposite of now without one.
static bool toggle_get(const std::string& argument, bool value) static bool is_toggled(const std::string& argument, bool value)
{ {
return argument.empty() ? !value : argument == "true" || argument == "1"; return argument.empty() ? !value : argument == "true" || argument == "1";
} }
@@ -126,11 +126,17 @@ void Console::execute(Play* play, std::vector<Character>& characters)
else if (name == "reload") else if (name == "reload")
action = ACTION_RELOAD; action = ACTION_RELOAD;
else if (name == "show_layers") else if (name == "show_layers")
Actor::isShowLayers = toggle_get(argument, Actor::isShowLayers); Actor::isShowLayers = is_toggled(argument, Actor::isShowLayers);
else if (name == "show_nulls") else if (name == "show_nulls")
Actor::isShowNulls = toggle_get(argument, Actor::isShowNulls); Actor::isShowNulls = is_toggled(argument, Actor::isShowNulls);
else if (name == "spawn" && !play->spawn_queue_add(SDL_atoi(argument.c_str()))) else if (name == "spawn")
history.push_back("Unable to spawn item " + argument + "."); {
int id = SDL_atoi(argument.c_str());
if (argument.empty() || id < 0 || id >= (int)play->character->items.size())
history.push_back("Unable to spawn item " + argument + ".");
else
play->spawn_queue_add(id);
}
else if (name == "stage" || name == "weight") else if (name == "stage" || name == "weight")
{ {
play->stage = name == "stage" ? std::max(0, SDL_atoi(argument.c_str()) - 1) : play->stage_get(number); play->stage = name == "stage" ? std::max(0, SDL_atoi(argument.c_str()) - 1) : play->stage_get(number);
+1 -1
View File
@@ -338,7 +338,7 @@ void Text::update(Play& play, Vec2 screenSize)
{ {
Choice& choice = current->choices[i]; Choice& choice = current->choices[i];
ImGui::PushID(i); ImGui::PushID(i);
if (fx(ImGui::Button(choice.text.c_str(), buttonSize))) if (is_fx(ImGui::Button(choice.text.c_str(), buttonSize)))
set(play, dialogue.entry_get(choice.next), TEXT_SEQUENCE); set(play, dialogue.entry_get(choice.next), TEXT_SEQUENCE);
ImGui::SetItemTooltip("%s", choice.text.c_str()); ImGui::SetItemTooltip("%s", choice.text.c_str());
ImGui::PopID(); ImGui::PopID();
+6 -8
View File
@@ -51,24 +51,23 @@ bool Play::is_spawn_possible()
return false; return false;
} }
bool Play::spawn_queue_add(int id) void Play::spawn_queue_add(int id)
{ {
if (id < 0 || id >= (int)character->items.size()) if (id < 0 || id >= (int)character->items.size())
return false; return;
Item& item = character->items[id]; Item& item = character->items[id];
audio->play(character->sounds[SOUND_SUMMON]); audio->play(character->sounds[SOUND_SUMMON]);
if (item.rarity >= 0) if (item.rarity >= 0)
audio->play(character->rarities[item.rarity].sound); audio->play(character->rarities[item.rarity].sound);
spawnQueue.push_back(id); spawnQueue.push_back(id);
spawnDelay = spawn_delay_get(); spawnDelay = spawn_delay_get();
return true;
} }
// Queues an item picked at random by rarity, once the spawn delay is up. // Queues an item picked at random by rarity, once the spawn delay is up.
bool Play::spawn_queue_add() void Play::spawn_queue_add()
{ {
if (spawnDelay > 0 || !is_spawn_possible()) if (spawnDelay > 0 || !is_spawn_possible())
return false; return;
float total = 0; float total = 0;
for (int i = 0; i < (int)character->items.size(); i++) for (int i = 0; i < (int)character->items.size(); i++)
total += spawn_weight_get(*this, i); total += spawn_weight_get(*this, i);
@@ -76,7 +75,6 @@ bool Play::spawn_queue_add()
for (int i = 0; i < (int)character->items.size(); i++) for (int i = 0; i < (int)character->items.size(); i++)
if (float weight = spawn_weight_get(*this, i); weight > 0 && (roll -= weight) < 0) if (float weight = spawn_weight_get(*this, i); weight > 0 && (roll -= weight) < 0)
return spawn_queue_add(i); return spawn_queue_add(i);
return false;
} }
// Where an eat area takes food: its null now, or at the start of its animation when it isn't showing. // Where an eat area takes food: its null now, or at the start of its animation when it isn't showing.
@@ -426,7 +424,7 @@ void Play::interact_update(Pointer pointer)
} }
// Moves the cursor toward a target, braking to land on it. Returns whether it's there. // Moves the cursor toward a target, braking to land on it. Returns whether it's there.
static bool move_toward(Vec2& position, Vec2& velocity, Vec2 target, float speed, float acceleration) static bool is_target_reached(Vec2& position, Vec2& velocity, Vec2 target, float speed, float acceleration)
{ {
Vec2 delta = target - position; Vec2 delta = target - position;
float distance = vec2_length(delta); float distance = vec2_length(delta);
@@ -520,7 +518,7 @@ void Play::autofeed_update(Pointer& itemPointer, Pointer& interactPointer)
auto move = [&](Vec2 target, float speed, float acceleration) { auto move = [&](Vec2 target, float speed, float acceleration) {
isAutofeedActive = autoCursor.isVisible = true; isAutofeedActive = autoCursor.isVisible = true;
itemPointer = interactPointer = {.position = autoCursor.position, .isAutomated = true}; itemPointer = interactPointer = {.position = autoCursor.position, .isAutomated = true};
bool isThere = move_toward(autoCursor.position, autoVelocity, target, speed * speedScale, bool isThere = is_target_reached(autoCursor.position, autoVelocity, target, speed * speedScale,
acceleration * speedScale); acceleration * speedScale);
itemPointer.position = interactPointer.position = autoCursor.position; itemPointer.position = interactPointer.position = autoCursor.position;
return isThere; return isThere;
+5 -5
View File
@@ -23,13 +23,13 @@ struct Json
{ {
simdjson::dom::parser parser; simdjson::dom::parser parser;
object root = json_empty_get("o").get_object().value_unsafe(); object root = json_empty_get("o").get_object().value_unsafe();
bool isValid = false;
bool load(const std::string& path) void load(const std::string& path)
{ {
if (!parser.load(path).get(root)) isValid = !parser.load(path).get(root);
return true; if (!isValid)
SDL_Log("Failed to load %s", path.c_str()); SDL_Log("Failed to load %s", path.c_str());
return false;
} }
}; };
+12 -7
View File
@@ -95,12 +95,13 @@ static void characters_reload()
play_start(character_get(key), GAME_CONTINUE); play_start(character_get(key), GAME_CONTINUE);
} }
static bool init() static void init()
{ {
if (!SDL_Init(SDL_INIT_VIDEO)) if (!SDL_Init(SDL_INIT_VIDEO))
{ {
SDL_Log("Failed to initialize SDL: %s", SDL_GetError()); SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
return false; game.isRunning = false;
return;
} }
game.settings.load(); game.settings.load();
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_MAJOR); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_MAJOR);
@@ -110,10 +111,13 @@ static bool init()
game.window = SDL_CreateWindow(GAME_NAME, game.settings.width, game.settings.height, game.window = SDL_CreateWindow(GAME_NAME, game.settings.width, game.settings.height,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
game.context = game.window ? SDL_GL_CreateContext(game.window) : nullptr; game.context = game.window ? SDL_GL_CreateContext(game.window) : nullptr;
if (!game.context || !game.renderer.init()) if (game.context)
game.renderer.init();
if (!game.renderer.isValid)
{ {
SDL_Log("Failed to initialize OpenGL: %s", SDL_GetError()); SDL_Log("Failed to initialize OpenGL: %s", SDL_GetError());
return false; game.isRunning = false;
return;
} }
SDL_GL_SetSwapInterval(1); SDL_GL_SetSwapInterval(1);
game.audio.init(); game.audio.init();
@@ -134,7 +138,6 @@ static bool init()
game.characters = characters_load(game.renderer); game.characters = characters_load(game.renderer);
play_start(character_get(game.settings.lastCharacter), GAME_CONTINUE); play_start(character_get(game.settings.lastCharacter), GAME_CONTINUE);
return true;
} }
static void events_handle() static void events_handle()
@@ -168,7 +171,8 @@ static void update()
Vec2 screen = {ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y}; Vec2 screen = {ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y};
if (game.screen == SCREEN_SELECT) if (game.screen == SCREEN_SELECT)
{ {
if (ui.select_update(game.characters, game.settings, game.renderer, game.audio)) ui.select_update(game.characters, game.settings, game.renderer, game.audio);
if (ui.isRefresh)
characters_reload(); characters_reload();
if (ui.isNewGame || ui.isContinue) if (ui.isNewGame || ui.isContinue)
play_start(ui.characterIndex, ui.isNewGame ? GAME_NEW : GAME_CONTINUE); play_start(ui.characterIndex, ui.isNewGame ? GAME_NEW : GAME_CONTINUE);
@@ -250,7 +254,8 @@ static void quit()
int main(int, char**) int main(int, char**)
{ {
if (!init()) init();
if (!game.isRunning)
return 1; return 1;
#ifdef SDL_PLATFORM_EMSCRIPTEN #ifdef SDL_PLATFORM_EMSCRIPTEN
emscripten_set_main_loop(loop, 0, true); emscripten_set_main_loop(loop, 0, true);
+3 -3
View File
@@ -268,7 +268,7 @@ void Play::values_clamp()
// Swaps a layer's region for the one a source layer shows at a time, while active. Turns itself off (and returns // Swaps a layer's region for the one a source layer shows at a time, while active. Turns itself off (and returns
// false) when the animation lacks the source or the time runs past it. // false) when the animation lacks the source or the time runs past it.
static bool region_swap(Actor& actor, int source, float& time, Override& swap, bool isActive) static bool is_region_swapped(Actor& actor, int source, float& time, Override& swap, bool isActive)
{ {
Anm2Animation* animation = actor.animation_get(); Anm2Animation* animation = actor.animation_get();
swap.isRegion = false; swap.isRegion = false;
@@ -373,14 +373,14 @@ void Play::body_update()
actor.overrides[OVERRIDE_EXPAND + i * 2 + 1].scale = {add, add}; actor.overrides[OVERRIDE_EXPAND + i * 2 + 1].scale = {add, add};
} }
isTalking = region_swap(actor, data.talkSource, talkTime, actor.overrides[OVERRIDE_TALK], isTalking); isTalking = is_region_swapped(actor, data.talkSource, talkTime, actor.overrides[OVERRIDE_TALK], isTalking);
if (talkTime > source_length_get(actor, data.talkSource)) if (talkTime > source_length_get(actor, data.talkSource))
talkTime = 0; talkTime = 0;
int blinkLength = source_length_get(actor, data.blinkSource); int blinkLength = source_length_get(actor, data.blinkSource);
if (blinkLength != 1) if (blinkLength != 1)
{ {
isBlinking |= is_chance(data.blinkChance); isBlinking |= is_chance(data.blinkChance);
isBlinking = region_swap(actor, data.blinkSource, blinkTime, actor.overrides[OVERRIDE_BLINK], isBlinking); isBlinking = is_region_swapped(actor, data.blinkSource, blinkTime, actor.overrides[OVERRIDE_BLINK], isBlinking);
if (blinkTime >= blinkLength) if (blinkTime >= blinkLength)
{ {
isBlinking = false; isBlinking = false;
+2 -2
View File
@@ -172,8 +172,8 @@ public:
void item_add(int id, Vec2 position, Vec2 velocity = {}, float rotation = 0); void item_add(int id, Vec2 position, Vec2 velocity = {}, float rotation = 0);
int spawn_delay_get(); int spawn_delay_get();
bool is_spawn_possible(); bool is_spawn_possible();
bool spawn_queue_add(int id); void spawn_queue_add(int id);
bool spawn_queue_add(); void spawn_queue_add();
void particles_spawn(const std::vector<std::string>& names, Vec2 position); void particles_spawn(const std::vector<std::string>& names, Vec2 position);
Rect eat_rect_get(const EatArea& eatArea); Rect eat_rect_get(const EatArea& eatArea);
+3 -3
View File
@@ -119,7 +119,7 @@ static Texture texture_create(int width, int height, const void* pixels)
return texture; return texture;
} }
bool Renderer::init() void Renderer::init()
{ {
bool isLoaded = true; bool isLoaded = true;
#define GL_FUNCTION_LOAD(name) \ #define GL_FUNCTION_LOAD(name) \
@@ -128,7 +128,7 @@ bool Renderer::init()
GL_FUNCTIONS(GL_FUNCTION_LOAD) GL_FUNCTIONS(GL_FUNCTION_LOAD)
#undef GL_FUNCTION_LOAD #undef GL_FUNCTION_LOAD
if (!isLoaded) if (!isLoaded)
return false; return;
program = gl.CreateProgram(); program = gl.CreateProgram();
GLuint vertexShader = shader_compile(GL_VERTEX_SHADER, VERTEX_SHADER); GLuint vertexShader = shader_compile(GL_VERTEX_SHADER, VERTEX_SHADER);
@@ -154,7 +154,7 @@ bool Renderer::init()
attribute(3, 4, offsetof(Vertex, offset)); attribute(3, 4, offsetof(Vertex, offset));
white = texture_create(WHITE_SIZE, WHITE_SIZE, &WHITE_PIXEL); white = texture_create(WHITE_SIZE, WHITE_SIZE, &WHITE_PIXEL);
return true; isValid = true;
} }
Texture* Renderer::texture_get(const std::string& path) Texture* Renderer::texture_get(const std::string& path)
+2 -1
View File
@@ -35,8 +35,9 @@ class Renderer
{ {
public: public:
Texture white; Texture white;
bool isValid = false;
bool init(); void init();
Texture* texture_get(const std::string& path); Texture* texture_get(const std::string& path);
void canvas_set(Canvas* canvas, int width, int height, Color clear = CLEAR); void canvas_set(Canvas* canvas, int width, int height, Color clear = CLEAR);
void quad_draw(const Texture& texture, Rect region, Affine transform, Color tint = WHITE, Color offset = CLEAR); void quad_draw(const Texture& texture, Rect region, Affine transform, Color tint = WHITE, Color offset = CLEAR);
+37 -38
View File
@@ -255,7 +255,7 @@ static void panel_bar_draw(Play& play, bool& isOpen, float slide, ImVec2 positio
{ {
ImVec2 buttonSize = ImGui::GetContentRegionAvail(); ImVec2 buttonSize = ImGui::GetContentRegionAvail();
ImVec2 cursor = ImGui::GetCursorScreenPos(); ImVec2 cursor = ImGui::GetCursorScreenPos();
bool isPressed = fx(ImGui::Button("##Toggle", buttonSize)); bool isPressed = is_fx(ImGui::Button("##Toggle", buttonSize));
if (slide <= 0 || slide >= 1) if (slide <= 0 || slide >= 1)
{ {
tooltip_set(character.strings[isOpen ? closeTooltip : openTooltip].c_str()); tooltip_set(character.strings[isOpen ? closeTooltip : openTooltip].c_str());
@@ -396,7 +396,7 @@ static void interact_tab_draw(Play& play, Settings& settings)
if (play.spawnDelay <= 0) if (play.spawnDelay <= 0)
{ {
ImGui::BeginDisabled(!play.is_spawn_possible()); ImGui::BeginDisabled(!play.is_spawn_possible());
if (fx(ImGui::Button(strings[STRING_InteractSpawnItemButton].c_str(), {width, 0}))) if (is_fx(ImGui::Button(strings[STRING_InteractSpawnItemButton].c_str(), {width, 0})))
play.spawn_queue_add(); play.spawn_queue_add();
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
@@ -416,7 +416,7 @@ static void interact_tab_draw(Play& play, Settings& settings)
isFood |= character.items[entity.id].use == USE_EDIBLE; isFood |= character.items[entity.id].use == USE_EDIBLE;
ImGui::BeginDisabled(!isFood); ImGui::BeginDisabled(!isFood);
StringId autofeedLabel = play.isAutofeed ? STRING_InventoryDisableAutofeedButton : STRING_InventoryEnableAutofeedButton; StringId autofeedLabel = play.isAutofeed ? STRING_InventoryDisableAutofeedButton : STRING_InventoryEnableAutofeedButton;
if (fx(ImGui::Button(strings[autofeedLabel].c_str(), {width, 0}))) if (is_fx(ImGui::Button(strings[autofeedLabel].c_str(), {width, 0})))
play.isAutofeed = !play.isAutofeed; play.isAutofeed = !play.isAutofeed;
ImGui::EndDisabled(); ImGui::EndDisabled();
tooltip_set(strings[play.isAutofeed ? STRING_InventoryDisableAutofeedTooltip : STRING_InventoryEnableAutofeedTooltip] tooltip_set(strings[play.isAutofeed ? STRING_InventoryDisableAutofeedTooltip : STRING_InventoryEnableAutofeedTooltip]
@@ -430,7 +430,7 @@ static void interact_tab_draw(Play& play, Settings& settings)
if (!isShown) if (!isShown)
return; return;
ImGui::PushFont(nullptr, size); ImGui::PushFont(nullptr, size);
if (fx(ImGui::Button(strings[label].c_str(), {width, 0}))) if (is_fx(ImGui::Button(strings[label].c_str(), {width, 0})))
play.text.set(play, (dialogue.*get)(name)); play.text.set(play, (dialogue.*get)(name));
ImGui::PopFont(); ImGui::PopFont();
}; };
@@ -476,7 +476,7 @@ static void catalog_tab_draw(Play& play, Ui& ui, Renderer& renderer, std::vector
ImVec2 padding = {(ITEM_SIZE - image.x) / 2, (ITEM_SIZE - image.y) / 2}; ImVec2 padding = {(ITEM_SIZE - image.x) / 2, (ITEM_SIZE - image.y) / 2};
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
{ImGui::GetStyle().FramePadding.x + padding.x, ImGui::GetStyle().FramePadding.y + padding.y}); {ImGui::GetStyle().FramePadding.x + padding.x, ImGui::GetStyle().FramePadding.y + padding.y});
if (fx(ImGui::ImageButton("##Item", texture_id_get(canvases[i].texture), image, {0, 0}, {1, 1}, {}, tint))) if (is_fx(ImGui::ImageButton("##Item", texture_id_get(canvases[i].texture), image, {0, 0}, {1, 1}, {}, tint)))
ui.selectedItem = i; ui.selectedItem = i;
ImGui::PopStyleVar(); ImGui::PopStyleVar();
if (isSelected) if (isSelected)
@@ -543,25 +543,25 @@ static void cheats_tab_draw(Play& play)
std::array<std::string, STRING_COUNT>& strings = character.strings; std::array<std::string, STRING_COUNT>& strings = character.strings;
if (!ImGui::BeginChild("##Cheats")) if (!ImGui::BeginChild("##Cheats"))
return ImGui::EndChild(); return ImGui::EndChild();
fx(ImGui::SliderFloat(strings[STRING_CheatsCalories].c_str(), &progress.calories, 0, play.capacity_max_get(), is_fx(ImGui::SliderFloat(strings[STRING_CheatsCalories].c_str(), &progress.calories, 0, play.capacity_max_get(),
"%0.0f kcal")); "%0.0f kcal"));
fx(ImGui::SliderFloat(strings[STRING_CheatsCapacity].c_str(), &progress.capacity, character.capacityMin, is_fx(ImGui::SliderFloat(strings[STRING_CheatsCapacity].c_str(), &progress.capacity, character.capacityMin,
character.capacityMax, "%0.0f kcal")); character.capacityMax, "%0.0f kcal"));
if (fx(ImGui::SliderFloat(strings[STRING_CheatsWeight].c_str(), &progress.weight, character.weight, if (is_fx(ImGui::SliderFloat(strings[STRING_CheatsWeight].c_str(), &progress.weight, character.weight,
character.weightMax, strings[STRING_CheatsWeightFormat].c_str()))) character.weightMax, strings[STRING_CheatsWeightFormat].c_str())))
play.idle_play(); play.idle_play();
int stage = play.stage + 1; int stage = play.stage + 1;
if (fx(ImGui::SliderInt(strings[STRING_CheatsStage].c_str(), &stage, 1, (int)character.stages.size() + 1))) if (is_fx(ImGui::SliderInt(strings[STRING_CheatsStage].c_str(), &stage, 1, (int)character.stages.size() + 1)))
{ {
play.stage = stage - 1; play.stage = stage - 1;
progress.weight = play.stage_threshold_get(play.stage); progress.weight = play.stage_threshold_get(play.stage);
play.idle_play(); play.idle_play();
} }
float rate = progress.digestionRate * TICK_RATE; float rate = progress.digestionRate * TICK_RATE;
if (fx(ImGui::SliderFloat(strings[STRING_CheatsDigestionRate].c_str(), &rate, character.digestionRateMin * TICK_RATE, if (is_fx(ImGui::SliderFloat(strings[STRING_CheatsDigestionRate].c_str(), &rate, character.digestionRateMin * TICK_RATE,
character.digestionRateMax * TICK_RATE, strings[STRING_CheatsDigestionRateFormat].c_str()))) character.digestionRateMax * TICK_RATE, strings[STRING_CheatsDigestionRateFormat].c_str())))
progress.digestionRate = rate / TICK_RATE; progress.digestionRate = rate / TICK_RATE;
if (fx(ImGui::Button(strings[STRING_CheatsDigestButton].c_str()))) if (is_fx(ImGui::Button(strings[STRING_CheatsDigestButton].c_str())))
progress.digestionProgress = DIGESTION_MAX; progress.digestionProgress = DIGESTION_MAX;
ImGui::EndChild(); ImGui::EndChild();
} }
@@ -581,7 +581,7 @@ static void tools_draw(Play& play, ImVec2 position, ImVec2 size)
if (isActive) if (isActive)
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered)); ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered));
ImGui::BeginDisabled(isDisabled); ImGui::BeginDisabled(isDisabled);
bool isPressed = fx(ImGui::Button(isIcon ? "##Icon" : label.c_str(), {side, side})); bool isPressed = is_fx(ImGui::Button(isIcon ? "##Icon" : label.c_str(), {side, side}));
if (isIcon) if (isIcon)
{ {
ImVec2 min = ImGui::GetItemRectMin(); ImVec2 min = ImGui::GetItemRectMin();
@@ -647,41 +647,41 @@ void Ui::settings_update(Settings& settings, Audio& audio, const Character* char
return character ? character->strings[id].c_str() : STRING_DEFAULTS[id]; return character ? character->strings[id].c_str() : STRING_DEFAULTS[id];
}; };
ImGui::SeparatorText(string_get(STRING_SettingsMeasurementSystem)); ImGui::SeparatorText(string_get(STRING_SettingsMeasurementSystem));
fx(ImGui::RadioButton(string_get(STRING_SettingsMetric), (int*)&settings.measurement, MEASUREMENT_METRIC)); is_fx(ImGui::RadioButton(string_get(STRING_SettingsMetric), (int*)&settings.measurement, MEASUREMENT_METRIC));
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsMetricTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsMetricTooltip));
ImGui::SameLine(); ImGui::SameLine();
fx(ImGui::RadioButton(string_get(STRING_SettingsImperial), (int*)&settings.measurement, MEASUREMENT_IMPERIAL)); is_fx(ImGui::RadioButton(string_get(STRING_SettingsImperial), (int*)&settings.measurement, MEASUREMENT_IMPERIAL));
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsImperialTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsImperialTooltip));
ImGui::SeparatorText(string_get(STRING_SettingsWindow)); ImGui::SeparatorText(string_get(STRING_SettingsWindow));
const char* modes[] = {string_get(STRING_SettingsWindowed), string_get(STRING_SettingsFullscreen)}; const char* modes[] = {string_get(STRING_SettingsWindowed), string_get(STRING_SettingsFullscreen)};
int mode = isFullscreen; int mode = isFullscreen;
if (fx(ImGui::Combo(string_get(STRING_SettingsWindowMode), &mode, modes, 2))) if (is_fx(ImGui::Combo(string_get(STRING_SettingsWindowMode), &mode, modes, 2)))
isFullscreenToggled = true; isFullscreenToggled = true;
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsWindowModeTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsWindowModeTooltip));
ImGui::SeparatorText(string_get(STRING_SettingsSound)); ImGui::SeparatorText(string_get(STRING_SettingsSound));
ImGui::BeginDisabled(settings.isMuted); ImGui::BeginDisabled(settings.isMuted);
if (fx(ImGui::SliderInt(string_get(STRING_SettingsVolume), &settings.volume, 0, (int)PERCENT, "%d%%"))) if (is_fx(ImGui::SliderInt(string_get(STRING_SettingsVolume), &settings.volume, 0, (int)PERCENT, "%d%%")))
audio.gain_set(settings.volume / PERCENT); audio.gain_set(settings.volume / PERCENT);
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsVolumeTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsVolumeTooltip));
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
if (fx(ImGui::Checkbox(string_get(STRING_SettingsMute), &settings.isMuted))) if (is_fx(ImGui::Checkbox(string_get(STRING_SettingsMute), &settings.isMuted)))
audio.gain_set(settings.isMuted ? 0 : settings.volume / PERCENT); audio.gain_set(settings.isMuted ? 0 : settings.volume / PERCENT);
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsMuteTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsMuteTooltip));
ImGui::SeparatorText(string_get(STRING_SettingsAppearance)); ImGui::SeparatorText(string_get(STRING_SettingsAppearance));
bool isColorChanged = fx(ImGui::Checkbox(string_get(STRING_SettingsUseCharacterColor), &settings.isCharacterColor)); bool isColorChanged = is_fx(ImGui::Checkbox(string_get(STRING_SettingsUseCharacterColor), &settings.isCharacterColor));
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsUseCharacterColorTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsUseCharacterColorTooltip));
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginDisabled(settings.isCharacterColor); ImGui::BeginDisabled(settings.isCharacterColor);
isColorChanged |= fx(ImGui::ColorEdit3(string_get(STRING_SettingsColor), &settings.color.r, isColorChanged |= is_fx(ImGui::ColorEdit3(string_get(STRING_SettingsColor), &settings.color.r,
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip)); ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip));
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsColorTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsColorTooltip));
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::Separator(); ImGui::Separator();
if (fx(ImGui::Button(string_get(STRING_SettingsResetButton), {-FLT_MIN, 0}))) if (is_fx(ImGui::Button(string_get(STRING_SettingsResetButton), {-FLT_MIN, 0})))
{ {
settings.reset(); settings.reset();
audio.gain_set(settings.volume / PERCENT); audio.gain_set(settings.volume / PERCENT);
@@ -693,10 +693,10 @@ void Ui::settings_update(Settings& settings, Audio& audio, const Character* char
if (!character) if (!character)
return; return;
ImGui::Separator(); ImGui::Separator();
if (fx(ImGui::Button(string_get(STRING_SettingsSaveButton), {-FLT_MIN, 0}))) if (is_fx(ImGui::Button(string_get(STRING_SettingsSaveButton), {-FLT_MIN, 0})))
isSave = true; isSave = true;
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsSaveTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsSaveTooltip));
if (fx(ImGui::Button(string_get(STRING_SettingsReturnToCharactersButton), {-FLT_MIN, 0}))) if (is_fx(ImGui::Button(string_get(STRING_SettingsReturnToCharactersButton), {-FLT_MIN, 0})))
isReturn = true; isReturn = true;
ImGui::SetItemTooltip("%s", string_get(STRING_SettingsReturnToCharactersTooltip)); ImGui::SetItemTooltip("%s", string_get(STRING_SettingsReturnToCharactersTooltip));
} }
@@ -738,22 +738,22 @@ void Ui::play_update(Play& play, Settings& settings, Renderer& renderer, Audio&
ImGui::SetNextWindowPos({menuX, style.WindowPadding.y}); ImGui::SetNextWindowPos({menuX, style.WindowPadding.y});
if (ImGui::Begin("##Menu", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Tabs")) if (ImGui::Begin("##Menu", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Tabs"))
{ {
if (fx(ImGui::BeginTabItem(strings[STRING_MenuTabInteract].c_str()))) if (is_fx(ImGui::BeginTabItem(strings[STRING_MenuTabInteract].c_str())))
{ {
interact_tab_draw(play, settings); interact_tab_draw(play, settings);
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (fx(ImGui::BeginTabItem(strings[STRING_MenuTabInventory].c_str()))) if (is_fx(ImGui::BeginTabItem(strings[STRING_MenuTabInventory].c_str())))
{ {
catalog_tab_draw(play, *this, renderer, itemCanvases, itemActors); catalog_tab_draw(play, *this, renderer, itemCanvases, itemActors);
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (fx(ImGui::BeginTabItem(strings[STRING_MenuTabSettings].c_str()))) if (is_fx(ImGui::BeginTabItem(strings[STRING_MenuTabSettings].c_str())))
{ {
settings_update(settings, audio, &character); settings_update(settings, audio, &character);
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (play.isCheats && fx(ImGui::BeginTabItem(strings[STRING_MenuTabCheats].c_str()))) if (play.isCheats && is_fx(ImGui::BeginTabItem(strings[STRING_MenuTabCheats].c_str())))
{ {
cheats_tab_draw(play); cheats_tab_draw(play);
ImGui::EndTabItem(); ImGui::EndTabItem();
@@ -767,12 +767,12 @@ void Ui::play_update(Play& play, Settings& settings, Renderer& renderer, Audio&
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
} }
bool Ui::select_update(std::vector<Character>& characters, Settings& settings, Renderer& renderer, Audio& audio) void Ui::select_update(std::vector<Character>& characters, Settings& settings, Renderer& renderer, Audio& audio)
{ {
ImGuiStyle& style = ImGui::GetStyle(); ImGuiStyle& style = ImGui::GetStyle();
ImVec2 screen = ImGui::GetIO().DisplaySize; ImVec2 screen = ImGui::GetIO().DisplaySize;
ImVec2 half = {screen.x / 2 - style.WindowPadding.x * 2, screen.y / 2 - style.WindowPadding.y * 2}; ImVec2 half = {screen.x / 2 - style.WindowPadding.x * 2, screen.y / 2 - style.WindowPadding.y * 2};
bool isRefresh = false; isRefresh = false;
characterIndex = characters.empty() ? -1 : std::clamp(characterIndex, 0, (int)characters.size() - 1); characterIndex = characters.empty() ? -1 : std::clamp(characterIndex, 0, (int)characters.size() - 1);
Character* selected = characterIndex >= 0 ? &characters[characterIndex] : nullptr; Character* selected = characterIndex >= 0 ? &characters[characterIndex] : nullptr;
if (selected && progressIndex != characterIndex) if (selected && progressIndex != characterIndex)
@@ -789,7 +789,7 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
if (ImGui::Begin("##Preview", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Preview Tabs")) if (ImGui::Begin("##Preview", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Preview Tabs"))
{ {
ImVec2 available = ImGui::GetContentRegionAvail(); ImVec2 available = ImGui::GetContentRegionAvail();
if (fx(ImGui::BeginTabItem("Render"))) if (is_fx(ImGui::BeginTabItem("Render")))
{ {
if (Texture* render = selected->render) if (Texture* render = selected->render)
{ {
@@ -803,7 +803,7 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
ImGui::TextWrapped("%s\n%s", selected->name.c_str(), selected->description.c_str()); ImGui::TextWrapped("%s\n%s", selected->name.c_str(), selected->description.c_str());
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
isPreview = fx(ImGui::BeginTabItem("In Game")); isPreview = is_fx(ImGui::BeginTabItem("In Game"));
if (isPreview) if (isPreview)
{ {
Anm2* anm2 = selected->anm2_get(selected->anm2Path, renderer, audio); Anm2* anm2 = selected->anm2_get(selected->anm2Path, renderer, audio);
@@ -866,11 +866,11 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
ImVec2 buttonSize = {row_width_get(selectedProgress.isValid ? 2 : 1), 0}; ImVec2 buttonSize = {row_width_get(selectedProgress.isValid ? 2 : 1), 0};
if (selectedProgress.isValid) if (selectedProgress.isValid)
{ {
isContinue = fx(ImGui::Button("Continue", buttonSize)); isContinue = is_fx(ImGui::Button("Continue", buttonSize));
tooltip_set("Continue from a saved game."); tooltip_set("Continue from a saved game.");
ImGui::SameLine(); ImGui::SameLine();
} }
if (fx(ImGui::Button("New Game", buttonSize))) if (is_fx(ImGui::Button("New Game", buttonSize)))
{ {
isNewGame = !selectedProgress.isValid; isNewGame = !selectedProgress.isValid;
isNewGameWarning = selectedProgress.isValid; isNewGameWarning = selectedProgress.isValid;
@@ -887,9 +887,9 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
{ {
ImGui::TextWrapped("This will delete saved progress! Are you sure?"); ImGui::TextWrapped("This will delete saved progress! Are you sure?");
ImVec2 popupSize = {row_width_get(2), 0}; ImVec2 popupSize = {row_width_get(2), 0};
isNewGame = fx(ImGui::Button("Yes", popupSize)); isNewGame = is_fx(ImGui::Button("Yes", popupSize));
ImGui::SameLine(); ImGui::SameLine();
if (fx(ImGui::Button("No", popupSize))) if (is_fx(ImGui::Button("No", popupSize)))
isNewGameWarning = false; isNewGameWarning = false;
ImGui::EndPopup(); ImGui::EndPopup();
} }
@@ -902,7 +902,7 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
ImGui::SetNextWindowPos({screen.x / 2, style.WindowPadding.y}); ImGui::SetNextWindowPos({screen.x / 2, style.WindowPadding.y});
if (ImGui::Begin("##Main Menu", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Main Tabs")) if (ImGui::Begin("##Main Menu", nullptr, PANEL_FLAGS) && ImGui::BeginTabBar("##Main Tabs"))
{ {
if (fx(ImGui::BeginTabItem("Characters"))) if (is_fx(ImGui::BeginTabItem("Characters")))
{ {
if (ImGui::BeginPopupContextWindow("##Characters Context")) if (ImGui::BeginPopupContextWindow("##Characters Context"))
{ {
@@ -923,7 +923,7 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
? ImGui::ImageButton(character.name.c_str(), texture_id_get(*character.portrait), ? ImGui::ImageButton(character.name.c_str(), texture_id_get(*character.portrait),
{side, side}) {side, side})
: ImGui::Button(character.name.c_str(), {side, side}); : ImGui::Button(character.name.c_str(), {side, side});
if (fx(isPressed)) if (is_fx(isPressed))
characterIndex = i; characterIndex = i;
if (isSelected) if (isSelected)
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -936,7 +936,7 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
} }
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (fx(ImGui::BeginTabItem("Settings"))) if (is_fx(ImGui::BeginTabItem("Settings")))
{ {
settings_update(settings, audio, nullptr); settings_update(settings, audio, nullptr);
ImGui::EndTabItem(); ImGui::EndTabItem();
@@ -946,5 +946,4 @@ bool Ui::select_update(std::vector<Character>& characters, Settings& settings, R
ImGui::End(); ImGui::End();
if (isRefresh) if (isRefresh)
progressIndex = -1; progressIndex = -1;
return isRefresh;
} }
+3 -2
View File
@@ -23,7 +23,7 @@ void widget_sounds_set(Audio* audio, const Sounds* hover, const Sounds* select);
void widget_fx(); void widget_fx();
// Passes a widget's result through after giving it its sounds and hover border. // Passes a widget's result through after giving it its sounds and hover border.
inline bool fx(bool result) inline bool is_fx(bool result)
{ {
widget_fx(); widget_fx();
return result; return result;
@@ -59,10 +59,11 @@ public:
bool isNewGame = false; bool isNewGame = false;
bool isNewGameWarning = false; bool isNewGameWarning = false;
bool isPreview = false; bool isPreview = false;
bool isRefresh = false;
bool isReturn = false; bool isReturn = false;
bool isSave = false; bool isSave = false;
void play_update(Play& play, Settings& settings, Renderer& renderer, Audio& audio); void play_update(Play& play, Settings& settings, Renderer& renderer, Audio& audio);
bool select_update(std::vector<Character>& characters, Settings& settings, Renderer& renderer, Audio& audio); void select_update(std::vector<Character>& characters, Settings& settings, Renderer& renderer, Audio& audio);
void settings_update(Settings& settings, Audio& audio, const Character* character); void settings_update(Settings& settings, Audio& audio, const Character* character);
}; };