mirror of
https://github.com/diasurgical/DevilutionX.git
synced 2026-10-12 02:13:06 +00:00
Extract baked sprite shadows and draw them separately
The original sprites have their shadow drawn into them as black pixels.
All of these shadows were made by one function: the sprite squashed to
a third of its height and sheared to the left, measured from a baseline
row B:
v = (B - y) / 3
ys = B - 2 - v
xs = x - (3 * v) / 4
Only the choice of baseline differs between sprites. It is now recorded
per sprite in a new `shadowRule` column of the monster, object, towner
and class sprite data files:
* `FirstFrame`: the lowest opaque row of the first frame
* `EachFrame`: the lowest opaque row of each frame
* `FirstFrame+N`: the same plus N rows, for flying monsters
* `None`: no shadow
* `Baked`: a shadow that cannot be told apart from black parts of the
sprite, so it is left in the sprite
A rule can be followed by `|Unexplained` and `|Stray` for the few
sprites that the function does not reproduce exactly. The rule is empty
for the Hellfire bard and barbarian, whose sprites were not available.
When sprites are loaded, `ExtractClxShadows` removes the shadow from
each sprite and stores it as a mask of horizontal spans in the CLX
frame header, so it is available wherever the sprite is. The shadow is
drawn right before its sprite, which gives the same picture as before.
A new "Transparent Shadows" graphics option blends the shadows with
what is underneath instead of covering it. It is off by default.
In debug builds, `dev.display.shadows()` toggles drawing them.
Transparent shadows are not perfect. A black pixel of a sprite that
lies inside the sprite's own shadow looks exactly like shadow, so it is
extracted with it and becomes see-through. For example, the warrior in
light armor with a sword and shield, facing southwest, is missing a few
pixels from his left knee. Getting this perfect will likely require
overriding the shadow masks of a handful of sprites, which is something
that can be done with AI in the future.
The rules were found and checked with `tools/shadow_search`, against
all player, monster, towner and object sprites of Diablo and Hellfire:
139,943 frames in 1,438 files. Of the 136,918 frames that have an
extractable shadow, 99.77% are reproduced pixel for pixel.
Extraction adds about 14 us per frame when a sprite is loaded
(about 14 ms for all animations of a character). For the original
player sprites, the sprites with their shadow masks take 0.3% less
memory than before.
Drawing a sprite with its shadow, per sprite, for all sprites of each
kind (`shadow_extract_check <mpq> render`, release build, fully visible
sprites, Ryzen AI Max+ 395):
Players Monsters Monk
Without lighting
Before 2,111 ns 2,444 ns 2,109 ns
Opaque shadow -9.5% -10.1% -6.1%
Transparent shadow +10.0% +7.5% +9.0%
With a light table
Before 2,659 ns 2,983 ns 2,548 ns
Opaque shadow -5.2% -4.5% -2.3%
Transparent shadow +10.9% +10.2% +10.4%
Opaque shadows are a little faster to draw than before: a shadow is now
a short list of spans instead of many commands in the sprite. Blending
a shadow costs about as much again as that saves. Either way the frame
rate is practically unchanged: with about 10 such sprites per frame,
it comes to about +0.2% FPS with opaque shadows and -0.3% with
transparent ones in an unthrottled demo playback, where the 3 modes
measured within noise of each other.
Selection outlines can differ by a pixel where a shadow touches the
sprite: their shape used to depend on how the black pixels of the
shadow were encoded.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3755190275
commit
debe33ca7b
@@ -475,3 +475,4 @@ uwp-project/Assets/ui_art
|
||||
/tmp/
|
||||
/compile_commands.json
|
||||
/tools/tmp
|
||||
/.claude/
|
||||
|
||||
@@ -51,6 +51,7 @@ set(tests
|
||||
)
|
||||
set(standalone_tests
|
||||
cl2_to_clx_test
|
||||
clx_shadow_test
|
||||
codec_test
|
||||
crawl_test
|
||||
data_file_test
|
||||
@@ -64,6 +65,7 @@ set(standalone_tests
|
||||
vision_test
|
||||
random_test
|
||||
rectangle_test
|
||||
shadow_rule_test
|
||||
sheen_bidi_test
|
||||
static_vector_test
|
||||
str_cat_test
|
||||
@@ -125,6 +127,17 @@ target_link_dependencies(cl2_to_clx_test
|
||||
libdevilutionx_log
|
||||
libdevilutionx_surface
|
||||
)
|
||||
target_link_dependencies(clx_shadow_test
|
||||
PRIVATE
|
||||
DevilutionX::SDL
|
||||
app_fatal_for_testing
|
||||
language_for_testing
|
||||
libdevilutionx_clx_render
|
||||
libdevilutionx_clx_shadow
|
||||
libdevilutionx_endian_write
|
||||
libdevilutionx_log
|
||||
libdevilutionx_surface
|
||||
)
|
||||
target_link_dependencies(codec_test PRIVATE libdevilutionx_codec app_fatal_for_testing)
|
||||
|
||||
add_custom_target(clx_render_benchmark_resources
|
||||
@@ -174,6 +187,7 @@ target_link_dependencies(palette_blending_benchmark
|
||||
app_fatal_for_testing
|
||||
)
|
||||
target_link_dependencies(parse_int_test PRIVATE libdevilutionx_parse_int)
|
||||
target_link_dependencies(shadow_rule_test PRIVATE libdevilutionx_strings)
|
||||
target_link_dependencies(path_test PRIVATE libdevilutionx_pathfinding libdevilutionx_direction app_fatal_for_testing)
|
||||
target_link_dependencies(vision_test PRIVATE libdevilutionx_vision)
|
||||
target_link_dependencies(path_benchmark PRIVATE libdevilutionx_pathfinding app_fatal_for_testing)
|
||||
|
||||
@@ -39,6 +39,7 @@ if(CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg.cmake$")
|
||||
option(USE_GETTEXT_FROM_VCPKG "Add vcpkg dependency for gettext[tools] for compiling translations" OFF)
|
||||
endif()
|
||||
option(BUILD_TESTING "Build tests." ON)
|
||||
option(BUILD_SHADOW_SEARCH "Build the player sprite shadow transform search tool (tools/shadow_search)." OFF)
|
||||
|
||||
# These must be included after the options above but before the `project` call.
|
||||
include(VcPkgManifestFeatures)
|
||||
@@ -443,6 +444,10 @@ if(BUILD_TESTING)
|
||||
include(Tests)
|
||||
endif()
|
||||
|
||||
if(BUILD_SHADOW_SEARCH)
|
||||
add_subdirectory(tools/shadow_search)
|
||||
endif()
|
||||
|
||||
include(functions/set_relative_file_macro)
|
||||
set_relative_file_macro(${BIN_TARGET})
|
||||
|
||||
|
||||
@@ -228,6 +228,16 @@ target_link_dependencies(libdevilutionx_cl2_to_clx
|
||||
libdevilutionx_endian_write
|
||||
)
|
||||
|
||||
add_devilutionx_object_library(libdevilutionx_clx_shadow
|
||||
utils/clx_shadow.cpp
|
||||
)
|
||||
target_link_dependencies(libdevilutionx_clx_shadow
|
||||
PUBLIC
|
||||
libdevilutionx_strings
|
||||
PRIVATE
|
||||
libdevilutionx_endian_write
|
||||
)
|
||||
|
||||
add_devilutionx_object_library(libdevilutionx_clx_render
|
||||
engine/render/clx_render.cpp
|
||||
)
|
||||
@@ -1060,6 +1070,7 @@ target_link_dependencies(libdevilutionx PUBLIC
|
||||
unordered_dense::unordered_dense
|
||||
libdevilutionx_assets
|
||||
libdevilutionx_clx_render
|
||||
libdevilutionx_clx_shadow
|
||||
libdevilutionx_codec
|
||||
libdevilutionx_config
|
||||
libdevilutionx_control
|
||||
|
||||
@@ -35,6 +35,7 @@ bool DebugInvisible = false;
|
||||
bool DebugVision = false;
|
||||
bool DebugPath = false;
|
||||
bool DebugGrid = false;
|
||||
bool DebugHideShadows = false;
|
||||
ankerl::unordered_dense::map<int, Point> DebugCoordsMap;
|
||||
bool DebugScrollViewEnabled = false;
|
||||
std::string debugTRN;
|
||||
|
||||
@@ -25,6 +25,8 @@ extern bool DebugInvisible;
|
||||
extern bool DebugVision;
|
||||
extern bool DebugPath;
|
||||
extern bool DebugGrid;
|
||||
/** Hides the shadows that are drawn separately from their sprites (see `shadow_mask.hpp`). */
|
||||
extern bool DebugHideShadows;
|
||||
extern ankerl::unordered_dense::map<int, Point> DebugCoordsMap;
|
||||
extern bool DebugScrollViewEnabled;
|
||||
extern std::string debugTRN;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 1. CLX frame header stores frame width and height.
|
||||
* 2. CLX frame header does not store 32-pixel block offsets.
|
||||
*
|
||||
* CLX frame header is 6 bytes:
|
||||
* CLX frame header is at least 6 bytes:
|
||||
*
|
||||
* Bytes | Type | Value
|
||||
* :-----:|:--------:|-------------
|
||||
@@ -20,6 +20,8 @@
|
||||
* 2..4 | uint16_t | width
|
||||
* 4..6 | uint16_t | height
|
||||
*
|
||||
* A header larger than 6 bytes holds the shadow mask of the sprite, see `shadow_mask.hpp`.
|
||||
*
|
||||
* CL2 reference: https://github.com/savagesteel/d1-file-formats/blob/master/PC-Mac/CL2.md#2-file-structure
|
||||
*/
|
||||
|
||||
@@ -42,6 +44,9 @@ class OptionalClxSprite;
|
||||
*/
|
||||
class ClxSprite {
|
||||
public:
|
||||
/** The size of a frame header without an extension. */
|
||||
static constexpr uint16_t BaseHeaderSize = 6;
|
||||
|
||||
explicit constexpr ClxSprite(const uint8_t *data, uint32_t dataSize)
|
||||
: data_(data)
|
||||
, pixel_data_size_(dataSize - LoadLE16(data))
|
||||
@@ -59,6 +64,18 @@ public:
|
||||
return LoadLE16(&data_[4]);
|
||||
}
|
||||
|
||||
/** @brief The size of the frame header, including anything that follows the height. */
|
||||
[[nodiscard]] constexpr uint16_t headerSize() const
|
||||
{
|
||||
return LoadLE16(data_);
|
||||
}
|
||||
|
||||
/** @brief The part of the frame header that follows the height. Empty for most sprites. */
|
||||
[[nodiscard]] constexpr const uint8_t *headerExtension() const
|
||||
{
|
||||
return &data_[BaseHeaderSize];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The raw pixel data (CL2 frame data).
|
||||
*
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
#include "engine/point.hpp"
|
||||
#include "engine/render/blit_impl.hpp"
|
||||
#include "engine/shadow_mask.hpp"
|
||||
#include "engine/surface.hpp"
|
||||
#include "utils/attributes.h"
|
||||
#include "utils/clx_decode.hpp"
|
||||
@@ -604,6 +606,66 @@ void ClxDrawBlended(const Surface &out, Point position, ClxSprite clx)
|
||||
DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlended {});
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Calls `spanFn(dst, width)` for every span of the shadow mask, clipped to the output buffer.
|
||||
*/
|
||||
template <typename SpanFn>
|
||||
DVL_ALWAYS_INLINE void DoRenderShadow(const Surface &out, Point position, ShadowMask mask, SpanFn &&spanFn)
|
||||
{
|
||||
const int outWidth = out.w();
|
||||
const int outHeight = out.h();
|
||||
mask.forEachSpan([&](uint16_t rowFromBottom, ShadowSpan span) {
|
||||
const int y = position.y - rowFromBottom;
|
||||
if (y < 0 || y >= outHeight) return;
|
||||
int x = position.x + span.x;
|
||||
int width = span.width;
|
||||
if (x < 0) {
|
||||
width += x;
|
||||
x = 0;
|
||||
}
|
||||
if (x + width > outWidth) width = outWidth - x;
|
||||
if (width <= 0) return;
|
||||
spanFn(out.at(x, y), static_cast<unsigned>(width));
|
||||
});
|
||||
}
|
||||
|
||||
/** Blends `width` pixels with black. */
|
||||
DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlendWithBlack(uint8_t *dst, unsigned width)
|
||||
{
|
||||
#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT
|
||||
// Two pixels per lookup. Unlike the rectangle blend in `primitive_render.cpp`, this does
|
||||
// not need the pixels to be aligned, which matters because shadow spans are short.
|
||||
for (; width >= 2; width -= 2, dst += 2) {
|
||||
const uint16_t blended = paletteTransparencyLookupBlack16[dst[0] | (dst[1] << 8U)];
|
||||
dst[0] = static_cast<uint8_t>(blended);
|
||||
dst[1] = static_cast<uint8_t>(blended >> 8U);
|
||||
}
|
||||
if (width != 0) *dst = paletteTransparencyLookup[0][*dst];
|
||||
#else
|
||||
const uint8_t *const lookupTable = paletteTransparencyLookup[0];
|
||||
for (; width != 0; --width, ++dst)
|
||||
*dst = lookupTable[*dst];
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ClxDrawShadow(const Surface &out, Point position, ClxSprite clx)
|
||||
{
|
||||
if (!HasShadowMask(clx)) return;
|
||||
DoRenderShadow(out, position, GetShadowMask(clx), [](uint8_t *dst, unsigned width) {
|
||||
std::memset(dst, 0, width);
|
||||
});
|
||||
}
|
||||
|
||||
void ClxDrawShadowBlended(const Surface &out, Point position, ClxSprite clx)
|
||||
{
|
||||
if (!HasShadowMask(clx)) return;
|
||||
DoRenderShadow(out, position, GetShadowMask(clx), BlendWithBlack);
|
||||
}
|
||||
|
||||
void ClxDrawBlendedTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t *trn)
|
||||
{
|
||||
DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlendedWithMap { trn });
|
||||
|
||||
@@ -88,6 +88,23 @@ void ClxDrawBlendedTRN(const Surface &out, Point position, ClxSprite clx, const
|
||||
*/
|
||||
void ClxDrawBlended(const Surface &out, Point position, ClxSprite clx);
|
||||
|
||||
/**
|
||||
* @brief Draws the shadow of the sprite in black.
|
||||
*
|
||||
* Does nothing if the sprite has no shadow mask (see `shadow_mask.hpp`).
|
||||
* Drawing the shadow and then the sprite itself gives the sprite as it was before its shadow was extracted.
|
||||
*
|
||||
* @param out Output buffer
|
||||
* @param position Target buffer coordinate, the same as for drawing the sprite
|
||||
* @param clx CLX frame
|
||||
*/
|
||||
void ClxDrawShadow(const Surface &out, Point position, ClxSprite clx);
|
||||
|
||||
/**
|
||||
* @brief Same as `ClxDrawShadow` but darkens what is underneath instead of covering it with black.
|
||||
*/
|
||||
void ClxDrawShadowBlended(const Surface &out, Point position, ClxSprite clx);
|
||||
|
||||
/**
|
||||
* @brief Blit CL2 sprite, and apply lighting, to the given buffer at the given coordinates
|
||||
* @param out Output buffer
|
||||
|
||||
@@ -244,13 +244,36 @@ bool ShouldShowCursor()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blit CL2 sprite, and apply lighting, to the given buffer at the given coordinates
|
||||
* @brief Draws the shadow of a sprite whose shadow is stored separately (see `shadow_mask.hpp`).
|
||||
*
|
||||
* Must be called right before drawing the sprite itself, after its outline:
|
||||
* a shadow that is baked into a sprite covers the outline, and so must this one.
|
||||
* Does nothing for sprites without a separate shadow.
|
||||
*/
|
||||
inline void DrawShadow(const Surface &out, Point position, ClxSprite clx)
|
||||
{
|
||||
static const bool skipDraw = getenv("SHADOW_NO_DRAW") != nullptr;
|
||||
if (skipDraw) return;
|
||||
#ifdef _DEBUG
|
||||
if (DebugHideShadows)
|
||||
return;
|
||||
#endif
|
||||
if (*GetOptions().Graphics.transparentShadows) {
|
||||
ClxDrawShadowBlended(out, position, clx);
|
||||
} else {
|
||||
ClxDrawShadow(out, position, clx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Blit CL2 sprite and its shadow, and apply lighting, to the given buffer at the given coordinates
|
||||
* @param out Output buffer
|
||||
* @param position Target buffer coordinate
|
||||
* @param clx CLX frame
|
||||
*/
|
||||
inline void ClxDrawLight(const Surface &out, Point position, ClxSprite clx, int lightTableIndex)
|
||||
{
|
||||
DrawShadow(out, position, clx);
|
||||
if (lightTableIndex != 0) {
|
||||
ClxDrawTRN(out, position, clx, LightTables[lightTableIndex].data());
|
||||
} else {
|
||||
@@ -344,10 +367,12 @@ void DrawMissilePrivate(const Surface &out, const Missile &missile, Point target
|
||||
const Point missileRenderPosition { targetBufferPosition + missile.position.offsetForRendering - Displacement { missile._miAnimWidth2, 0 } };
|
||||
const ClxSprite sprite = (*missile._miAnimData)[missile._miAnimFrame - 1];
|
||||
if (missile._miUniqTrans != 0) {
|
||||
DrawShadow(out, missileRenderPosition, sprite);
|
||||
ClxDrawTRN(out, missileRenderPosition, sprite, Monsters[missile._misource].uniqueMonsterTRN.get());
|
||||
} else if (missile._miLightFlag) {
|
||||
ClxDrawLight(out, missileRenderPosition, sprite, lightTableIndex);
|
||||
} else {
|
||||
DrawShadow(out, missileRenderPosition, sprite);
|
||||
ClxDraw(out, missileRenderPosition, sprite);
|
||||
}
|
||||
}
|
||||
@@ -385,6 +410,7 @@ void DrawMonster(const Surface &out, Point tilePosition, Point targetBufferPosit
|
||||
const ClxSprite sprite = monster.animInfo.currentSprite();
|
||||
|
||||
if (!IsTileLit(tilePosition)) {
|
||||
DrawShadow(out, targetBufferPosition, sprite);
|
||||
ClxDrawTRN(out, targetBufferPosition, sprite, GetInfravisionTRN());
|
||||
return;
|
||||
}
|
||||
@@ -395,10 +421,12 @@ void DrawMonster(const Surface &out, Point tilePosition, Point targetBufferPosit
|
||||
trn = GetStoneTRN();
|
||||
if (MyPlayer->_pInfraFlag && lightTableIndex > 8)
|
||||
trn = GetInfravisionTRN();
|
||||
if (trn != nullptr)
|
||||
if (trn != nullptr) {
|
||||
DrawShadow(out, targetBufferPosition, sprite);
|
||||
ClxDrawTRN(out, targetBufferPosition, sprite, trn);
|
||||
else
|
||||
} else {
|
||||
ClxDrawLight(out, targetBufferPosition, sprite, lightTableIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,12 +506,14 @@ void DrawPlayer(const Surface &out, const Player &player, Point tilePosition, Po
|
||||
ClxDrawOutlineSkipColorZero(out, GetPlayerOutlineColor(player.getId()), spriteBufferPosition, sprite);
|
||||
|
||||
if (&player == MyPlayer && IsNoneOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) {
|
||||
DrawShadow(out, spriteBufferPosition, sprite);
|
||||
ClxDraw(out, spriteBufferPosition, sprite);
|
||||
DrawPlayerIcons(out, player, targetBufferPosition, /*infraVision=*/false, lightTableIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsTileLit(tilePosition) || ((MyPlayer->_pInfraFlag || MyPlayer->isOnArenaLevel()) && lightTableIndex > 8)) {
|
||||
DrawShadow(out, spriteBufferPosition, sprite);
|
||||
ClxDrawTRN(out, spriteBufferPosition, sprite, GetInfravisionTRN());
|
||||
DrawPlayerIcons(out, player, targetBufferPosition, /*infraVision=*/true, lightTableIndex);
|
||||
return;
|
||||
@@ -533,6 +563,7 @@ void DrawObject(const Surface &out, const Object &objectToDraw, Point tilePositi
|
||||
if (objectToDraw.applyLighting) {
|
||||
ClxDrawLight(out, screenPosition, sprite, lightTableIndex);
|
||||
} else {
|
||||
DrawShadow(out, screenPosition, sprite);
|
||||
ClxDraw(out, screenPosition, sprite);
|
||||
}
|
||||
}
|
||||
@@ -743,6 +774,7 @@ void DrawMonsterHelper(const Surface &out, Point tilePosition, Point targetBuffe
|
||||
if (mi == pcursmonst) {
|
||||
ClxDrawOutlineSkipColorZero(out, OutlineColorsTowner, position, sprite);
|
||||
}
|
||||
DrawShadow(out, position, sprite);
|
||||
ClxDraw(out, position, sprite);
|
||||
return;
|
||||
}
|
||||
@@ -804,6 +836,7 @@ void DrawDungeon(const Surface &out, const Lightmap &lightmap, Point tilePositio
|
||||
const ClxSprite sprite = corpse.spritesForDirection(static_cast<Direction>((bDead >> 5) & 7))[corpse.frame];
|
||||
if (corpse.translationPaletteIndex != 0) {
|
||||
const uint8_t *trn = Monsters[corpse.translationPaletteIndex - 1].uniqueMonsterTRN.get();
|
||||
DrawShadow(out, position, sprite);
|
||||
ClxDrawTRN(out, position, sprite, trn);
|
||||
} else {
|
||||
ClxDrawLight(out, position, sprite, lightTableIndex);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
/**
|
||||
* @file shadow_mask.hpp
|
||||
*
|
||||
* @brief Shadow masks: the shadows extracted from sprites, stored as horizontal spans.
|
||||
*
|
||||
* A shadow is a single-colour shape that is drawn by darkening what is underneath it,
|
||||
* so it is stored as spans rather than as pixels.
|
||||
*
|
||||
* Shadow mask of a single sprite:
|
||||
*
|
||||
* Bytes | Type | Value
|
||||
* :-----:|:-------:|------------------------------------------------------
|
||||
* 0 | uint8_t | number of sprite rows below the lowest shadow row
|
||||
* 1 | uint8_t | number of shadow rows
|
||||
* 2.. | rows | from the lowest row upwards
|
||||
*
|
||||
* Each row is a sequence of spans sorted by x. A span is 2 bytes:
|
||||
*
|
||||
* Byte | Bits | Value
|
||||
* :----:|:----:|--------------------------------------------
|
||||
* 0 | 0..7 | x coordinate of the first pixel
|
||||
* 1 | 0..6 | width
|
||||
* 1 | 7 | set if this is the last span of the row
|
||||
*
|
||||
* A row without shadow pixels is a single span of width 0.
|
||||
* Runs longer than `MaxShadowSpanWidth` are stored as several spans.
|
||||
*
|
||||
* Rows keep their own spans, with absolute x coordinates, instead of letting runs flow from
|
||||
* one row into the next like CLX does: that way a span can be drawn or clipped on its own,
|
||||
* without tracking where the previous one ended.
|
||||
*
|
||||
* Like CLX sprites, masks are stored bottom-up, so that a mask can be drawn knowing only the
|
||||
* position of the bottom-left corner of its sprite.
|
||||
*
|
||||
* The mask of a sprite is stored in the CLX frame header of that sprite, after the height,
|
||||
* so it is available wherever the sprite is. Sprites without a shadow have a plain header.
|
||||
*/
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "appfat.h"
|
||||
#include "engine/clx_sprite.hpp"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
constexpr size_t ShadowMaskHeaderSize = 2;
|
||||
|
||||
/** The largest sprite that a shadow mask can describe. */
|
||||
constexpr unsigned MaxShadowMaskSpriteWidth = 256;
|
||||
constexpr unsigned MaxShadowMaskSpriteHeight = 255;
|
||||
|
||||
/** The largest shadow mask, in bytes: it has to fit in a CLX frame header. */
|
||||
constexpr size_t MaxShadowMaskSize = 0xFFFF - ClxSprite::BaseHeaderSize;
|
||||
|
||||
constexpr uint8_t MaxShadowSpanWidth = 0x7F;
|
||||
constexpr uint8_t ShadowSpanEndOfRow = 0x80;
|
||||
|
||||
struct ShadowSpan {
|
||||
uint8_t x;
|
||||
uint8_t width;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The shadow mask of a single sprite.
|
||||
*/
|
||||
class ShadowMask {
|
||||
public:
|
||||
explicit constexpr ShadowMask(const uint8_t *data)
|
||||
: data_(data)
|
||||
{
|
||||
assert(data != nullptr);
|
||||
}
|
||||
|
||||
/** @brief The number of sprite rows below the lowest shadow row. */
|
||||
[[nodiscard]] constexpr uint8_t bottomOffset() const
|
||||
{
|
||||
return data_[0];
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr uint8_t numRows() const
|
||||
{
|
||||
return data_[1];
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool empty() const
|
||||
{
|
||||
return numRows() == 0;
|
||||
}
|
||||
|
||||
/** @brief The rows, from the lowest one upwards. */
|
||||
[[nodiscard]] constexpr const uint8_t *rows() const
|
||||
{
|
||||
return &data_[ShadowMaskHeaderSize];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calls `fn(rowFromBottom, span)` for every span.
|
||||
*
|
||||
* `rowFromBottom` is the number of sprite rows below the span.
|
||||
*/
|
||||
template <typename F>
|
||||
constexpr void forEachSpan(F &&fn) const
|
||||
{
|
||||
const uint8_t *src = rows();
|
||||
uint16_t row = bottomOffset();
|
||||
for (unsigned i = numRows(); i != 0; --i, ++row) {
|
||||
uint8_t widthAndFlag;
|
||||
do {
|
||||
widthAndFlag = src[1];
|
||||
const uint8_t width = widthAndFlag & MaxShadowSpanWidth;
|
||||
if (width != 0) fn(row, ShadowSpan { src[0], width });
|
||||
src += 2;
|
||||
} while ((widthAndFlag & ShadowSpanEndOfRow) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
};
|
||||
|
||||
/** @brief Whether the sprite carries a shadow mask. */
|
||||
[[nodiscard]] constexpr bool HasShadowMask(ClxSprite sprite)
|
||||
{
|
||||
return sprite.headerSize() > ClxSprite::BaseHeaderSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The shadow mask of the sprite.
|
||||
*
|
||||
* Requires `HasShadowMask(sprite)`.
|
||||
*/
|
||||
[[nodiscard]] constexpr ShadowMask GetShadowMask(ClxSprite sprite)
|
||||
{
|
||||
assert(HasShadowMask(sprite));
|
||||
return ShadowMask { sprite.headerExtension() };
|
||||
}
|
||||
|
||||
} // namespace devilution
|
||||
@@ -0,0 +1,148 @@
|
||||
#pragma once
|
||||
/**
|
||||
* @file shadow_rule.hpp
|
||||
*
|
||||
* @brief Describes how the shadow baked into a sprite was generated.
|
||||
*
|
||||
* The original sprites have their shadow drawn into them as black (palette index 0) pixels.
|
||||
* Every shadow was cast by the same function, measured from a baseline row `B`:
|
||||
*
|
||||
* v = (B - y) / 3
|
||||
* ys = B - 2 - v
|
||||
* xs = x - (3 * v) / 4
|
||||
*
|
||||
* where `(x, y)` is a pixel of the sprite at or above the baseline, `(xs, ys)` is the shadow
|
||||
* pixel it casts, and both divisions are integer divisions.
|
||||
*
|
||||
* The sprites only differ in how the baseline was chosen. That choice is the shadow rule.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "utils/parse_int.hpp"
|
||||
#include "utils/str_cat.hpp"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
enum class ShadowRuleType : uint8_t {
|
||||
/** The sprite has black pixels but no known rule reproduces them, or it was never analysed. */
|
||||
Unknown,
|
||||
/** The sprite has no shadow. */
|
||||
None,
|
||||
/** The baseline is the lowest opaque row of the first frame of the animation (per direction). */
|
||||
FirstFrame,
|
||||
/** The baseline is the lowest opaque row of each frame. */
|
||||
EachFrame,
|
||||
/**
|
||||
* The sprite has a shadow, but it cannot be told apart from the black pixels of the sprite itself:
|
||||
* part of the sprite is black and lies inside its own shadow. The shadow stays part of the sprite.
|
||||
*/
|
||||
Baked,
|
||||
};
|
||||
|
||||
/**
|
||||
* Notes on how well the shadow rule reproduces the shadow of the sprite.
|
||||
*
|
||||
* They do not change how the shadow is extracted.
|
||||
*/
|
||||
enum class ShadowRuleFlags : uint8_t {
|
||||
// clang-format off
|
||||
None = 0,
|
||||
/** Some black pixels of the sprite are neither a shadow that the rule casts nor plausibly part of the sprite itself. */
|
||||
Unexplained = 1 << 0,
|
||||
/** The rule casts a shadow onto some pixels that are transparent in the sprite. */
|
||||
Stray = 1 << 1,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
[[nodiscard]] constexpr ShadowRuleFlags operator|(ShadowRuleFlags a, ShadowRuleFlags b)
|
||||
{
|
||||
return static_cast<ShadowRuleFlags>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr bool HasAnyOf(ShadowRuleFlags flags, ShadowRuleFlags test)
|
||||
{
|
||||
return (static_cast<uint8_t>(flags) & static_cast<uint8_t>(test)) != 0;
|
||||
}
|
||||
|
||||
struct ShadowRule {
|
||||
ShadowRuleType type = ShadowRuleType::Unknown;
|
||||
|
||||
/**
|
||||
* @brief Number of rows added to the baseline.
|
||||
*
|
||||
* Used by flying monsters, whose shadow is detached from the body. Only valid with `FirstFrame`.
|
||||
*/
|
||||
uint8_t baselineOffset = 0;
|
||||
|
||||
ShadowRuleFlags flags = ShadowRuleFlags::None;
|
||||
|
||||
constexpr bool operator==(const ShadowRule &other) const = default;
|
||||
|
||||
/** @brief Whether the sprite has a shadow that can be separated from it. */
|
||||
[[nodiscard]] constexpr bool canExtractShadow() const
|
||||
{
|
||||
return type == ShadowRuleType::FirstFrame || type == ShadowRuleType::EachFrame;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parses a shadow rule as written in the TSV data files.
|
||||
*
|
||||
* A rule is one of: an empty string (same as `Unknown`), `Unknown`, `None`, `Baked`, `FirstFrame`,
|
||||
* `EachFrame`, and `FirstFrame+N` where `N` is the baseline offset (1 to 255).
|
||||
*
|
||||
* It can be followed by the tags `Unexplained` and `Stray` (see `ShadowRuleFlags`),
|
||||
* each preceded by `|`, e.g. `FirstFrame|Stray`.
|
||||
*/
|
||||
inline std::expected<ShadowRule, std::string> ParseShadowRule(std::string_view value)
|
||||
{
|
||||
constexpr char Separator = '|';
|
||||
const size_t ruleEnd = std::min(value.find(Separator), value.size());
|
||||
const std::string_view ruleStr = value.substr(0, ruleEnd);
|
||||
|
||||
ShadowRule result;
|
||||
constexpr std::string_view FirstFrame = "FirstFrame";
|
||||
if (ruleStr.empty() || ruleStr == "Unknown") {
|
||||
result.type = ShadowRuleType::Unknown;
|
||||
} else if (ruleStr == "None") {
|
||||
result.type = ShadowRuleType::None;
|
||||
} else if (ruleStr == "Baked") {
|
||||
result.type = ShadowRuleType::Baked;
|
||||
} else if (ruleStr == "EachFrame") {
|
||||
result.type = ShadowRuleType::EachFrame;
|
||||
} else if (ruleStr == FirstFrame) {
|
||||
result.type = ShadowRuleType::FirstFrame;
|
||||
} else if (ruleStr.starts_with(FirstFrame) && ruleStr.size() > FirstFrame.size() + 1 && ruleStr[FirstFrame.size()] == '+') {
|
||||
const std::string_view offsetStr = ruleStr.substr(FirstFrame.size() + 1);
|
||||
const char *end = nullptr;
|
||||
const ParseIntResult<uint8_t> offset = ParseInt<uint8_t>(offsetStr, 1, 255, &end);
|
||||
if (!offset.has_value() || end != offsetStr.data() + offsetStr.size())
|
||||
return std::unexpected(StrCat("Unknown shadow rule: ", ruleStr));
|
||||
result.type = ShadowRuleType::FirstFrame;
|
||||
result.baselineOffset = *offset;
|
||||
} else {
|
||||
return std::unexpected(StrCat("Unknown shadow rule: ", ruleStr));
|
||||
}
|
||||
|
||||
for (size_t pos = ruleEnd; pos < value.size();) {
|
||||
const size_t tagBegin = pos + 1;
|
||||
const size_t tagEnd = std::min(value.find(Separator, tagBegin), value.size());
|
||||
const std::string_view tag = value.substr(tagBegin, tagEnd - tagBegin);
|
||||
if (tag == "Unexplained") {
|
||||
result.flags = result.flags | ShadowRuleFlags::Unexplained;
|
||||
} else if (tag == "Stray") {
|
||||
result.flags = result.flags | ShadowRuleFlags::Stray;
|
||||
} else {
|
||||
return std::unexpected(StrCat("Unknown shadow rule tag: ", tag));
|
||||
}
|
||||
pos = tagEnd;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace devilution
|
||||
@@ -34,6 +34,12 @@ std::string DebugCmdPath(std::optional<bool> on)
|
||||
return StrCat("Path highlighting: ", DebugPath ? "On" : "Off");
|
||||
}
|
||||
|
||||
std::string DebugCmdShadows(std::optional<bool> on)
|
||||
{
|
||||
DebugHideShadows = !on.value_or(DebugHideShadows);
|
||||
return StrCat("Sprite shadows: ", DebugHideShadows ? "Off" : "On");
|
||||
}
|
||||
|
||||
std::string DebugCmdFullbright(std::optional<bool> on)
|
||||
{
|
||||
ToggleLighting();
|
||||
@@ -126,6 +132,7 @@ sol::table LuaDevDisplayModule(sol::state_view &lua)
|
||||
LuaSetDocFn(table, "grid", "(on: boolean = nil)", "Toggle showing the grid.", &DebugCmdShowGrid);
|
||||
LuaSetDocFn(table, "path", "(on: boolean = nil)", "Toggle path debug rendering.", &DebugCmdPath);
|
||||
LuaSetDocFn(table, "scrollView", "(on: boolean = nil)", "Toggle view scrolling via Shift+Mouse.", &DebugCmdScrollView);
|
||||
LuaSetDocFn(table, "shadows", "(on: boolean = nil)", "Toggle drawing the shadows of sprites. Shadows that are still part of their sprite stay visible.", &DebugCmdShadows);
|
||||
LuaSetDocFn(table, "tileData", "(name: string = nil)", "Toggle showing tile data.", &DebugCmdShowTileData);
|
||||
LuaSetDocFn(table, "vision", "(on: boolean = nil)", "Toggle vision debug rendering.", &DebugCmdVision);
|
||||
return table;
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
#include "utils/algorithm/container.hpp"
|
||||
#include "utils/attributes.h"
|
||||
#include "utils/cl2_to_clx.hpp"
|
||||
#include "utils/clx_shadow.hpp"
|
||||
#include "utils/endian_swap.hpp"
|
||||
#include "utils/enum_traits.h"
|
||||
#include "utils/file_name_generator.hpp"
|
||||
@@ -3211,6 +3212,52 @@ bool UpdateModeStance(Monster &monster)
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Separates the shadows baked into the monster's sprites (see `utils/clx_shadow.hpp`).
|
||||
*
|
||||
* Each animation has its own shadow rule.
|
||||
*/
|
||||
void ExtractMonsterShadows(const MonsterData &monsterData, MonsterSpritesData &sprites)
|
||||
{
|
||||
const size_t numAnims = GetNumAnims(monsterData);
|
||||
std::array<std::optional<OwnedClxSpriteListOrSheet>, MonsterSpritesData::MaxAnims> extracted;
|
||||
std::array<uint32_t, MonsterSpritesData::MaxAnims + 1> offsets;
|
||||
bool anyExtracted = false;
|
||||
size_t size = 0;
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; i < numAnims; ++i) {
|
||||
if (!monsterData.hasAnim(i))
|
||||
continue;
|
||||
const auto *data = reinterpret_cast<const uint8_t *>(&sprites.data[sprites.offsets[j]]);
|
||||
const size_t dataSize = sprites.offsets[j + 1] - sprites.offsets[j];
|
||||
const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(data, dataSize);
|
||||
extracted[j] = ExtractClxShadows(ClxSpriteListOrSheet { data, numLists }, monsterData.shadowRules[i]);
|
||||
anyExtracted = anyExtracted || extracted[j].has_value();
|
||||
offsets[j] = static_cast<uint32_t>(size);
|
||||
size += extracted[j].has_value() ? extracted[j]->dataSize() : dataSize;
|
||||
++j;
|
||||
}
|
||||
if (!anyExtracted)
|
||||
return;
|
||||
offsets[j] = static_cast<uint32_t>(size);
|
||||
|
||||
std::unique_ptr<std::byte[]> data { new std::byte[size] };
|
||||
for (size_t k = 0; k < j; ++k) {
|
||||
if (extracted[k].has_value()) {
|
||||
const ClxSpriteListOrSheet clx { *extracted[k] };
|
||||
memcpy(&data[offsets[k]], clx.isSheet() ? clx.sheet().data() : clx.list().data(), offsets[k + 1] - offsets[k]);
|
||||
} else {
|
||||
memcpy(&data[offsets[k]], &sprites.data[sprites.offsets[k]], offsets[k + 1] - offsets[k]);
|
||||
}
|
||||
}
|
||||
sprites.data = std::move(data);
|
||||
sprites.offsets = offsets;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MonsterSpritesData LoadMonsterSpritesData(const MonsterData &monsterData)
|
||||
{
|
||||
const size_t numAnims = GetNumAnims(monsterData);
|
||||
@@ -3246,6 +3293,8 @@ MonsterSpritesData LoadMonsterSpritesData(const MonsterData &monsterData)
|
||||
}
|
||||
#endif
|
||||
|
||||
ExtractMonsterShadows(monsterData, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "towners.h"
|
||||
#include "track.h"
|
||||
#include "utils/algorithm/container.hpp"
|
||||
#include "utils/clx_shadow.hpp"
|
||||
#include "utils/endian_swap.hpp"
|
||||
#include "utils/format.hpp"
|
||||
#include "utils/is_of.hpp"
|
||||
@@ -3705,6 +3706,10 @@ std::expected<void, std::string> LoadLevelObjects(uint16_t filesWidths[65])
|
||||
char filestr[32];
|
||||
*BufCopy(filestr, "objects\\", ObjMasterLoadList[i]) = '\0';
|
||||
ASSIGN_OR_RETURN(pObjCels[numobjfiles], LoadCelWithStatus(filestr, filesWidths[i]));
|
||||
// Objects that share a sprite file share its shadow rule.
|
||||
const auto objectDataIt = std::find_if(AllObjects.begin(), AllObjects.end(), [i](const ObjectData &objectData) { return objectData.ofindex == i; });
|
||||
if (objectDataIt != AllObjects.end())
|
||||
ExtractClxShadowsInPlace(*pObjCels[numobjfiles], objectDataIt->shadowRule);
|
||||
numobjfiles++;
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -797,6 +797,7 @@ GraphicsOptions::GraphicsOptions()
|
||||
, zoom("Zoom", OptionEntryFlags::None, N_("Zoom"), N_("Zoom on when enabled."), false)
|
||||
, perPixelLighting("Per-pixel Lighting", OptionEntryFlags::None, N_("Per-pixel Lighting"), N_("Subtile lighting for smoother light gradients."), DEFAULT_PER_PIXEL_LIGHTING)
|
||||
, colorCycling("Color Cycling", OptionEntryFlags::None, N_("Color Cycling"), N_("Color cycling effect used for water, lava, and acid animation."), true)
|
||||
, transparentShadows("Transparent Shadows", OptionEntryFlags::None, N_("Transparent Shadows"), N_("Shadows of characters, monsters and objects darken the ground instead of covering it."), false)
|
||||
, alternateNestArt("Alternate nest art", OptionEntryFlags::OnlyHellfire | OptionEntryFlags::CantChangeInGame, N_("Alternate nest art"), N_("The game will use an alternative palette for Hellfire’s nest tileset."), false)
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
||||
, hardwareCursor("Hardware Cursor", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI | (HardwareCursorSupported() ? OptionEntryFlags::None : OptionEntryFlags::Invisible), N_("Hardware Cursor"), N_("Use a hardware cursor"), HardwareCursorDefault())
|
||||
@@ -828,6 +829,7 @@ std::vector<OptionEntryBase *> GraphicsOptions::GetEntries()
|
||||
&showFPS,
|
||||
&perPixelLighting,
|
||||
&colorCycling,
|
||||
&transparentShadows,
|
||||
&alternateNestArt,
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
||||
&hardwareCursor,
|
||||
|
||||
@@ -562,6 +562,8 @@ struct GraphicsOptions : OptionCategoryBase {
|
||||
OptionEntryBoolean perPixelLighting;
|
||||
/** @brief Enable color cycling animations. */
|
||||
OptionEntryBoolean colorCycling;
|
||||
/** @brief Draw the shadows of sprites by darkening what is underneath instead of covering it. */
|
||||
OptionEntryBoolean transparentShadows;
|
||||
/** @brief Use alternate nest palette. */
|
||||
OptionEntryBoolean alternateNestArt;
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 0)
|
||||
|
||||
@@ -312,7 +312,8 @@ void DrawPartyMemberInfoPanel(const Surface &out)
|
||||
PortraitFrameSize.width - (FrameBorderSize * 2),
|
||||
PortraitFrameSize.height - (FrameBorderSize * 2));
|
||||
|
||||
// Draw the portrait sprite
|
||||
// Draw the portrait sprite, with its shadow
|
||||
ClxDrawShadow(frameSubregion, { portraitPos.x, portraitPos.y + static_cast<int>(monsterPortraitSprite.height()) - 1 }, monsterPortraitSprite);
|
||||
RenderClxSprite(
|
||||
frameSubregion,
|
||||
monsterPortraitSprite,
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "spells.h"
|
||||
#include "stores.h"
|
||||
#include "towners.h"
|
||||
#include "utils/clx_shadow.hpp"
|
||||
#include "utils/format.hpp"
|
||||
#include "utils/is_of.hpp"
|
||||
#include "utils/language.h"
|
||||
@@ -2160,6 +2161,7 @@ void LoadPlrGFX(Player &player, player_graphic graphic)
|
||||
GetPlayerGraphicsPath(path, std::string_view(prefixBuf, 3), szCel, pszName);
|
||||
const uint16_t animationWidth = GetPlayerSpriteWidth(cls, graphic, animWeaponId);
|
||||
animationData.sprites = LoadCl2Sheet(pszName, animationWidth);
|
||||
ExtractClxShadowsInPlace(*animationData.sprites, spriteData.shadowRule);
|
||||
std::optional<std::array<uint8_t, 256>> graphicTRN = GetPlayerGraphicTRN(pszName);
|
||||
if (graphicTRN) {
|
||||
ClxApplyTrans(*animationData.sprites, graphicTRN->data());
|
||||
|
||||
@@ -399,6 +399,7 @@ void LoadMonstDatFromFile(DataFile &dataFile, const std::string_view filename, b
|
||||
reader.read("treasure", monster.treasure, ParseMonsterTreasure);
|
||||
|
||||
reader.readInt("exp", monster.exp);
|
||||
reader.readEnumArray("shadowRule", std::optional<ShadowRule> { ShadowRule {} }, monster.shadowRules, ParseShadowRule);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
|
||||
#include "cursor_defs.hpp"
|
||||
#include "engine/shadow_rule.hpp"
|
||||
#include "tables/textdat.h"
|
||||
#include "utils/attributes.h"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
@@ -143,6 +145,8 @@ struct MonsterData {
|
||||
/** Using monster_treasure */
|
||||
uint16_t treasure = 0;
|
||||
uint16_t exp = 0;
|
||||
/** How the shadow baked into each animation's sprites was generated, indexed like `frames`. */
|
||||
ShadowRule shadowRules[6] {};
|
||||
|
||||
[[nodiscard]] const char *spritePath() const;
|
||||
|
||||
@@ -347,7 +351,7 @@ struct UniqueMonsterData {
|
||||
_speech_id mtalkmsg;
|
||||
};
|
||||
|
||||
extern std::vector<MonsterData> MonstersData;
|
||||
extern DVL_API_FOR_TEST std::vector<MonsterData> MonstersData;
|
||||
extern const _monster_id MonstConvTbl[];
|
||||
extern std::vector<UniqueMonsterData> UniqueMonstersData;
|
||||
|
||||
|
||||
@@ -292,6 +292,7 @@ void LoadObjectData()
|
||||
reader.readInt("animLen", item.animLen);
|
||||
reader.readInt("animWidth", item.animWidth);
|
||||
reader.readEnumList("selectionRegion", item.selectionRegion, ParseSelectionRegion);
|
||||
reader.read("shadowRule", item.shadowRule, ParseShadowRule);
|
||||
}
|
||||
|
||||
// Sanity check because we do not actually parse the IDs yet.
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
#include <vector>
|
||||
|
||||
#include "cursor_defs.hpp"
|
||||
#include "engine/shadow_rule.hpp"
|
||||
#include "levels/gendung_defs.hpp"
|
||||
#include "tables/questdat.hpp"
|
||||
#include "utils/attributes.h"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
@@ -177,6 +179,8 @@ struct ObjectData {
|
||||
uint8_t animLen; // Number of frames in current animation
|
||||
uint8_t animWidth;
|
||||
SelectionRegion selectionRegion;
|
||||
/** How the shadow baked into the object's sprite was generated. */
|
||||
ShadowRule shadowRule;
|
||||
|
||||
[[nodiscard]] bool isAnimated() const
|
||||
{
|
||||
@@ -210,7 +214,7 @@ struct ObjectData {
|
||||
};
|
||||
|
||||
extern const _object_id ObjTypeConv[];
|
||||
extern std::vector<ObjectData> AllObjects;
|
||||
extern DVL_API_FOR_TEST std::vector<ObjectData> AllObjects;
|
||||
extern std::vector<std::string> ObjMasterLoadList;
|
||||
|
||||
void LoadObjectData();
|
||||
|
||||
@@ -250,6 +250,7 @@ void LoadClassSpriteData(std::string_view classPath, PlayerSpriteData &spriteDat
|
||||
reader.readInt("fire", spriteData.fire);
|
||||
reader.readInt("magic", spriteData.magic);
|
||||
reader.readInt("death", spriteData.death);
|
||||
reader.read("shadowRule", spriteData.shadowRule, ParseShadowRule);
|
||||
}
|
||||
|
||||
void LoadClassAnimData(std::string_view classPath, PlayerAnimData &animData)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
|
||||
#include "engine/shadow_rule.hpp"
|
||||
#include "sound_effect_enums.h"
|
||||
#include "tables/itemdat.h"
|
||||
#include "tables/spelldat.h"
|
||||
@@ -173,6 +174,8 @@ struct PlayerSpriteData {
|
||||
uint8_t magic;
|
||||
/* Sprite width: Death */
|
||||
uint8_t death;
|
||||
/* How the shadow baked into the class sprites was generated */
|
||||
ShadowRule shadowRule;
|
||||
};
|
||||
|
||||
struct PlayerAnimData {
|
||||
|
||||
@@ -125,6 +125,8 @@ void LoadTownersFromFile()
|
||||
std::string animOrderStr;
|
||||
reader.readString("animOrder", animOrderStr);
|
||||
ParseAnimOrder(animOrderStr, entry.animOrder);
|
||||
|
||||
reader.read("shadowRule", entry.shadowRule, ParseShadowRule);
|
||||
}
|
||||
|
||||
TownersDataEntries.shrink_to_fit();
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "engine/direction.hpp"
|
||||
#include "engine/shadow_rule.hpp"
|
||||
#include "levels/gendung.h"
|
||||
#include "tables/objdat.h"
|
||||
#include "tables/textdat.h"
|
||||
@@ -33,6 +34,8 @@ struct TownerDataEntry {
|
||||
int16_t animDelay;
|
||||
std::vector<_speech_id> gossipTexts;
|
||||
std::vector<uint8_t> animOrder;
|
||||
/** How the shadow baked into the towner's sprite was generated. */
|
||||
ShadowRule shadowRule;
|
||||
};
|
||||
|
||||
/** Contains the data for all towners loaded from TSV. */
|
||||
|
||||
+11
-5
@@ -10,12 +10,14 @@
|
||||
#include "engine/load_cel.hpp"
|
||||
#include "engine/load_file.hpp"
|
||||
#include "engine/random.hpp"
|
||||
#include "engine/shadow_rule.hpp"
|
||||
#include "game_mode.hpp"
|
||||
#include "inv.h"
|
||||
#include "minitext.h"
|
||||
#include "stores.h"
|
||||
#include "tables/textdat.h"
|
||||
#include "tables/townerdat.hpp"
|
||||
#include "utils/clx_shadow.hpp"
|
||||
#include "utils/is_of.hpp"
|
||||
#include "utils/language.h"
|
||||
#include "utils/str_case.hpp"
|
||||
@@ -104,10 +106,11 @@ void InitTownerInfo(Towner &towner, const TownerData &townerData, const TownerDa
|
||||
}
|
||||
}
|
||||
|
||||
void LoadTownerAnimations(Towner &towner, const char *path, int frames, int delay)
|
||||
void LoadTownerAnimations(Towner &towner, const char *path, int frames, int delay, ShadowRule shadowRule)
|
||||
{
|
||||
towner.ownedAnim = std::nullopt;
|
||||
towner.ownedAnim = LoadCel(path, towner._tAnimWidth);
|
||||
ExtractClxShadowsInPlace(*towner.ownedAnim, shadowRule);
|
||||
NewTownerAnim(towner, *towner.ownedAnim, frames, delay);
|
||||
}
|
||||
|
||||
@@ -127,7 +130,7 @@ void InitTownerFromData(Towner &towner, const TownerDataEntry &entry)
|
||||
}
|
||||
|
||||
if (!entry.animPath.empty()) {
|
||||
LoadTownerAnimations(towner, entry.animPath.c_str(), entry.animFrames, entry.animDelay);
|
||||
LoadTownerAnimations(towner, entry.animPath.c_str(), entry.animFrames, entry.animDelay, entry.shadowRule);
|
||||
}
|
||||
|
||||
// Set gossip from TSV data
|
||||
@@ -182,10 +185,13 @@ void InitCowFarmer(Towner &towner, const TownerDataEntry &entry)
|
||||
|
||||
// CowFarmer has special logic for quest state
|
||||
const char *celPath = "towners\\farmer\\cfrmrn2";
|
||||
// The data file only describes the shadow of the first sprite.
|
||||
ShadowRule shadowRule = entry.shadowRule;
|
||||
if (Quests[Q_JERSEY]._qactive == QUEST_DONE) {
|
||||
celPath = "towners\\farmer\\mfrmrn2";
|
||||
shadowRule = {};
|
||||
}
|
||||
LoadTownerAnimations(towner, celPath, 15, 3);
|
||||
LoadTownerAnimations(towner, celPath, 15, 3, shadowRule);
|
||||
}
|
||||
|
||||
void TownDead(Towner &towner)
|
||||
@@ -858,7 +864,7 @@ void UpdateGirlAnimAfterQuestComplete()
|
||||
if (girl == nullptr || !girl->ownedAnim)
|
||||
return; // Girl is not spawned in town yet
|
||||
auto curFrame = girl->_tAnimFrame;
|
||||
LoadTownerAnimations(*girl, "towners\\girl\\girls1", 20, 6);
|
||||
LoadTownerAnimations(*girl, "towners\\girl\\girls1", 20, 6, /*shadowRule=*/ {});
|
||||
girl->_tAnimFrame = std::min<uint8_t>(curFrame, girl->_tAnimLen - 1);
|
||||
}
|
||||
|
||||
@@ -866,7 +872,7 @@ void UpdateCowFarmerAnimAfterQuestComplete()
|
||||
{
|
||||
Towner *cowFarmer = GetTowner(TOWN_COWFARM);
|
||||
auto curFrame = cowFarmer->_tAnimFrame;
|
||||
LoadTownerAnimations(*cowFarmer, "towners\\farmer\\mfrmrn2", 15, 3);
|
||||
LoadTownerAnimations(*cowFarmer, "towners\\farmer\\mfrmrn2", 15, 3, /*shadowRule=*/ {});
|
||||
cowFarmer->_tAnimFrame = std::min<uint8_t>(curFrame, cowFarmer->_tAnimLen - 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
#include "utils/clx_shadow.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "appfat.h"
|
||||
#include "utils/clx_decode.hpp"
|
||||
#include "utils/clx_encode.hpp"
|
||||
#include "utils/endian_write.hpp"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
namespace {
|
||||
|
||||
enum PixelFlags : uint8_t {
|
||||
Opaque = 1 << 0,
|
||||
/** Part of the sprite itself. */
|
||||
Body = 1 << 1,
|
||||
/** Covered by the shadow of a body pixel. */
|
||||
Shadow = 1 << 2,
|
||||
/** A black pixel that is neither covered by a shadow nor plausibly a body pixel. */
|
||||
Orphan = 1 << 3,
|
||||
};
|
||||
|
||||
/** The columns `[begin, end)` of a row. Empty if `begin >= end`. */
|
||||
struct Extent {
|
||||
uint16_t begin;
|
||||
uint16_t end;
|
||||
|
||||
void include(int from, int to)
|
||||
{
|
||||
if (from < begin) begin = static_cast<uint16_t>(from);
|
||||
if (to > end) end = static_cast<uint16_t>(to);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A decoded sprite. Rows are stored top-down.
|
||||
*
|
||||
* Most of a sprite is empty, so the rows that have pixels and the extent of the pixels
|
||||
* of each row are kept, and everything works on those instead of on whole rows.
|
||||
*/
|
||||
struct Frame {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
/** Only valid for opaque pixels. */
|
||||
std::vector<uint8_t> colors;
|
||||
std::vector<uint8_t> flags;
|
||||
/** The extent of the opaque pixels of each row. */
|
||||
std::vector<Extent> opaque;
|
||||
/** An extent that contains the pixels of each row that have the `Shadow` flag. */
|
||||
std::vector<Extent> shadow;
|
||||
/** The indices of the black pixels, in order. Filled by `SeparateShadow`. */
|
||||
std::vector<uint32_t> blackPixels;
|
||||
/** Scratch space of `SeparateShadow`: columns of a row. */
|
||||
std::vector<int> uncovered;
|
||||
/** The first and the last row that have an opaque pixel. `bottom` is -1 if there are none. */
|
||||
int top = 0;
|
||||
int bottom = -1;
|
||||
|
||||
[[nodiscard]] size_t index(int x, int y) const
|
||||
{
|
||||
return static_cast<size_t>(y) * width + x;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isBlack(size_t i) const
|
||||
{
|
||||
return (flags[i] & Opaque) != 0 && colors[i] == 0;
|
||||
}
|
||||
|
||||
void setShadow(int x, int y)
|
||||
{
|
||||
flags[index(x, y)] |= Shadow;
|
||||
shadow[y].include(x, x + 1);
|
||||
}
|
||||
};
|
||||
|
||||
void DecodeFrame(ClxSprite sprite, Frame &frame)
|
||||
{
|
||||
const int width = sprite.width();
|
||||
const int height = sprite.height();
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
const size_t numPixels = static_cast<size_t>(width) * height;
|
||||
frame.colors.resize(numPixels);
|
||||
frame.flags.assign(numPixels, 0);
|
||||
const Extent empty { static_cast<uint16_t>(width), 0 };
|
||||
frame.opaque.assign(height, empty);
|
||||
frame.shadow.assign(height, empty);
|
||||
frame.top = height;
|
||||
frame.bottom = -1;
|
||||
|
||||
// CLX pixel data is stored bottom-up. Transparent runs can continue on the next row.
|
||||
// Opaque runs must not, but a run that does is taken to continue there too, like the
|
||||
// run of the original image that it was made from.
|
||||
int x = 0;
|
||||
int y = height - 1;
|
||||
// Writes an opaque run, either the given pixels or `length` pixels of the fill color.
|
||||
const auto opaqueRun = [&](unsigned length, const uint8_t *pixels, uint8_t fillColor) {
|
||||
while (length != 0 && y >= 0) {
|
||||
const unsigned chunk = std::min<unsigned>(length, static_cast<unsigned>(width - x));
|
||||
const size_t i = frame.index(x, y);
|
||||
if (pixels != nullptr) {
|
||||
memcpy(&frame.colors[i], pixels, chunk);
|
||||
pixels += chunk;
|
||||
} else {
|
||||
memset(&frame.colors[i], fillColor, chunk);
|
||||
}
|
||||
memset(&frame.flags[i], Opaque, chunk);
|
||||
frame.opaque[y].include(x, x + static_cast<int>(chunk));
|
||||
if (y > frame.bottom) frame.bottom = y;
|
||||
if (y < frame.top) frame.top = y;
|
||||
x += static_cast<int>(chunk);
|
||||
length -= chunk;
|
||||
if (x == width) {
|
||||
x = 0;
|
||||
--y;
|
||||
}
|
||||
}
|
||||
};
|
||||
const uint8_t *src = sprite.pixelData();
|
||||
const uint8_t *end = src + sprite.pixelDataSize();
|
||||
while (src < end) {
|
||||
const uint8_t control = *src++;
|
||||
if (!IsClxOpaque(control)) {
|
||||
x += control;
|
||||
if (x >= width) {
|
||||
y -= x / width;
|
||||
x %= width;
|
||||
}
|
||||
} else if (IsClxOpaqueFill(control)) {
|
||||
opaqueRun(GetClxOpaqueFillWidth(control), nullptr, *src++);
|
||||
} else {
|
||||
const unsigned length = GetClxOpaquePixelsWidth(control);
|
||||
opaqueRun(length, src, 0);
|
||||
src += length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The shadow function for a row: where the pixels of the row cast their shadow.
|
||||
*/
|
||||
struct RowShadow {
|
||||
/** Whether the pixels of the row cast a shadow inside the frame. */
|
||||
bool casts;
|
||||
/** The row that the shadow falls on. */
|
||||
int y;
|
||||
/** The number of pixels that the shadow is shifted to the left. */
|
||||
int shift;
|
||||
};
|
||||
|
||||
RowShadow ShadowOfRow(const Frame &frame, int baseline, int y)
|
||||
{
|
||||
if (y > baseline) return { false, 0, 0 };
|
||||
const int v = (baseline - y) / 3;
|
||||
const int shadowY = baseline - 2 - v;
|
||||
return { shadowY >= 0 && shadowY < frame.height, shadowY, (3 * v) / 4 };
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the `Body`, `Shadow` and `Orphan` flags of the frame.
|
||||
*/
|
||||
void SeparateShadow(Frame &frame, int baseline)
|
||||
{
|
||||
// Everything that is not black is certainly not a shadow.
|
||||
// The black pixels are collected for the second pass.
|
||||
frame.blackPixels.clear();
|
||||
for (int y = frame.top; y <= frame.bottom; ++y) {
|
||||
const Extent extent = frame.opaque[y];
|
||||
if (extent.begin >= extent.end) continue;
|
||||
const RowShadow rowShadow = ShadowOfRow(frame, baseline, y);
|
||||
const size_t row = frame.index(0, y);
|
||||
// The shadow of the pixels to the left of `shift` falls outside of the frame.
|
||||
const size_t shadowRow = rowShadow.casts ? frame.index(0, rowShadow.y) : 0;
|
||||
const int shift = rowShadow.casts ? rowShadow.shift : frame.width;
|
||||
for (int x = extent.begin; x < extent.end; ++x) {
|
||||
if ((frame.flags[row + x] & Opaque) == 0) continue;
|
||||
if (frame.colors[row + x] == 0) {
|
||||
frame.blackPixels.push_back(static_cast<uint32_t>(row + x));
|
||||
continue;
|
||||
}
|
||||
frame.flags[row + x] |= Body;
|
||||
if (x >= shift) frame.flags[shadowRow + x - shift] |= Shadow;
|
||||
}
|
||||
if (rowShadow.casts && extent.end > shift) frame.shadow[rowShadow.y].include(std::max<int>(extent.begin, shift) - shift, extent.end - shift);
|
||||
}
|
||||
|
||||
// A black pixel that is not in the shadow of the pixels above it must be a body pixel,
|
||||
// and so it casts a shadow too. Shadows only fall downwards (except within the lowest
|
||||
// rows, where a pixel shadows its own column), so one top-down pass settles them all.
|
||||
std::vector<int> &uncovered = frame.uncovered;
|
||||
const size_t numBlackPixels = frame.blackPixels.size();
|
||||
for (size_t next = 0; next < numBlackPixels;) {
|
||||
// The black pixels of one row.
|
||||
const int y = static_cast<int>(frame.blackPixels[next] / frame.width);
|
||||
const size_t row = frame.index(0, y);
|
||||
const size_t rowEnd = row + frame.width;
|
||||
uncovered.clear();
|
||||
for (; next < numBlackPixels && frame.blackPixels[next] < rowEnd; ++next) {
|
||||
const size_t i = frame.blackPixels[next];
|
||||
if ((frame.flags[i] & Shadow) == 0) uncovered.push_back(static_cast<int>(i - row));
|
||||
}
|
||||
if (uncovered.empty()) continue;
|
||||
|
||||
const RowShadow rowShadow = ShadowOfRow(frame, baseline, y);
|
||||
for (const int x : uncovered) {
|
||||
const size_t i = row + x;
|
||||
// A pixel whose shadow would fall on a transparent pixel is not a body pixel.
|
||||
if (!rowShadow.casts || x < rowShadow.shift || (frame.flags[frame.index(x - rowShadow.shift, rowShadow.y)] & Opaque) != 0) {
|
||||
frame.flags[i] |= Body;
|
||||
continue;
|
||||
}
|
||||
|
||||
// It may be the shadow of a black body pixel that we took for a shadow because it lies
|
||||
// inside the shadow of the pixels above it. Look for one in the 3 rows that shadow this row.
|
||||
const int v = baseline - 2 - y;
|
||||
const int sourceX = x + (3 * v) / 4;
|
||||
bool found = false;
|
||||
for (int sourceY = baseline - 3 * v - 2; v >= 2 && sourceY <= baseline - 3 * v && !found; ++sourceY) {
|
||||
if (sourceX >= frame.width || sourceY < 0) continue;
|
||||
const size_t source = frame.index(sourceX, sourceY);
|
||||
if (!frame.isBlack(source) || (frame.flags[source] & (Body | Orphan)) != 0) continue;
|
||||
frame.flags[source] |= Body;
|
||||
frame.setShadow(x, y);
|
||||
found = true;
|
||||
}
|
||||
if (!found) frame.flags[i] |= Orphan;
|
||||
}
|
||||
if (!rowShadow.casts) continue;
|
||||
for (const int x : uncovered) {
|
||||
if ((frame.flags[row + x] & Body) != 0 && x >= rowShadow.shift) frame.setShadow(x - rowShadow.shift, rowShadow.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the pixel stays in the sprite once its shadow is removed. */
|
||||
bool IsKept(const Frame &frame, size_t i)
|
||||
{
|
||||
const uint8_t flags = frame.flags[i];
|
||||
if ((flags & Opaque) == 0) return false;
|
||||
return frame.colors[i] != 0 || (flags & (Body | Orphan)) != 0;
|
||||
}
|
||||
|
||||
/** Appends the shadow mask of the frame. Appends nothing if the frame has no shadow. */
|
||||
void AppendShadowMask(const Frame &frame, std::vector<uint8_t> &out)
|
||||
{
|
||||
constexpr uint8_t Mask = Opaque | Shadow;
|
||||
// The mask is limited to opaque pixels, so only look where a row has both.
|
||||
const auto maskExtent = [&](int y) {
|
||||
return Extent { std::max(frame.opaque[y].begin, frame.shadow[y].begin), std::min(frame.opaque[y].end, frame.shadow[y].end) };
|
||||
};
|
||||
const auto rowHasShadow = [&](int y) {
|
||||
const Extent extent = maskExtent(y);
|
||||
const size_t row = frame.index(0, y);
|
||||
for (int x = extent.begin; x < extent.end; ++x) {
|
||||
if ((frame.flags[row + x] & Mask) == Mask) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
int lowest = frame.bottom;
|
||||
while (lowest >= frame.top && !rowHasShadow(lowest))
|
||||
--lowest;
|
||||
if (lowest < frame.top) return;
|
||||
int highest = frame.top;
|
||||
while (highest < lowest && !rowHasShadow(highest))
|
||||
++highest;
|
||||
|
||||
[[maybe_unused]] const size_t begin = out.size();
|
||||
out.push_back(static_cast<uint8_t>(frame.height - 1 - lowest));
|
||||
out.push_back(static_cast<uint8_t>(lowest - highest + 1));
|
||||
|
||||
for (int y = lowest; y >= highest; --y) {
|
||||
const size_t rowBegin = out.size();
|
||||
const Extent extent = maskExtent(y);
|
||||
const size_t row = frame.index(0, y);
|
||||
int x = extent.begin;
|
||||
while (x < extent.end) {
|
||||
if ((frame.flags[row + x] & Mask) != Mask) {
|
||||
++x;
|
||||
continue;
|
||||
}
|
||||
int runEnd = x + 1;
|
||||
while (runEnd < extent.end && (frame.flags[row + runEnd] & Mask) == Mask)
|
||||
++runEnd;
|
||||
while (x < runEnd) {
|
||||
const int width = runEnd - x > MaxShadowSpanWidth ? MaxShadowSpanWidth : runEnd - x;
|
||||
out.push_back(static_cast<uint8_t>(x));
|
||||
out.push_back(static_cast<uint8_t>(width));
|
||||
x += width;
|
||||
}
|
||||
}
|
||||
// A row without shadow pixels is a span of width 0.
|
||||
if (out.size() == rowBegin) {
|
||||
out.push_back(0);
|
||||
out.push_back(0);
|
||||
}
|
||||
out.back() |= ShadowSpanEndOfRow;
|
||||
}
|
||||
// At most one span for every 2 pixels of a row.
|
||||
static_assert(ShadowMaskHeaderSize + MaxShadowMaskSpriteHeight * MaxShadowMaskSpriteWidth <= MaxShadowMaskSize);
|
||||
assert(out.size() - begin <= MaxShadowMaskSize);
|
||||
}
|
||||
|
||||
/** Appends the frame without its shadow, with the shadow mask in the frame header. */
|
||||
void AppendSpriteWithoutShadow(const Frame &frame, std::vector<uint8_t> &out)
|
||||
{
|
||||
const size_t headerPos = out.size();
|
||||
out.resize(out.size() + ClxFrameHeaderSize);
|
||||
AppendShadowMask(frame, out);
|
||||
WriteLE16(&out[headerPos], static_cast<uint16_t>(out.size() - headerPos));
|
||||
WriteLE16(&out[headerPos + 2], static_cast<uint16_t>(frame.width));
|
||||
WriteLE16(&out[headerPos + 4], static_cast<uint16_t>(frame.height));
|
||||
|
||||
// Opaque runs end with their row: only transparent runs can continue on the next one.
|
||||
unsigned transparentRunWidth = 0;
|
||||
for (int y = frame.height - 1; y >= 0; --y) {
|
||||
const Extent extent = frame.opaque[y];
|
||||
if (extent.begin >= extent.end) {
|
||||
transparentRunWidth += frame.width;
|
||||
continue;
|
||||
}
|
||||
const size_t rowBegin = frame.index(0, y);
|
||||
transparentRunWidth += extent.begin;
|
||||
int x = extent.begin;
|
||||
while (x < extent.end) {
|
||||
if (!IsKept(frame, rowBegin + x)) {
|
||||
++transparentRunWidth;
|
||||
++x;
|
||||
continue;
|
||||
}
|
||||
int runEnd = x + 1;
|
||||
while (runEnd < extent.end && IsKept(frame, rowBegin + runEnd))
|
||||
++runEnd;
|
||||
AppendClxTransparentRun(transparentRunWidth, out);
|
||||
transparentRunWidth = 0;
|
||||
AppendClxPixelsOrFillRun(&frame.colors[rowBegin + x], static_cast<size_t>(runEnd - x), out);
|
||||
x = runEnd;
|
||||
}
|
||||
transparentRunWidth += frame.width - extent.end;
|
||||
}
|
||||
AppendClxTransparentRun(transparentRunWidth, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extracts the shadows of a list, appending the new list to the output.
|
||||
*
|
||||
* @return false if a sprite is too large.
|
||||
*/
|
||||
bool ExtractList(ClxSpriteList list, ShadowRule rule, std::vector<uint8_t> &clxOut)
|
||||
{
|
||||
const uint32_t numSprites = list.numSprites();
|
||||
const size_t clxBegin = clxOut.size();
|
||||
clxOut.resize(clxBegin + 4 * (2 + static_cast<size_t>(numSprites)));
|
||||
WriteLE32(&clxOut[clxBegin], numSprites);
|
||||
|
||||
Frame frame;
|
||||
int firstFrameBaseline = 0;
|
||||
for (uint32_t i = 0; i < numSprites; ++i) {
|
||||
const ClxSprite sprite = list[i];
|
||||
if (sprite.width() > MaxShadowMaskSpriteWidth || sprite.height() > MaxShadowMaskSpriteHeight) return false;
|
||||
DecodeFrame(sprite, frame);
|
||||
|
||||
// The lowest row that has an opaque pixel, -1 if there is none.
|
||||
const int lowest = frame.bottom;
|
||||
if (i == 0) firstFrameBaseline = lowest;
|
||||
int baseline = rule.type == ShadowRuleType::EachFrame ? lowest : firstFrameBaseline;
|
||||
// A sprite without pixels has no baseline.
|
||||
if (baseline >= 0) {
|
||||
baseline += rule.baselineOffset;
|
||||
SeparateShadow(frame, baseline);
|
||||
}
|
||||
|
||||
WriteLE32(&clxOut[clxBegin + 4 * (1 + static_cast<size_t>(i))], static_cast<uint32_t>(clxOut.size() - clxBegin));
|
||||
AppendSpriteWithoutShadow(frame, clxOut);
|
||||
}
|
||||
WriteLE32(&clxOut[clxBegin + 4 * (1 + static_cast<size_t>(numSprites))], static_cast<uint32_t>(clxOut.size() - clxBegin));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<uint8_t[]> ToOwnedBuffer(const std::vector<uint8_t> &data)
|
||||
{
|
||||
std::unique_ptr<uint8_t[]> result { new uint8_t[data.size()] };
|
||||
memcpy(result.get(), data.data(), data.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<OwnedClxSpriteListOrSheet> ExtractClxShadows(ClxSpriteListOrSheet sprites, ShadowRule rule)
|
||||
{
|
||||
if (!rule.canExtractShadow()) return std::nullopt;
|
||||
|
||||
std::vector<uint8_t> clxData;
|
||||
// The result is about as large as the sprites it is made from.
|
||||
clxData.reserve(sprites.dataSize() + sprites.dataSize() / 16);
|
||||
uint16_t numLists = 0;
|
||||
if (sprites.isSheet()) {
|
||||
const ClxSpriteSheet sheet = sprites.sheet();
|
||||
numLists = sheet.numLists();
|
||||
clxData.resize(static_cast<size_t>(numLists) * 4);
|
||||
for (uint16_t i = 0; i < numLists; ++i) {
|
||||
WriteLE32(&clxData[static_cast<size_t>(i) * 4], static_cast<uint32_t>(clxData.size()));
|
||||
if (!ExtractList(sheet[i], rule, clxData)) return std::nullopt;
|
||||
}
|
||||
} else if (!ExtractList(sprites.list(), rule, clxData)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return OwnedClxSpriteListOrSheet { ToOwnedBuffer(clxData), numLists };
|
||||
}
|
||||
|
||||
} // namespace devilution
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
/**
|
||||
* @file clx_shadow.hpp
|
||||
*
|
||||
* @brief Separates the shadows baked into CLX sprites from the sprites themselves.
|
||||
*/
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "engine/clx_sprite.hpp"
|
||||
#include "engine/shadow_mask.hpp"
|
||||
#include "engine/shadow_rule.hpp"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
/**
|
||||
* @brief Separates the shadows baked into the given sprites from the sprites.
|
||||
*
|
||||
* A shadow pixel is a black (palette index 0) pixel that lies where the shadow function
|
||||
* (see `shadow_rule.hpp`) casts a pixel of the sprite. Such pixels are removed from the sprite.
|
||||
* The shadow becomes the shadow mask of the sprite (see `shadow_mask.hpp`), stored in its frame header.
|
||||
*
|
||||
* The mask of a sprite is the whole shadow that it casts, including the part underneath the
|
||||
* sprite itself, limited to the pixels that are opaque in the input. Drawing a sprite over its
|
||||
* mask filled with black gives back the input exactly.
|
||||
*
|
||||
* Black pixels that are not explained as shadow are kept in the sprite.
|
||||
*
|
||||
* For sprite sheets, each list is treated as a separate animation (for `ShadowRuleType::FirstFrame`
|
||||
* the baseline comes from the first sprite of each list).
|
||||
*
|
||||
* @return The sprites without their shadows, with the same structure and dimensions as the input.
|
||||
* `std::nullopt` if `rule` does not describe a shadow (`!rule.canExtractShadow()`)
|
||||
* or if a sprite is larger than `MaxShadowMaskSpriteWidth` by `MaxShadowMaskSpriteHeight`.
|
||||
*/
|
||||
std::optional<OwnedClxSpriteListOrSheet> ExtractClxShadows(ClxSpriteListOrSheet sprites, ShadowRule rule);
|
||||
|
||||
/**
|
||||
* @brief Separates the shadows baked into the given sprites, replacing the sprites.
|
||||
*
|
||||
* Leaves the sprites as they are if there is nothing to extract (see `ExtractClxShadows`).
|
||||
*/
|
||||
inline void ExtractClxShadowsInPlace(OwnedClxSpriteList &sprites, ShadowRule rule)
|
||||
{
|
||||
std::optional<OwnedClxSpriteListOrSheet> result = ExtractClxShadows(ClxSpriteListOrSheet { ClxSpriteList { sprites }.data(), 0 }, rule);
|
||||
if (result.has_value()) sprites = std::move(*result).list();
|
||||
}
|
||||
|
||||
inline void ExtractClxShadowsInPlace(OwnedClxSpriteSheet &sprites, ShadowRule rule)
|
||||
{
|
||||
const ClxSpriteSheet sheet { sprites };
|
||||
std::optional<OwnedClxSpriteListOrSheet> result = ExtractClxShadows(ClxSpriteListOrSheet { sheet.data(), sheet.numLists() }, rule);
|
||||
if (result.has_value()) sprites = std::move(*result).sheet();
|
||||
}
|
||||
|
||||
} // namespace devilution
|
||||
@@ -18,3 +18,4 @@ lightning 96
|
||||
fire 96
|
||||
magic 96
|
||||
death 128
|
||||
shadowRule
|
||||
|
||||
|
@@ -18,3 +18,4 @@ lightning 96
|
||||
fire 96
|
||||
magic 96
|
||||
death 128
|
||||
shadowRule
|
||||
|
||||
|
@@ -18,3 +18,4 @@ lightning 114
|
||||
fire 114
|
||||
magic 114
|
||||
death 160
|
||||
shadowRule EachFrame
|
||||
|
||||
|
@@ -18,3 +18,4 @@ lightning 96
|
||||
fire 96
|
||||
magic 96
|
||||
death 128
|
||||
shadowRule FirstFrame
|
||||
|
||||
|
@@ -18,3 +18,4 @@ lightning 128
|
||||
fire 128
|
||||
magic 128
|
||||
death 128
|
||||
shadowRule FirstFrame
|
||||
|
||||
|
@@ -18,3 +18,4 @@ lightning 96
|
||||
fire 96
|
||||
magic 96
|
||||
death 128
|
||||
shadowRule FirstFrame|Unexplained|Stray
|
||||
|
||||
|
@@ -1,113 +1,113 @@
|
||||
_monster_id name assetsSuffix soundSuffix trnFile availability width image hasSpecial hasSpecialSound frames[6] rate[6] minDunLvl maxDunLvl level hitPointsMinimum hitPointsMaximum ai abilityFlags intelligence toHit animFrameNum minDamage maxDamage toHitSpecial animFrameNumSpecial minDamageSpecial maxDamageSpecial reducePlayerStrength reducePlayerMagic reducePlayerDexterity reducePlayerVitality reducePlayerMaxHP reducePlayerMaxMana armorClass monsterClass resistance resistanceHell selectionRegion treasure exp
|
||||
MT_NZOMBIE Zombie zombie\zombie Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 1 2 1 4 7 Zombie 0 10 8 2 5 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 54
|
||||
MT_BZOMBIE Ghoul zombie\zombie zombie\bluered Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 3 2 7 11 Zombie 1 10 8 3 10 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 58
|
||||
MT_GZOMBIE Rotting Carcass zombie\zombie zombie\grey Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 4 4 15 25 Zombie 2 25 8 5 15 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 136
|
||||
MT_YZOMBIE Black Death zombie\zombie zombie\yellow Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 3 5 6 25 40 Zombie 3 30 8 6 22 0 0 0 0 0 0 0 0 1 0 20 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 240
|
||||
MT_RFALLSP Fallen One falspear\phall falspear\fallent Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 1 2 1 1 4 Fallen 0 15 7 1 3 0 5 0 0 0 0 0 0 0 0 0 Animal Bottom,Middle 46
|
||||
MT_DFALLSP Carver falspear\phall falspear\dark Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 3 3 4 8 Fallen 2 20 7 2 5 0 5 0 0 0 0 0 0 0 0 5 Animal Bottom,Middle 80
|
||||
MT_YFALLSP Devil Kin falspear\phall Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 4 5 12 24 Fallen 2 25 7 3 7 0 5 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 155
|
||||
MT_BFALLSP Dark One falspear\phall falspear\blue Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 3 5 7 20 36 Fallen 3 30 7 4 8 0 5 0 0 0 0 0 0 0 0 15 Animal RESIST_LIGHTNING Bottom,Middle 255
|
||||
MT_WSKELAX Skeleton skelaxe\sklax skelaxe\white Always 128 553 true false 12,8,13,6,17,16 5,1,1,1,1,1 1 2 1 2 4 SkeletonMelee 0 20 8 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 64
|
||||
MT_TSKELAX Corpse Axe skelaxe\sklax skelaxe\skelt Always 128 553 true false 12,8,13,6,17,16 4,1,1,1,1,1 2 3 2 4 7 SkeletonMelee 1 25 8 3 5 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 68
|
||||
MT_RSKELAX Burning Dead skelaxe\sklax Always 128 553 true false 12,8,13,6,17,16 2,1,1,1,1,1 2 4 4 8 12 SkeletonMelee 2 30 8 3 7 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 154
|
||||
MT_XSKELAX Horror skelaxe\sklax skelaxe\black Always 128 553 true false 12,8,13,6,17,16 3,1,1,1,1,1 3 5 6 12 20 SkeletonMelee 3 35 8 4 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 264
|
||||
MT_RFALLSD Fallen One falsword\fall falsword\fallent Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 1 2 1 2 5 Fallen 0 15 8 1 4 0 5 0 0 0 0 0 0 0 0 10 Animal Bottom,Middle 52
|
||||
MT_DFALLSD Carver falsword\fall falsword\dark Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 3 3 5 9 Fallen 1 20 8 2 7 0 5 0 0 0 0 0 0 0 0 15 Animal Bottom,Middle 90
|
||||
MT_YFALLSD Devil Kin falsword\fall Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 4 5 16 24 Fallen 2 25 8 4 10 0 5 0 0 0 0 0 0 0 0 20 Animal RESIST_FIRE Bottom,Middle 180
|
||||
MT_BFALLSD Dark One falsword\fall falsword\blue Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 3 5 7 24 36 Fallen 3 30 8 4 12 0 5 0 0 0 0 0 0 0 0 25 Animal RESIST_LIGHTNING Bottom,Middle 280
|
||||
MT_NSCAV Scavenger scav\scav Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 1 3 2 3 6 Scavenger 0 20 7 1 5 0 0 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 80
|
||||
MT_BSCAV Plague Eater scav\scav scav\scavbr Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 2 4 4 12 24 Scavenger 1 30 7 1 8 0 0 0 0 0 0 0 0 0 0 20 Animal RESIST_LIGHTNING Bottom,Middle 188
|
||||
MT_WSCAV Shadow Beast scav\scav scav\scavbe Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 3 5 6 24 36 Scavenger 2 35 7 3 12 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_FIRE Bottom,Middle 375
|
||||
MT_YSCAV Bone Gasher scav\scav scav\scavw Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 4 6 8 28 40 Scavenger 3 35 7 5 15 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_LIGHTNING Bottom,Middle 552
|
||||
MT_WSKELBW Skeleton skelbow\sklbw skelbow\white Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 3 3 2 4 SkeletonRanged 0 15 12 1 2 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 110
|
||||
MT_TSKELBW Corpse Bow skelbow\sklbw skelbow\skelt Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 4 5 8 16 SkeletonRanged 1 25 12 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 210
|
||||
MT_RSKELBW Burning Dead skelbow\sklbw Always 128 567 true false 9,8,16,5,16,16 2,1,1,1,1,1 3 5 7 10 24 SkeletonRanged 2 30 12 1 6 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 364
|
||||
MT_XSKELBW Horror skelbow\sklbw skelbow\black Always 128 567 true false 9,8,16,5,16,16 3,1,1,1,1,1 4 6 9 15 45 SkeletonRanged 3 35 12 2 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 594
|
||||
MT_WSKELSD Skeleton Captain skelsd\sklsr skelsd\white Always 128 575 true true 13,8,12,7,15,16 4,1,1,1,1,1 1 3 2 3 6 SkeletonMelee 0 20 8 2 7 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 90
|
||||
MT_TSKELSD Corpse Captain skelsd\sklsr skelsd\skelt Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 2 4 4 12 20 SkeletonMelee 1 30 8 3 9 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 200
|
||||
MT_RSKELSD Burning Dead Captain skelsd\sklsr Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 3 5 6 16 30 SkeletonMelee 2 35 8 4 10 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 393
|
||||
MT_XSKELSD Horror Captain skelsd\sklsr skelsd\black Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 4 6 8 35 50 SkeletonMelee SEARCH 3 40 8 5 14 0 0 0 0 0 0 0 0 0 0 30 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 604
|
||||
MT_INVILORD Invisible Lord tsneak\tsneak Never 128 800 false false 13,13,15,11,16,0 2,1,1,1,1,1 19 20 14 278 278 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 65 8 16 30 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2000
|
||||
MT_SNEAK Hidden sneak\sneak Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 2 5 5 8 24 Sneak HIDDEN 0 35 8 3 6 0 0 0 0 0 0 0 0 0 0 25 Demon Bottom,Middle 278
|
||||
MT_STALKER Stalker sneak\sneak sneak\sneakv2 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 5 7 9 30 45 Sneak HIDDEN,SEARCH 1 40 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Demon Bottom,Middle 630
|
||||
MT_UNSEEN Unseen sneak\sneak sneak\sneakv3 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 6 8 11 35 50 Sneak HIDDEN,SEARCH 2 45 8 12 20 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 935
|
||||
MT_ILLWEAV Illusion Weaver sneak\sneak sneak\sneakv1 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 8 10 13 40 60 Sneak HIDDEN,SEARCH 3 60 8 16 24 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC,RESIST_FIRE IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 1500
|
||||
MT_LRDSAYTR Satyr Lord goatlord\goatl newsfx\satyr Never 160 800 false false 13,13,14,9,16,0 2,1,1,1,1,1 21 22 28 160 200 SkeletonMelee SEARCH 3 90 8 20 30 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 2800
|
||||
MT_NGOATMC Flesh Clan goatmace\goat Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 4 6 8 30 45 GoatMelee SEARCH,CAN_OPEN_DOOR 0 50 8 4 10 0 0 0 0 0 0 0 0 0 0 40 Demon Bottom,Middle 460
|
||||
MT_BGOATMC Stone Clan goatmace\goat goatmace\beige Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 5 7 10 40 55 GoatMelee SEARCH,CAN_OPEN_DOOR 1 60 8 6 12 0 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 685
|
||||
MT_RGOATMC Fire Clan goatmace\goat goatmace\red Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 6 8 12 50 65 GoatMelee SEARCH,CAN_OPEN_DOOR 2 70 8 8 16 0 0 0 0 0 0 0 0 0 0 45 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 906
|
||||
MT_GGOATMC Night Clan goatmace\goat goatmace\gray Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 7 9 14 55 70 GoatMelee SEARCH,CAN_OPEN_DOOR 3 80 8 10 20 15 0 30 30 0 0 0 0 0 0 50 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1190
|
||||
MT_FIEND Fiend bat\bat bat\red Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 2 3 3 3 6 Bat 0 35 5 1 6 0 0 0 0 0 0 0 0 0 0 0 Animal Middle,Top None 102
|
||||
MT_BLINK Blink bat\bat Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 3 5 7 12 28 Bat 1 45 5 1 8 0 0 0 0 0 0 0 0 0 0 15 Animal Middle,Top None 340
|
||||
MT_GLOOM Gloom bat\bat bat\grey Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 4 6 9 28 36 Bat SEARCH 2 70 5 4 12 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC RESIST_MAGIC Middle,Top None 509
|
||||
MT_FAMILIAR Familiar bat\bat bat\orange Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 6 8 13 20 35 Bat SEARCH 3 50 5 4 16 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Middle,Top None 448
|
||||
MT_NGOATBW Flesh Clan goatbow\goatb Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 4 6 8 20 35 GoatRanged CAN_OPEN_DOOR 0 35 13 1 7 0 0 0 0 0 0 0 0 0 0 35 Demon Bottom,Middle 448
|
||||
MT_BGOATBW Stone Clan goatbow\goatb goatbow\beige Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 5 7 10 30 40 GoatRanged CAN_OPEN_DOOR 1 40 13 2 9 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 645
|
||||
MT_RGOATBW Fire Clan goatbow\goatb goatbow\red Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 6 8 12 40 50 GoatRanged SEARCH,CAN_OPEN_DOOR 2 45 13 3 11 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 822
|
||||
MT_GGOATBW Night Clan goatbow\goatb goatbow\gray Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 7 9 14 50 65 GoatRanged SEARCH,CAN_OPEN_DOOR 3 50 13 4 13 15 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1092
|
||||
MT_NACID Acid Beast acid\acid Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 6 8 11 40 66 Acid 0 40 8 4 12 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 846
|
||||
MT_RACID Poison Spitter acid\acid acid\acidblk Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 8 10 15 60 85 Acid 1 45 8 4 16 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 1248
|
||||
MT_BACID Pit Beast acid\acid acid\acidb Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 10 12 21 80 110 Acid 2 55 8 8 18 35 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_ACID IMMUNE_MAGIC,RESIST_LIGHTNING,IMMUNE_ACID Bottom,Middle 2060
|
||||
MT_XACID Lava Maw acid\acid acid\acidr Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 12 14 25 100 150 Acid 3 65 8 10 20 40 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_ACID Bottom,Middle 2940
|
||||
MT_SKING Skeleton King sking\sking skelaxe\white Never 160 1010 true true 8,6,16,6,16,6 2,1,1,1,1,2 4 4 9 140 140 SkeletonKing SEARCH,CAN_OPEN_DOOR 3 60 8 6 16 0 0 0 0 0 0 0 0 0 0 70 Undead IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top Uniq(SKCROWN) 570
|
||||
MT_CLEAVER The Butcher fatc\fatc Never 128 980 false false 10,8,12,6,16,0 1,1,1,1,1,1 1 1 1 320 320 Butcher 3 50 8 6 12 0 0 0 0 0 0 0 0 0 0 50 Demon RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle Uniq(CLEAVER) 710
|
||||
MT_FAT Overlord fat\fat Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 5 7 10 60 80 Fat 0 55 8 6 12 0 0 0 0 0 0 0 0 0 0 55 Demon RESIST_FIRE Bottom,Middle 635
|
||||
MT_MUDMAN Mud Man fat\fat fat\blue Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 7 9 14 100 125 Fat SEARCH 1 60 8 8 16 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_LIGHTNING Bottom,Middle 1165
|
||||
MT_TOAD Toad Demon fat\fat fat\fatb Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 8 10 16 135 160 Fat SEARCH 2 70 8 8 16 40 0 8 20 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 1380
|
||||
MT_FLAYED Flayed One fat\fat fat\fatf Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 10 12 20 160 200 Fat SEARCH 3 85 8 10 20 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2058
|
||||
MT_WYRM Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 5 7 11 60 90 SkeletonMelee 0 40 8 4 10 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 660
|
||||
MT_CAVSLUG Cave Slug worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 6 8 13 75 110 SkeletonMelee 1 50 8 6 13 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 994
|
||||
MT_DVLWYRM Devil Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 7 9 15 100 140 SkeletonMelee 2 55 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1320
|
||||
MT_DEVOUR Devourer worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 8 10 17 125 200 SkeletonMelee 3 60 8 10 20 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1827
|
||||
MT_NMAGMA Magma Demon magma\magma Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 9 13 50 70 Magma SEARCH,CAN_OPEN_DOOR 0 45 4 2 10 50 13 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1076
|
||||
MT_YMAGMA Blood Stone magma\magma magma\yellow Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 10 14 55 75 Magma SEARCH,CAN_OPEN_DOOR 1 50 4 2 12 50 14 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1309
|
||||
MT_BMAGMA Hell Stone magma\magma magma\blue Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 16 60 80 Magma SEARCH,CAN_OPEN_DOOR 2 60 4 2 20 60 14 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1680
|
||||
MT_WMAGMA Lava Lord magma\magma magma\wierd Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 18 70 85 Magma SEARCH,CAN_OPEN_DOOR 3 75 4 4 24 60 14 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2124
|
||||
MT_HORNED Horned Demon rhino\rhino Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 7 9 13 40 80 Rhino SEARCH,CAN_OPEN_DOOR 0 60 7 2 16 100 0 5 32 0 0 0 0 0 0 40 Animal RESIST_FIRE Bottom,Middle,Top 1172
|
||||
MT_MUDRUN Mud Runner rhino\rhino rhino\orange Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 8 10 15 50 90 Rhino SEARCH,CAN_OPEN_DOOR 1 70 7 6 18 100 0 12 36 0 0 0 0 0 0 45 Animal RESIST_FIRE Bottom,Middle,Top 1404
|
||||
MT_FROSTC Frost Charger rhino\rhino rhino\blue Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 9 11 17 60 100 Rhino SEARCH,CAN_OPEN_DOOR 2 80 7 8 20 100 0 20 40 0 0 0 0 0 0 50 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 1720
|
||||
MT_OBLORD Obsidian Lord rhino\rhino rhino\rhinob Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 10 12 19 70 110 Rhino SEARCH,CAN_OPEN_DOOR 3 90 7 10 22 100 0 20 50 0 0 0 0 0 0 55 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 1809
|
||||
MT_BONEDMN oldboned demskel\demskl Never 128 1740 true true 10,8,20,6,24,16 3,1,1,1,1,1 24 24 12 70 70 Storm 0 60 8 6 14 12 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 1344
|
||||
MT_REDDTH Red Death thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 8 10 16 96 96 Storm 1 75 5 10 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2168
|
||||
MT_LTCHDMN Litch Demon thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 110 110 Storm 2 80 5 10 24 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2736
|
||||
MT_UDEDBLRG Undead Balrog thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 130 130 Storm 3 85 5 12 30 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3575
|
||||
MT_INCIN Incinerator fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 21 22 16 30 45 FireMan 0 75 8 8 16 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 1888
|
||||
MT_FLAMLRD Flame Lord fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 22 23 18 40 55 FireMan 1 75 8 10 20 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2250
|
||||
MT_DOOMFIRE Doom Fire fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 23 24 20 50 65 FireMan 2 80 8 12 24 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 2740
|
||||
MT_HELLBURN Hell Burner fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 24 24 22 60 80 FireMan 3 85 8 15 30 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 3355
|
||||
MT_STORM Red Storm thin\thin thin\thinv3 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 55 110 Storm SEARCH,CAN_OPEN_DOOR 0 80 5 8 18 75 8 4 16 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2160
|
||||
MT_RSTORM Storm Rider thin\thin Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 10 12 20 60 120 Storm SEARCH,CAN_OPEN_DOOR 1 80 5 8 18 80 8 4 16 0 0 0 0 0 0 30 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2391
|
||||
MT_STORML Storm Lord thin\thin thin\thinv2 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 75 135 Storm SEARCH,CAN_OPEN_DOOR 2 85 5 12 24 75 8 4 16 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2775
|
||||
MT_MAEL Maelstrom thin\thin thin\thinv1 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 12 14 24 90 150 Storm SEARCH,CAN_OPEN_DOOR 3 90 5 12 28 75 8 4 16 0 0 0 0 0 0 40 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3177
|
||||
MT_BIGFALL Devil Kin Brute bigfall\fallg newsfx\kbrute Never 128 800 true false 10,8,11,8,17,0 1,1,1,1,2,2 21 22 27 120 160 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 100 6 18 24 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2400
|
||||
MT_WINGED Winged-Demon gargoyle\gargo Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 5 7 9 45 60 Gargoyle CAN_OPEN_DOOR 0 50 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Middle,Top 662
|
||||
MT_GARGOYLE Gargoyle gargoyle\gargo gargoyle\gare Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 7 9 13 60 90 Gargoyle CAN_OPEN_DOOR 1 65 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Middle,Top 1205
|
||||
MT_BLOODCLW Blood Claw gargoyle\gargo gargoyle\gargbr Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 9 11 19 75 125 Gargoyle CAN_OPEN_DOOR 2 80 7 14 22 0 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Middle,Top 1873
|
||||
MT_DEATHW Death Wing gargoyle\gargo gargoyle\gargb Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 10 12 23 90 150 Gargoyle CAN_OPEN_DOOR 3 95 7 16 28 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Middle,Top 2278
|
||||
MT_MEGA Slayer mega\mega Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 10 12 20 120 140 Mega SEARCH,CAN_OPEN_DOOR 0 100 8 12 20 0 3 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2300
|
||||
MT_GUARD Guardian mega\mega mega\guard Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 11 13 22 140 160 Mega SEARCH,CAN_OPEN_DOOR 1 110 8 14 22 0 3 0 0 0 0 0 0 0 0 65 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2714
|
||||
MT_VTEXLRD Vortex Lord mega\mega mega\vtexl Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 12 14 24 160 180 Mega SEARCH,CAN_OPEN_DOOR 2 120 8 18 24 0 3 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3252
|
||||
MT_BALROG Balrog mega\mega mega\balr Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 13 15 26 180 200 Mega SEARCH,CAN_OPEN_DOOR 3 130 8 22 30 0 3 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3643
|
||||
MT_NSNAKE Cave Viper snake\snake Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 11 13 21 100 150 Snake SEARCH 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 2725
|
||||
MT_RSNAKE Fire Drake snake\snake snake\snakr Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 12 14 23 120 170 Snake SEARCH 1 105 8 12 24 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3139
|
||||
MT_BSNAKE Gold Viper snake\snake snake\snakg Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 13 14 25 140 180 Snake SEARCH 2 120 8 15 26 0 0 0 0 0 0 0 0 0 0 70 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 3540
|
||||
MT_GSNAKE Azure Drake snake\snake snake\snakb Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 15 16 27 160 200 Snake SEARCH 3 130 8 18 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 3791
|
||||
MT_NBLACK Black Knight black\black Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 12 14 24 150 150 SkeletonMelee SEARCH 0 110 8 15 20 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3360
|
||||
MT_RTBLACK Doom Guard black\black black\blkkntrt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 15 26 165 165 SkeletonMelee SEARCH 0 130 8 18 25 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3650
|
||||
MT_BTBLACK Steel Lord black\black black\blkkntbt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 14 16 28 180 180 SkeletonMelee SEARCH 1 120 8 20 30 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4252
|
||||
MT_RBLACK Blood Knight black\black black\blkkntbe Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 14 30 200 200 SkeletonMelee SEARCH 1 130 8 25 35 0 0 0 0 0 0 0 0 0 0 85 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5130
|
||||
MT_UNRAV The Shredded unrav\unrav newsfx\shred Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 17 18 23 70 90 SkeletonMelee 0 75 7 4 12 0 0 0 0 0 0 0 0 0 0 65 Undead RESIST_FIRE,RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 900
|
||||
MT_HOLOWONE Hollow One unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 18 19 27 135 240 SkeletonMelee 1 75 7 12 24 0 0 0 0 0 0 0 0 0 0 75 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4374
|
||||
MT_PAINMSTR Pain Master unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 19 20 29 110 200 SkeletonMelee 2 80 7 16 30 0 0 0 0 0 0 0 0 0 0 80 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 5147
|
||||
MT_REALWEAV Reality Weaver unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 20 20 30 135 240 SkeletonMelee 3 85 7 20 35 0 0 0 0 0 0 0 0 0 0 85 Undead RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 5925
|
||||
MT_SUCCUBUS Succubus succ\scbs Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 12 14 24 120 150 Succubus CAN_OPEN_DOOR 0 100 10 1 20 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 3696
|
||||
MT_SNOWWICH Snow Witch succ\scbs succ\succb Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 13 15 26 135 175 Succubus CAN_OPEN_DOOR 1 110 10 1 24 0 0 0 0 0 0 0 0 0 0 65 Demon RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 4084
|
||||
MT_HLSPWN Hell Spawn succ\scbs succ\succrw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 14 16 28 150 200 Succubus SEARCH,CAN_OPEN_DOOR 2 115 10 1 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4480
|
||||
MT_SOLBRNR Soul Burner succ\scbs succ\succbw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 15 16 30 140 225 Succubus SEARCH,CAN_OPEN_DOOR 3 120 10 1 35 0 0 0 0 0 0 0 0 0 0 85 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 4644
|
||||
MT_COUNSLR Counselor mage\mage Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 13 14 25 70 70 Counselor CAN_OPEN_DOOR 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4070
|
||||
MT_MAGISTR Magistrate mage\mage mage\cnselg Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 14 15 27 85 85 Counselor CAN_OPEN_DOOR 1 100 8 10 24 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4478
|
||||
MT_CABALIST Cabalist mage\mage mage\cnselgd Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 15 16 29 120 120 Counselor CAN_OPEN_DOOR 2 110 8 14 30 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4929
|
||||
MT_ADVOCATE Advocate mage\mage mage\cnselbk Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 16 16 30 145 145 Counselor CAN_OPEN_DOOR 3 120 8 15 25 0 0 0 0 0 0 0 0 0 0 0 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968
|
||||
MT_GOLEM Golem golem\golem golem\golm Never 96 386 true false 0,16,12,0,12,20 1,1,1,1,1,1 1 1 12 1 1 Golem CAN_OPEN_DOOR 0 0 7 1 1 0 0 0 0 0 0 0 0 0 0 1 Demon 0
|
||||
MT_DIABLO The Dark Lord diablo\diablo Never 160 2000 true true 16,6,16,6,16,16 1,1,1,1,1,1 26 26 30 1666 1666 Diablo KNOCKBACK,SEARCH,CAN_OPEN_DOOR 3 220 4 30 60 0 11 0 0 0 0 0 0 0 0 90 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 31666
|
||||
MT_DARKMAGE The Arch-Litch Malignus darkmage\dmage darkmage\dmag Never 128 1060 true false 6,1,21,6,23,18 1,1,1,1,1,1 21 21 30 160 160 Counselor CAN_OPEN_DOOR 3 120 8 20 40 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968
|
||||
_monster_id name assetsSuffix soundSuffix trnFile availability width image hasSpecial hasSpecialSound frames[6] rate[6] minDunLvl maxDunLvl level hitPointsMinimum hitPointsMaximum ai abilityFlags intelligence toHit animFrameNum minDamage maxDamage toHitSpecial animFrameNumSpecial minDamageSpecial maxDamageSpecial reducePlayerStrength reducePlayerMagic reducePlayerDexterity reducePlayerVitality reducePlayerMaxHP reducePlayerMaxMana armorClass monsterClass resistance resistanceHell selectionRegion treasure exp shadowRule[6]
|
||||
MT_NZOMBIE Zombie zombie\zombie Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 1 2 1 4 7 Zombie 0 10 8 2 5 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 54 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_BZOMBIE Ghoul zombie\zombie zombie\bluered Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 3 2 7 11 Zombie 1 10 8 3 10 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 58 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_GZOMBIE Rotting Carcass zombie\zombie zombie\grey Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 4 4 15 25 Zombie 2 25 8 5 15 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 136 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_YZOMBIE Black Death zombie\zombie zombie\yellow Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 3 5 6 25 40 Zombie 3 30 8 6 22 0 0 0 0 0 0 0 0 1 0 20 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 240 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RFALLSP Fallen One falspear\phall falspear\fallent Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 1 2 1 1 4 Fallen 0 15 7 1 3 0 5 0 0 0 0 0 0 0 0 0 Animal Bottom,Middle 46 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_DFALLSP Carver falspear\phall falspear\dark Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 3 3 4 8 Fallen 2 20 7 2 5 0 5 0 0 0 0 0 0 0 0 5 Animal Bottom,Middle 80 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YFALLSP Devil Kin falspear\phall Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 4 5 12 24 Fallen 2 25 7 3 7 0 5 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 155 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BFALLSP Dark One falspear\phall falspear\blue Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 3 5 7 20 36 Fallen 3 30 7 4 8 0 5 0 0 0 0 0 0 0 0 15 Animal RESIST_LIGHTNING Bottom,Middle 255 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSKELAX Skeleton skelaxe\sklax skelaxe\white Always 128 553 true false 12,8,13,6,17,16 5,1,1,1,1,1 1 2 1 2 4 SkeletonMelee 0 20 8 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 64 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TSKELAX Corpse Axe skelaxe\sklax skelaxe\skelt Always 128 553 true false 12,8,13,6,17,16 4,1,1,1,1,1 2 3 2 4 7 SkeletonMelee 1 25 8 3 5 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 68 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSKELAX Burning Dead skelaxe\sklax Always 128 553 true false 12,8,13,6,17,16 2,1,1,1,1,1 2 4 4 8 12 SkeletonMelee 2 30 8 3 7 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 154 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XSKELAX Horror skelaxe\sklax skelaxe\black Always 128 553 true false 12,8,13,6,17,16 3,1,1,1,1,1 3 5 6 12 20 SkeletonMelee 3 35 8 4 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 264 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RFALLSD Fallen One falsword\fall falsword\fallent Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 1 2 1 2 5 Fallen 0 15 8 1 4 0 5 0 0 0 0 0 0 0 0 10 Animal Bottom,Middle 52 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_DFALLSD Carver falsword\fall falsword\dark Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 3 3 5 9 Fallen 1 20 8 2 7 0 5 0 0 0 0 0 0 0 0 15 Animal Bottom,Middle 90 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YFALLSD Devil Kin falsword\fall Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 4 5 16 24 Fallen 2 25 8 4 10 0 5 0 0 0 0 0 0 0 0 20 Animal RESIST_FIRE Bottom,Middle 180 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BFALLSD Dark One falsword\fall falsword\blue Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 3 5 7 24 36 Fallen 3 30 8 4 12 0 5 0 0 0 0 0 0 0 0 25 Animal RESIST_LIGHTNING Bottom,Middle 280 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_NSCAV Scavenger scav\scav Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 1 3 2 3 6 Scavenger 0 20 7 1 5 0 0 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 80 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BSCAV Plague Eater scav\scav scav\scavbr Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 2 4 4 12 24 Scavenger 1 30 7 1 8 0 0 0 0 0 0 0 0 0 0 20 Animal RESIST_LIGHTNING Bottom,Middle 188 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSCAV Shadow Beast scav\scav scav\scavbe Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 3 5 6 24 36 Scavenger 2 35 7 3 12 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_FIRE Bottom,Middle 375 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YSCAV Bone Gasher scav\scav scav\scavw Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 4 6 8 28 40 Scavenger 3 35 7 5 15 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_LIGHTNING Bottom,Middle 552 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSKELBW Skeleton skelbow\sklbw skelbow\white Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 3 3 2 4 SkeletonRanged 0 15 12 1 2 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 110 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TSKELBW Corpse Bow skelbow\sklbw skelbow\skelt Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 4 5 8 16 SkeletonRanged 1 25 12 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 210 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSKELBW Burning Dead skelbow\sklbw Always 128 567 true false 9,8,16,5,16,16 2,1,1,1,1,1 3 5 7 10 24 SkeletonRanged 2 30 12 1 6 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 364 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XSKELBW Horror skelbow\sklbw skelbow\black Always 128 567 true false 9,8,16,5,16,16 3,1,1,1,1,1 4 6 9 15 45 SkeletonRanged 3 35 12 2 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 594 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSKELSD Skeleton Captain skelsd\sklsr skelsd\white Always 128 575 true true 13,8,12,7,15,16 4,1,1,1,1,1 1 3 2 3 6 SkeletonMelee 0 20 8 2 7 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 90 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TSKELSD Corpse Captain skelsd\sklsr skelsd\skelt Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 2 4 4 12 20 SkeletonMelee 1 30 8 3 9 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 200 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSKELSD Burning Dead Captain skelsd\sklsr Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 3 5 6 16 30 SkeletonMelee 2 35 8 4 10 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 393 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XSKELSD Horror Captain skelsd\sklsr skelsd\black Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 4 6 8 35 50 SkeletonMelee SEARCH 3 40 8 5 14 0 0 0 0 0 0 0 0 0 0 30 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 604 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_INVILORD Invisible Lord tsneak\tsneak Never 128 800 false false 13,13,15,11,16,0 2,1,1,1,1,1 19 20 14 278 278 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 65 8 16 30 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2000 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_SNEAK Hidden sneak\sneak Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 2 5 5 8 24 Sneak HIDDEN 0 35 8 3 6 0 0 0 0 0 0 0 0 0 0 25 Demon Bottom,Middle 278 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_STALKER Stalker sneak\sneak sneak\sneakv2 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 5 7 9 30 45 Sneak HIDDEN,SEARCH 1 40 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Demon Bottom,Middle 630 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_UNSEEN Unseen sneak\sneak sneak\sneakv3 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 6 8 11 35 50 Sneak HIDDEN,SEARCH 2 45 8 12 20 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 935 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_ILLWEAV Illusion Weaver sneak\sneak sneak\sneakv1 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 8 10 13 40 60 Sneak HIDDEN,SEARCH 3 60 8 16 24 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC,RESIST_FIRE IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 1500 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_LRDSAYTR Satyr Lord goatlord\goatl newsfx\satyr Never 160 800 false false 13,13,14,9,16,0 2,1,1,1,1,1 21 22 28 160 200 SkeletonMelee SEARCH 3 90 8 20 30 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 2800 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_NGOATMC Flesh Clan goatmace\goat Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 4 6 8 30 45 GoatMelee SEARCH,CAN_OPEN_DOOR 0 50 8 4 10 0 0 0 0 0 0 0 0 0 0 40 Demon Bottom,Middle 460 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BGOATMC Stone Clan goatmace\goat goatmace\beige Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 5 7 10 40 55 GoatMelee SEARCH,CAN_OPEN_DOOR 1 60 8 6 12 0 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 685 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RGOATMC Fire Clan goatmace\goat goatmace\red Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 6 8 12 50 65 GoatMelee SEARCH,CAN_OPEN_DOOR 2 70 8 8 16 0 0 0 0 0 0 0 0 0 0 45 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 906 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_GGOATMC Night Clan goatmace\goat goatmace\gray Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 7 9 14 55 70 GoatMelee SEARCH,CAN_OPEN_DOOR 3 80 8 10 20 15 0 30 30 0 0 0 0 0 0 50 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1190 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_FIEND Fiend bat\bat bat\red Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 2 3 3 3 6 Bat 0 35 5 1 6 0 0 0 0 0 0 0 0 0 0 0 Animal Middle,Top None 102 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_BLINK Blink bat\bat Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 3 5 7 12 28 Bat 1 45 5 1 8 0 0 0 0 0 0 0 0 0 0 15 Animal Middle,Top None 340 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_GLOOM Gloom bat\bat bat\grey Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 4 6 9 28 36 Bat SEARCH 2 70 5 4 12 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC RESIST_MAGIC Middle,Top None 509 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_FAMILIAR Familiar bat\bat bat\orange Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 6 8 13 20 35 Bat SEARCH 3 50 5 4 16 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Middle,Top None 448 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_NGOATBW Flesh Clan goatbow\goatb Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 4 6 8 20 35 GoatRanged CAN_OPEN_DOOR 0 35 13 1 7 0 0 0 0 0 0 0 0 0 0 35 Demon Bottom,Middle 448 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_BGOATBW Stone Clan goatbow\goatb goatbow\beige Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 5 7 10 30 40 GoatRanged CAN_OPEN_DOOR 1 40 13 2 9 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 645 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RGOATBW Fire Clan goatbow\goatb goatbow\red Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 6 8 12 40 50 GoatRanged SEARCH,CAN_OPEN_DOOR 2 45 13 3 11 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 822 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_GGOATBW Night Clan goatbow\goatb goatbow\gray Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 7 9 14 50 65 GoatRanged SEARCH,CAN_OPEN_DOOR 3 50 13 4 13 15 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1092 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_NACID Acid Beast acid\acid Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 6 8 11 40 66 Acid 0 40 8 4 12 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 846 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RACID Poison Spitter acid\acid acid\acidblk Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 8 10 15 60 85 Acid 1 45 8 4 16 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 1248 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BACID Pit Beast acid\acid acid\acidb Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 10 12 21 80 110 Acid 2 55 8 8 18 35 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_ACID IMMUNE_MAGIC,RESIST_LIGHTNING,IMMUNE_ACID Bottom,Middle 2060 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XACID Lava Maw acid\acid acid\acidr Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 12 14 25 100 150 Acid 3 65 8 10 20 40 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_ACID Bottom,Middle 2940 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_SKING Skeleton King sking\sking skelaxe\white Never 160 1010 true true 8,6,16,6,16,6 2,1,1,1,1,2 4 4 9 140 140 SkeletonKing SEARCH,CAN_OPEN_DOOR 3 60 8 6 16 0 0 0 0 0 0 0 0 0 0 70 Undead IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top Uniq(SKCROWN) 570 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_CLEAVER The Butcher fatc\fatc Never 128 980 false false 10,8,12,6,16,0 1,1,1,1,1,1 1 1 1 320 320 Butcher 3 50 8 6 12 0 0 0 0 0 0 0 0 0 0 50 Demon RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle Uniq(CLEAVER) 710 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_FAT Overlord fat\fat Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 5 7 10 60 80 Fat 0 55 8 6 12 0 0 0 0 0 0 0 0 0 0 55 Demon RESIST_FIRE Bottom,Middle 635 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MUDMAN Mud Man fat\fat fat\blue Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 7 9 14 100 125 Fat SEARCH 1 60 8 8 16 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_LIGHTNING Bottom,Middle 1165 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TOAD Toad Demon fat\fat fat\fatb Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 8 10 16 135 160 Fat SEARCH 2 70 8 8 16 40 0 8 20 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 1380 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_FLAYED Flayed One fat\fat fat\fatf Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 10 12 20 160 200 Fat SEARCH 3 85 8 10 20 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2058 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WYRM Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 5 7 11 60 90 SkeletonMelee 0 40 8 4 10 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 660
|
||||
MT_CAVSLUG Cave Slug worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 6 8 13 75 110 SkeletonMelee 1 50 8 6 13 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 994
|
||||
MT_DVLWYRM Devil Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 7 9 15 100 140 SkeletonMelee 2 55 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1320
|
||||
MT_DEVOUR Devourer worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 8 10 17 125 200 SkeletonMelee 3 60 8 10 20 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1827
|
||||
MT_NMAGMA Magma Demon magma\magma Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 9 13 50 70 Magma SEARCH,CAN_OPEN_DOOR 0 45 4 2 10 50 13 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1076 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YMAGMA Blood Stone magma\magma magma\yellow Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 10 14 55 75 Magma SEARCH,CAN_OPEN_DOOR 1 50 4 2 12 50 14 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1309 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BMAGMA Hell Stone magma\magma magma\blue Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 16 60 80 Magma SEARCH,CAN_OPEN_DOOR 2 60 4 2 20 60 14 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1680 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WMAGMA Lava Lord magma\magma magma\wierd Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 18 70 85 Magma SEARCH,CAN_OPEN_DOOR 3 75 4 4 24 60 14 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2124 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_HORNED Horned Demon rhino\rhino Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 7 9 13 40 80 Rhino SEARCH,CAN_OPEN_DOOR 0 60 7 2 16 100 0 5 32 0 0 0 0 0 0 40 Animal RESIST_FIRE Bottom,Middle,Top 1172 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MUDRUN Mud Runner rhino\rhino rhino\orange Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 8 10 15 50 90 Rhino SEARCH,CAN_OPEN_DOOR 1 70 7 6 18 100 0 12 36 0 0 0 0 0 0 45 Animal RESIST_FIRE Bottom,Middle,Top 1404 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_FROSTC Frost Charger rhino\rhino rhino\blue Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 9 11 17 60 100 Rhino SEARCH,CAN_OPEN_DOOR 2 80 7 8 20 100 0 20 40 0 0 0 0 0 0 50 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 1720 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_OBLORD Obsidian Lord rhino\rhino rhino\rhinob Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 10 12 19 70 110 Rhino SEARCH,CAN_OPEN_DOOR 3 90 7 10 22 100 0 20 50 0 0 0 0 0 0 55 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 1809 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BONEDMN oldboned demskel\demskl Never 128 1740 true true 10,8,20,6,24,16 3,1,1,1,1,1 24 24 12 70 70 Storm 0 60 8 6 14 12 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 1344 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_REDDTH Red Death thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 8 10 16 96 96 Storm 1 75 5 10 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2168 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_LTCHDMN Litch Demon thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 110 110 Storm 2 80 5 10 24 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2736 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_UDEDBLRG Undead Balrog thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 130 130 Storm 3 85 5 12 30 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3575 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_INCIN Incinerator fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 21 22 16 30 45 FireMan 0 75 8 8 16 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 1888 None,None,None,None,None,None
|
||||
MT_FLAMLRD Flame Lord fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 22 23 18 40 55 FireMan 1 75 8 10 20 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2250 None,None,None,None,None,None
|
||||
MT_DOOMFIRE Doom Fire fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 23 24 20 50 65 FireMan 2 80 8 12 24 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 2740 None,None,None,None,None,None
|
||||
MT_HELLBURN Hell Burner fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 24 24 22 60 80 FireMan 3 85 8 15 30 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 3355 None,None,None,None,None,None
|
||||
MT_STORM Red Storm thin\thin thin\thinv3 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 55 110 Storm SEARCH,CAN_OPEN_DOOR 0 80 5 8 18 75 8 4 16 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2160 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSTORM Storm Rider thin\thin Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 10 12 20 60 120 Storm SEARCH,CAN_OPEN_DOOR 1 80 5 8 18 80 8 4 16 0 0 0 0 0 0 30 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2391 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_STORML Storm Lord thin\thin thin\thinv2 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 75 135 Storm SEARCH,CAN_OPEN_DOOR 2 85 5 12 24 75 8 4 16 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2775 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MAEL Maelstrom thin\thin thin\thinv1 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 12 14 24 90 150 Storm SEARCH,CAN_OPEN_DOOR 3 90 5 12 28 75 8 4 16 0 0 0 0 0 0 40 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3177 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BIGFALL Devil Kin Brute bigfall\fallg newsfx\kbrute Never 128 800 true false 10,8,11,8,17,0 1,1,1,1,2,2 21 22 27 120 160 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 100 6 18 24 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2400 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_WINGED Winged-Demon gargoyle\gargo Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 5 7 9 45 60 Gargoyle CAN_OPEN_DOOR 0 50 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Middle,Top 662 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_GARGOYLE Gargoyle gargoyle\gargo gargoyle\gare Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 7 9 13 60 90 Gargoyle CAN_OPEN_DOOR 1 65 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Middle,Top 1205 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_BLOODCLW Blood Claw gargoyle\gargo gargoyle\gargbr Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 9 11 19 75 125 Gargoyle CAN_OPEN_DOOR 2 80 7 14 22 0 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Middle,Top 1873 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_DEATHW Death Wing gargoyle\gargo gargoyle\gargb Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 10 12 23 90 150 Gargoyle CAN_OPEN_DOOR 3 95 7 16 28 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Middle,Top 2278 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_MEGA Slayer mega\mega Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 10 12 20 120 140 Mega SEARCH,CAN_OPEN_DOOR 0 100 8 12 20 0 3 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2300 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_GUARD Guardian mega\mega mega\guard Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 11 13 22 140 160 Mega SEARCH,CAN_OPEN_DOOR 1 110 8 14 22 0 3 0 0 0 0 0 0 0 0 65 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2714 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_VTEXLRD Vortex Lord mega\mega mega\vtexl Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 12 14 24 160 180 Mega SEARCH,CAN_OPEN_DOOR 2 120 8 18 24 0 3 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3252 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_BALROG Balrog mega\mega mega\balr Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 13 15 26 180 200 Mega SEARCH,CAN_OPEN_DOOR 3 130 8 22 30 0 3 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3643 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_NSNAKE Cave Viper snake\snake Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 11 13 21 100 150 Snake SEARCH 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 2725 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_RSNAKE Fire Drake snake\snake snake\snakr Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 12 14 23 120 170 Snake SEARCH 1 105 8 12 24 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3139 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_BSNAKE Gold Viper snake\snake snake\snakg Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 13 14 25 140 180 Snake SEARCH 2 120 8 15 26 0 0 0 0 0 0 0 0 0 0 70 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 3540 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_GSNAKE Azure Drake snake\snake snake\snakb Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 15 16 27 160 200 Snake SEARCH 3 130 8 18 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 3791 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_NBLACK Black Knight black\black Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 12 14 24 150 150 SkeletonMelee SEARCH 0 110 8 15 20 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3360 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RTBLACK Doom Guard black\black black\blkkntrt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 15 26 165 165 SkeletonMelee SEARCH 0 130 8 18 25 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3650 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_BTBLACK Steel Lord black\black black\blkkntbt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 14 16 28 180 180 SkeletonMelee SEARCH 1 120 8 20 30 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4252 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RBLACK Blood Knight black\black black\blkkntbe Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 14 30 200 200 SkeletonMelee SEARCH 1 130 8 25 35 0 0 0 0 0 0 0 0 0 0 85 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5130 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_UNRAV The Shredded unrav\unrav newsfx\shred Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 17 18 23 70 90 SkeletonMelee 0 75 7 4 12 0 0 0 0 0 0 0 0 0 0 65 Undead RESIST_FIRE,RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 900 None,None,None,None,None,None
|
||||
MT_HOLOWONE Hollow One unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 18 19 27 135 240 SkeletonMelee 1 75 7 12 24 0 0 0 0 0 0 0 0 0 0 75 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4374 None,None,None,None,None,None
|
||||
MT_PAINMSTR Pain Master unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 19 20 29 110 200 SkeletonMelee 2 80 7 16 30 0 0 0 0 0 0 0 0 0 0 80 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 5147 None,None,None,None,None,None
|
||||
MT_REALWEAV Reality Weaver unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 20 20 30 135 240 SkeletonMelee 3 85 7 20 35 0 0 0 0 0 0 0 0 0 0 85 Undead RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 5925 None,None,None,None,None,None
|
||||
MT_SUCCUBUS Succubus succ\scbs Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 12 14 24 120 150 Succubus CAN_OPEN_DOOR 0 100 10 1 20 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 3696 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_SNOWWICH Snow Witch succ\scbs succ\succb Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 13 15 26 135 175 Succubus CAN_OPEN_DOOR 1 110 10 1 24 0 0 0 0 0 0 0 0 0 0 65 Demon RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 4084 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_HLSPWN Hell Spawn succ\scbs succ\succrw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 14 16 28 150 200 Succubus SEARCH,CAN_OPEN_DOOR 2 115 10 1 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4480 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_SOLBRNR Soul Burner succ\scbs succ\succbw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 15 16 30 140 225 Succubus SEARCH,CAN_OPEN_DOOR 3 120 10 1 35 0 0 0 0 0 0 0 0 0 0 85 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 4644 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_COUNSLR Counselor mage\mage Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 13 14 25 70 70 Counselor CAN_OPEN_DOOR 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4070 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MAGISTR Magistrate mage\mage mage\cnselg Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 14 15 27 85 85 Counselor CAN_OPEN_DOOR 1 100 8 10 24 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4478 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_CABALIST Cabalist mage\mage mage\cnselgd Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 15 16 29 120 120 Counselor CAN_OPEN_DOOR 2 110 8 14 30 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4929 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_ADVOCATE Advocate mage\mage mage\cnselbk Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 16 16 30 145 145 Counselor CAN_OPEN_DOOR 3 120 8 15 25 0 0 0 0 0 0 0 0 0 0 0 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_GOLEM Golem golem\golem golem\golm Never 96 386 true false 0,16,12,0,12,20 1,1,1,1,1,1 1 1 12 1 1 Golem CAN_OPEN_DOOR 0 0 7 1 1 0 0 0 0 0 0 0 0 0 0 1 Demon 0 None,EachFrame,EachFrame,None,EachFrame,EachFrame
|
||||
MT_DIABLO The Dark Lord diablo\diablo Never 160 2000 true true 16,6,16,6,16,16 1,1,1,1,1,1 26 26 30 1666 1666 Diablo KNOCKBACK,SEARCH,CAN_OPEN_DOOR 3 220 4 30 60 0 11 0 0 0 0 0 0 0 0 90 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 31666 FirstFrame,FirstFrame,EachFrame,FirstFrame,EachFrame,EachFrame
|
||||
MT_DARKMAGE The Arch-Litch Malignus darkmage\dmage darkmage\dmag Never 128 1060 true false 6,1,21,6,23,18 1,1,1,1,1,1 21 21 30 160 160 Counselor CAN_OPEN_DOOR 3 120 8 20 40 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968 FirstFrame,None,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
|
||||
|
+110
-110
@@ -1,110 +1,110 @@
|
||||
id file minLevel maxLevel levelType theme quest flags animDelay animLen animWidth selectionRegion
|
||||
OBJ_L1LIGHT l1braz 0 0 DTYPE_CATHEDRAL Animated,Solid,MissilesPassThrough 1 26 64
|
||||
OBJ_L1LDOOR l1doors 0 0 DTYPE_CATHEDRAL Light,Trap 1 0 64 Bottom,Middle
|
||||
OBJ_L1RDOOR l1doors 0 0 DTYPE_CATHEDRAL Light,Trap 2 0 64 Bottom,Middle
|
||||
OBJ_SKFIRE skulfire 0 0 THEME_SKELROOM Animated,Solid,MissilesPassThrough 2 11 96
|
||||
OBJ_LEVER lever 0 0 Solid,MissilesPassThrough,Light,Trap 1 1 96 Bottom
|
||||
OBJ_CHEST1 chest1 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom
|
||||
OBJ_CHEST2 chest2 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom
|
||||
OBJ_CHEST3 chest3 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom
|
||||
OBJ_CANDLE1 l1braz 0 0 0 0 0
|
||||
OBJ_CANDLE2 candle2 0 0 THEME_SHRINE Q_PWATER Animated,Solid,MissilesPassThrough,Light 2 4 96
|
||||
OBJ_CANDLEO l1braz 0 0 0 0 0
|
||||
OBJ_BANNERL banner 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 2 0 96
|
||||
OBJ_BANNERM banner 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 1 0 96
|
||||
OBJ_BANNERR banner 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 3 0 96
|
||||
OBJ_SKPILE skulpile 0 0 Solid,MissilesPassThrough,Light 1 1 96
|
||||
OBJ_SKSTICK1 l1braz 0 0 0 0 0
|
||||
OBJ_SKSTICK2 l1braz 0 0 0 0 0
|
||||
OBJ_SKSTICK3 l1braz 0 0 0 0 0
|
||||
OBJ_SKSTICK4 l1braz 0 0 0 0 0
|
||||
OBJ_SKSTICK5 l1braz 0 0 0 0 0
|
||||
OBJ_CRUX1 cruxsk1 0 0 Solid,Light,Breakable 1 15 96 Bottom,Middle
|
||||
OBJ_CRUX2 cruxsk2 0 0 Solid,Light,Breakable 1 15 96 Bottom,Middle
|
||||
OBJ_CRUX3 cruxsk3 0 0 Solid,Light,Breakable 1 15 96 Bottom,Middle
|
||||
OBJ_STAND rockstan 5 5 Solid,MissilesPassThrough,Light 1 0 96
|
||||
OBJ_ANGEL angel 0 0 Solid,Light 1 0 96
|
||||
OBJ_BOOK2L book2 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle
|
||||
OBJ_BCROSS burncros 0 0 Animated,Solid 0 10 160
|
||||
OBJ_NUDEW2R nude2 0 0 Animated,Solid,Light 3 6 128
|
||||
OBJ_SWITCHSKL switch4 16 16 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom
|
||||
OBJ_TNUDEM1 tnudem 13 15 Q_BUTCHER Solid,Light 1 0 128
|
||||
OBJ_TNUDEM2 tnudem 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 2 0 128
|
||||
OBJ_TNUDEM3 tnudem 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 3 0 128
|
||||
OBJ_TNUDEM4 tnudem 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 4 0 128
|
||||
OBJ_TNUDEW1 tnudew 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 1 0 128
|
||||
OBJ_TNUDEW2 tnudew 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 2 0 128
|
||||
OBJ_TNUDEW3 tnudew 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 3 0 128
|
||||
OBJ_TORTURE1 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 1 0 128
|
||||
OBJ_TORTURE2 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 2 0 128
|
||||
OBJ_TORTURE3 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 3 0 128
|
||||
OBJ_TORTURE4 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 4 0 128
|
||||
OBJ_TORTURE5 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 5 0 128
|
||||
OBJ_BOOK2R book2 6 6 Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle
|
||||
OBJ_L2LDOOR l2doors 0 0 DTYPE_CATACOMBS Light,Trap 1 0 64 Bottom,Middle
|
||||
OBJ_L2RDOOR l2doors 0 0 DTYPE_CATACOMBS Light,Trap 2 0 64 Bottom,Middle
|
||||
OBJ_TORCHL wtorch4 5 8 Animated,MissilesPassThrough 1 9 96
|
||||
OBJ_TORCHR wtorch3 5 8 Animated,MissilesPassThrough 1 9 96
|
||||
OBJ_TORCHL2 wtorch1 5 8 Animated,MissilesPassThrough 1 9 96
|
||||
OBJ_TORCHR2 wtorch2 5 8 Animated,MissilesPassThrough 1 9 96
|
||||
OBJ_SARC sarc 1 4 Solid,MissilesPassThrough,Light,Trap 1 5 128 Bottom,Middle
|
||||
OBJ_FLAMEHOLE flame1 0 0 MissilesPassThrough,Light 1 20 96
|
||||
OBJ_FLAMELVR lever 0 0 Solid,MissilesPassThrough,Light,Trap 1 2 96 Bottom
|
||||
OBJ_WATER miniwatr 0 0 Animated,Solid,Light 1 10 64
|
||||
OBJ_BOOKLVR book1 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle
|
||||
OBJ_TRAPL traphole 1 24 MissilesPassThrough,Light 1 0 64
|
||||
OBJ_TRAPR traphole 1 24 MissilesPassThrough,Light 2 0 64
|
||||
OBJ_BOOKSHELF bcase 0 0 Solid,Light 1 0 96
|
||||
OBJ_WEAPRACK weapstnd 0 0 Solid,Light 1 0 96
|
||||
OBJ_BARREL barrel 1 15 Solid,MissilesPassThrough,Light,Breakable 1 9 96 Bottom,Middle
|
||||
OBJ_BARRELEX barrelex 1 15 Solid,MissilesPassThrough,Light,Breakable 1 10 96 Bottom,Middle
|
||||
OBJ_SHRINEL lshrineg 0 0 THEME_SHRINE Light 1 11 128 Bottom,Middle
|
||||
OBJ_SHRINER rshrineg 0 0 THEME_SHRINE Light 1 11 128 Bottom,Middle
|
||||
OBJ_SKELBOOK book2 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle
|
||||
OBJ_BOOKCASEL bcase 0 0 THEME_LIBRARY Solid,Light 3 0 96 Bottom,Middle
|
||||
OBJ_BOOKCASER bcase 0 0 THEME_LIBRARY Solid,Light 4 0 96 Bottom,Middle
|
||||
OBJ_BOOKSTAND book2 0 0 THEME_LIBRARY Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle
|
||||
OBJ_BOOKCANDLE candle2 0 0 THEME_LIBRARY Animated,Solid,MissilesPassThrough,Light 2 4 96
|
||||
OBJ_BLOODFTN bloodfnt 0 0 THEME_BLOODFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 10 96 Bottom,Middle
|
||||
OBJ_DECAP decap 13 15 THEME_DECAPITATED Solid,MissilesPassThrough,Light 1 0 96 Bottom
|
||||
OBJ_TCHEST1 chest1 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom
|
||||
OBJ_TCHEST2 chest2 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom
|
||||
OBJ_TCHEST3 chest3 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom
|
||||
OBJ_BLINDBOOK book1 0 0 Q_BLIND Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle
|
||||
OBJ_BLOODBOOK book1 0 0 Q_BLOOD Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle
|
||||
OBJ_PEDESTAL pedistl 0 0 Q_BLOOD Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle
|
||||
OBJ_L3LDOOR l3doors 0 0 DTYPE_CAVES Light,Trap 1 0 64 Bottom,Middle
|
||||
OBJ_L3RDOOR l3doors 0 0 DTYPE_CAVES Light,Trap 2 0 64 Bottom,Middle
|
||||
OBJ_PURIFYINGFTN pfountn 0 0 THEME_PURIFYINGFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 10 128 Bottom,Middle
|
||||
OBJ_ARMORSTAND armstand 0 0 THEME_ARMORSTAND Solid,Light 1 0 96 Bottom,Middle
|
||||
OBJ_ARMORSTANDN armstand 0 0 THEME_ARMORSTAND Solid,Light 2 0 96
|
||||
OBJ_GOATSHRINE goatshrn 0 0 THEME_GOATSHRINE Animated,Solid,MissilesPassThrough,Light 2 10 96 Bottom,Middle
|
||||
OBJ_CAULDRON cauldren 13 15 Solid,Light 1 0 96 Bottom,Middle
|
||||
OBJ_MURKYFTN mfountn 0 0 THEME_MURKYFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 10 128 Bottom,Middle
|
||||
OBJ_TEARFTN tfountn 0 0 THEME_TEARFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 4 128 Bottom,Middle
|
||||
OBJ_ALTBOY altboy 0 0 Q_BETRAYER Solid,MissilesPassThrough,Light 1 0 128
|
||||
OBJ_MCIRCLE1 mcirl 0 0 Q_BETRAYER MissilesPassThrough,Light 1 0 96
|
||||
OBJ_MCIRCLE2 mcirl 0 0 Q_BETRAYER MissilesPassThrough,Light 1 0 96
|
||||
OBJ_STORYBOOK bkslbrnt 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle
|
||||
OBJ_STORYCANDLE candle2 0 0 Q_BETRAYER Animated,Solid,MissilesPassThrough,Light 2 4 96
|
||||
OBJ_STEELTOME book1 0 0 Q_WARLORD Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle
|
||||
OBJ_WARARMOR armstand 0 0 Q_WARLORD Solid,Light 1 0 96 Bottom,Middle
|
||||
OBJ_WARWEAP weapstnd 0 0 Q_WARLORD Solid,Light 1 0 96 Bottom,Middle
|
||||
OBJ_TBCROSS burncros 0 0 THEME_BRNCROSS Animated,Solid 0 10 160
|
||||
OBJ_WEAPONRACK weapstnd 0 0 THEME_WEAPONRACK Solid,Light 1 0 96 Bottom,Middle
|
||||
OBJ_WEAPONRACKN weapstnd 0 0 THEME_WEAPONRACK Solid,Light 2 0 96
|
||||
OBJ_MUSHPATCH mushptch 0 0 Q_MUSHROOM Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom,Middle
|
||||
OBJ_LAZSTAND lzstand 0 0 Q_BETRAYER Solid,Light 1 0 128 Bottom,Middle
|
||||
OBJ_SLAINHERO decap 9 9 Solid,MissilesPassThrough,Light 2 0 96 Bottom
|
||||
OBJ_SIGNCHEST chest3 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom
|
||||
OBJ_BOOKSHELFR bcase 0 0 Solid,Light 2 0 96
|
||||
OBJ_POD l6pod1 17 20 Solid,MissilesPassThrough,Light,Breakable 1 9 96 Bottom,Middle
|
||||
OBJ_PODEX l6pod2 17 20 Solid,MissilesPassThrough,Light,Breakable 1 10 96 Bottom,Middle
|
||||
OBJ_URN urn 21 24 Solid,MissilesPassThrough,Light,Breakable 1 9 96 Bottom,Middle
|
||||
OBJ_URNEX urnexpld 21 24 Solid,MissilesPassThrough,Light,Breakable 1 10 96 Bottom,Middle
|
||||
OBJ_L5BOOKS l5books 21 24 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle
|
||||
OBJ_L5CANDLE l5light 21 23 Animated,Solid,MissilesPassThrough,Light 2 4 96
|
||||
OBJ_L5LDOOR l5door 0 0 DTYPE_CRYPT Light,Trap 1 0 64 Bottom,Middle
|
||||
OBJ_L5RDOOR l5door 0 0 DTYPE_CRYPT Light,Trap 2 0 64 Bottom,Middle
|
||||
OBJ_L5LEVER l5lever 24 24 Solid,MissilesPassThrough,Light,Trap 1 1 96 Bottom
|
||||
OBJ_L5SARC l5sarco 21 24 Solid,MissilesPassThrough,Light,Trap 1 5 128 Bottom,Middle
|
||||
id file minLevel maxLevel levelType theme quest flags animDelay animLen animWidth selectionRegion shadowRule
|
||||
OBJ_L1LIGHT l1braz 0 0 DTYPE_CATHEDRAL Animated,Solid,MissilesPassThrough 1 26 64 None
|
||||
OBJ_L1LDOOR l1doors 0 0 DTYPE_CATHEDRAL Light,Trap 1 0 64 Bottom,Middle None
|
||||
OBJ_L1RDOOR l1doors 0 0 DTYPE_CATHEDRAL Light,Trap 2 0 64 Bottom,Middle None
|
||||
OBJ_SKFIRE skulfire 0 0 THEME_SKELROOM Animated,Solid,MissilesPassThrough 2 11 96 FirstFrame
|
||||
OBJ_LEVER lever 0 0 Solid,MissilesPassThrough,Light,Trap 1 1 96 Bottom FirstFrame
|
||||
OBJ_CHEST1 chest1 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom EachFrame
|
||||
OBJ_CHEST2 chest2 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom EachFrame
|
||||
OBJ_CHEST3 chest3 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom EachFrame
|
||||
OBJ_CANDLE1 l1braz 0 0 0 0 0 None
|
||||
OBJ_CANDLE2 candle2 0 0 THEME_SHRINE Q_PWATER Animated,Solid,MissilesPassThrough,Light 2 4 96 FirstFrame
|
||||
OBJ_CANDLEO l1braz 0 0 0 0 0 None
|
||||
OBJ_BANNERL banner 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 2 0 96 EachFrame
|
||||
OBJ_BANNERM banner 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 1 0 96 EachFrame
|
||||
OBJ_BANNERR banner 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 3 0 96 EachFrame
|
||||
OBJ_SKPILE skulpile 0 0 Solid,MissilesPassThrough,Light 1 1 96 FirstFrame
|
||||
OBJ_SKSTICK1 l1braz 0 0 0 0 0 None
|
||||
OBJ_SKSTICK2 l1braz 0 0 0 0 0 None
|
||||
OBJ_SKSTICK3 l1braz 0 0 0 0 0 None
|
||||
OBJ_SKSTICK4 l1braz 0 0 0 0 0 None
|
||||
OBJ_SKSTICK5 l1braz 0 0 0 0 0 None
|
||||
OBJ_CRUX1 cruxsk1 0 0 Solid,Light,Breakable 1 15 96 Bottom,Middle EachFrame
|
||||
OBJ_CRUX2 cruxsk2 0 0 Solid,Light,Breakable 1 15 96 Bottom,Middle EachFrame
|
||||
OBJ_CRUX3 cruxsk3 0 0 Solid,Light,Breakable 1 15 96 Bottom,Middle EachFrame
|
||||
OBJ_STAND rockstan 5 5 Solid,MissilesPassThrough,Light 1 0 96 None
|
||||
OBJ_ANGEL angel 0 0 Solid,Light 1 0 96 EachFrame
|
||||
OBJ_BOOK2L book2 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle EachFrame
|
||||
OBJ_BCROSS burncros 0 0 Animated,Solid 0 10 160 None
|
||||
OBJ_NUDEW2R nude2 0 0 Animated,Solid,Light 3 6 128 None
|
||||
OBJ_SWITCHSKL switch4 16 16 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom FirstFrame
|
||||
OBJ_TNUDEM1 tnudem 13 15 Q_BUTCHER Solid,Light 1 0 128 EachFrame
|
||||
OBJ_TNUDEM2 tnudem 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 2 0 128 EachFrame
|
||||
OBJ_TNUDEM3 tnudem 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 3 0 128 EachFrame
|
||||
OBJ_TNUDEM4 tnudem 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 4 0 128 EachFrame
|
||||
OBJ_TNUDEW1 tnudew 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 1 0 128 EachFrame
|
||||
OBJ_TNUDEW2 tnudew 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 2 0 128 EachFrame
|
||||
OBJ_TNUDEW3 tnudew 13 15 THEME_TORTURE Q_BUTCHER Solid,Light 3 0 128 EachFrame
|
||||
OBJ_TORTURE1 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 1 0 128 None
|
||||
OBJ_TORTURE2 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 2 0 128 None
|
||||
OBJ_TORTURE3 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 3 0 128 None
|
||||
OBJ_TORTURE4 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 4 0 128 None
|
||||
OBJ_TORTURE5 tsoul 13 15 Q_BUTCHER MissilesPassThrough,Light 5 0 128 None
|
||||
OBJ_BOOK2R book2 6 6 Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle EachFrame
|
||||
OBJ_L2LDOOR l2doors 0 0 DTYPE_CATACOMBS Light,Trap 1 0 64 Bottom,Middle None
|
||||
OBJ_L2RDOOR l2doors 0 0 DTYPE_CATACOMBS Light,Trap 2 0 64 Bottom,Middle None
|
||||
OBJ_TORCHL wtorch4 5 8 Animated,MissilesPassThrough 1 9 96 None
|
||||
OBJ_TORCHR wtorch3 5 8 Animated,MissilesPassThrough 1 9 96 None
|
||||
OBJ_TORCHL2 wtorch1 5 8 Animated,MissilesPassThrough 1 9 96 None
|
||||
OBJ_TORCHR2 wtorch2 5 8 Animated,MissilesPassThrough 1 9 96 None
|
||||
OBJ_SARC sarc 1 4 Solid,MissilesPassThrough,Light,Trap 1 5 128 Bottom,Middle FirstFrame
|
||||
OBJ_FLAMEHOLE flame1 0 0 MissilesPassThrough,Light 1 20 96 None
|
||||
OBJ_FLAMELVR lever 0 0 Solid,MissilesPassThrough,Light,Trap 1 2 96 Bottom FirstFrame
|
||||
OBJ_WATER miniwatr 0 0 Animated,Solid,Light 1 10 64 None
|
||||
OBJ_BOOKLVR book1 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle EachFrame
|
||||
OBJ_TRAPL traphole 1 24 MissilesPassThrough,Light 1 0 64 None
|
||||
OBJ_TRAPR traphole 1 24 MissilesPassThrough,Light 2 0 64 None
|
||||
OBJ_BOOKSHELF bcase 0 0 Solid,Light 1 0 96 None
|
||||
OBJ_WEAPRACK weapstnd 0 0 Solid,Light 1 0 96 EachFrame
|
||||
OBJ_BARREL barrel 1 15 Solid,MissilesPassThrough,Light,Breakable 1 9 96 Bottom,Middle FirstFrame
|
||||
OBJ_BARRELEX barrelex 1 15 Solid,MissilesPassThrough,Light,Breakable 1 10 96 Bottom,Middle FirstFrame
|
||||
OBJ_SHRINEL lshrineg 0 0 THEME_SHRINE Light 1 11 128 Bottom,Middle None
|
||||
OBJ_SHRINER rshrineg 0 0 THEME_SHRINE Light 1 11 128 Bottom,Middle None
|
||||
OBJ_SKELBOOK book2 0 0 THEME_SKELROOM Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle EachFrame
|
||||
OBJ_BOOKCASEL bcase 0 0 THEME_LIBRARY Solid,Light 3 0 96 Bottom,Middle None
|
||||
OBJ_BOOKCASER bcase 0 0 THEME_LIBRARY Solid,Light 4 0 96 Bottom,Middle None
|
||||
OBJ_BOOKSTAND book2 0 0 THEME_LIBRARY Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle EachFrame
|
||||
OBJ_BOOKCANDLE candle2 0 0 THEME_LIBRARY Animated,Solid,MissilesPassThrough,Light 2 4 96 FirstFrame
|
||||
OBJ_BLOODFTN bloodfnt 0 0 THEME_BLOODFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 10 96 Bottom,Middle None
|
||||
OBJ_DECAP decap 13 15 THEME_DECAPITATED Solid,MissilesPassThrough,Light 1 0 96 Bottom FirstFrame
|
||||
OBJ_TCHEST1 chest1 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom EachFrame
|
||||
OBJ_TCHEST2 chest2 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom EachFrame
|
||||
OBJ_TCHEST3 chest3 1 24 Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom EachFrame
|
||||
OBJ_BLINDBOOK book1 0 0 Q_BLIND Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle EachFrame
|
||||
OBJ_BLOODBOOK book1 0 0 Q_BLOOD Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle EachFrame
|
||||
OBJ_PEDESTAL pedistl 0 0 Q_BLOOD Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle FirstFrame
|
||||
OBJ_L3LDOOR l3doors 0 0 DTYPE_CAVES Light,Trap 1 0 64 Bottom,Middle None
|
||||
OBJ_L3RDOOR l3doors 0 0 DTYPE_CAVES Light,Trap 2 0 64 Bottom,Middle None
|
||||
OBJ_PURIFYINGFTN pfountn 0 0 THEME_PURIFYINGFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 10 128 Bottom,Middle None
|
||||
OBJ_ARMORSTAND armstand 0 0 THEME_ARMORSTAND Solid,Light 1 0 96 Bottom,Middle FirstFrame
|
||||
OBJ_ARMORSTANDN armstand 0 0 THEME_ARMORSTAND Solid,Light 2 0 96 FirstFrame
|
||||
OBJ_GOATSHRINE goatshrn 0 0 THEME_GOATSHRINE Animated,Solid,MissilesPassThrough,Light 2 10 96 Bottom,Middle None
|
||||
OBJ_CAULDRON cauldren 13 15 Solid,Light 1 0 96 Bottom,Middle Baked
|
||||
OBJ_MURKYFTN mfountn 0 0 THEME_MURKYFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 10 128 Bottom,Middle None
|
||||
OBJ_TEARFTN tfountn 0 0 THEME_TEARFOUNTAIN Animated,Solid,MissilesPassThrough,Light 2 4 128 Bottom,Middle None
|
||||
OBJ_ALTBOY altboy 0 0 Q_BETRAYER Solid,MissilesPassThrough,Light 1 0 128 FirstFrame
|
||||
OBJ_MCIRCLE1 mcirl 0 0 Q_BETRAYER MissilesPassThrough,Light 1 0 96 None
|
||||
OBJ_MCIRCLE2 mcirl 0 0 Q_BETRAYER MissilesPassThrough,Light 1 0 96 None
|
||||
OBJ_STORYBOOK bkslbrnt 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle FirstFrame
|
||||
OBJ_STORYCANDLE candle2 0 0 Q_BETRAYER Animated,Solid,MissilesPassThrough,Light 2 4 96 FirstFrame
|
||||
OBJ_STEELTOME book1 0 0 Q_WARLORD Solid,MissilesPassThrough,Light 4 0 96 Bottom,Middle EachFrame
|
||||
OBJ_WARARMOR armstand 0 0 Q_WARLORD Solid,Light 1 0 96 Bottom,Middle FirstFrame
|
||||
OBJ_WARWEAP weapstnd 0 0 Q_WARLORD Solid,Light 1 0 96 Bottom,Middle EachFrame
|
||||
OBJ_TBCROSS burncros 0 0 THEME_BRNCROSS Animated,Solid 0 10 160 None
|
||||
OBJ_WEAPONRACK weapstnd 0 0 THEME_WEAPONRACK Solid,Light 1 0 96 Bottom,Middle EachFrame
|
||||
OBJ_WEAPONRACKN weapstnd 0 0 THEME_WEAPONRACK Solid,Light 2 0 96 EachFrame
|
||||
OBJ_MUSHPATCH mushptch 0 0 Q_MUSHROOM Solid,MissilesPassThrough,Light,Trap 1 0 96 Bottom,Middle None
|
||||
OBJ_LAZSTAND lzstand 0 0 Q_BETRAYER Solid,Light 1 0 128 Bottom,Middle FirstFrame
|
||||
OBJ_SLAINHERO decap 9 9 Solid,MissilesPassThrough,Light 2 0 96 Bottom FirstFrame
|
||||
OBJ_SIGNCHEST chest3 0 0 Solid,MissilesPassThrough,Light 1 0 96 Bottom EachFrame
|
||||
OBJ_BOOKSHELFR bcase 0 0 Solid,Light 2 0 96 None
|
||||
OBJ_POD l6pod1 17 20 Solid,MissilesPassThrough,Light,Breakable 1 9 96 Bottom,Middle None
|
||||
OBJ_PODEX l6pod2 17 20 Solid,MissilesPassThrough,Light,Breakable 1 10 96 Bottom,Middle None
|
||||
OBJ_URN urn 21 24 Solid,MissilesPassThrough,Light,Breakable 1 9 96 Bottom,Middle EachFrame
|
||||
OBJ_URNEX urnexpld 21 24 Solid,MissilesPassThrough,Light,Breakable 1 10 96 Bottom,Middle EachFrame
|
||||
OBJ_L5BOOKS l5books 21 24 Solid,MissilesPassThrough,Light 1 0 96 Bottom,Middle None
|
||||
OBJ_L5CANDLE l5light 21 23 Animated,Solid,MissilesPassThrough,Light 2 4 96 FirstFrame
|
||||
OBJ_L5LDOOR l5door 0 0 DTYPE_CRYPT Light,Trap 1 0 64 Bottom,Middle None
|
||||
OBJ_L5RDOOR l5door 0 0 DTYPE_CRYPT Light,Trap 2 0 64 Bottom,Middle None
|
||||
OBJ_L5LEVER l5lever 24 24 Solid,MissilesPassThrough,Light,Trap 1 1 96 Bottom None
|
||||
OBJ_L5SARC l5sarco 21 24 Solid,MissilesPassThrough,Light,Trap 1 5 128 Bottom,Middle None
|
||||
|
||||
|
@@ -1,13 +1,13 @@
|
||||
type name position_x position_y direction animWidth animPath animFrames animDelay gossipTexts animOrder
|
||||
TOWN_SMITH Griswold the Blacksmith 62 63 SouthWest 96 towners\smith\smithn 16 3 TEXT_GRISWOLD2,TEXT_GRISWOLD3,TEXT_GRISWOLD4,TEXT_GRISWOLD5,TEXT_GRISWOLD6,TEXT_GRISWOLD7,TEXT_GRISWOLD8,TEXT_GRISWOLD9,TEXT_GRISWOLD10,TEXT_GRISWOLD12,TEXT_GRISWOLD13 4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,14,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3
|
||||
TOWN_HEALER Pepin the Healer 55 79 SouthEast 96 towners\healer\healer 20 6 TEXT_PEPIN2,TEXT_PEPIN3,TEXT_PEPIN4,TEXT_PEPIN5,TEXT_PEPIN6,TEXT_PEPIN7,TEXT_PEPIN9,TEXT_PEPIN10,TEXT_PEPIN11 0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
|
||||
TOWN_DEADGUY Wounded Townsman 24 32 North 96 towners\butch\deadguy 8 6
|
||||
TOWN_TAVERN Ogden the Tavern owner 55 62 SouthWest 96 towners\twnf\twnfn 16 3 TEXT_OGDEN2,TEXT_OGDEN3,TEXT_OGDEN4,TEXT_OGDEN5,TEXT_OGDEN6,TEXT_OGDEN8,TEXT_OGDEN9,TEXT_OGDEN10 0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,1,0,15,14,13,13,14,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
|
||||
TOWN_STORY Cain the Elder 62 71 South 96 towners\strytell\strytell 25 3 TEXT_STORY2,TEXT_STORY3,TEXT_STORY4,TEXT_STORY5,TEXT_STORY6,TEXT_STORY7,TEXT_STORY9,TEXT_STORY10,TEXT_STORY11 0,0,24,24,23,22,21,20,19,18,17,16,15,14,15,16,17,18,19,20,21,22,23,24,24,24,0,0,0,24,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0
|
||||
TOWN_DRUNK Farnham the Drunk 71 84 South 96 towners\drunk\twndrunk 18 3 TEXT_FARNHAM2,TEXT_FARNHAM3,TEXT_FARNHAM4,TEXT_FARNHAM5,TEXT_FARNHAM6,TEXT_FARNHAM8,TEXT_FARNHAM9,TEXT_FARNHAM10,TEXT_FARNHAM11,TEXT_FARNHAM12,TEXT_FARNHAM13 0,0,0,1,2,3,4,5,6,7,8,9,10,10,10,10,11,12,13,14,15,16,17,17,0,0,0,17,16,15,14,13,12,11,10,9,10,11,12,13,14,15,16,17,0,1,2,3,4,4,4,3,2,1
|
||||
TOWN_WITCH Adria the Witch 80 20 South 96 towners\townwmn1\witch 19 6 TEXT_ADRIA2,TEXT_ADRIA3,TEXT_ADRIA4,TEXT_ADRIA5,TEXT_ADRIA6,TEXT_ADRIA7,TEXT_ADRIA8,TEXT_ADRIA9,TEXT_ADRIA10,TEXT_ADRIA12,TEXT_ADRIA13 3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,2,1,0,18,17,18,0,1,0,18,17,18,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,14,14,13,12,12,12,12,13,14,14,14,13,12,11,11,11,10,9,9,9,8,7,8,9,9,10,11,12,13,14,15,16,17,18,0,1,0,18,17,18,0,1,0,1,2
|
||||
TOWN_BMAID Gillian the Barmaid 43 66 South 96 towners\townwmn1\wmnn 18 6 TEXT_GILLIAN2,TEXT_GILLIAN3,TEXT_GILLIAN4,TEXT_GILLIAN5,TEXT_GILLIAN6,TEXT_GILLIAN7,TEXT_GILLIAN9,TEXT_GILLIAN10
|
||||
TOWN_PEGBOY Wirt the Peg-legged boy 11 53 South 96 towners\townboy\pegkid1 20 6 TEXT_WIRT2,TEXT_WIRT3,TEXT_WIRT4,TEXT_WIRT5,TEXT_WIRT6,TEXT_WIRT7,TEXT_WIRT8,TEXT_WIRT9,TEXT_WIRT11,TEXT_WIRT12
|
||||
TOWN_COW Cow 58 16 SouthWest 128 12 3
|
||||
TOWN_COW Cow 56 14 NorthWest 128 12 3
|
||||
TOWN_COW Cow 59 20 North 128 12 3
|
||||
type name position_x position_y direction animWidth animPath animFrames animDelay gossipTexts animOrder shadowRule
|
||||
TOWN_SMITH Griswold the Blacksmith 62 63 SouthWest 96 towners\smith\smithn 16 3 TEXT_GRISWOLD2,TEXT_GRISWOLD3,TEXT_GRISWOLD4,TEXT_GRISWOLD5,TEXT_GRISWOLD6,TEXT_GRISWOLD7,TEXT_GRISWOLD8,TEXT_GRISWOLD9,TEXT_GRISWOLD10,TEXT_GRISWOLD12,TEXT_GRISWOLD13 4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,14,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3 FirstFrame
|
||||
TOWN_HEALER Pepin the Healer 55 79 SouthEast 96 towners\healer\healer 20 6 TEXT_PEPIN2,TEXT_PEPIN3,TEXT_PEPIN4,TEXT_PEPIN5,TEXT_PEPIN6,TEXT_PEPIN7,TEXT_PEPIN9,TEXT_PEPIN10,TEXT_PEPIN11 0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 FirstFrame
|
||||
TOWN_DEADGUY Wounded Townsman 24 32 North 96 towners\butch\deadguy 8 6 None
|
||||
TOWN_TAVERN Ogden the Tavern owner 55 62 SouthWest 96 towners\twnf\twnfn 16 3 TEXT_OGDEN2,TEXT_OGDEN3,TEXT_OGDEN4,TEXT_OGDEN5,TEXT_OGDEN6,TEXT_OGDEN8,TEXT_OGDEN9,TEXT_OGDEN10 0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,1,0,15,14,13,13,14,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 FirstFrame
|
||||
TOWN_STORY Cain the Elder 62 71 South 96 towners\strytell\strytell 25 3 TEXT_STORY2,TEXT_STORY3,TEXT_STORY4,TEXT_STORY5,TEXT_STORY6,TEXT_STORY7,TEXT_STORY9,TEXT_STORY10,TEXT_STORY11 0,0,24,24,23,22,21,20,19,18,17,16,15,14,15,16,17,18,19,20,21,22,23,24,24,24,0,0,0,24,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 FirstFrame
|
||||
TOWN_DRUNK Farnham the Drunk 71 84 South 96 towners\drunk\twndrunk 18 3 TEXT_FARNHAM2,TEXT_FARNHAM3,TEXT_FARNHAM4,TEXT_FARNHAM5,TEXT_FARNHAM6,TEXT_FARNHAM8,TEXT_FARNHAM9,TEXT_FARNHAM10,TEXT_FARNHAM11,TEXT_FARNHAM12,TEXT_FARNHAM13 0,0,0,1,2,3,4,5,6,7,8,9,10,10,10,10,11,12,13,14,15,16,17,17,0,0,0,17,16,15,14,13,12,11,10,9,10,11,12,13,14,15,16,17,0,1,2,3,4,4,4,3,2,1 FirstFrame
|
||||
TOWN_WITCH Adria the Witch 80 20 South 96 towners\townwmn1\witch 19 6 TEXT_ADRIA2,TEXT_ADRIA3,TEXT_ADRIA4,TEXT_ADRIA5,TEXT_ADRIA6,TEXT_ADRIA7,TEXT_ADRIA8,TEXT_ADRIA9,TEXT_ADRIA10,TEXT_ADRIA12,TEXT_ADRIA13 3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,2,1,0,18,17,18,0,1,0,18,17,18,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,14,14,13,12,12,12,12,13,14,14,14,13,12,11,11,11,10,9,9,9,8,7,8,9,9,10,11,12,13,14,15,16,17,18,0,1,0,18,17,18,0,1,0,1,2 FirstFrame
|
||||
TOWN_BMAID Gillian the Barmaid 43 66 South 96 towners\townwmn1\wmnn 18 6 TEXT_GILLIAN2,TEXT_GILLIAN3,TEXT_GILLIAN4,TEXT_GILLIAN5,TEXT_GILLIAN6,TEXT_GILLIAN7,TEXT_GILLIAN9,TEXT_GILLIAN10 FirstFrame
|
||||
TOWN_PEGBOY Wirt the Peg-legged boy 11 53 South 96 towners\townboy\pegkid1 20 6 TEXT_WIRT2,TEXT_WIRT3,TEXT_WIRT4,TEXT_WIRT5,TEXT_WIRT6,TEXT_WIRT7,TEXT_WIRT8,TEXT_WIRT9,TEXT_WIRT11,TEXT_WIRT12 FirstFrame|Stray
|
||||
TOWN_COW Cow 58 16 SouthWest 128 12 3
|
||||
TOWN_COW Cow 56 14 NorthWest 128 12 3
|
||||
TOWN_COW Cow 59 20 North 128 12 3
|
||||
|
||||
|
@@ -1,139 +1,139 @@
|
||||
_monster_id name assetsSuffix soundSuffix trnFile availability width image hasSpecial hasSpecialSound frames[6] rate[6] minDunLvl maxDunLvl level hitPointsMinimum hitPointsMaximum ai abilityFlags intelligence toHit animFrameNum minDamage maxDamage toHitSpecial animFrameNumSpecial minDamageSpecial maxDamageSpecial reducePlayerStrength reducePlayerMagic reducePlayerDexterity reducePlayerVitality reducePlayerMaxHP reducePlayerMaxMana armorClass monsterClass resistance resistanceHell selectionRegion treasure exp
|
||||
MT_NZOMBIE Zombie zombie\zombie Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 1 2 1 4 7 Zombie 0 10 8 2 5 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 54
|
||||
MT_BZOMBIE Ghoul zombie\zombie zombie\bluered Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 3 2 7 11 Zombie 1 10 8 3 10 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 58
|
||||
MT_GZOMBIE Rotting Carcass zombie\zombie zombie\grey Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 4 4 15 25 Zombie 2 25 8 5 15 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 136
|
||||
MT_YZOMBIE Black Death zombie\zombie zombie\yellow Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 3 5 6 25 40 Zombie 3 30 8 6 22 0 0 0 0 0 0 0 0 1 0 20 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 240
|
||||
MT_RFALLSP Fallen One falspear\phall falspear\fallent Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 1 2 1 1 4 Fallen 0 15 7 1 3 0 5 0 0 0 0 0 0 0 0 0 Animal Bottom,Middle 46
|
||||
MT_DFALLSP Carver falspear\phall falspear\dark Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 3 3 4 8 Fallen 2 20 7 2 5 0 5 0 0 0 0 0 0 0 0 5 Animal Bottom,Middle 80
|
||||
MT_YFALLSP Devil Kin falspear\phall Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 4 5 12 24 Fallen 2 25 7 3 7 0 5 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 155
|
||||
MT_BFALLSP Dark One falspear\phall falspear\blue Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 3 5 7 20 36 Fallen 3 30 7 4 8 0 5 0 0 0 0 0 0 0 0 15 Animal RESIST_LIGHTNING Bottom,Middle 255
|
||||
MT_WSKELAX Skeleton skelaxe\sklax skelaxe\white Always 128 553 true false 12,8,13,6,17,16 5,1,1,1,1,1 1 2 1 2 4 SkeletonMelee 0 20 8 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 64
|
||||
MT_TSKELAX Corpse Axe skelaxe\sklax skelaxe\skelt Always 128 553 true false 12,8,13,6,17,16 4,1,1,1,1,1 2 3 2 4 7 SkeletonMelee 1 25 8 3 5 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 68
|
||||
MT_RSKELAX Burning Dead skelaxe\sklax Always 128 553 true false 12,8,13,6,17,16 2,1,1,1,1,1 2 4 4 8 12 SkeletonMelee 2 30 8 3 7 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 154
|
||||
MT_XSKELAX Horror skelaxe\sklax skelaxe\black Always 128 553 true false 12,8,13,6,17,16 3,1,1,1,1,1 3 5 6 12 20 SkeletonMelee 3 35 8 4 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 264
|
||||
MT_RFALLSD Fallen One falsword\fall falsword\fallent Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 1 2 1 2 5 Fallen 0 15 8 1 4 0 5 0 0 0 0 0 0 0 0 10 Animal Bottom,Middle 52
|
||||
MT_DFALLSD Carver falsword\fall falsword\dark Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 3 3 5 9 Fallen 1 20 8 2 7 0 5 0 0 0 0 0 0 0 0 15 Animal Bottom,Middle 90
|
||||
MT_YFALLSD Devil Kin falsword\fall Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 4 5 16 24 Fallen 2 25 8 4 10 0 5 0 0 0 0 0 0 0 0 20 Animal RESIST_FIRE Bottom,Middle 180
|
||||
MT_BFALLSD Dark One falsword\fall falsword\blue Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 3 5 7 24 36 Fallen 3 30 8 4 12 0 5 0 0 0 0 0 0 0 0 25 Animal RESIST_LIGHTNING Bottom,Middle 280
|
||||
MT_NSCAV Scavenger scav\scav Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 1 3 2 3 6 Scavenger 0 20 7 1 5 0 0 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 80
|
||||
MT_BSCAV Plague Eater scav\scav scav\scavbr Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 2 4 4 12 24 Scavenger 1 30 7 1 8 0 0 0 0 0 0 0 0 0 0 20 Animal RESIST_LIGHTNING Bottom,Middle 188
|
||||
MT_WSCAV Shadow Beast scav\scav scav\scavbe Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 3 5 6 24 36 Scavenger 2 35 7 3 12 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_FIRE Bottom,Middle 375
|
||||
MT_YSCAV Bone Gasher scav\scav scav\scavw Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 4 6 8 28 40 Scavenger 3 35 7 5 15 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_LIGHTNING Bottom,Middle 552
|
||||
MT_WSKELBW Skeleton skelbow\sklbw skelbow\white Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 3 3 2 4 SkeletonRanged 0 15 12 1 2 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 110
|
||||
MT_TSKELBW Corpse Bow skelbow\sklbw skelbow\skelt Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 4 5 8 16 SkeletonRanged 1 25 12 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 210
|
||||
MT_RSKELBW Burning Dead skelbow\sklbw Always 128 567 true false 9,8,16,5,16,16 2,1,1,1,1,1 3 5 7 10 24 SkeletonRanged 2 30 12 1 6 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 364
|
||||
MT_XSKELBW Horror skelbow\sklbw skelbow\black Always 128 567 true false 9,8,16,5,16,16 3,1,1,1,1,1 4 6 9 15 45 SkeletonRanged 3 35 12 2 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 594
|
||||
MT_WSKELSD Skeleton Captain skelsd\sklsr skelsd\white Always 128 575 true true 13,8,12,7,15,16 4,1,1,1,1,1 1 3 2 3 6 SkeletonMelee 0 20 8 2 7 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 90
|
||||
MT_TSKELSD Corpse Captain skelsd\sklsr skelsd\skelt Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 2 4 4 12 20 SkeletonMelee 1 30 8 3 9 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 200
|
||||
MT_RSKELSD Burning Dead Captain skelsd\sklsr Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 3 5 6 16 30 SkeletonMelee 2 35 8 4 10 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 393
|
||||
MT_XSKELSD Horror Captain skelsd\sklsr skelsd\black Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 4 6 8 35 50 SkeletonMelee SEARCH 3 40 8 5 14 0 0 0 0 0 0 0 0 0 0 30 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 604
|
||||
MT_INVILORD Invisible Lord tsneak\tsneak Never 128 800 false false 13,13,15,11,16,0 2,1,1,1,1,1 19 20 14 278 278 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 65 8 16 30 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2000
|
||||
MT_SNEAK Hidden sneak\sneak Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 2 5 5 8 24 Sneak HIDDEN 0 35 8 3 6 0 0 0 0 0 0 0 0 0 0 25 Demon Bottom,Middle 278
|
||||
MT_STALKER Stalker sneak\sneak sneak\sneakv2 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 5 7 9 30 45 Sneak HIDDEN,SEARCH 1 40 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Demon Bottom,Middle 630
|
||||
MT_UNSEEN Unseen sneak\sneak sneak\sneakv3 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 6 8 11 35 50 Sneak HIDDEN,SEARCH 2 45 8 12 20 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 935
|
||||
MT_ILLWEAV Illusion Weaver sneak\sneak sneak\sneakv1 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 8 10 13 40 60 Sneak HIDDEN,SEARCH 3 60 8 16 24 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC,RESIST_FIRE IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 1500
|
||||
MT_LRDSAYTR Satyr Lord goatlord\goatl newsfx\satyr Never 160 800 false false 13,13,14,9,16,0 2,1,1,1,1,1 21 22 28 160 200 SkeletonMelee SEARCH 3 90 8 20 30 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 2800
|
||||
MT_NGOATMC Flesh Clan goatmace\goat Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 4 6 8 30 45 GoatMelee SEARCH,CAN_OPEN_DOOR 0 50 8 4 10 0 0 0 0 0 0 0 0 0 0 40 Demon Bottom,Middle 460
|
||||
MT_BGOATMC Stone Clan goatmace\goat goatmace\beige Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 5 7 10 40 55 GoatMelee SEARCH,CAN_OPEN_DOOR 1 60 8 6 12 0 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 685
|
||||
MT_RGOATMC Fire Clan goatmace\goat goatmace\red Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 6 8 12 50 65 GoatMelee SEARCH,CAN_OPEN_DOOR 2 70 8 8 16 0 0 0 0 0 0 0 0 0 0 45 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 906
|
||||
MT_GGOATMC Night Clan goatmace\goat goatmace\gray Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 7 9 14 55 70 GoatMelee SEARCH,CAN_OPEN_DOOR 3 80 8 10 20 15 0 30 30 0 0 0 0 0 0 50 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1190
|
||||
MT_FIEND Fiend bat\bat bat\red Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 2 3 3 3 6 Bat 0 35 5 1 6 0 0 0 0 0 0 0 0 0 0 0 Animal Middle,Top None 102
|
||||
MT_BLINK Blink bat\bat Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 3 5 7 12 28 Bat 1 45 5 1 8 0 0 0 0 0 0 0 0 0 0 15 Animal Middle,Top None 340
|
||||
MT_GLOOM Gloom bat\bat bat\grey Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 4 6 9 28 36 Bat SEARCH 2 70 5 4 12 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC RESIST_MAGIC Middle,Top None 509
|
||||
MT_FAMILIAR Familiar bat\bat bat\orange Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 6 8 13 20 35 Bat SEARCH 3 50 5 4 16 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Middle,Top None 448
|
||||
MT_NGOATBW Flesh Clan goatbow\goatb Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 4 6 8 20 35 GoatRanged CAN_OPEN_DOOR 0 35 13 1 7 0 0 0 0 0 0 0 0 0 0 35 Demon Bottom,Middle 448
|
||||
MT_BGOATBW Stone Clan goatbow\goatb goatbow\beige Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 5 7 10 30 40 GoatRanged CAN_OPEN_DOOR 1 40 13 2 9 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 645
|
||||
MT_RGOATBW Fire Clan goatbow\goatb goatbow\red Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 6 8 12 40 50 GoatRanged SEARCH,CAN_OPEN_DOOR 2 45 13 3 11 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 822
|
||||
MT_GGOATBW Night Clan goatbow\goatb goatbow\gray Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 7 9 14 50 65 GoatRanged SEARCH,CAN_OPEN_DOOR 3 50 13 4 13 15 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1092
|
||||
MT_NACID Acid Beast acid\acid Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 6 8 11 40 66 Acid 0 40 8 4 12 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 846
|
||||
MT_RACID Poison Spitter acid\acid acid\acidblk Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 8 10 15 60 85 Acid 1 45 8 4 16 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 1248
|
||||
MT_BACID Pit Beast acid\acid acid\acidb Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 10 12 21 80 110 Acid 2 55 8 8 18 35 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_ACID IMMUNE_MAGIC,RESIST_LIGHTNING,IMMUNE_ACID Bottom,Middle 2060
|
||||
MT_XACID Lava Maw acid\acid acid\acidr Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 12 14 25 100 150 Acid 3 65 8 10 20 40 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_ACID Bottom,Middle 2940
|
||||
MT_SKING Skeleton King sking\sking skelaxe\white Never 160 1010 true true 8,6,16,6,16,6 2,1,1,1,1,2 4 4 9 140 140 SkeletonKing SEARCH,CAN_OPEN_DOOR 3 60 8 6 16 0 0 0 0 0 0 0 0 0 0 70 Undead IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top Uniq(SKCROWN) 570
|
||||
MT_CLEAVER The Butcher fatc\fatc Never 128 980 false false 10,8,12,6,16,0 1,1,1,1,1,1 1 1 1 320 320 Butcher 3 50 8 6 12 0 0 0 0 0 0 0 0 0 0 50 Demon RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle Uniq(CLEAVER) 710
|
||||
MT_FAT Overlord fat\fat Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 5 7 10 60 80 Fat 0 55 8 6 12 0 0 0 0 0 0 0 0 0 0 55 Demon RESIST_FIRE Bottom,Middle 635
|
||||
MT_MUDMAN Mud Man fat\fat fat\blue Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 7 9 14 100 125 Fat SEARCH 1 60 8 8 16 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_LIGHTNING Bottom,Middle 1165
|
||||
MT_TOAD Toad Demon fat\fat fat\fatb Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 8 10 16 135 160 Fat SEARCH 2 70 8 8 16 40 0 8 20 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 1380
|
||||
MT_FLAYED Flayed One fat\fat fat\fatf Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 10 12 20 160 200 Fat SEARCH 3 85 8 10 20 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2058
|
||||
MT_WYRM Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 5 7 11 60 90 SkeletonMelee 0 40 8 4 10 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 660
|
||||
MT_CAVSLUG Cave Slug worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 6 8 13 75 110 SkeletonMelee 1 50 8 6 13 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 994
|
||||
MT_DVLWYRM Devil Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 7 9 15 100 140 SkeletonMelee 2 55 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1320
|
||||
MT_DEVOUR Devourer worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 8 10 17 125 200 SkeletonMelee 3 60 8 10 20 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1827
|
||||
MT_NMAGMA Magma Demon magma\magma Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 9 13 50 70 Magma SEARCH,CAN_OPEN_DOOR 0 45 4 2 10 50 13 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1076
|
||||
MT_YMAGMA Blood Stone magma\magma magma\yellow Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 10 14 55 75 Magma SEARCH,CAN_OPEN_DOOR 1 50 4 2 12 50 14 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1309
|
||||
MT_BMAGMA Hell Stone magma\magma magma\blue Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 16 60 80 Magma SEARCH,CAN_OPEN_DOOR 2 60 4 2 20 60 14 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1680
|
||||
MT_WMAGMA Lava Lord magma\magma magma\wierd Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 18 70 85 Magma SEARCH,CAN_OPEN_DOOR 3 75 4 4 24 60 14 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2124
|
||||
MT_HORNED Horned Demon rhino\rhino Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 7 9 13 40 80 Rhino SEARCH,CAN_OPEN_DOOR 0 60 7 2 16 100 0 5 32 0 0 0 0 0 0 40 Animal RESIST_FIRE Bottom,Middle,Top 1172
|
||||
MT_MUDRUN Mud Runner rhino\rhino rhino\orange Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 8 10 15 50 90 Rhino SEARCH,CAN_OPEN_DOOR 1 70 7 6 18 100 0 12 36 0 0 0 0 0 0 45 Animal RESIST_FIRE Bottom,Middle,Top 1404
|
||||
MT_FROSTC Frost Charger rhino\rhino rhino\blue Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 9 11 17 60 100 Rhino SEARCH,CAN_OPEN_DOOR 2 80 7 8 20 100 0 20 40 0 0 0 0 0 0 50 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 1720
|
||||
MT_OBLORD Obsidian Lord rhino\rhino rhino\rhinob Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 10 12 19 70 110 Rhino SEARCH,CAN_OPEN_DOOR 3 90 7 10 22 100 0 20 50 0 0 0 0 0 0 55 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 1809
|
||||
MT_BONEDMN oldboned demskel\demskl Never 128 1740 true true 10,8,20,6,24,16 3,1,1,1,1,1 24 24 12 70 70 Storm 0 60 8 6 14 12 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 1344
|
||||
MT_REDDTH Red Death thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 8 10 16 96 96 Storm 1 75 5 10 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2168
|
||||
MT_LTCHDMN Litch Demon thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 110 110 Storm 2 80 5 10 24 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2736
|
||||
MT_UDEDBLRG Undead Balrog thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 130 130 Storm 3 85 5 12 30 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3575
|
||||
MT_INCIN Incinerator fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 21 22 16 30 45 FireMan 0 75 8 8 16 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 1888
|
||||
MT_FLAMLRD Flame Lord fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 22 23 18 40 55 FireMan 1 75 8 10 20 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2250
|
||||
MT_DOOMFIRE Doom Fire fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 23 24 20 50 65 FireMan 2 80 8 12 24 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 2740
|
||||
MT_HELLBURN Hell Burner fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 24 24 22 60 80 FireMan 3 85 8 15 30 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 3355
|
||||
MT_STORM Red Storm thin\thin thin\thinv3 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 55 110 Storm SEARCH,CAN_OPEN_DOOR 0 80 5 8 18 75 8 4 16 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2160
|
||||
MT_RSTORM Storm Rider thin\thin Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 10 12 20 60 120 Storm SEARCH,CAN_OPEN_DOOR 1 80 5 8 18 80 8 4 16 0 0 0 0 0 0 30 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2391
|
||||
MT_STORML Storm Lord thin\thin thin\thinv2 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 75 135 Storm SEARCH,CAN_OPEN_DOOR 2 85 5 12 24 75 8 4 16 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2775
|
||||
MT_MAEL Maelstrom thin\thin thin\thinv1 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 12 14 24 90 150 Storm SEARCH,CAN_OPEN_DOOR 3 90 5 12 28 75 8 4 16 0 0 0 0 0 0 40 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3177
|
||||
MT_BIGFALL Devil Kin Brute bigfall\fallg newsfx\kbrute Never 128 800 true false 10,8,11,8,17,0 1,1,1,1,2,2 21 22 27 120 160 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 100 6 18 24 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2400
|
||||
MT_WINGED Winged-Demon gargoyle\gargo Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 5 7 9 45 60 Gargoyle CAN_OPEN_DOOR 0 50 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Middle,Top 662
|
||||
MT_GARGOYLE Gargoyle gargoyle\gargo gargoyle\gare Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 7 9 13 60 90 Gargoyle CAN_OPEN_DOOR 1 65 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Middle,Top 1205
|
||||
MT_BLOODCLW Blood Claw gargoyle\gargo gargoyle\gargbr Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 9 11 19 75 125 Gargoyle CAN_OPEN_DOOR 2 80 7 14 22 0 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Middle,Top 1873
|
||||
MT_DEATHW Death Wing gargoyle\gargo gargoyle\gargb Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 10 12 23 90 150 Gargoyle CAN_OPEN_DOOR 3 95 7 16 28 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Middle,Top 2278
|
||||
MT_MEGA Slayer mega\mega Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 10 12 20 120 140 Mega SEARCH,CAN_OPEN_DOOR 0 100 8 12 20 0 3 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2300
|
||||
MT_GUARD Guardian mega\mega mega\guard Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 11 13 22 140 160 Mega SEARCH,CAN_OPEN_DOOR 1 110 8 14 22 0 3 0 0 0 0 0 0 0 0 65 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2714
|
||||
MT_VTEXLRD Vortex Lord mega\mega mega\vtexl Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 12 14 24 160 180 Mega SEARCH,CAN_OPEN_DOOR 2 120 8 18 24 0 3 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3252
|
||||
MT_BALROG Balrog mega\mega mega\balr Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 13 15 26 180 200 Mega SEARCH,CAN_OPEN_DOOR 3 130 8 22 30 0 3 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3643
|
||||
MT_NSNAKE Cave Viper snake\snake Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 11 13 21 100 150 Snake SEARCH 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 2725
|
||||
MT_RSNAKE Fire Drake snake\snake snake\snakr Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 12 14 23 120 170 Snake SEARCH 1 105 8 12 24 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3139
|
||||
MT_BSNAKE Gold Viper snake\snake snake\snakg Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 13 14 25 140 180 Snake SEARCH 2 120 8 15 26 0 0 0 0 0 0 0 0 0 0 70 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 3540
|
||||
MT_GSNAKE Azure Drake snake\snake snake\snakb Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 15 16 27 160 200 Snake SEARCH 3 130 8 18 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 3791
|
||||
MT_NBLACK Black Knight black\black Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 12 14 24 150 150 SkeletonMelee SEARCH 0 110 8 15 20 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3360
|
||||
MT_RTBLACK Doom Guard black\black black\blkkntrt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 15 26 165 165 SkeletonMelee SEARCH 0 130 8 18 25 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3650
|
||||
MT_BTBLACK Steel Lord black\black black\blkkntbt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 14 16 28 180 180 SkeletonMelee SEARCH 1 120 8 20 30 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4252
|
||||
MT_RBLACK Blood Knight black\black black\blkkntbe Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 14 30 200 200 SkeletonMelee SEARCH 1 130 8 25 35 0 0 0 0 0 0 0 0 0 0 85 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5130
|
||||
MT_UNRAV The Shredded unrav\unrav newsfx\shred Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 17 18 23 70 90 SkeletonMelee 0 75 7 4 12 0 0 0 0 0 0 0 0 0 0 65 Undead RESIST_FIRE,RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 900
|
||||
MT_HOLOWONE Hollow One unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 18 19 27 135 240 SkeletonMelee 1 75 7 12 24 0 0 0 0 0 0 0 0 0 0 75 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4374
|
||||
MT_PAINMSTR Pain Master unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 19 20 29 110 200 SkeletonMelee 2 80 7 16 30 0 0 0 0 0 0 0 0 0 0 80 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 5147
|
||||
MT_REALWEAV Reality Weaver unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 20 20 30 135 240 SkeletonMelee 3 85 7 20 35 0 0 0 0 0 0 0 0 0 0 85 Undead RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 5925
|
||||
MT_SUCCUBUS Succubus succ\scbs Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 12 14 24 120 150 Succubus CAN_OPEN_DOOR 0 100 10 1 20 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 3696
|
||||
MT_SNOWWICH Snow Witch succ\scbs succ\succb Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 13 15 26 135 175 Succubus CAN_OPEN_DOOR 1 110 10 1 24 0 0 0 0 0 0 0 0 0 0 65 Demon RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 4084
|
||||
MT_HLSPWN Hell Spawn succ\scbs succ\succrw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 14 16 28 150 200 Succubus SEARCH,CAN_OPEN_DOOR 2 115 10 1 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4480
|
||||
MT_SOLBRNR Soul Burner succ\scbs succ\succbw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 15 16 30 140 225 Succubus SEARCH,CAN_OPEN_DOOR 3 120 10 1 35 0 0 0 0 0 0 0 0 0 0 85 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 4644
|
||||
MT_COUNSLR Counselor mage\mage Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 13 14 25 70 70 Counselor CAN_OPEN_DOOR 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4070
|
||||
MT_MAGISTR Magistrate mage\mage mage\cnselg Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 14 15 27 85 85 Counselor CAN_OPEN_DOOR 1 100 8 10 24 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4478
|
||||
MT_CABALIST Cabalist mage\mage mage\cnselgd Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 15 16 29 120 120 Counselor CAN_OPEN_DOOR 2 110 8 14 30 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4929
|
||||
MT_ADVOCATE Advocate mage\mage mage\cnselbk Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 16 16 30 145 145 Counselor CAN_OPEN_DOOR 3 120 8 15 25 0 0 0 0 0 0 0 0 0 0 0 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968
|
||||
MT_GOLEM Golem golem\golem golem\golm Never 96 386 true false 0,16,12,0,12,20 1,1,1,1,1,1 1 1 12 1 1 Golem CAN_OPEN_DOOR 0 0 7 1 1 0 0 0 0 0 0 0 0 0 0 1 Demon 0
|
||||
MT_DIABLO The Dark Lord diablo\diablo Never 160 2000 true true 16,6,16,6,16,16 1,1,1,1,1,1 26 26 30 1666 1666 Diablo KNOCKBACK,SEARCH,CAN_OPEN_DOOR 3 220 4 30 60 0 11 0 0 0 0 0 0 0 0 90 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 31666
|
||||
MT_DARKMAGE The Arch-Litch Malignus darkmage\dmage darkmage\dmag Never 128 1060 true false 6,1,21,6,23,18 1,1,1,1,1,1 21 21 30 160 160 Counselor CAN_OPEN_DOOR 3 120 8 20 40 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968
|
||||
MT_HELLBOAR Hellboar fork\fork newsfx\hboar Retail 188 800 false false 10,10,15,6,16,0 2,1,1,1,1,1 17 18 23 80 100 SkeletonMelee KNOCKBACK,SEARCH 2 70 7 16 24 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 750
|
||||
MT_STINGER Stinger scorp\scorp newsfx\stingr Retail 64 305 false false 10,10,12,6,15,0 2,1,1,1,1,1 17 18 22 30 40 SkeletonMelee 3 85 8 1 20 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_LIGHTNING Bottom 500
|
||||
MT_PSYCHORB Psychorb eye\eye newsfx\psyco Retail 156 800 false false 12,13,13,7,21,0 2,1,1,1,1,1 17 18 22 20 30 Psychorb 3 80 8 10 10 0 0 0 0 0 0 0 0 0 0 40 Animal RESIST_FIRE Middle,Top 450
|
||||
MT_ARACHNON Arachnon spider\spider newsfx\slord Retail 148 800 false false 12,10,15,6,20,0 2,1,1,1,1,1 17 18 22 60 80 SkeletonMelee SEARCH 3 50 8 5 15 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_LIGHTNING Bottom,Middle,Top 500
|
||||
MT_FELLTWIN Felltwin tsneak\tsneak newsfx\ftwin Retail 128 800 false false 13,13,15,11,16,0 2,1,1,1,1,1 17 18 22 50 70 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 70 8 10 18 0 0 0 0 0 0 0 0 0 0 50 Demon RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 600
|
||||
MT_HORKSPWN Hork Spawn spawn\spawn newsfx\hspawn Retail 164 520 false true 15,12,14,11,14,0 1,1,1,1,1,1 18 19 22 30 30 SkeletonMelee 3 60 8 10 25 0 0 0 0 0 0 0 0 0 0 25 Demon RESIST_MAGIC RESIST_MAGIC Bottom,Middle 250
|
||||
MT_VENMTAIL Venomtail wscorp\wscorp newsfx\stingr Retail 86 305 false false 10,10,12,6,15,0 2,1,1,1,1,1 19 20 24 40 50 SkeletonMelee 3 85 8 1 30 0 0 0 0 0 0 0 0 0 0 60 Animal RESIST_LIGHTNING IMMUNE_LIGHTNING Bottom 1000
|
||||
MT_NECRMORB Necromorb eye2\eye2 newsfx\psyco Retail 140 800 false false 12,13,13,7,21,0 2,1,1,1,1,1 19 20 24 30 40 Necromorb 3 80 8 20 20 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_FIRE IMMUNE_FIRE,RESIST_LIGHTNING Middle,Top 1100
|
||||
MT_SPIDLORD Spider Lord bspidr\bspidr newsfx\slord Retail 148 800 true true 12,10,15,6,20,10 2,1,1,1,1,1 19 20 24 80 100 Acid SEARCH 3 60 8 8 20 75 8 10 10 0 0 0 0 0 0 60 Animal RESIST_LIGHTNING RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 1250
|
||||
MT_LASHWORM Lashworm clasp\clasp newsfx\lworm Retail 176 800 false false 10,12,15,6,16,0 1,1,1,1,1,1 19 20 20 30 30 SkeletonMelee 3 90 8 12 20 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_FIRE Bottom,Middle 600
|
||||
MT_TORCHANT Torchant antworm\worm newsfx\tchant Retail 192 800 false false 14,12,12,6,20,0 2,1,1,1,1,1 19 20 22 60 80 Torchant 3 75 8 20 30 0 0 0 0 0 0 0 0 0 0 70 Animal IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 1250
|
||||
MT_HORKDMN Hork Demon horkd\horkd newsfx\hdemon Never 138 800 true true 15,8,16,6,16,9 2,1,1,1,1,2 19 19 27 120 160 SkeletonMelee 3 60 8 20 35 80 8 0 0 0 0 0 0 0 0 80 Demon RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2000
|
||||
MT_DEFILER Hell Bug hellbug\hellbg newsfx\defile Never 198 800 true true 8,8,14,6,14,12 1,1,1,1,1,1 20 20 30 240 240 SkeletonMelee SEARCH 3 110 8 20 30 90 8 50 60 0 0 0 0 0 0 80 Demon RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5000
|
||||
MT_GRAVEDIG Gravedigger gravdg\gravdg newsfx\gdiggr Retail 124 800 true true 24,24,12,6,16,16 2,1,1,1,1,1 21 21 26 120 240 Scavenger CAN_OPEN_DOOR 3 80 6 2 12 0 0 0 0 0 0 0 0 0 0 20 Undead IMMUNE_LIGHTNING RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle 2000
|
||||
MT_TOMBRAT Tomb Rat rat\rat newsfx\tmbrat Retail 104 550 false false 11,8,12,6,20,0 2,1,1,1,1,1 21 22 24 80 120 SkeletonMelee 3 120 8 12 25 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 1800
|
||||
MT_FIREBAT Firebat hellbat\helbat newsfx\helbat Retail 96 550 false false 18,16,14,6,18,11 2,1,1,1,1,1 21 22 24 60 80 FireBat 3 100 8 15 20 0 0 0 0 0 0 0 0 0 0 70 Animal IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 2400
|
||||
MT_SKLWING Skullwing demskel\demskl newsfx\swing skelaxe\skelt Retail 128 1740 true false 10,8,20,6,24,16 3,1,1,1,1,1 21 22 27 70 70 SkeletonMelee 0 75 7 15 20 75 9 15 20 0 0 0 0 0 0 80 Undead RESIST_FIRE,RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3000
|
||||
MT_LICH Lich lich\lich newsfx\lich Retail 96 800 false true 12,10,10,7,21,0 2,1,1,1,2,1 21 22 25 80 100 Lich 3 100 8 15 20 0 0 0 0 0 0 0 0 0 0 60 Undead RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle 3000
|
||||
MT_CRYPTDMN Crypt Demon bubba\bubba newsfx\crypt Retail 154 800 false true 8,18,12,8,21,0 3,1,1,1,1,1 22 23 28 200 240 SkeletonMelee 3 100 8 20 40 0 0 0 0 0 0 0 0 0 0 85 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 3200
|
||||
MT_HELLBAT Hellbat hellbat2\bhelbt newsfx\helbat Retail 96 550 true false 18,16,14,6,18,11 2,1,1,1,1,1 23 24 29 100 140 Torchant 3 110 8 30 30 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 3600
|
||||
MT_BONEDEMN Bone Demon demskel\demskl newsfx\swing Retail 128 1740 true true 10,8,20,6,24,16 3,1,1,1,1,1 23 24 30 240 280 BoneDemon 0 100 8 40 50 160 12 50 50 0 0 0 0 0 0 50 Undead IMMUNE_FIRE,IMMUNE_LIGHTNING IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5000
|
||||
MT_ARCHLICH Arch Lich lich2\lich2 newsfx\lich Retail 136 800 false true 12,10,10,7,21,0 2,1,1,1,2,1 23 24 30 180 200 ArchLich 3 120 8 30 30 0 0 0 0 0 0 0 0 0 0 75 Undead RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 4000
|
||||
MT_BICLOPS Biclops byclps\byclps newsfx\biclop Retail 180 800 false false 10,11,16,6,16,0 2,1,1,1,2,1 23 24 30 200 240 SkeletonMelee KNOCKBACK,CAN_OPEN_DOOR 3 90 8 40 50 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 4000
|
||||
MT_FLESTHNG Flesh Thing flesh\flesh newsfx\flesht Retail 164 800 false true 15,24,15,6,16,0 1,1,1,1,1,1 23 24 28 300 400 SkeletonMelee 3 150 8 12 18 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 4000
|
||||
MT_REAPER Reaper reaper\reap newsfx\reaper Retail 180 800 false false 12,10,14,6,16,0 2,1,1,1,1,1 23 24 30 260 300 SkeletonMelee 3 120 8 30 35 0 0 0 0 0 0 0 0 0 0 90 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 6000
|
||||
MT_NAKRUL Na-Krul nkr\nkr newsfx\nakrul Never 226 1200 true true 2,6,16,3,16,16 1,1,1,1,1,1 31 31 40 1332 1332 SkeletonMelee KNOCKBACK,SEARCH,CAN_OPEN_DOOR 3 150 7 40 50 150 10 40 50 0 0 0 0 0 0 125 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 13333
|
||||
_monster_id name assetsSuffix soundSuffix trnFile availability width image hasSpecial hasSpecialSound frames[6] rate[6] minDunLvl maxDunLvl level hitPointsMinimum hitPointsMaximum ai abilityFlags intelligence toHit animFrameNum minDamage maxDamage toHitSpecial animFrameNumSpecial minDamageSpecial maxDamageSpecial reducePlayerStrength reducePlayerMagic reducePlayerDexterity reducePlayerVitality reducePlayerMaxHP reducePlayerMaxMana armorClass monsterClass resistance resistanceHell selectionRegion treasure exp shadowRule[6]
|
||||
MT_NZOMBIE Zombie zombie\zombie Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 1 2 1 4 7 Zombie 0 10 8 2 5 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 54 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_BZOMBIE Ghoul zombie\zombie zombie\bluered Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 3 2 7 11 Zombie 1 10 8 3 10 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 58 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_GZOMBIE Rotting Carcass zombie\zombie zombie\grey Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 2 4 4 15 25 Zombie 2 25 8 5 15 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 136 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_YZOMBIE Black Death zombie\zombie zombie\yellow Always 128 799 false false 11,24,12,6,16,0 4,1,1,1,1,1 3 5 6 25 40 Zombie 3 30 8 6 22 0 0 0 0 0 0 0 0 1 0 20 Undead IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 240 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RFALLSP Fallen One falspear\phall falspear\fallent Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 1 2 1 1 4 Fallen 0 15 7 1 3 0 5 0 0 0 0 0 0 0 0 0 Animal Bottom,Middle 46 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_DFALLSP Carver falspear\phall falspear\dark Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 3 3 4 8 Fallen 2 20 7 2 5 0 5 0 0 0 0 0 0 0 0 5 Animal Bottom,Middle 80 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YFALLSP Devil Kin falspear\phall Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 2 4 5 12 24 Fallen 2 25 7 3 7 0 5 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 155 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BFALLSP Dark One falspear\phall falspear\blue Always 128 543 true true 11,11,13,11,18,13 3,1,1,1,1,1 3 5 7 20 36 Fallen 3 30 7 4 8 0 5 0 0 0 0 0 0 0 0 15 Animal RESIST_LIGHTNING Bottom,Middle 255 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSKELAX Skeleton skelaxe\sklax skelaxe\white Always 128 553 true false 12,8,13,6,17,16 5,1,1,1,1,1 1 2 1 2 4 SkeletonMelee 0 20 8 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 64 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TSKELAX Corpse Axe skelaxe\sklax skelaxe\skelt Always 128 553 true false 12,8,13,6,17,16 4,1,1,1,1,1 2 3 2 4 7 SkeletonMelee 1 25 8 3 5 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 68 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSKELAX Burning Dead skelaxe\sklax Always 128 553 true false 12,8,13,6,17,16 2,1,1,1,1,1 2 4 4 8 12 SkeletonMelee 2 30 8 3 7 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 154 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XSKELAX Horror skelaxe\sklax skelaxe\black Always 128 553 true false 12,8,13,6,17,16 3,1,1,1,1,1 3 5 6 12 20 SkeletonMelee 3 35 8 4 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 264 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RFALLSD Fallen One falsword\fall falsword\fallent Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 1 2 1 2 5 Fallen 0 15 8 1 4 0 5 0 0 0 0 0 0 0 0 10 Animal Bottom,Middle 52 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_DFALLSD Carver falsword\fall falsword\dark Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 3 3 5 9 Fallen 1 20 8 2 7 0 5 0 0 0 0 0 0 0 0 15 Animal Bottom,Middle 90 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YFALLSD Devil Kin falsword\fall Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 2 4 5 16 24 Fallen 2 25 8 4 10 0 5 0 0 0 0 0 0 0 0 20 Animal RESIST_FIRE Bottom,Middle 180 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BFALLSD Dark One falsword\fall falsword\blue Always 128 623 true true 12,12,13,11,14,15 3,1,1,1,1,1 3 5 7 24 36 Fallen 3 30 8 4 12 0 5 0 0 0 0 0 0 0 0 25 Animal RESIST_LIGHTNING Bottom,Middle 280 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_NSCAV Scavenger scav\scav Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 1 3 2 3 6 Scavenger 0 20 7 1 5 0 0 0 0 0 0 0 0 0 0 10 Animal RESIST_FIRE Bottom,Middle 80 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BSCAV Plague Eater scav\scav scav\scavbr Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 2 4 4 12 24 Scavenger 1 30 7 1 8 0 0 0 0 0 0 0 0 0 0 20 Animal RESIST_LIGHTNING Bottom,Middle 188 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSCAV Shadow Beast scav\scav scav\scavbe Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 3 5 6 24 36 Scavenger 2 35 7 3 12 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_FIRE Bottom,Middle 375 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YSCAV Bone Gasher scav\scav scav\scavw Always 128 410 true false 12,8,12,6,20,11 2,1,1,1,1,1 4 6 8 28 40 Scavenger 3 35 7 5 15 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_LIGHTNING Bottom,Middle 552 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSKELBW Skeleton skelbow\sklbw skelbow\white Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 3 3 2 4 SkeletonRanged 0 15 12 1 2 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 110 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TSKELBW Corpse Bow skelbow\sklbw skelbow\skelt Always 128 567 true false 9,8,16,5,16,16 4,1,1,1,1,1 2 4 5 8 16 SkeletonRanged 1 25 12 1 4 0 0 0 0 0 0 0 0 0 0 0 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 210 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSKELBW Burning Dead skelbow\sklbw Always 128 567 true false 9,8,16,5,16,16 2,1,1,1,1,1 3 5 7 10 24 SkeletonRanged 2 30 12 1 6 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 364 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XSKELBW Horror skelbow\sklbw skelbow\black Always 128 567 true false 9,8,16,5,16,16 3,1,1,1,1,1 4 6 9 15 45 SkeletonRanged 3 35 12 2 9 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 594 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WSKELSD Skeleton Captain skelsd\sklsr skelsd\white Always 128 575 true true 13,8,12,7,15,16 4,1,1,1,1,1 1 3 2 3 6 SkeletonMelee 0 20 8 2 7 0 0 0 0 0 0 0 0 0 0 10 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 90 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TSKELSD Corpse Captain skelsd\sklsr skelsd\skelt Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 2 4 4 12 20 SkeletonMelee 1 30 8 3 9 0 0 0 0 0 0 0 0 0 0 5 Undead IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle 200 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSKELSD Burning Dead Captain skelsd\sklsr Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 3 5 6 16 30 SkeletonMelee 2 35 8 4 10 0 0 0 0 0 0 0 0 0 0 15 Undead IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 393 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XSKELSD Horror Captain skelsd\sklsr skelsd\black Always 128 575 true false 13,8,12,7,15,16 4,1,1,1,1,1 4 6 8 35 50 SkeletonMelee SEARCH 3 40 8 5 14 0 0 0 0 0 0 0 0 0 0 30 Undead IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 604 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_INVILORD Invisible Lord tsneak\tsneak Never 128 800 false false 13,13,15,11,16,0 2,1,1,1,1,1 19 20 14 278 278 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 65 8 16 30 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2000 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_SNEAK Hidden sneak\sneak Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 2 5 5 8 24 Sneak HIDDEN 0 35 8 3 6 0 0 0 0 0 0 0 0 0 0 25 Demon Bottom,Middle 278 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_STALKER Stalker sneak\sneak sneak\sneakv2 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 5 7 9 30 45 Sneak HIDDEN,SEARCH 1 40 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Demon Bottom,Middle 630 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_UNSEEN Unseen sneak\sneak sneak\sneakv3 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 6 8 11 35 50 Sneak HIDDEN,SEARCH 2 45 8 12 20 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 935 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_ILLWEAV Illusion Weaver sneak\sneak sneak\sneakv1 Retail 128 992 true false 16,8,12,8,24,15 2,1,1,1,1,1 8 10 13 40 60 Sneak HIDDEN,SEARCH 3 60 8 16 24 0 0 0 0 0 0 0 0 0 0 30 Demon RESIST_MAGIC,RESIST_FIRE IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 1500 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_LRDSAYTR Satyr Lord goatlord\goatl newsfx\satyr Never 160 800 false false 13,13,14,9,16,0 2,1,1,1,1,1 21 22 28 160 200 SkeletonMelee SEARCH 3 90 8 20 30 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 2800 FirstFrame,FirstFrame,FirstFrame,FirstFrame,None,None
|
||||
MT_NGOATMC Flesh Clan goatmace\goat Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 4 6 8 30 45 GoatMelee SEARCH,CAN_OPEN_DOOR 0 50 8 4 10 0 0 0 0 0 0 0 0 0 0 40 Demon Bottom,Middle 460 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BGOATMC Stone Clan goatmace\goat goatmace\beige Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 5 7 10 40 55 GoatMelee SEARCH,CAN_OPEN_DOOR 1 60 8 6 12 0 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 685 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RGOATMC Fire Clan goatmace\goat goatmace\red Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 6 8 12 50 65 GoatMelee SEARCH,CAN_OPEN_DOOR 2 70 8 8 16 0 0 0 0 0 0 0 0 0 0 45 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 906 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_GGOATMC Night Clan goatmace\goat goatmace\gray Retail 128 1030 true false 12,8,12,6,20,12 2,1,1,1,1,1 7 9 14 55 70 GoatMelee SEARCH,CAN_OPEN_DOOR 3 80 8 10 20 15 0 30 30 0 0 0 0 0 0 50 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1190 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_FIEND Fiend bat\bat bat\red Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 2 3 3 3 6 Bat 0 35 5 1 6 0 0 0 0 0 0 0 0 0 0 0 Animal Middle,Top None 102 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_BLINK Blink bat\bat Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 3 5 7 12 28 Bat 1 45 5 1 8 0 0 0 0 0 0 0 0 0 0 15 Animal Middle,Top None 340 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_GLOOM Gloom bat\bat bat\grey Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 4 6 9 28 36 Bat SEARCH 2 70 5 4 12 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC RESIST_MAGIC Middle,Top None 509 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_FAMILIAR Familiar bat\bat bat\orange Always 96 364 false false 9,13,10,9,13,0 1,1,1,1,1,1 6 8 13 20 35 Bat SEARCH 3 50 5 4 16 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Middle,Top None 448 FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame+15,FirstFrame,None
|
||||
MT_NGOATBW Flesh Clan goatbow\goatb Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 4 6 8 20 35 GoatRanged CAN_OPEN_DOOR 0 35 13 1 7 0 0 0 0 0 0 0 0 0 0 35 Demon Bottom,Middle 448 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_BGOATBW Stone Clan goatbow\goatb goatbow\beige Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 5 7 10 30 40 GoatRanged CAN_OPEN_DOOR 1 40 13 2 9 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 645 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RGOATBW Fire Clan goatbow\goatb goatbow\red Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 6 8 12 40 50 GoatRanged SEARCH,CAN_OPEN_DOOR 2 45 13 3 11 0 0 0 0 0 0 0 0 0 0 35 Demon RESIST_FIRE IMMUNE_FIRE Bottom,Middle 822 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_GGOATBW Night Clan goatbow\goatb goatbow\gray Retail 128 1040 false false 12,8,16,6,20,0 3,1,1,1,1,1 7 9 14 50 65 GoatRanged SEARCH,CAN_OPEN_DOOR 3 50 13 4 13 15 0 0 0 0 0 0 0 0 0 40 Demon RESIST_MAGIC IMMUNE_MAGIC Bottom,Middle 1092 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_NACID Acid Beast acid\acid Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 6 8 11 40 66 Acid 0 40 8 4 12 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 846 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RACID Poison Spitter acid\acid acid\acidblk Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 8 10 15 60 85 Acid 1 45 8 4 16 25 8 0 0 0 0 0 0 0 0 30 Animal IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_ACID Bottom,Middle 1248 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BACID Pit Beast acid\acid acid\acidb Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 10 12 21 80 110 Acid 2 55 8 8 18 35 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_ACID IMMUNE_MAGIC,RESIST_LIGHTNING,IMMUNE_ACID Bottom,Middle 2060 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_XACID Lava Maw acid\acid acid\acidr Retail 128 716 true true 13,8,12,8,16,12 1,1,1,1,1,1 12 14 25 100 150 Acid 3 65 8 10 20 40 8 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_ACID IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_ACID Bottom,Middle 2940 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_SKING Skeleton King sking\sking skelaxe\white Never 160 1010 true true 8,6,16,6,16,6 2,1,1,1,1,2 4 4 9 140 140 SkeletonKing SEARCH,CAN_OPEN_DOOR 3 60 8 6 16 0 0 0 0 0 0 0 0 0 0 70 Undead IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top Uniq(SKCROWN) 570 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_CLEAVER The Butcher fatc\fatc Never 128 980 false false 10,8,12,6,16,0 1,1,1,1,1,1 1 1 1 320 320 Butcher 3 50 8 6 12 0 0 0 0 0 0 0 0 0 0 50 Demon RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle Uniq(CLEAVER) 710 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_FAT Overlord fat\fat Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 5 7 10 60 80 Fat 0 55 8 6 12 0 0 0 0 0 0 0 0 0 0 55 Demon RESIST_FIRE Bottom,Middle 635 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MUDMAN Mud Man fat\fat fat\blue Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 7 9 14 100 125 Fat SEARCH 1 60 8 8 16 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_LIGHTNING Bottom,Middle 1165 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_TOAD Toad Demon fat\fat fat\fatb Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 8 10 16 135 160 Fat SEARCH 2 70 8 8 16 40 0 8 20 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 1380 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_FLAYED Flayed One fat\fat fat\fatf Retail 128 1130 true false 8,10,15,6,16,10 4,1,1,1,1,1 10 12 20 160 200 Fat SEARCH 3 85 8 10 20 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2058 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WYRM Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 5 7 11 60 90 SkeletonMelee 0 40 8 4 10 0 0 0 0 0 0 0 0 0 0 25 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 660
|
||||
MT_CAVSLUG Cave Slug worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 6 8 13 75 110 SkeletonMelee 1 50 8 6 13 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC RESIST_MAGIC Bottom,Middle 994
|
||||
MT_DVLWYRM Devil Wyrm worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 7 9 15 100 140 SkeletonMelee 2 55 8 8 16 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1320
|
||||
MT_DEVOUR Devourer worm\worm Never 160 2420 false false 13,13,13,11,19,0 1,1,1,1,1,1 8 10 17 125 200 SkeletonMelee 3 60 8 10 20 0 0 0 0 0 0 0 0 0 0 35 Animal RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,RESIST_FIRE Bottom,Middle 1827
|
||||
MT_NMAGMA Magma Demon magma\magma Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 9 13 50 70 Magma SEARCH,CAN_OPEN_DOOR 0 45 4 2 10 50 13 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1076 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_YMAGMA Blood Stone magma\magma magma\yellow Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 8 10 14 55 75 Magma SEARCH,CAN_OPEN_DOOR 1 50 4 2 12 50 14 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1309 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BMAGMA Hell Stone magma\magma magma\blue Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 16 60 80 Magma SEARCH,CAN_OPEN_DOOR 2 60 4 2 20 60 14 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 1680 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_WMAGMA Lava Lord magma\magma magma\wierd Retail 128 1680 true true 8,10,14,7,18,18 2,1,1,1,1,1 9 11 18 70 85 Magma SEARCH,CAN_OPEN_DOOR 3 75 4 4 24 60 14 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2124 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_HORNED Horned Demon rhino\rhino Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 7 9 13 40 80 Rhino SEARCH,CAN_OPEN_DOOR 0 60 7 2 16 100 0 5 32 0 0 0 0 0 0 40 Animal RESIST_FIRE Bottom,Middle,Top 1172 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MUDRUN Mud Runner rhino\rhino rhino\orange Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 8 10 15 50 90 Rhino SEARCH,CAN_OPEN_DOOR 1 70 7 6 18 100 0 12 36 0 0 0 0 0 0 45 Animal RESIST_FIRE Bottom,Middle,Top 1404 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_FROSTC Frost Charger rhino\rhino rhino\blue Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 9 11 17 60 100 Rhino SEARCH,CAN_OPEN_DOOR 2 80 7 8 20 100 0 20 40 0 0 0 0 0 0 50 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 1720 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_OBLORD Obsidian Lord rhino\rhino rhino\rhinob Retail 160 1630 true true 8,8,14,6,16,6 2,1,1,1,1,1 10 12 19 70 110 Rhino SEARCH,CAN_OPEN_DOOR 3 90 7 10 22 100 0 20 50 0 0 0 0 0 0 55 Animal IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 1809 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BONEDMN oldboned demskel\demskl Never 128 1740 true true 10,8,20,6,24,16 3,1,1,1,1,1 24 24 12 70 70 Storm 0 60 8 6 14 12 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 1344 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_REDDTH Red Death thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 8 10 16 96 96 Storm 1 75 5 10 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2168 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_LTCHDMN Litch Demon thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 110 110 Storm 2 80 5 10 24 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2736 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_UDEDBLRG Undead Balrog thin\thin thin\thinv3 Never 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 130 130 Storm 3 85 5 12 30 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3575 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_INCIN Incinerator fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 21 22 16 30 45 FireMan 0 75 8 8 16 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 1888 None,None,None,None,None,None
|
||||
MT_FLAMLRD Flame Lord fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 22 23 18 40 55 FireMan 1 75 8 10 20 0 0 0 0 0 0 0 0 0 0 25 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle 2250 None,None,None,None,None,None
|
||||
MT_DOOMFIRE Doom Fire fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 23 24 20 50 65 FireMan 2 80 8 12 24 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 2740 None,None,None,None,None,None
|
||||
MT_HELLBURN Hell Burner fireman\firem Never 128 1460 true false 14,19,20,8,14,23 1,1,1,1,1,1 24 24 22 60 80 FireMan 3 85 8 15 30 0 0 0 0 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 3355 None,None,None,None,None,None
|
||||
MT_STORM Red Storm thin\thin thin\thinv3 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 9 11 18 55 110 Storm SEARCH,CAN_OPEN_DOOR 0 80 5 8 18 75 8 4 16 0 0 0 0 0 0 30 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2160 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_RSTORM Storm Rider thin\thin Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 10 12 20 60 120 Storm SEARCH,CAN_OPEN_DOOR 1 80 5 8 18 80 8 4 16 0 0 0 0 0 0 30 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2391 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_STORML Storm Lord thin\thin thin\thinv2 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 11 13 22 75 135 Storm SEARCH,CAN_OPEN_DOOR 2 85 5 12 24 75 8 4 16 0 0 0 0 0 0 35 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2775 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MAEL Maelstrom thin\thin thin\thinv1 Retail 160 1740 true true 8,8,18,4,17,14 3,1,1,1,1,1 12 14 24 90 150 Storm SEARCH,CAN_OPEN_DOOR 3 90 5 12 28 75 8 4 16 0 0 0 0 0 0 40 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3177 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_BIGFALL Devil Kin Brute bigfall\fallg newsfx\kbrute Never 128 800 true false 10,8,11,8,17,0 1,1,1,1,2,2 21 22 27 120 160 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 100 6 18 24 0 0 0 0 0 0 0 0 0 0 70 Animal RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 2400 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_WINGED Winged-Demon gargoyle\gargo Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 5 7 9 45 60 Gargoyle CAN_OPEN_DOOR 0 50 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Middle,Top 662 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_GARGOYLE Gargoyle gargoyle\gargo gargoyle\gare Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,2 7 9 13 60 90 Gargoyle CAN_OPEN_DOOR 1 65 7 10 16 0 0 0 0 0 0 0 0 0 0 45 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_LIGHTNING Middle,Top 1205 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_BLOODCLW Blood Claw gargoyle\gargo gargoyle\gargbr Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 9 11 19 75 125 Gargoyle CAN_OPEN_DOOR 2 80 7 14 22 0 0 0 0 0 0 0 0 0 0 50 Demon IMMUNE_MAGIC,IMMUNE_FIRE IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Middle,Top 1873 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_DEATHW Death Wing gargoyle\gargo gargoyle\gargb Retail 160 1650 true false 14,14,14,10,18,14 1,1,1,1,1,1 10 12 23 90 150 Gargoyle CAN_OPEN_DOOR 3 95 7 16 28 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Middle,Top 2278 FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,FirstFrame+11,EachFrame
|
||||
MT_MEGA Slayer mega\mega Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 10 12 20 120 140 Mega SEARCH,CAN_OPEN_DOOR 0 100 8 12 20 0 3 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2300 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_GUARD Guardian mega\mega mega\guard Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 11 13 22 140 160 Mega SEARCH,CAN_OPEN_DOOR 1 110 8 14 22 0 3 0 0 0 0 0 0 0 0 65 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 2714 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_VTEXLRD Vortex Lord mega\mega mega\vtexl Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 12 14 24 160 180 Mega SEARCH,CAN_OPEN_DOOR 2 120 8 18 24 0 3 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3252 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_BALROG Balrog mega\mega mega\balr Retail 160 2220 true true 6,7,14,1,24,5 3,1,1,1,2,1 13 15 26 180 200 Mega SEARCH,CAN_OPEN_DOOR 3 130 8 22 30 0 3 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3643 EachFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame,EachFrame
|
||||
MT_NSNAKE Cave Viper snake\snake Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 11 13 21 100 150 Snake SEARCH 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 60 Demon IMMUNE_MAGIC IMMUNE_MAGIC Bottom,Middle,Top 2725 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_RSNAKE Fire Drake snake\snake snake\snakr Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 12 14 23 120 170 Snake SEARCH 1 105 8 12 24 0 0 0 0 0 0 0 0 0 0 65 Demon IMMUNE_MAGIC,RESIST_FIRE IMMUNE_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3139 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_BSNAKE Gold Viper snake\snake snake\snakg Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 13 14 25 140 180 Snake SEARCH 2 120 8 15 26 0 0 0 0 0 0 0 0 0 0 70 Demon IMMUNE_MAGIC,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle,Top 3540 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_GSNAKE Azure Drake snake\snake snake\snakb Retail 160 1270 false false 12,11,13,5,18,0 2,1,1,1,1,1 15 16 27 160 200 Snake SEARCH 3 130 8 18 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 3791 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,None
|
||||
MT_NBLACK Black Knight black\black Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 12 14 24 150 150 SkeletonMelee SEARCH 0 110 8 15 20 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 3360 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RTBLACK Doom Guard black\black black\blkkntrt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 15 26 165 165 SkeletonMelee SEARCH 0 130 8 18 25 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,RESIST_FIRE RESIST_MAGIC,IMMUNE_FIRE Bottom,Middle,Top 3650 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_BTBLACK Steel Lord black\black black\blkkntbt Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 14 16 28 180 180 SkeletonMelee SEARCH 1 120 8 20 30 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4252 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_RBLACK Blood Knight black\black black\blkkntbe Retail 160 2120 false false 8,8,16,4,24,0 2,1,1,1,1,1 13 14 30 200 200 SkeletonMelee SEARCH 1 130 8 25 35 0 0 0 0 0 0 0 0 0 0 85 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5130 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_UNRAV The Shredded unrav\unrav newsfx\shred Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 17 18 23 70 90 SkeletonMelee 0 75 7 4 12 0 0 0 0 0 0 0 0 0 0 65 Undead RESIST_FIRE,RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 900 None,EachFrame,EachFrame,None,None,None
|
||||
MT_HOLOWONE Hollow One unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 18 19 27 135 240 SkeletonMelee 1 75 7 12 24 0 0 0 0 0 0 0 0 0 0 75 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4374 None,EachFrame,EachFrame,None,None,None
|
||||
MT_PAINMSTR Pain Master unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 19 20 29 110 200 SkeletonMelee 2 80 7 16 30 0 0 0 0 0 0 0 0 0 0 80 Undead IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 5147 None,EachFrame,EachFrame,None,None,None
|
||||
MT_REALWEAV Reality Weaver unrav\unrav acid\acid Never 96 484 false false 10,10,12,5,16,0 1,1,1,1,1,1 20 20 30 135 240 SkeletonMelee 3 85 7 20 35 0 0 0 0 0 0 0 0 0 0 85 Undead RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 5925 None,EachFrame,EachFrame,None,None,None
|
||||
MT_SUCCUBUS Succubus succ\scbs Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 12 14 24 120 150 Succubus CAN_OPEN_DOOR 0 100 10 1 20 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_MAGIC IMMUNE_MAGIC,RESIST_FIRE Bottom,Middle 3696 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_SNOWWICH Snow Witch succ\scbs succ\succb Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 13 15 26 135 175 Succubus CAN_OPEN_DOOR 1 110 10 1 24 0 0 0 0 0 0 0 0 0 0 65 Demon RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_LIGHTNING Bottom,Middle 4084 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_HLSPWN Hell Spawn succ\scbs succ\succrw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 14 16 28 150 200 Succubus SEARCH,CAN_OPEN_DOOR 2 115 10 1 30 0 0 0 0 0 0 0 0 0 0 75 Demon RESIST_MAGIC,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 4480 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_SOLBRNR Soul Burner succ\scbs succ\succbw Retail 128 980 false false 14,8,16,7,24,0 1,1,1,1,1,1 15 16 30 140 225 Succubus SEARCH,CAN_OPEN_DOOR 3 120 10 1 35 0 0 0 0 0 0 0 0 0 0 85 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 4644 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_COUNSLR Counselor mage\mage Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 13 14 25 70 70 Counselor CAN_OPEN_DOOR 0 90 8 8 20 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4070 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_MAGISTR Magistrate mage\mage mage\cnselg Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 14 15 27 85 85 Counselor CAN_OPEN_DOOR 1 100 8 10 24 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 4478 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_CABALIST Cabalist mage\mage mage\cnselgd Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 15 16 29 120 120 Counselor CAN_OPEN_DOOR 2 110 8 14 30 0 0 0 0 0 0 0 0 0 0 0 Demon RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4929 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_ADVOCATE Advocate mage\mage mage\cnselbk Retail 128 2000 true false 12,1,20,8,28,20 1,1,1,1,1,1 16 16 30 145 145 Counselor CAN_OPEN_DOOR 3 120 8 15 25 0 0 0 0 0 0 0 0 0 0 0 Demon IMMUNE_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968 FirstFrame,FirstFrame,EachFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_GOLEM Golem golem\golem golem\golm Never 96 386 true false 0,16,12,0,12,20 1,1,1,1,1,1 1 1 12 1 1 Golem CAN_OPEN_DOOR 0 0 7 1 1 0 0 0 0 0 0 0 0 0 0 1 Demon 0 None,EachFrame,EachFrame,None,EachFrame,EachFrame
|
||||
MT_DIABLO The Dark Lord diablo\diablo Never 160 2000 true true 16,6,16,6,16,16 1,1,1,1,1,1 26 26 30 1666 1666 Diablo KNOCKBACK,SEARCH,CAN_OPEN_DOOR 3 220 4 30 60 0 11 0 0 0 0 0 0 0 0 90 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 31666 FirstFrame,FirstFrame,EachFrame,FirstFrame,EachFrame,EachFrame
|
||||
MT_DARKMAGE The Arch-Litch Malignus darkmage\dmage darkmage\dmag Never 128 1060 true false 6,1,21,6,23,18 1,1,1,1,1,1 21 21 30 160 160 Counselor CAN_OPEN_DOOR 3 120 8 20 40 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 4968 FirstFrame,None,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_HELLBOAR Hellboar fork\fork newsfx\hboar Retail 188 800 false false 10,10,15,6,16,0 2,1,1,1,1,1 17 18 23 80 100 SkeletonMelee KNOCKBACK,SEARCH 2 70 7 16 24 0 0 0 0 0 0 0 0 0 0 60 Demon RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 750 EachFrame,EachFrame,EachFrame,EachFrame,None,None
|
||||
MT_STINGER Stinger scorp\scorp newsfx\stingr Retail 64 305 false false 10,10,12,6,15,0 2,1,1,1,1,1 17 18 22 30 40 SkeletonMelee 3 85 8 1 20 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_LIGHTNING Bottom 500 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_PSYCHORB Psychorb eye\eye newsfx\psyco Retail 156 800 false false 12,13,13,7,21,0 2,1,1,1,1,1 17 18 22 20 30 Psychorb 3 80 8 10 10 0 0 0 0 0 0 0 0 0 0 40 Animal RESIST_FIRE Middle,Top 450 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_ARACHNON Arachnon spider\spider newsfx\slord Retail 148 800 false false 12,10,15,6,20,0 2,1,1,1,1,1 17 18 22 60 80 SkeletonMelee SEARCH 3 50 8 5 15 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_LIGHTNING Bottom,Middle,Top 500 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_FELLTWIN Felltwin tsneak\tsneak newsfx\ftwin Retail 128 800 false false 13,13,15,11,16,0 2,1,1,1,1,1 17 18 22 50 70 SkeletonMelee SEARCH,CAN_OPEN_DOOR 3 70 8 10 18 0 0 0 0 0 0 0 0 0 0 50 Demon RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 600 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,None
|
||||
MT_HORKSPWN Hork Spawn spawn\spawn newsfx\hspawn Retail 164 520 false true 15,12,14,11,14,0 1,1,1,1,1,1 18 19 22 30 30 SkeletonMelee 3 60 8 10 25 0 0 0 0 0 0 0 0 0 0 25 Demon RESIST_MAGIC RESIST_MAGIC Bottom,Middle 250 EachFrame,EachFrame,EachFrame,EachFrame,None,None
|
||||
MT_VENMTAIL Venomtail wscorp\wscorp newsfx\stingr Retail 86 305 false false 10,10,12,6,15,0 2,1,1,1,1,1 19 20 24 40 50 SkeletonMelee 3 85 8 1 30 0 0 0 0 0 0 0 0 0 0 60 Animal RESIST_LIGHTNING IMMUNE_LIGHTNING Bottom 1000 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_NECRMORB Necromorb eye2\eye2 newsfx\psyco Retail 140 800 false false 12,13,13,7,21,0 2,1,1,1,1,1 19 20 24 30 40 Necromorb 3 80 8 20 20 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_FIRE IMMUNE_FIRE,RESIST_LIGHTNING Middle,Top 1100 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_SPIDLORD Spider Lord bspidr\bspidr newsfx\slord Retail 148 800 true true 12,10,15,6,20,10 2,1,1,1,1,1 19 20 24 80 100 Acid SEARCH 3 60 8 8 20 75 8 10 10 0 0 0 0 0 0 60 Animal RESIST_LIGHTNING RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 1250 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,EachFrame
|
||||
MT_LASHWORM Lashworm clasp\clasp newsfx\lworm Retail 176 800 false false 10,12,15,6,16,0 1,1,1,1,1,1 19 20 20 30 30 SkeletonMelee 3 90 8 12 20 0 0 0 0 0 0 0 0 0 0 50 Animal RESIST_FIRE Bottom,Middle 600 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_TORCHANT Torchant antworm\worm newsfx\tchant Retail 192 800 false false 14,12,12,6,20,0 2,1,1,1,1,1 19 20 22 60 80 Torchant 3 75 8 20 30 0 0 0 0 0 0 0 0 0 0 70 Animal IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 1250 EachFrame,EachFrame,EachFrame,EachFrame,None,None
|
||||
MT_HORKDMN Hork Demon horkd\horkd newsfx\hdemon Never 138 800 true true 15,8,16,6,16,9 2,1,1,1,1,2 19 19 27 120 160 SkeletonMelee 3 60 8 20 35 80 8 0 0 0 0 0 0 0 0 80 Demon RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_LIGHTNING Bottom,Middle,Top 2000 EachFrame,EachFrame,EachFrame,EachFrame,None,EachFrame
|
||||
MT_DEFILER Hell Bug hellbug\hellbg newsfx\defile Never 198 800 true true 8,8,14,6,14,12 1,1,1,1,1,1 20 20 30 240 240 SkeletonMelee SEARCH 3 110 8 20 30 90 8 50 60 0 0 0 0 0 0 80 Demon RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5000 EachFrame,EachFrame,EachFrame,EachFrame,None,EachFrame
|
||||
MT_GRAVEDIG Gravedigger gravdg\gravdg newsfx\gdiggr Retail 124 800 true true 24,24,12,6,16,16 2,1,1,1,1,1 21 21 26 120 240 Scavenger CAN_OPEN_DOOR 3 80 6 2 12 0 0 0 0 0 0 0 0 0 0 20 Undead IMMUNE_LIGHTNING RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle 2000 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,EachFrame
|
||||
MT_TOMBRAT Tomb Rat rat\rat newsfx\tmbrat Retail 104 550 false false 11,8,12,6,20,0 2,1,1,1,1,1 21 22 24 80 120 SkeletonMelee 3 120 8 12 25 0 0 0 0 0 0 0 0 0 0 30 Animal RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 1800 EachFrame,EachFrame,EachFrame,EachFrame,None,None
|
||||
MT_FIREBAT Firebat hellbat\helbat newsfx\helbat Retail 96 550 false false 18,16,14,6,18,11 2,1,1,1,1,1 21 22 24 60 80 FireBat 3 100 8 15 20 0 0 0 0 0 0 0 0 0 0 70 Animal IMMUNE_FIRE RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 2400 EachFrame,EachFrame,EachFrame,EachFrame,None,EachFrame
|
||||
MT_SKLWING Skullwing demskel\demskl newsfx\swing skelaxe\skelt Retail 128 1740 true false 10,8,20,6,24,16 3,1,1,1,1,1 21 22 27 70 70 SkeletonMelee 0 75 7 15 20 75 9 15 20 0 0 0 0 0 0 80 Undead RESIST_FIRE,RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle,Top 3000 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_LICH Lich lich\lich newsfx\lich Retail 96 800 false true 12,10,10,7,21,0 2,1,1,1,2,1 21 22 25 80 100 Lich 3 100 8 15 20 0 0 0 0 0 0 0 0 0 0 60 Undead RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING Bottom,Middle 3000 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_CRYPTDMN Crypt Demon bubba\bubba newsfx\crypt Retail 154 800 false true 8,18,12,8,21,0 3,1,1,1,1,1 22 23 28 200 240 SkeletonMelee 3 100 8 20 40 0 0 0 0 0 0 0 0 0 0 85 Demon IMMUNE_MAGIC,RESIST_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING Bottom,Middle 3200 EachFrame,EachFrame,EachFrame,EachFrame,None,None
|
||||
MT_HELLBAT Hellbat hellbat2\bhelbt newsfx\helbat Retail 96 550 true false 18,16,14,6,18,11 2,1,1,1,1,1 23 24 29 100 140 Torchant 3 110 8 30 30 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING RESIST_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 3600 EachFrame,EachFrame,EachFrame,EachFrame,None,EachFrame
|
||||
MT_BONEDEMN Bone Demon demskel\demskl newsfx\swing Retail 128 1740 true true 10,8,20,6,24,16 3,1,1,1,1,1 23 24 30 240 280 BoneDemon 0 100 8 40 50 160 12 50 50 0 0 0 0 0 0 50 Undead IMMUNE_FIRE,IMMUNE_LIGHTNING IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 5000 FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame,FirstFrame
|
||||
MT_ARCHLICH Arch Lich lich2\lich2 newsfx\lich Retail 136 800 false true 12,10,10,7,21,0 2,1,1,1,2,1 23 24 30 180 200 ArchLich 3 120 8 30 30 0 0 0 0 0 0 0 0 0 0 75 Undead RESIST_MAGIC,RESIST_FIRE,IMMUNE_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 4000 EachFrame,EachFrame,EachFrame,EachFrame,None,None
|
||||
MT_BICLOPS Biclops byclps\byclps newsfx\biclop Retail 180 800 false false 10,11,16,6,16,0 2,1,1,1,2,1 23 24 30 200 240 SkeletonMelee KNOCKBACK,CAN_OPEN_DOOR 3 90 8 40 50 0 0 0 0 0 0 0 0 0 0 80 Demon RESIST_LIGHTNING RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 4000 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_FLESTHNG Flesh Thing flesh\flesh newsfx\flesht Retail 164 800 false true 15,24,15,6,16,0 1,1,1,1,1,1 23 24 28 300 400 SkeletonMelee 3 150 8 12 18 0 0 0 0 0 0 0 0 0 0 70 Demon RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING RESIST_MAGIC,RESIST_FIRE,RESIST_LIGHTNING Bottom,Middle 4000 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame,None
|
||||
MT_REAPER Reaper reaper\reap newsfx\reaper Retail 180 800 false false 12,10,14,6,16,0 2,1,1,1,1,1 23 24 30 260 300 SkeletonMelee 3 120 8 30 35 0 0 0 0 0 0 0 0 0 0 90 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle 6000 EachFrame,EachFrame,EachFrame,EachFrame,EachFrame|Unexplained,None
|
||||
MT_NAKRUL Na-Krul nkr\nkr newsfx\nakrul Never 226 1200 true true 2,6,16,3,16,16 1,1,1,1,1,1 31 31 40 1332 1332 SkeletonMelee KNOCKBACK,SEARCH,CAN_OPEN_DOOR 3 150 7 40 50 150 10 40 50 0 0 0 0 0 0 125 Demon IMMUNE_MAGIC,IMMUNE_FIRE,RESIST_LIGHTNING IMMUNE_MAGIC,IMMUNE_FIRE,IMMUNE_LIGHTNING Bottom,Middle,Top 13333 FirstFrame,EachFrame,EachFrame|Unexplained,EachFrame,EachFrame|Unexplained,EachFrame|Unexplained
|
||||
|
||||
|
@@ -1,16 +1,16 @@
|
||||
type name position_x position_y direction animWidth animPath animFrames animDelay gossipTexts animOrder
|
||||
TOWN_SMITH Griswold the Blacksmith 62 63 SouthWest 96 towners\smith\smithn 16 3 TEXT_GRISWOLD2,TEXT_GRISWOLD3,TEXT_GRISWOLD4,TEXT_GRISWOLD5,TEXT_GRISWOLD6,TEXT_GRISWOLD7,TEXT_GRISWOLD8,TEXT_GRISWOLD9,TEXT_GRISWOLD10,TEXT_GRISWOLD12,TEXT_GRISWOLD13 4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,14,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3
|
||||
TOWN_HEALER Pepin the Healer 55 79 SouthEast 96 towners\healer\healer 20 6 TEXT_PEPIN2,TEXT_PEPIN3,TEXT_PEPIN4,TEXT_PEPIN5,TEXT_PEPIN6,TEXT_PEPIN7,TEXT_PEPIN9,TEXT_PEPIN10,TEXT_PEPIN11 0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
|
||||
TOWN_DEADGUY Wounded Townsman 24 32 North 96 towners\butch\deadguy 8 6
|
||||
TOWN_TAVERN Ogden the Tavern owner 55 62 SouthWest 96 towners\twnf\twnfn 16 3 TEXT_OGDEN2,TEXT_OGDEN3,TEXT_OGDEN4,TEXT_OGDEN5,TEXT_OGDEN6,TEXT_OGDEN8,TEXT_OGDEN9,TEXT_OGDEN10 0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,1,0,15,14,13,13,14,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
|
||||
TOWN_STORY Cain the Elder 62 71 South 96 towners\strytell\strytell 25 3 TEXT_STORY2,TEXT_STORY3,TEXT_STORY4,TEXT_STORY5,TEXT_STORY6,TEXT_STORY7,TEXT_STORY9,TEXT_STORY10,TEXT_STORY11 0,0,24,24,23,22,21,20,19,18,17,16,15,14,15,16,17,18,19,20,21,22,23,24,24,24,0,0,0,24,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0
|
||||
TOWN_DRUNK Farnham the Drunk 71 84 South 96 towners\drunk\twndrunk 18 3 TEXT_FARNHAM2,TEXT_FARNHAM3,TEXT_FARNHAM4,TEXT_FARNHAM5,TEXT_FARNHAM6,TEXT_FARNHAM8,TEXT_FARNHAM9,TEXT_FARNHAM10,TEXT_FARNHAM11,TEXT_FARNHAM12,TEXT_FARNHAM13 0,0,0,1,2,3,4,5,6,7,8,9,10,10,10,10,11,12,13,14,15,16,17,17,0,0,0,17,16,15,14,13,12,11,10,9,10,11,12,13,14,15,16,17,0,1,2,3,4,4,4,3,2,1
|
||||
TOWN_WITCH Adria the Witch 80 20 South 96 towners\townwmn1\witch 19 6 TEXT_ADRIA2,TEXT_ADRIA3,TEXT_ADRIA4,TEXT_ADRIA5,TEXT_ADRIA6,TEXT_ADRIA7,TEXT_ADRIA8,TEXT_ADRIA9,TEXT_ADRIA10,TEXT_ADRIA12,TEXT_ADRIA13 3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,2,1,0,18,17,18,0,1,0,18,17,18,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,14,14,13,12,12,12,12,13,14,14,14,13,12,11,11,11,10,9,9,9,8,7,8,9,9,10,11,12,13,14,15,16,17,18,0,1,0,18,17,18,0,1,0,1,2
|
||||
TOWN_BMAID Gillian the Barmaid 43 66 South 96 towners\townwmn1\wmnn 18 6 TEXT_GILLIAN2,TEXT_GILLIAN3,TEXT_GILLIAN4,TEXT_GILLIAN5,TEXT_GILLIAN6,TEXT_GILLIAN7,TEXT_GILLIAN9,TEXT_GILLIAN10
|
||||
TOWN_PEGBOY Wirt the Peg-legged boy 11 53 South 96 towners\townboy\pegkid1 20 6 TEXT_WIRT2,TEXT_WIRT3,TEXT_WIRT4,TEXT_WIRT5,TEXT_WIRT6,TEXT_WIRT7,TEXT_WIRT8,TEXT_WIRT9,TEXT_WIRT11,TEXT_WIRT12
|
||||
type name position_x position_y direction animWidth animPath animFrames animDelay gossipTexts animOrder shadowRule
|
||||
TOWN_SMITH Griswold the Blacksmith 62 63 SouthWest 96 towners\smith\smithn 16 3 TEXT_GRISWOLD2,TEXT_GRISWOLD3,TEXT_GRISWOLD4,TEXT_GRISWOLD5,TEXT_GRISWOLD6,TEXT_GRISWOLD7,TEXT_GRISWOLD8,TEXT_GRISWOLD9,TEXT_GRISWOLD10,TEXT_GRISWOLD12,TEXT_GRISWOLD13 4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,13,12,11,10,9,8,7,6,5,4,4,5,6,7,8,9,10,11,12,13,14,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3 FirstFrame
|
||||
TOWN_HEALER Pepin the Healer 55 79 SouthEast 96 towners\healer\healer 20 6 TEXT_PEPIN2,TEXT_PEPIN3,TEXT_PEPIN4,TEXT_PEPIN5,TEXT_PEPIN6,TEXT_PEPIN7,TEXT_PEPIN9,TEXT_PEPIN10,TEXT_PEPIN11 0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,2,1,0,19,18,18,19,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,14,13,12,11,10,9,8,7,6,5,4,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 FirstFrame
|
||||
TOWN_DEADGUY Wounded Townsman 24 32 North 96 towners\butch\deadguy 8 6 None
|
||||
TOWN_TAVERN Ogden the Tavern owner 55 62 SouthWest 96 towners\twnf\twnfn 16 3 TEXT_OGDEN2,TEXT_OGDEN3,TEXT_OGDEN4,TEXT_OGDEN5,TEXT_OGDEN6,TEXT_OGDEN8,TEXT_OGDEN9,TEXT_OGDEN10 0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,2,1,0,15,14,13,13,14,15,0,1,2,1,0,15,14,13,13,14,15,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 FirstFrame
|
||||
TOWN_STORY Cain the Elder 62 71 South 96 towners\strytell\strytell 25 3 TEXT_STORY2,TEXT_STORY3,TEXT_STORY4,TEXT_STORY5,TEXT_STORY6,TEXT_STORY7,TEXT_STORY9,TEXT_STORY10,TEXT_STORY11 0,0,24,24,23,22,21,20,19,18,17,16,15,14,15,16,17,18,19,20,21,22,23,24,24,24,0,0,0,24,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 FirstFrame
|
||||
TOWN_DRUNK Farnham the Drunk 71 84 South 96 towners\drunk\twndrunk 18 3 TEXT_FARNHAM2,TEXT_FARNHAM3,TEXT_FARNHAM4,TEXT_FARNHAM5,TEXT_FARNHAM6,TEXT_FARNHAM8,TEXT_FARNHAM9,TEXT_FARNHAM10,TEXT_FARNHAM11,TEXT_FARNHAM12,TEXT_FARNHAM13 0,0,0,1,2,3,4,5,6,7,8,9,10,10,10,10,11,12,13,14,15,16,17,17,0,0,0,17,16,15,14,13,12,11,10,9,10,11,12,13,14,15,16,17,0,1,2,3,4,4,4,3,2,1 FirstFrame
|
||||
TOWN_WITCH Adria the Witch 80 20 South 96 towners\townwmn1\witch 19 6 TEXT_ADRIA2,TEXT_ADRIA3,TEXT_ADRIA4,TEXT_ADRIA5,TEXT_ADRIA6,TEXT_ADRIA7,TEXT_ADRIA8,TEXT_ADRIA9,TEXT_ADRIA10,TEXT_ADRIA12,TEXT_ADRIA13 3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,3,3,4,5,5,5,4,3,14,13,12,12,12,13,14,3,4,5,5,5,4,3,2,1,0,18,17,18,0,1,0,18,17,18,0,1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,14,14,13,12,12,12,12,13,14,14,14,13,12,11,11,11,10,9,9,9,8,7,8,9,9,10,11,12,13,14,15,16,17,18,0,1,0,18,17,18,0,1,0,1,2 FirstFrame
|
||||
TOWN_BMAID Gillian the Barmaid 43 66 South 96 towners\townwmn1\wmnn 18 6 TEXT_GILLIAN2,TEXT_GILLIAN3,TEXT_GILLIAN4,TEXT_GILLIAN5,TEXT_GILLIAN6,TEXT_GILLIAN7,TEXT_GILLIAN9,TEXT_GILLIAN10 FirstFrame
|
||||
TOWN_PEGBOY Wirt the Peg-legged boy 11 53 South 96 towners\townboy\pegkid1 20 6 TEXT_WIRT2,TEXT_WIRT3,TEXT_WIRT4,TEXT_WIRT5,TEXT_WIRT6,TEXT_WIRT7,TEXT_WIRT8,TEXT_WIRT9,TEXT_WIRT11,TEXT_WIRT12 FirstFrame|Stray
|
||||
TOWN_COW Cow 58 16 SouthWest 128 12 3
|
||||
TOWN_COW Cow 56 14 NorthWest 128 12 3
|
||||
TOWN_COW Cow 59 20 North 128 12 3
|
||||
TOWN_COWFARM Complete Nut 61 22 SouthWest 96 towners\farmer\cfrmrn2 15 3
|
||||
TOWN_FARMER Lester the farmer 62 16 South 96 towners\farmer\farmrn2 15 3
|
||||
TOWN_GIRL Celia 77 43 South 96 towners\girl\girlw1 20 6
|
||||
TOWN_COWFARM Complete Nut 61 22 SouthWest 96 towners\farmer\cfrmrn2 15 3 Baked
|
||||
TOWN_FARMER Lester the farmer 62 16 South 96 towners\farmer\farmrn2 15 3 FirstFrame
|
||||
TOWN_GIRL Celia 77 43 South 96 towners\girl\girlw1 20 6 FirstFrame
|
||||
|
||||
|
@@ -0,0 +1,529 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "engine/clx_sprite.hpp"
|
||||
#include "engine/render/clx_render.hpp"
|
||||
#include "engine/shadow_mask.hpp"
|
||||
#include "engine/shadow_rule.hpp"
|
||||
#include "engine/surface.hpp"
|
||||
#include "utils/clx_decode.hpp"
|
||||
#include "utils/clx_encode.hpp"
|
||||
#include "utils/clx_shadow.hpp"
|
||||
#include "utils/endian_write.hpp"
|
||||
#include "utils/palette_blending.hpp"
|
||||
#include "utils/sdl_wrap.h"
|
||||
|
||||
namespace devilution {
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* A sprite as rows of characters, top row first:
|
||||
* '.' is transparent, '#' is black (palette index 0), any other character is that palette index.
|
||||
*/
|
||||
using Picture = std::vector<std::string>;
|
||||
|
||||
void AppendSprite(const Picture &picture, std::vector<uint8_t> &out)
|
||||
{
|
||||
const size_t headerPos = out.size();
|
||||
out.resize(out.size() + ClxFrameHeaderSize);
|
||||
WriteLE16(&out[headerPos], ClxFrameHeaderSize);
|
||||
WriteLE16(&out[headerPos + 2], static_cast<uint16_t>(picture[0].size()));
|
||||
WriteLE16(&out[headerPos + 4], static_cast<uint16_t>(picture.size()));
|
||||
for (size_t i = picture.size(); i-- > 0;) {
|
||||
for (const char c : picture[i]) {
|
||||
if (c == '.') {
|
||||
AppendClxTransparentRun(1, out);
|
||||
} else {
|
||||
const uint8_t color = c == '#' ? 0 : static_cast<uint8_t>(c);
|
||||
AppendClxPixelsRun(&color, 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppendList(const std::vector<Picture> &pictures, std::vector<uint8_t> &out)
|
||||
{
|
||||
const size_t begin = out.size();
|
||||
out.resize(out.size() + 4 * (2 + pictures.size()));
|
||||
WriteLE32(&out[begin], static_cast<uint32_t>(pictures.size()));
|
||||
for (size_t i = 0; i < pictures.size(); ++i) {
|
||||
WriteLE32(&out[begin + 4 * (1 + i)], static_cast<uint32_t>(out.size() - begin));
|
||||
AppendSprite(pictures[i], out);
|
||||
}
|
||||
WriteLE32(&out[begin + 4 * (1 + pictures.size())], static_cast<uint32_t>(out.size() - begin));
|
||||
}
|
||||
|
||||
OwnedClxSpriteListOrSheet MakeList(const std::vector<Picture> &pictures)
|
||||
{
|
||||
std::vector<uint8_t> data;
|
||||
AppendList(pictures, data);
|
||||
std::unique_ptr<uint8_t[]> buffer { new uint8_t[data.size()] };
|
||||
std::copy(data.begin(), data.end(), buffer.get());
|
||||
return OwnedClxSpriteListOrSheet { std::move(buffer), 0 };
|
||||
}
|
||||
|
||||
OwnedClxSpriteListOrSheet MakeSheet(const std::vector<std::vector<Picture>> &lists)
|
||||
{
|
||||
std::vector<uint8_t> data(lists.size() * 4);
|
||||
for (size_t i = 0; i < lists.size(); ++i) {
|
||||
WriteLE32(&data[i * 4], static_cast<uint32_t>(data.size()));
|
||||
AppendList(lists[i], data);
|
||||
}
|
||||
std::unique_ptr<uint8_t[]> buffer { new uint8_t[data.size()] };
|
||||
std::copy(data.begin(), data.end(), buffer.get());
|
||||
return OwnedClxSpriteListOrSheet { std::move(buffer), static_cast<uint16_t>(lists.size()) };
|
||||
}
|
||||
|
||||
Picture ToPicture(ClxSprite sprite)
|
||||
{
|
||||
const size_t width = sprite.width();
|
||||
Picture result(sprite.height(), std::string(width, '.'));
|
||||
size_t pos = 0;
|
||||
const auto put = [&](uint8_t color) {
|
||||
result[sprite.height() - 1 - pos / width][pos % width] = color == 0 ? '#' : static_cast<char>(color);
|
||||
++pos;
|
||||
};
|
||||
const uint8_t *src = sprite.pixelData();
|
||||
const uint8_t *end = src + sprite.pixelDataSize();
|
||||
while (src < end) {
|
||||
const uint8_t control = *src++;
|
||||
if (!IsClxOpaque(control)) {
|
||||
pos += control;
|
||||
} else if (IsClxOpaqueFill(control)) {
|
||||
const uint8_t color = *src++;
|
||||
for (unsigned i = GetClxOpaqueFillWidth(control); i != 0; --i)
|
||||
put(color);
|
||||
} else {
|
||||
for (unsigned i = GetClxOpaquePixelsWidth(control); i != 0; --i)
|
||||
put(*src++);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The mask drawn as '#' on a transparent picture of the given size. */
|
||||
Picture ToPicture(ShadowMask mask, size_t width, size_t height)
|
||||
{
|
||||
Picture result(height, std::string(width, '.'));
|
||||
mask.forEachSpan([&](uint16_t rowFromBottom, ShadowSpan span) {
|
||||
for (unsigned x = span.x; x < static_cast<unsigned>(span.x) + span.width; ++x)
|
||||
result[height - 1 - rowFromBottom][x] = '#';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
constexpr ShadowRule FirstFrame { ShadowRuleType::FirstFrame, 0 };
|
||||
constexpr ShadowRule EachFrame { ShadowRuleType::EachFrame, 0 };
|
||||
|
||||
// A 12 pixel tall bar with its baseline on row 11.
|
||||
// Rows 9..11 and 6..8 shadow the bar itself, rows 3..5 shadow (5, 7) and rows 0..2 shadow (4, 6).
|
||||
const Picture Bar = {
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"....#.a.",
|
||||
".....#a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
};
|
||||
const Picture BarWithoutShadow = {
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
};
|
||||
const Picture BarShadow = {
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"....#...",
|
||||
".....#..",
|
||||
"......#.",
|
||||
"......#.",
|
||||
"........",
|
||||
"........",
|
||||
};
|
||||
|
||||
TEST(ClxShadowTest, ExtractsShadow)
|
||||
{
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeList({ Bar });
|
||||
const std::optional<OwnedClxSpriteListOrSheet> result = ExtractClxShadows(sprites, FirstFrame);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
|
||||
ASSERT_FALSE(result->isSheet());
|
||||
const ClxSpriteList list = result->list();
|
||||
ASSERT_EQ(list.numSprites(), 1U);
|
||||
EXPECT_EQ(list[0].width(), 8);
|
||||
EXPECT_EQ(list[0].height(), 12);
|
||||
EXPECT_EQ(ToPicture(list[0]), BarWithoutShadow);
|
||||
|
||||
// The mask includes the shadow underneath the sprite.
|
||||
ASSERT_TRUE(HasShadowMask(list[0]));
|
||||
const ShadowMask mask = GetShadowMask(list[0]);
|
||||
EXPECT_EQ(mask.bottomOffset(), 2);
|
||||
EXPECT_EQ(mask.numRows(), 4);
|
||||
EXPECT_EQ(ToPicture(mask, 8, 12), BarShadow);
|
||||
// 2 bytes of header and one span for each of the 4 rows.
|
||||
EXPECT_EQ(list[0].headerSize(), ClxSprite::BaseHeaderSize + 2 + 4 * 2);
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, KeepsBlackPixelsThatAreNotShadow)
|
||||
{
|
||||
Picture picture = Bar;
|
||||
// A black pixel of the bar. It casts the same shadow as the pixel it replaces.
|
||||
picture[2][6] = '#';
|
||||
// Its shadow would fall outside of the frame, so it cannot be ruled out as a body pixel.
|
||||
picture[0][0] = '#';
|
||||
// Not in any shadow, and its own shadow would fall on a transparent pixel.
|
||||
picture[3][3] = '#';
|
||||
Picture expected = BarWithoutShadow;
|
||||
expected[2][6] = '#';
|
||||
expected[0][0] = '#';
|
||||
expected[3][3] = '#';
|
||||
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeList({ picture });
|
||||
const std::optional<OwnedClxSpriteListOrSheet> result = ExtractClxShadows(sprites, FirstFrame);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(ToPicture(result->list()[0]), expected);
|
||||
EXPECT_EQ(ToPicture(GetShadowMask(result->list()[0]), 8, 12), BarShadow);
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, RecognisesBlackBodyPixelInsideAShadow)
|
||||
{
|
||||
// The baseline is row 23. 'b' at (7, 0) shadows (2, 14), so the black pixel there looks like
|
||||
// a shadow. It must be a body pixel, because nothing else casts the black pixel at (0, 18).
|
||||
Picture picture(24, std::string(8, '.'));
|
||||
picture[0][7] = 'b';
|
||||
picture[14][2] = '#';
|
||||
picture[18][0] = '#';
|
||||
picture[23][7] = 'a';
|
||||
Picture expected = picture;
|
||||
expected[18][0] = '.';
|
||||
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeList({ picture });
|
||||
const std::optional<OwnedClxSpriteListOrSheet> result = ExtractClxShadows(sprites, FirstFrame);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(ToPicture(result->list()[0]), expected);
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, BaselineRules)
|
||||
{
|
||||
// The second sprite ends 3 rows higher, with the shadow that its own baseline (row 8) gives.
|
||||
const Picture shortBar = {
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
".....#a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
};
|
||||
Picture shortBarWithoutShadow = shortBar;
|
||||
shortBarWithoutShadow[4][5] = '.';
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeList({ Bar, shortBar });
|
||||
|
||||
const std::optional<OwnedClxSpriteListOrSheet> eachFrame = ExtractClxShadows(sprites, EachFrame);
|
||||
ASSERT_TRUE(eachFrame.has_value());
|
||||
EXPECT_EQ(ToPicture(eachFrame->list()[0]), BarWithoutShadow);
|
||||
EXPECT_EQ(ToPicture(eachFrame->list()[1]), shortBarWithoutShadow);
|
||||
EXPECT_EQ(GetShadowMask(eachFrame->list()[1]).bottomOffset(), 5);
|
||||
EXPECT_EQ(GetShadowMask(eachFrame->list()[1]).numRows(), 3);
|
||||
|
||||
// With the baseline of the first sprite (row 11) that black pixel is not a shadow.
|
||||
const std::optional<OwnedClxSpriteListOrSheet> firstFrame = ExtractClxShadows(sprites, FirstFrame);
|
||||
ASSERT_TRUE(firstFrame.has_value());
|
||||
EXPECT_EQ(ToPicture(firstFrame->list()[0]), BarWithoutShadow);
|
||||
EXPECT_EQ(ToPicture(firstFrame->list()[1]), shortBar);
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, BaselineOffset)
|
||||
{
|
||||
// A flying sprite. Its lowest opaque row (row 5) is its shadow.
|
||||
const Picture picture = {
|
||||
"......a.",
|
||||
"......a.",
|
||||
"......a.",
|
||||
"........",
|
||||
"........",
|
||||
"....#...",
|
||||
};
|
||||
Picture expected = picture;
|
||||
expected[5][4] = '.';
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeList({ picture });
|
||||
|
||||
// With the baseline on row 5 the sprite only shadows itself, at (6, 2).
|
||||
const std::optional<OwnedClxSpriteListOrSheet> noOffset = ExtractClxShadows(sprites, FirstFrame);
|
||||
ASSERT_TRUE(noOffset.has_value());
|
||||
EXPECT_EQ(ToPicture(noOffset->list()[0]), picture);
|
||||
|
||||
// With the baseline on row 10, rows 0 and 1 shadow (4, 5). The shadow of row 2 is outside the frame.
|
||||
const std::optional<OwnedClxSpriteListOrSheet> withOffset = ExtractClxShadows(sprites, ShadowRule { ShadowRuleType::FirstFrame, 5 });
|
||||
ASSERT_TRUE(withOffset.has_value());
|
||||
EXPECT_EQ(ToPicture(withOffset->list()[0]), expected);
|
||||
const ShadowMask mask = GetShadowMask(withOffset->list()[0]);
|
||||
EXPECT_EQ(mask.bottomOffset(), 0);
|
||||
EXPECT_EQ(mask.numRows(), 1);
|
||||
EXPECT_EQ(ToPicture(mask, 8, 6), (Picture { "........", "........", "........", "........", "........", "....#..." }));
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, PreservesSheetStructure)
|
||||
{
|
||||
const Picture empty(12, std::string(8, '.'));
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeSheet({ { Bar, Bar }, { empty }, { Bar } });
|
||||
const std::optional<OwnedClxSpriteListOrSheet> result = ExtractClxShadows(sprites, FirstFrame);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
|
||||
ASSERT_TRUE(result->isSheet());
|
||||
const ClxSpriteSheet sheet = result->sheet();
|
||||
ASSERT_EQ(sheet.numLists(), 3);
|
||||
ASSERT_EQ(sheet[0].numSprites(), 2U);
|
||||
ASSERT_EQ(sheet[1].numSprites(), 1U);
|
||||
ASSERT_EQ(sheet[2].numSprites(), 1U);
|
||||
EXPECT_EQ(ToPicture(sheet[0][1]), BarWithoutShadow);
|
||||
EXPECT_EQ(ToPicture(sheet[1][0]), empty);
|
||||
EXPECT_EQ(ToPicture(sheet[2][0]), BarWithoutShadow);
|
||||
|
||||
EXPECT_EQ(ToPicture(GetShadowMask(sheet[0][1]), 8, 12), BarShadow);
|
||||
// A sprite without a shadow has a plain header.
|
||||
EXPECT_FALSE(HasShadowMask(sheet[1][0]));
|
||||
EXPECT_EQ(sheet[1][0].headerSize(), ClxSprite::BaseHeaderSize);
|
||||
EXPECT_EQ(ToPicture(GetShadowMask(sheet[2][0]), 8, 12), BarShadow);
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, SplitsLongRunsAndKeepsEmptyRows)
|
||||
{
|
||||
// The baseline is row 8. 'a' (row 0) shadows row 4 shifted left by 1, 'b' (rows 6..8) shadows row 6,
|
||||
// and nothing shadows row 5.
|
||||
Picture picture(9, std::string(200, '.'));
|
||||
for (size_t x = 1; x < 200; ++x)
|
||||
picture[0][x] = 'a';
|
||||
for (size_t x = 0; x < 199; ++x)
|
||||
picture[4][x] = '#';
|
||||
picture[8][10] = 'b';
|
||||
picture[7][10] = 'b';
|
||||
picture[6][10] = 'b';
|
||||
Picture expectedSprite = picture;
|
||||
expectedSprite[4].assign(200, '.');
|
||||
Picture expectedMask(9, std::string(200, '.'));
|
||||
expectedMask[4].replace(0, 199, std::string(199, '#'));
|
||||
expectedMask[6][10] = '#';
|
||||
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeList({ picture });
|
||||
const std::optional<OwnedClxSpriteListOrSheet> result = ExtractClxShadows(sprites, FirstFrame);
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_EQ(ToPicture(result->list()[0]), expectedSprite);
|
||||
|
||||
const ShadowMask mask = GetShadowMask(result->list()[0]);
|
||||
EXPECT_EQ(mask.bottomOffset(), 2);
|
||||
EXPECT_EQ(mask.numRows(), 3);
|
||||
EXPECT_EQ(ToPicture(mask, 200, 9), expectedMask);
|
||||
// Row 6: one span. Row 5: an empty span. Row 4: 199 pixels as spans of 127 and 72.
|
||||
const std::vector<uint8_t> expectedRows = { 10, 0x80 | 1, 0, 0x80 | 0, 0, 127, 127, 0x80 | 72 };
|
||||
EXPECT_EQ(std::vector<uint8_t>(mask.rows(), mask.rows() + expectedRows.size()), expectedRows);
|
||||
}
|
||||
|
||||
/** An 8-bit surface filled with a pattern that has no black in it. */
|
||||
SDLSurfaceUniquePtr MakeBackground(int width, int height)
|
||||
{
|
||||
SDLSurfaceUniquePtr surface = SDLWrap::CreateRGBSurfaceWithFormat(/*flags=*/0, width, height, /*depth=*/8, SDL_PIXELFORMAT_INDEX8);
|
||||
const Surface out { surface.get() };
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x)
|
||||
*out.at(x, y) = static_cast<uint8_t>(1 + (x * 7 + y * 13) % 200);
|
||||
}
|
||||
return surface;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Pixels(const Surface &out)
|
||||
{
|
||||
std::vector<uint8_t> result;
|
||||
for (int y = 0; y < out.h(); ++y)
|
||||
result.insert(result.end(), out.at(0, y), out.at(0, y) + out.w());
|
||||
return result;
|
||||
}
|
||||
|
||||
// A sprite whose shadow reaches the left edge, with a black body pixel and a body pixel inside the shadow.
|
||||
const Picture Walker = {
|
||||
"...........aaa..",
|
||||
"...........a#a..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
"........###aaa..",
|
||||
"........###aaa..",
|
||||
".........##aaa..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
"...........aaa..",
|
||||
};
|
||||
|
||||
TEST(ClxShadowTest, DrawingShadowThenSpriteMatchesTheOriginal)
|
||||
{
|
||||
constexpr int Width = 24, Height = 20;
|
||||
for (const Picture &picture : { Bar, Walker }) {
|
||||
const OwnedClxSpriteListOrSheet original = MakeList({ picture });
|
||||
const std::optional<OwnedClxSpriteListOrSheet> extracted = ExtractClxShadows(original, FirstFrame);
|
||||
ASSERT_TRUE(extracted.has_value());
|
||||
ASSERT_TRUE(HasShadowMask(extracted->list()[0]));
|
||||
// The shadow must really have been removed, or this test proves nothing.
|
||||
ASSERT_NE(ToPicture(extracted->list()[0]), picture);
|
||||
|
||||
// Every position, including those where the sprite is clipped or entirely outside.
|
||||
const int spriteWidth = static_cast<int>(picture[0].size());
|
||||
const int spriteHeight = static_cast<int>(picture.size());
|
||||
for (int y = -2; y < Height + spriteHeight + 2; ++y) {
|
||||
for (int x = -spriteWidth - 2; x < Width + 2; ++x) {
|
||||
const SDLSurfaceUniquePtr expectedSurface = MakeBackground(Width, Height);
|
||||
const Surface expected { expectedSurface.get() };
|
||||
ClxDraw(expected, { x, y }, original.list()[0]);
|
||||
|
||||
const SDLSurfaceUniquePtr actualSurface = MakeBackground(Width, Height);
|
||||
const Surface actual { actualSurface.get() };
|
||||
ClxDrawShadow(actual, { x, y }, extracted->list()[0]);
|
||||
ClxDraw(actual, { x, y }, extracted->list()[0]);
|
||||
|
||||
ASSERT_EQ(Pixels(actual), Pixels(expected)) << "at " << x << ", " << y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, OpaqueRunsThatContinueOnTheNextRow)
|
||||
{
|
||||
// CLX sprites must not have opaque runs that continue on the next row, but sprites that were
|
||||
// converted before `Cl2ToClx` stopped producing them do. Such a run is taken to continue on the
|
||||
// next row, as in the image that it was made from, and the result has no such runs.
|
||||
// A 6x4 sprite, from the bottom row upwards:
|
||||
// "....ab" and the run goes on with "cd"
|
||||
// "cd#..."
|
||||
// "....ee"
|
||||
// "....e#"
|
||||
std::vector<uint8_t> data(4 * 3 + ClxFrameHeaderSize);
|
||||
WriteLE32(&data[0], 1);
|
||||
WriteLE32(&data[4], 12);
|
||||
WriteLE16(&data[12], ClxFrameHeaderSize);
|
||||
WriteLE16(&data[14], 6);
|
||||
WriteLE16(&data[16], 4);
|
||||
const auto transparent = [&](unsigned width) { AppendClxTransparentRun(width, data); };
|
||||
const auto pixels = [&](std::initializer_list<uint8_t> colors) { AppendClxPixelsRun(colors.begin(), static_cast<unsigned>(colors.size()), data); };
|
||||
transparent(4);
|
||||
pixels({ 'a', 'b', 'c', 'd' });
|
||||
pixels({ 0 });
|
||||
transparent(3 + 4);
|
||||
pixels({ 'e', 'e' });
|
||||
transparent(4);
|
||||
pixels({ 'e', 0 });
|
||||
WriteLE32(&data[8], static_cast<uint32_t>(data.size()));
|
||||
std::unique_ptr<uint8_t[]> buffer { new uint8_t[data.size()] };
|
||||
std::copy(data.begin(), data.end(), buffer.get());
|
||||
const OwnedClxSpriteListOrSheet original { std::move(buffer), 0 };
|
||||
|
||||
const std::optional<OwnedClxSpriteListOrSheet> extracted = ExtractClxShadows(original, FirstFrame);
|
||||
ASSERT_TRUE(extracted.has_value());
|
||||
const ClxSprite sprite = extracted->list()[0];
|
||||
EXPECT_EQ(ToPicture(sprite), (Picture { "....e#", "....ee", "cd#...", "....ab" }));
|
||||
|
||||
// No opaque command of the result goes past the end of its row.
|
||||
const uint8_t *src = sprite.pixelData();
|
||||
const uint8_t *end = src + sprite.pixelDataSize();
|
||||
unsigned x = 0;
|
||||
while (src < end) {
|
||||
const uint8_t control = *src++;
|
||||
if (!IsClxOpaque(control)) {
|
||||
x += control;
|
||||
} else {
|
||||
const unsigned width = IsClxOpaqueFill(control) ? GetClxOpaqueFillWidth(control) : GetClxOpaquePixelsWidth(control);
|
||||
src += IsClxOpaqueFill(control) ? 1 : width;
|
||||
EXPECT_LE(x + width, 6U);
|
||||
x += width;
|
||||
}
|
||||
x %= 6;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, DrawsBlendedShadow)
|
||||
{
|
||||
// A recognisable "blend with black" table: every colour maps to itself plus 50.
|
||||
for (unsigned i = 0; i < 256; ++i)
|
||||
paletteTransparencyLookup[0][i] = static_cast<uint8_t>(i + 50);
|
||||
#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT
|
||||
for (unsigned i = 0; i < 256; ++i) {
|
||||
for (unsigned j = 0; j < 256; ++j)
|
||||
paletteTransparencyLookupBlack16[i | (j << 8U)] = static_cast<uint16_t>(paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8U));
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr int Width = 24, Height = 20;
|
||||
const std::optional<OwnedClxSpriteListOrSheet> extracted = ExtractClxShadows(MakeList({ Walker }), FirstFrame);
|
||||
ASSERT_TRUE(extracted.has_value());
|
||||
const ClxSprite sprite = extracted->list()[0];
|
||||
const Picture body = ToPicture(sprite);
|
||||
const Picture shadow = ToPicture(GetShadowMask(sprite), Walker[0].size(), Walker.size());
|
||||
|
||||
// Positions that clip the shadow on every side, with odd and even span starts.
|
||||
for (const Point position : { Point { 3, 16 }, Point { 4, 16 }, Point { -9, 16 }, Point { 12, 10 }, Point { 2, 25 } }) {
|
||||
const SDLSurfaceUniquePtr backgroundSurface = MakeBackground(Width, Height);
|
||||
const std::vector<uint8_t> background = Pixels(Surface { backgroundSurface.get() });
|
||||
|
||||
const SDLSurfaceUniquePtr surface = MakeBackground(Width, Height);
|
||||
const Surface out { surface.get() };
|
||||
ClxDrawShadowBlended(out, position, sprite);
|
||||
ClxDraw(out, position, sprite);
|
||||
|
||||
std::vector<uint8_t> expected = background;
|
||||
for (int spriteY = 0; spriteY < static_cast<int>(Walker.size()); ++spriteY) {
|
||||
for (int spriteX = 0; spriteX < static_cast<int>(Walker[0].size()); ++spriteX) {
|
||||
const int x = position.x + spriteX;
|
||||
const int y = position.y - (static_cast<int>(Walker.size()) - 1 - spriteY);
|
||||
if (x < 0 || x >= Width || y < 0 || y >= Height) continue;
|
||||
uint8_t &pixel = expected[static_cast<size_t>(y) * Width + x];
|
||||
if (body[spriteY][spriteX] != '.') {
|
||||
pixel = body[spriteY][spriteX] == '#' ? 0 : static_cast<uint8_t>(body[spriteY][spriteX]);
|
||||
} else if (shadow[spriteY][spriteX] == '#') {
|
||||
pixel = static_cast<uint8_t>(pixel + 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(Pixels(out), expected) << "at " << position.x << ", " << position.y;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ClxShadowTest, NothingToExtractWithoutAKnownRule)
|
||||
{
|
||||
const OwnedClxSpriteListOrSheet sprites = MakeList({ Bar });
|
||||
EXPECT_FALSE(ExtractClxShadows(sprites, ShadowRule { ShadowRuleType::Unknown, 0 }).has_value());
|
||||
EXPECT_FALSE(ExtractClxShadows(sprites, ShadowRule { ShadowRuleType::None, 0 }).has_value());
|
||||
EXPECT_FALSE(ExtractClxShadows(sprites, ShadowRule { ShadowRuleType::Baked, 0 }).has_value());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace devilution
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "cursor.h"
|
||||
#include "engine/assets.hpp"
|
||||
#include "init.hpp"
|
||||
#include "tables/monstdat.h"
|
||||
#include "tables/objdat.h"
|
||||
#include "tables/playerdat.hpp"
|
||||
|
||||
using namespace devilution;
|
||||
@@ -187,6 +189,42 @@ static void AssertPlayer(devilution::Player &player)
|
||||
ASSERT_EQ(player._pILMaxDam, 0);
|
||||
}
|
||||
|
||||
TEST(Player, ShadowRulesAreLoaded)
|
||||
{
|
||||
LoadCoreArchives();
|
||||
LoadPlayerDataFiles();
|
||||
LoadMonsterData();
|
||||
LoadObjectData();
|
||||
|
||||
// One of the warrior's sprite files does not reproduce exactly, which the tags record.
|
||||
EXPECT_EQ(GetPlayerSpriteDataForClass(HeroClass::Warrior).shadowRule, (ShadowRule { ShadowRuleType::FirstFrame, 0, ShadowRuleFlags::Unexplained | ShadowRuleFlags::Stray }));
|
||||
EXPECT_EQ(GetPlayerSpriteDataForClass(HeroClass::Rogue).shadowRule, (ShadowRule { ShadowRuleType::FirstFrame, 0 }));
|
||||
EXPECT_EQ(GetPlayerSpriteDataForClass(HeroClass::Monk).shadowRule, (ShadowRule { ShadowRuleType::EachFrame, 0 }));
|
||||
// Sprites that were never analysed have an empty value.
|
||||
EXPECT_EQ(GetPlayerSpriteDataForClass(HeroClass::Bard).shadowRule, (ShadowRule { ShadowRuleType::Unknown, 0 }));
|
||||
|
||||
// One rule per animation: stand, walk, attack, hit, death, special.
|
||||
const MonsterData &zombie = MonstersData[MT_NZOMBIE];
|
||||
EXPECT_EQ(zombie.shadowRules[0], (ShadowRule { ShadowRuleType::FirstFrame, 0 }));
|
||||
EXPECT_EQ(zombie.shadowRules[5], (ShadowRule { ShadowRuleType::None, 0 }));
|
||||
// Flying monsters have their baseline below the sprite, except when they fall to the ground.
|
||||
const MonsterData &fiend = MonstersData[MT_FIEND];
|
||||
EXPECT_EQ(fiend.shadowRules[0], (ShadowRule { ShadowRuleType::FirstFrame, 15 }));
|
||||
EXPECT_EQ(fiend.shadowRules[4], (ShadowRule { ShadowRuleType::FirstFrame, 0 }));
|
||||
const MonsterData &golem = MonstersData[MT_GOLEM];
|
||||
EXPECT_EQ(golem.shadowRules[1], (ShadowRule { ShadowRuleType::EachFrame, 0 }));
|
||||
// Monsters whose sprites are missing from the game data have an empty value.
|
||||
EXPECT_EQ(MonstersData[MT_WYRM].shadowRules[0], (ShadowRule { ShadowRuleType::Unknown, 0 }));
|
||||
|
||||
EXPECT_EQ(AllObjects[OBJ_BARREL].shadowRule, (ShadowRule { ShadowRuleType::FirstFrame, 0 }));
|
||||
EXPECT_EQ(AllObjects[OBJ_CHEST1].shadowRule, (ShadowRule { ShadowRuleType::EachFrame, 0 }));
|
||||
// The cauldron's shadow cannot be told apart from its black side.
|
||||
EXPECT_EQ(AllObjects[OBJ_CAULDRON].shadowRule, (ShadowRule { ShadowRuleType::Baked, 0 }));
|
||||
EXPECT_EQ(AllObjects[OBJ_L1LIGHT].shadowRule, (ShadowRule { ShadowRuleType::None, 0 }));
|
||||
// Doors have black pixels but no cast shadow.
|
||||
EXPECT_EQ(AllObjects[OBJ_L1LDOOR].shadowRule, (ShadowRule { ShadowRuleType::None, 0 }));
|
||||
}
|
||||
|
||||
TEST(Player, CreatePlayer)
|
||||
{
|
||||
LoadCoreArchives();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "engine/shadow_rule.hpp"
|
||||
|
||||
namespace devilution {
|
||||
namespace {
|
||||
|
||||
TEST(ShadowRuleTest, ParsesTypes)
|
||||
{
|
||||
EXPECT_EQ(ParseShadowRule(""), (ShadowRule { ShadowRuleType::Unknown, 0 }));
|
||||
EXPECT_EQ(ParseShadowRule("Unknown"), (ShadowRule { ShadowRuleType::Unknown, 0 }));
|
||||
EXPECT_EQ(ParseShadowRule("None"), (ShadowRule { ShadowRuleType::None, 0 }));
|
||||
EXPECT_EQ(ParseShadowRule("Baked"), (ShadowRule { ShadowRuleType::Baked, 0 }));
|
||||
EXPECT_EQ(ParseShadowRule("FirstFrame"), (ShadowRule { ShadowRuleType::FirstFrame, 0 }));
|
||||
EXPECT_EQ(ParseShadowRule("EachFrame"), (ShadowRule { ShadowRuleType::EachFrame, 0 }));
|
||||
}
|
||||
|
||||
TEST(ShadowRuleTest, ParsesBaselineOffset)
|
||||
{
|
||||
EXPECT_EQ(ParseShadowRule("FirstFrame+15"), (ShadowRule { ShadowRuleType::FirstFrame, 15 }));
|
||||
EXPECT_EQ(ParseShadowRule("FirstFrame+255"), (ShadowRule { ShadowRuleType::FirstFrame, 255 }));
|
||||
}
|
||||
|
||||
TEST(ShadowRuleTest, ParsesTags)
|
||||
{
|
||||
EXPECT_EQ(ParseShadowRule("FirstFrame|Stray"), (ShadowRule { ShadowRuleType::FirstFrame, 0, ShadowRuleFlags::Stray }));
|
||||
EXPECT_EQ(ParseShadowRule("EachFrame|Unexplained"), (ShadowRule { ShadowRuleType::EachFrame, 0, ShadowRuleFlags::Unexplained }));
|
||||
EXPECT_EQ(ParseShadowRule("FirstFrame|Unexplained|Stray"), (ShadowRule { ShadowRuleType::FirstFrame, 0, ShadowRuleFlags::Unexplained | ShadowRuleFlags::Stray }));
|
||||
EXPECT_EQ(ParseShadowRule("FirstFrame+15|Stray"), (ShadowRule { ShadowRuleType::FirstFrame, 15, ShadowRuleFlags::Stray }));
|
||||
|
||||
const ShadowRule rule = *ParseShadowRule("FirstFrame|Stray");
|
||||
EXPECT_TRUE(HasAnyOf(rule.flags, ShadowRuleFlags::Stray));
|
||||
EXPECT_FALSE(HasAnyOf(rule.flags, ShadowRuleFlags::Unexplained));
|
||||
// The tags are notes: the shadow is extracted all the same.
|
||||
EXPECT_TRUE(rule.canExtractShadow());
|
||||
EXPECT_EQ(ParseShadowRule("FirstFrame")->flags, ShadowRuleFlags::None);
|
||||
}
|
||||
|
||||
TEST(ShadowRuleTest, RejectsInvalidValues)
|
||||
{
|
||||
EXPECT_FALSE(ParseShadowRule("firstframe").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame+").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame+0").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame+256").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame+1x").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame-1").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("EachFrame+1").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame|").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame|stray").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame|Stray|Other").has_value());
|
||||
EXPECT_FALSE(ParseShadowRule("Stray").has_value());
|
||||
// A comma separates the rules of a list, not a rule from its tags.
|
||||
EXPECT_FALSE(ParseShadowRule("FirstFrame,Stray").has_value());
|
||||
}
|
||||
|
||||
TEST(ShadowRuleTest, CanExtractShadow)
|
||||
{
|
||||
EXPECT_FALSE(ShadowRule {}.canExtractShadow());
|
||||
EXPECT_FALSE((ShadowRule { ShadowRuleType::None, 0 }).canExtractShadow());
|
||||
// A baked shadow is known but stays part of the sprite.
|
||||
EXPECT_FALSE((ShadowRule { ShadowRuleType::Baked, 0 }).canExtractShadow());
|
||||
EXPECT_TRUE((ShadowRule { ShadowRuleType::FirstFrame, 11 }).canExtractShadow());
|
||||
EXPECT_TRUE((ShadowRule { ShadowRuleType::EachFrame, 0 }).canExtractShadow());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace devilution
|
||||
@@ -83,6 +83,12 @@ TEST(TownerDat, LoadTownerData)
|
||||
ASSERT_GE(smith->animOrder.size(), 4u);
|
||||
EXPECT_EQ(smith->animOrder[0], 4);
|
||||
EXPECT_EQ(smith->animOrder[3], 7);
|
||||
EXPECT_EQ(smith->shadowRule, (ShadowRule { ShadowRuleType::FirstFrame, 0 }));
|
||||
|
||||
// Wirt's shadow has a few pixels that the rule does not account for.
|
||||
const TownerDataEntry *pegBoy = FindTownerDataByType(TOWN_PEGBOY);
|
||||
ASSERT_NE(pegBoy, nullptr) << "Should find TOWN_PEGBOY data";
|
||||
EXPECT_EQ(pegBoy->shadowRule, (ShadowRule { ShadowRuleType::FirstFrame, 0, ShadowRuleFlags::Stray }));
|
||||
|
||||
// Check Pepin (TOWN_HEALER)
|
||||
const TownerDataEntry *healer = FindTownerDataByType(TOWN_HEALER);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Research tool: searches for the transform that was used to generate
|
||||
# the baked-in shadows of the original player sprites.
|
||||
find_package(ZLIB REQUIRED)
|
||||
add_executable(shadow_search shadow_search.cpp)
|
||||
set_target_properties(shadow_search PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
target_include_directories(shadow_search PRIVATE "${PROJECT_SOURCE_DIR}/Source")
|
||||
target_link_dependencies(shadow_search
|
||||
PRIVATE
|
||||
tl
|
||||
libdevilutionx_cel_to_clx
|
||||
libdevilutionx_cl2_to_clx
|
||||
libdevilutionx_mpq
|
||||
ZLIB::ZLIB
|
||||
)
|
||||
|
||||
# Checks the game's shadow extraction (utils/clx_shadow.hpp) against the original sprites.
|
||||
add_executable(shadow_extract_check extract_check.cpp)
|
||||
set_target_properties(shadow_extract_check PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
target_include_directories(shadow_extract_check PRIVATE "${PROJECT_SOURCE_DIR}/Source")
|
||||
target_link_dependencies(shadow_extract_check
|
||||
PRIVATE
|
||||
tl
|
||||
libdevilutionx_cel_to_clx
|
||||
libdevilutionx_cl2_to_clx
|
||||
DevilutionX::SDL
|
||||
libdevilutionx_clx_render
|
||||
libdevilutionx_clx_shadow
|
||||
libdevilutionx_mpq
|
||||
libdevilutionx_surface
|
||||
)
|
||||
@@ -0,0 +1,606 @@
|
||||
<title>Diablo Sprite Shadows</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&family=Spectral:wght@500;600&display=swap">
|
||||
<style>
|
||||
/* Layout: one reading column; the sprite bench (4 enlarged views + 8-direction strip) breaks out to full width. */
|
||||
:root {
|
||||
--bg: #f3f1ee;
|
||||
--surface: #fbfaf8;
|
||||
--fg: #1d1a17;
|
||||
--muted: #6a625a;
|
||||
--rule: #d9d3cb;
|
||||
--accent: #8c2f1b;
|
||||
--stage: #8f8676; /* sprite backdrop: mid-tone so black shadows and bright armour both read */
|
||||
--stage-alt: #867d6d;
|
||||
--shadow-ok: #1f9d6b;
|
||||
--body-black: #3a6fd8;
|
||||
--orphan: #e0218a;
|
||||
--body-dim: #c9c2b6;
|
||||
--display: "Spectral", Georgia, "Times New Roman", serif;
|
||||
--body: "IBM Plex Sans", "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
--mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #171513; --surface: #201d1a; --fg: #ece7e0; --muted: #a39a8f; --rule: #3a352f; --accent: #e0785c;
|
||||
--stage: #8f8676; --stage-alt: #867d6d; --shadow-ok: #35c48b; --body-black: #6f9cf2; --orphan: #ff5cb0; --body-dim: #c9c2b6;
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #171513; --surface: #201d1a; --fg: #ece7e0; --muted: #a39a8f; --rule: #3a352f; --accent: #e0785c;
|
||||
--stage: #8f8676; --stage-alt: #867d6d; --shadow-ok: #35c48b; --body-black: #6f9cf2; --orphan: #ff5cb0; --body-dim: #c9c2b6;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
body { background: var(--bg); color: var(--fg); font-family: var(--body); font-size: 16px; line-height: 1.55; }
|
||||
.page { max-width: 1260px; margin: 0 auto; padding-inline: 20px; padding-block: 40px 64px; display: flex; flex-direction: column; gap: 40px; }
|
||||
.prose { max-width: 66ch; display: flex; flex-direction: column; gap: 14px; }
|
||||
h1, h2 { font-family: var(--display); font-weight: 600; text-wrap: balance; margin: 0; line-height: 1.15; }
|
||||
h1 { font-size: clamp(1.9rem, 4.5vw, 2.7rem); }
|
||||
h2 { font-size: 1.45rem; }
|
||||
p { margin: 0; }
|
||||
.eyebrow { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
||||
.lede { font-size: 1.1rem; }
|
||||
.byline { font-size: 0.9rem; color: var(--muted); }
|
||||
code, pre, .num { font-family: var(--mono); }
|
||||
code { font-size: 0.9em; }
|
||||
section { display: flex; flex-direction: column; gap: 18px; }
|
||||
|
||||
.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0; border-block: 1px solid var(--rule); margin: 0; }
|
||||
.facts div { padding: 14px 16px 14px 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.facts dt { font-size: 0.8rem; color: var(--muted); order: 2; }
|
||||
.facts dd { margin: 0; font-family: var(--mono); font-size: 1.35rem; font-weight: 500; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.code { overflow-x: auto; background: var(--surface); border: 1px solid var(--rule); border-left: 3px solid var(--accent); }
|
||||
.code pre { margin: 0; padding: 16px 18px; font-size: 0.88rem; line-height: 1.6; }
|
||||
.code .c { color: var(--muted); }
|
||||
|
||||
.controls { display: flex; flex-wrap: wrap; gap: 12px 20px; align-items: end; }
|
||||
.field { display: flex; flex-direction: column; gap: 4px; font-size: 0.8rem; color: var(--muted); min-width: 0; }
|
||||
select, button, input[type="range"] { font: inherit; }
|
||||
select { font-size: 0.95rem; color: var(--fg); background: var(--surface); border: 1px solid var(--rule); padding: 7px 10px; max-width: 100%; }
|
||||
button { font-size: 0.95rem; color: var(--fg); background: var(--surface); border: 1px solid var(--rule); padding: 7px 14px; cursor: pointer; }
|
||||
button:hover { border-color: var(--accent); }
|
||||
select:focus-visible, button:focus-visible, input:focus-visible, .dir:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.scrub { flex: 1 1 200px; }
|
||||
.scrub input { width: 100%; accent-color: var(--accent); }
|
||||
|
||||
.views { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
|
||||
.view { display: flex; flex-direction: column; gap: 6px; min-width: 0; margin: 0; }
|
||||
.view figcaption { display: flex; flex-direction: column; gap: 2px; }
|
||||
.view strong { font-weight: 600; font-size: 0.95rem; }
|
||||
.view span { font-size: 0.8rem; color: var(--muted); }
|
||||
canvas { display: block; width: 100%; height: auto; image-rendering: pixelated; image-rendering: crisp-edges; image-rendering: pixelated;
|
||||
background-color: var(--stage);
|
||||
background-image: linear-gradient(45deg, var(--stage-alt) 25%, transparent 25%, transparent 75%, var(--stage-alt) 75%), linear-gradient(45deg, var(--stage-alt) 25%, transparent 25%, transparent 75%, var(--stage-alt) 75%);
|
||||
background-size: 16px 16px; background-position: 0 0, 8px 8px; }
|
||||
|
||||
.legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 0.85rem; color: var(--muted); padding: 0; margin: 0; list-style: none; }
|
||||
.legend li { display: flex; align-items: center; gap: 6px; }
|
||||
.sw { width: 12px; height: 12px; display: inline-block; }
|
||||
|
||||
.readout { overflow-x: auto; }
|
||||
table { border-collapse: collapse; font-size: 0.88rem; font-variant-numeric: tabular-nums; }
|
||||
th, td { text-align: right; padding: 6px 14px 6px 0; border-bottom: 1px solid var(--rule); white-space: nowrap; }
|
||||
th { font-weight: 500; color: var(--muted); font-size: 0.8rem; }
|
||||
th:first-child, td:first-child { text-align: left; }
|
||||
td { font-family: var(--mono); }
|
||||
|
||||
.dirs { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 8px; }
|
||||
@media (max-width: 720px) { .dirs { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
||||
.dir { padding: 0; border: 1px solid var(--rule); background: var(--surface); display: flex; flex-direction: column; min-width: 0; }
|
||||
.dir[aria-pressed="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
||||
.dir small { font-family: var(--mono); font-size: 0.72rem; padding: 4px 6px; display: flex; justify-content: space-between; gap: 4px; color: var(--muted); }
|
||||
.dir small b { font-weight: 500; color: var(--fg); }
|
||||
|
||||
ul.notes { margin: 0; padding-left: 1.1em; display: flex; flex-direction: column; gap: 8px; }
|
||||
a { color: var(--accent); }
|
||||
|
||||
.masthead { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 24px 48px; }
|
||||
.masthead .prose { flex: 1 1 420px; }
|
||||
.hero { flex: 0 1 320px; margin: 0 auto; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
||||
.hero figcaption { font-size: 0.8rem; color: var(--muted); }
|
||||
.case { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 12px 48px; padding-top: 18px; border-top: 1px solid var(--rule); }
|
||||
.case .prose { flex: 1 1 420px; }
|
||||
.case .hero { flex-basis: 240px; }
|
||||
</style>
|
||||
|
||||
<div class="page">
|
||||
<header class="masthead">
|
||||
<div class="prose">
|
||||
<p class="eyebrow">DevilutionX research notes</p>
|
||||
<h1>How Diablo drew its sprite shadows</h1>
|
||||
<p class="byline">By Gleb Mazovetskiy (<a href="https://github.com/glebm">GitHub</a>, <a href="https://x.com/glebm">X</a>) and <a href="https://claude.com/claude-code">Claude</a>, Anthropic's AI model</p>
|
||||
<p class="lede">The shadow under every Diablo character is part of its sprite, stored as black pixels. Each shadow is a copy of the sprite's own outline, squashed to a third of its height and sheared to the left, so a few lines of integer arithmetic can recompute it from the rest of the frame. The same function works for players, monsters, townspeople and dungeon objects, in both Diablo and the Hellfire expansion.</p>
|
||||
</div>
|
||||
<figure class="hero">
|
||||
<canvas data-sprite="m:diablow" data-mode="orig" aria-label="Diablo walking, with the shadow stored in the sprite"></canvas>
|
||||
<figcaption>Diablo's walk animation as stored in the game data. The black shape at his feet is part of each frame.</figcaption>
|
||||
</figure>
|
||||
</header>
|
||||
|
||||
<dl class="facts">
|
||||
<div><dd>139,943</dd><dt>frames checked, from 1,438 sprite files in Diablo and Hellfire</dt></div>
|
||||
<div><dd>99.77%</dd><dt>of the 136,936 frames with a shadow are reproduced pixel for pixel</dt></div>
|
||||
<div><dd>763 of 765</dd><dt>original player files in which every frame is reproduced exactly</dt></div>
|
||||
<div><dd>30,744 of 30,744</dd><dt>frames of the Hellfire monk reproduced exactly</dt></div>
|
||||
<div><dd>3</dd><dt>rules for placing the ground line, which is the function's only input besides the sprite</dt></div>
|
||||
</dl>
|
||||
|
||||
<section class="masthead">
|
||||
<div class="prose">
|
||||
<h2>The shadows were added in 2D</h2>
|
||||
<p>Diablo's characters and monsters were modelled and animated in 3D, then rendered to small 2D frames, one set per facing direction. The game ships only those frames. Each one is a grid of palette indices, and its shadow is part of it, as a patch of pure black pixels next to the feet.</p>
|
||||
<p>These shadows did not come from the 3D renderer. A rendered shadow would depend on the depth of the model, and the ones in the game files can be computed from the flat frame alone. Each is an affine transform of the 2D sprite: the silhouette is sheared to the left, squashed to a third of its height and filled with black. Done in integer arithmetic, with the rounding described in the next section, the transform matches the game's art to the pixel.</p>
|
||||
<p>It is applied to every pixel that is not transparent, including black pixels of the body such as dark boots, so it must have run on the finished 2D image.</p>
|
||||
</div>
|
||||
<figure class="hero">
|
||||
<canvas data-sprite="m:diablow" data-mode="body" aria-label="Diablo walking, with the shadow removed"></canvas>
|
||||
<figcaption>The same animation with the shadow removed. The function recomputes the shadow from these pixels alone.</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="prose">
|
||||
<h2>The function</h2>
|
||||
<p>The function takes the sprite and one number: <code>B</code>, the row of the ground line that the shadow is measured from. Rows are numbered downwards from the top of the frame.</p>
|
||||
</div>
|
||||
<div class="code"><pre><span class="c">// B: the row of the ground line, chosen by one of the rules further down.</span>
|
||||
<span class="c">// Called for every pixel (x, y) of the shadowless sprite with y <= B.</span>
|
||||
<span class="c">// Pixels below B cast nothing. Both divisions are integer divisions.</span>
|
||||
int v = (B - y) / 3; <span class="c">// three sprite rows collapse into one shadow row</span>
|
||||
int ys = B - 2 - v; <span class="c">// the shadow starts two rows above the ground line</span>
|
||||
int xs = x - (3 * v) / 4; <span class="c">// sheared left by 3/4 px per shadow row</span>
|
||||
shadow[ys][xs] = BLACK; <span class="c">// the sprite is then drawn on top</span></pre></div>
|
||||
<div class="prose">
|
||||
<p>In matrix form, with <code>h = B - y</code> for the height of a pixel above the ground line, this is a shear combined with a vertical squash:</p>
|
||||
</div>
|
||||
<div class="code"><pre>[ x' ] [ 1 -1/4 ] [ x ]
|
||||
[ h' ] = [ 0 1/3 ] [ h ] y' = B - 2 - h'</pre></div>
|
||||
<div class="prose">
|
||||
<p>A pixel moves left by a quarter of its height and keeps a third of its height. The matrix only approximates the code, which rounds twice: it first reduces the height to a whole shadow row and then computes the shift from that row. Rounding the matrix product once puts some pixels in a different column, up to 1.25 pixels away. Only the two-step integer version matches the game's art exactly.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="prose">
|
||||
<h2>Try it on the original sprites</h2>
|
||||
<p>This page runs the function in your browser. For each original frame it removes the shadow, casts a new one from what is left, and compares the result with the original.</p>
|
||||
</div>
|
||||
<p id="loadError" hidden>The sprites could not be unpacked. This page needs a browser with <code>DecompressionStream</code>, which all current browsers have.</p>
|
||||
<div class="controls">
|
||||
<label class="field" for="sprite">Sprite
|
||||
<select id="sprite"></select>
|
||||
</label>
|
||||
<button id="play" type="button">Pause</button>
|
||||
<label class="field scrub" for="frame"><span id="frameLabel">Frame</span>
|
||||
<input id="frame" type="range" min="0" max="0" value="0">
|
||||
</label>
|
||||
</div>
|
||||
<div class="views">
|
||||
<figure class="view"><canvas id="cOrig"></canvas><figcaption><strong>Original</strong><span>As stored in the game data</span></figcaption></figure>
|
||||
<figure class="view"><canvas id="cBody"></canvas><figcaption><strong>Shadow removed</strong><span>The frame as it was before the shadow pass</span></figcaption></figure>
|
||||
<figure class="view"><canvas id="cRegen"></canvas><figcaption><strong>Reproduced</strong><span>Shadow cast by the function, sprite drawn on top</span></figcaption></figure>
|
||||
<figure class="view"><canvas id="cDiff"></canvas><figcaption><strong>Where each black pixel comes from</strong><span id="diffNote"> </span></figcaption></figure>
|
||||
</div>
|
||||
<ul class="legend">
|
||||
<li><span class="sw" style="background:var(--shadow-ok)"></span>shadow, reproduced</li>
|
||||
<li><span class="sw" style="background:var(--body-black)"></span>black pixel of the body</li>
|
||||
<li><span class="sw" style="background:var(--orphan)"></span>unexplained: black pixel that nothing in the sprite casts</li>
|
||||
<li><span class="sw" style="background:#f00"></span>stray: shadow cast onto a transparent pixel</li>
|
||||
<li><span class="sw" style="background:var(--body-dim)"></span>rest of the body</li>
|
||||
</ul>
|
||||
<div class="dirs" id="dirs"></div>
|
||||
<div class="readout">
|
||||
<table>
|
||||
<thead><tr><th>Direction</th><th>Frames</th><th>Ground line rule</th><th>Row B</th><th>Black pixels</th><th>Reproduced shadow</th><th>Body black</th><th>Unexplained</th><th>Stray</th><th>Exact frames</th></tr></thead>
|
||||
<tbody id="readout"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>Five sprites are in the list because the function does not fully explain them: the warrior with only a shield, standing and walking in town, Wirt, Na-Krul's death animation, and the cauldron. They are covered under <a href="#exceptions">What is not reproduced</a>. Every sprite is drawn with the town palette, so the colours of dungeon monsters may be slightly off.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="prose">
|
||||
<h2>How the ground line is chosen</h2>
|
||||
<p>The ground line is always derived from the art, in one of three ways, and the choice differs from file to file. The table counts animation directions of the original game's monsters.</p>
|
||||
</div>
|
||||
<div class="readout">
|
||||
<table>
|
||||
<thead><tr><th>Ground line row B</th><th>Monster directions</th><th style="text-align:left">Used by</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Lowest opaque row of the first frame</td><td>1,223</td><td style="text-align:left">all original players, most monsters, townspeople, barrels, levers, sarcophagi</td></tr>
|
||||
<tr><td>Lowest opaque row of each frame</td><td>75</td><td style="text-align:left">golem, some Diablo, mage, snake and mega demon animations, chests, books, nearly all Hellfire art</td></tr>
|
||||
<tr><td>First frame's lowest row plus a constant</td><td>72</td><td style="text-align:left">flying monsters: bat +15 rows, gargoyle +11</td></tr>
|
||||
<tr><td>No shadow in the art</td><td>80</td><td style="text-align:left">two monsters with no black pixels at all</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>Under the first-frame rule the shadow stays where it is while the body moves. Under the each-frame rule it follows a body that bobs up and down, as the golem's does.</p>
|
||||
<p>For flying monsters the ground line lies below the sprite, which separates the shadow from the body. The offsets of 15 and 11 rows make more sense when measured from the body. The ground line is exactly 40 rows below the lowest non-black row of the first frame for the bat, in all 32 directions checked, and 28 rows below it for the gargoyle, in all 40. In those first frames the lowest opaque row is the bottom of the shadow, not of the body, which is why the offset from it comes out as +15 and +11.</p>
|
||||
<p>We found no property of a sprite that predicts which rule its file uses. Animations of the same monster can use different rules, and no single rule covers both the first-frame and the each-frame files. Our guess is that the choice reflects how each file was processed at the time.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="prose">
|
||||
<h2>Hellfire</h2>
|
||||
<p>Hellfire was made by a different studio a year later, and it uses the same function. Its ground line is almost always the lowest opaque row of each frame, where the original game mostly uses the first frame.</p>
|
||||
<ul class="notes">
|
||||
<li>The monk has 291 files and 30,744 frames. With the each-frame rule every frame is reproduced pixel for pixel. The first-frame rule works only where the bottom row never moves, which is the case in 536 of 2,328 directions.</li>
|
||||
<li>Of the 125 Hellfire monster files checked, 114 have a shadow. All 114 fit the each-frame rule with no stray pixels, and none needs the first-frame rule. Four of them leave unexplained black pixels, 4,815 in total, nearly all in two Na-Krul animations.</li>
|
||||
<li>The other 11 are death animations with no cast shadow. Six have no black pixels at all. In the other five the few black pixels are painted detail: Hork Spawn, Hell Bug, Hellbat, Arch Lich and Hellfire's Satyr Lord. Black makes up at most 0.5% of their pixels, against 15% or more in death animations that have a shadow.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="prose">
|
||||
<h2>Results by sprite set</h2>
|
||||
<p>Each row is one set of sprite files, measured with the ground line rule that the DevilutionX data files record for each file. The rule column counts files. The table above counts monster animation directions, so the two do not match.</p>
|
||||
</div>
|
||||
<div class="readout">
|
||||
<table>
|
||||
<thead><tr><th style="text-align:left">Sprite set</th><th>Files</th><th>Frames</th><th>Files with a shadow</th><th>First / each / first + offset</th><th>Files with no shadow</th><th>Frames with a shadow reproduced exactly</th><th>Stray pixels</th><th>Unexplained pixels</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="text-align:left">Players, original game</td><td>765</td><td>78,016</td><td>765</td><td>765 / 0 / 0</td><td>0</td><td>77,872 of 78,016 (99.82%)</td><td>771</td><td>1,333</td></tr>
|
||||
<tr><td style="text-align:left">Monk, Hellfire</td><td>291</td><td>30,744</td><td>291</td><td>0 / 291 / 0</td><td>0</td><td>30,744 of 30,744 (100%)</td><td>0</td><td>0</td></tr>
|
||||
<tr><td style="text-align:left">Monsters, original game</td><td>183</td><td>18,088</td><td>173</td><td>151 / 13 / 9</td><td>10</td><td>16,912 of 16,912 (100%)</td><td>0</td><td>0</td></tr>
|
||||
<tr><td style="text-align:left">Monsters, Hellfire</td><td>125</td><td>12,424</td><td>114</td><td>1 / 113 / 0</td><td>11</td><td>10,717 of 10,872 (98.57%)</td><td>0</td><td>4,815</td></tr>
|
||||
<tr><td style="text-align:left">Townspeople, original game</td><td>9</td><td>160</td><td>8</td><td>8 / 0 / 0</td><td>1</td><td>132 of 152 (86.84%)</td><td>38</td><td>0</td></tr>
|
||||
<tr><td style="text-align:left">Townspeople, Hellfire</td><td>3</td><td>50</td><td>3</td><td>3 / 0 / 0</td><td>0</td><td>50 of 50 (100%)</td><td>0</td><td>0</td></tr>
|
||||
<tr><td style="text-align:left">Objects, original game</td><td>53</td><td>396</td><td>28</td><td>15 / 13 / 0</td><td>25</td><td>161 of 161 (100%)</td><td>0</td><td>0</td></tr>
|
||||
<tr><td style="text-align:left">Objects, Hellfire</td><td>9</td><td>65</td><td>3</td><td>1 / 2 / 0</td><td>6</td><td>29 of 29 (100%)</td><td>0</td><td>0</td></tr>
|
||||
<tr><td style="text-align:left"><strong>All sprite sets</strong></td><td><strong>1,438</strong></td><td><strong>139,943</strong></td><td><strong>1,385</strong></td><td><strong>944 / 432 / 9</strong></td><td><strong>53</strong></td><td><strong>136,617 of 136,936 (99.77%)</strong></td><td><strong>809</strong></td><td><strong>6,148</strong></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<ul class="notes">
|
||||
<li>A frame is <strong>reproduced exactly</strong> when removing the shadow, casting a new one with the function and drawing the body on top gives back the original frame, pixel for pixel. Only frames of files with a shadow are counted.</li>
|
||||
<li><strong>Stray pixels</strong> are transparent pixels that the function casts a shadow onto. Each pixel is counted once.</li>
|
||||
<li><strong>Unexplained pixels</strong> are black pixels that are neither reproduced shadow nor body. Nothing left in the sprite casts them, and if they were body pixels they would cast a stray shadow themselves.</li>
|
||||
<li>The rule for each file is the one recorded in the DevilutionX data files. A file recorded as having no shadow is counted that way even if it has black pixels. This applies to 13 files: five Hellfire death animations, six original objects and two Hellfire objects.</li>
|
||||
<li>The Hellfire rows count only the files found in the Hellfire archives. The Hellfire object row uses the same object list as the original game.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="exceptions">
|
||||
<div class="prose">
|
||||
<h2>What is not reproduced</h2>
|
||||
<p>Of the 136,936 frames with a shadow, 319 are not reproduced exactly. They fall into four groups. A fifth case, the cauldron, is reproduced exactly but cannot be split cleanly into body and shadow. The sprites on the right are drawn as stored, with unexplained pixels marked in pink and stray pixels in red.</p>
|
||||
</div>
|
||||
<div class="case">
|
||||
<div class="prose">
|
||||
<p><strong>The warrior with only a shield, standing in town in heavy armour.</strong> This file leaves 771 stray and 1,318 unexplained pixels over its eight directions, and we have not found the reason. Five directions fit best with the normal ground line and three with one a row higher, but no direction is exact. The errors are on the edges of the shield's shadow. We tried and ruled out a different ground line per frame, a shifted shadow, casting from another frame or another weapon variant, a separately shadowed shield layer with its own ground line or position, and two ground lines split by row. Our best guess is that the shield was redrawn after the shadow pass, but we have no direct evidence for it.</p>
|
||||
</div>
|
||||
<figure class="hero">
|
||||
<canvas data-sprite="p:whust" data-dir="5" data-mode="errors" aria-label="Warrior with a shield standing in town, with the pixels the function does not reproduce marked"></canvas>
|
||||
<figcaption>Facing north-east, the direction with the most errors.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="case">
|
||||
<div class="prose">
|
||||
<p><strong>The same warrior walking in light armour.</strong> One direction leaves 15 unexplained pixels, two or three per frame on the upper left edge of the shadow, in seven of its eight frames.</p>
|
||||
</div>
|
||||
<figure class="hero">
|
||||
<canvas data-sprite="p:wluwl" data-dir="3" data-mode="errors" aria-label="Warrior with a shield walking in town, with the unexplained pixels marked"></canvas>
|
||||
<figcaption>Facing north-west, the only direction with errors.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="case">
|
||||
<div class="prose">
|
||||
<p><strong>Wirt.</strong> He fits the first-frame rule except for 38 stray pixels over his 20 frames. They are cast by 44 pixels on the edge of his outline, one of them next to the peg leg in every frame. No other ground line does better. Those edge pixels were probably added after the shadow pass, though again we have no direct evidence.</p>
|
||||
</div>
|
||||
<figure class="hero">
|
||||
<canvas data-sprite="t:pegkid1" data-dir="0" data-mode="errors" aria-label="Wirt, with the stray shadow pixels marked"></canvas>
|
||||
<figcaption>Wirt has a single view. Every frame has one to four stray pixels.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="case">
|
||||
<div class="prose">
|
||||
<p><strong>Four Hellfire monster files.</strong> They have no stray pixels but leave 4,815 unexplained black pixels, 4,787 of them in Na-Krul's death and special animations. We have not investigated these yet.</p>
|
||||
</div>
|
||||
<figure class="hero">
|
||||
<canvas data-sprite="m:nkrd" data-dir="0" data-mode="errors" aria-label="Na-Krul's death animation, with the unexplained pixels marked"></canvas>
|
||||
<figcaption>Na-Krul's death animation, facing south.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="case">
|
||||
<div class="prose">
|
||||
<p><strong>The cauldron.</strong> All three frames count as reproduced exactly, but the split into body and shadow is wrong. The pot is nearly black: 664 to 802 of its pixels per frame are pure black, and 365 of them lie where the pixels above them cast a shadow. A black body pixel in that position cannot be told apart from shadow, so the function takes the lower part of the pot for shadow, and removing the shadow removes it too.</p>
|
||||
</div>
|
||||
<figure class="hero">
|
||||
<canvas data-sprite="o:cauldren" data-dir="0" data-mode="body" aria-label="The cauldron with its shadow removed, missing part of its base"></canvas>
|
||||
<figcaption>The cauldron with the shadow removed. The gaps in its base are body pixels that were taken for shadow.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>Some sprites have black pixels and no cast shadow at all. In the original game these are two door types, the bookcase, the trap hole and the mushroom patch. Their black pixels are painted cracks, outlines and holes, and the area where a shadow would fall is empty. The fountain of tears has eight frames: the four that the game plays have no black pixels, and the other four carry a shadow that the function reproduces exactly. Hellfire's two pod objects have four black pixels each and no shadow.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="prose">
|
||||
<h2>How it was found</h2>
|
||||
<ul class="notes">
|
||||
<li>The starting point was Joan Karadimov's description of the transform and an approximate matrix for it, with a height scale near 1/3 and a shear near 0.26, posted in <a href="https://github.com/diasurgical/DevilutionX/issues/8718#issuecomment-5893938916">a comment on DevilutionX issue #8718</a>. Fitting a single transform, in several number formats, got close but never exact.</li>
|
||||
<li>The next pass assumed no formula. For each sprite row it searched for the shadow row and horizontal shift that never put a shadow pixel on a transparent one. Across all frames exactly one combination survived per row, and together they form the table that the function above generates.</li>
|
||||
<li>The ground line was first taken to be the lowest non-black row, which fits the shareware warrior. The rogue's heavy armour has black boots, and for it only the lowest opaque row works.</li>
|
||||
<li>A black body pixel that lies inside the shadow of the pixels above it cannot be told apart from shadow. Where such a pixel is in the right place to cast an otherwise unexplained shadow pixel, the page counts it as body. In the original player sprites that leaves 1,333 black pixels unexplained, out of 23 million.</li>
|
||||
<li>Monsters and objects were classified by the rule under which their non-black pixels cast no stray shadow. Objects are weaker evidence because most have only a few frames. Of 34 original objects with black pixels, 15 fit the first-frame rule and 13 the each-frame rule.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const DATA = /*SHADOW_DATA*/;
|
||||
|
||||
const CLASSES = { w: 'Warrior', r: 'Rogue', s: 'Sorcerer' };
|
||||
const ARMOURS = { l: 'light', m: 'medium', h: 'heavy' };
|
||||
const ANIMS = { as: 'Stand, dungeon', st: 'Stand, town', aw: 'Walk, dungeon', wl: 'Walk, town', at: 'Attack', ht: 'Hit', lm: 'Lightning spell', fm: 'Fire spell', qm: 'Magic spell', dt: 'Death', bl: 'Block' };
|
||||
const WEAPONS = { n: 'unarmed', u: 'shield', s: 'sword', m: 'mace', b: 'bow', a: 'axe', d: 'sword and shield', t: 'staff', h: 'mace and shield' };
|
||||
const DIRS = ['S', 'SW', 'W', 'NW', 'N', 'NE', 'E', 'SE'];
|
||||
const T = 255; // transparent
|
||||
|
||||
function bytes(b64) {
|
||||
const s = atob(b64), out = new Uint8Array(s.length);
|
||||
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
const palette = bytes(DATA.palette);
|
||||
|
||||
// The pixels of all frames are one zlib stream, which the browser inflates.
|
||||
async function inflate(data) {
|
||||
const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream('deflate'));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
|
||||
// The shadow function. Returns the index of the shadow pixel, -1 if (x, y) casts none
|
||||
// and -2 if the shadow falls outside the frame.
|
||||
function castTo(x, y, B, w, h) {
|
||||
if (y > B) return -1;
|
||||
const v = Math.trunc((B - y) / 3);
|
||||
const ys = B - 2 - v;
|
||||
const xs = x - Math.trunc((3 * v) / 4);
|
||||
if (xs < 0 || xs >= w || ys < 0 || ys >= h) return -2;
|
||||
return ys * w + xs;
|
||||
}
|
||||
|
||||
function baselineOf(px, w, h) {
|
||||
for (let y = h - 1; y >= 0; y--)
|
||||
for (let x = 0; x < w; x++)
|
||||
if (px[y * w + x] !== T) return y;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Splits a frame into body and shadow, then casts the shadow again.
|
||||
function analyse(px, w, h, B) {
|
||||
const n = w * h;
|
||||
const body = new Uint8Array(n), shadow = new Uint8Array(n), orphan = new Uint8Array(n);
|
||||
for (let y = 0; y < h; y++)
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = y * w + x;
|
||||
if (px[i] === T || px[i] === 0) continue;
|
||||
body[i] = 1;
|
||||
const t = castTo(x, y, B, w, h);
|
||||
if (t >= 0) shadow[t] = 1;
|
||||
}
|
||||
// A black pixel that no pixel above it shadows must be part of the body, and casts too.
|
||||
for (let y = 0; y < h; y++) {
|
||||
const row = [];
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = y * w + x;
|
||||
if (px[i] === 0 && !shadow[i]) row.push(x);
|
||||
}
|
||||
for (const x of row) {
|
||||
const i = y * w + x, t = castTo(x, y, B, w, h);
|
||||
if (t >= 0 && px[t] === T) {
|
||||
// A black pixel hidden in the shadow above may be the body pixel that casts this one.
|
||||
const v = B - 2 - y, sx = x + Math.trunc((3 * v) / 4);
|
||||
let found = false;
|
||||
for (let sy = B - 3 * v - 2; v >= 2 && sy <= B - 3 * v && !found; sy++) {
|
||||
const j = sy * w + sx;
|
||||
if (sx < 0 || sx >= w || sy < 0 || sy >= h || px[j] !== 0 || body[j] || orphan[j]) continue;
|
||||
body[j] = 1;
|
||||
shadow[i] = 1;
|
||||
found = true;
|
||||
}
|
||||
if (!found) orphan[i] = 1;
|
||||
continue;
|
||||
}
|
||||
body[i] = 1;
|
||||
if (t >= 0) shadow[t] = 1;
|
||||
}
|
||||
}
|
||||
const s = { black: 0, shadow: 0, bodyBlack: 0, orphan: 0, stray: 0 };
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (px[i] === 0) {
|
||||
s.black++;
|
||||
if (orphan[i]) s.orphan++; else if (body[i]) s.bodyBlack++; else s.shadow++;
|
||||
}
|
||||
if (shadow[i] && px[i] === T) s.stray++;
|
||||
}
|
||||
s.exact = s.orphan === 0 && s.stray === 0;
|
||||
return { body, shadow, orphan, stats: s };
|
||||
}
|
||||
|
||||
// Decodes every sprite and analyses every frame up front. `pixels` holds the cropped frames
|
||||
// back to back, in the order they are listed.
|
||||
const groups = new Map(); // name -> [per direction]
|
||||
function decode(pixels) {
|
||||
let at = 0;
|
||||
for (const sp of DATA.sprites) {
|
||||
const frames = sp.frames.map(([x0, y0, cw, ch]) => {
|
||||
const px = new Uint8Array(sp.w * sp.h).fill(T);
|
||||
for (let y = 0; y < ch; y++, at += cw) px.set(pixels.subarray(at, at + cw), (y0 + y) * sp.w + x0);
|
||||
return { px };
|
||||
});
|
||||
// The baseline is read from the first frame (plus a constant for flying monsters) or from each frame.
|
||||
const B = baselineOf(frames[0].px, sp.w, sp.h) + sp.offset;
|
||||
const total = { frames: frames.length, black: 0, shadow: 0, bodyBlack: 0, orphan: 0, stray: 0, exact: 0 };
|
||||
for (const f of frames) {
|
||||
f.B = sp.own ? baselineOf(f.px, sp.w, sp.h) : B;
|
||||
Object.assign(f, analyse(f.px, sp.w, sp.h, f.B));
|
||||
for (const k of ['black', 'shadow', 'bodyBlack', 'orphan', 'stray']) total[k] += f.stats[k];
|
||||
if (f.stats.exact) total.exact++;
|
||||
}
|
||||
const key = sp.kind + ':' + sp.name;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
const rule = sp.own ? 'each frame' : sp.offset ? `first frame + ${sp.offset}` : 'first frame';
|
||||
groups.get(key)[sp.dir] = { w: sp.w, h: sp.h, kind: sp.kind, label: sp.label, rule, own: sp.own, frames, total };
|
||||
}
|
||||
}
|
||||
|
||||
function cssColor(name) {
|
||||
const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
const m = /^#([0-9a-f]{6})$/i.exec(v);
|
||||
const n = m ? parseInt(m[1], 16) : 0x808080;
|
||||
return [n >> 16, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
|
||||
function themeColors() {
|
||||
return { ok: cssColor('--shadow-ok'), bodyBlack: cssColor('--body-black'), orphan: cssColor('--orphan'), dim: cssColor('--body-dim') };
|
||||
}
|
||||
|
||||
// Modes: 'orig' as stored, 'body' without the shadow, 'regen' with the shadow cast by the function,
|
||||
// 'errors' as stored with the unexplained and stray pixels marked, anything else the classification.
|
||||
function paint(canvas, g, f, mode, colors) {
|
||||
if (canvas.width !== g.w || canvas.height !== g.h) { canvas.width = g.w; canvas.height = g.h; }
|
||||
const ctx = canvas.getContext('2d'), img = ctx.createImageData(g.w, g.h), d = img.data;
|
||||
const put = (i, r, gg, b) => { d[i * 4] = r; d[i * 4 + 1] = gg; d[i * 4 + 2] = b; d[i * 4 + 3] = 255; };
|
||||
const pal = (i, c) => put(i, palette[c * 3], palette[c * 3 + 1], palette[c * 3 + 2]);
|
||||
for (let i = 0; i < g.w * g.h; i++) {
|
||||
const c = f.px[i];
|
||||
if (mode === 'orig') { if (c !== T) pal(i, c); }
|
||||
else if (mode === 'body') { if (f.body[i]) pal(i, c); }
|
||||
else if (mode === 'regen') { if (f.body[i]) pal(i, c); else if (f.shadow[i]) put(i, 0, 0, 0); }
|
||||
else if (mode === 'errors') { if (f.orphan[i]) put(i, ...colors.orphan); else if (c !== T) pal(i, c); else if (f.shadow[i]) put(i, 255, 0, 0); }
|
||||
else {
|
||||
if (f.orphan[i]) put(i, ...colors.orphan);
|
||||
else if (f.body[i]) put(i, ...(c === 0 ? colors.bodyBlack : colors.dim));
|
||||
else if (f.shadow[i]) put(i, ...(c === T ? [255, 0, 0] : colors.ok));
|
||||
}
|
||||
}
|
||||
ctx.putImageData(img, 0, 0);
|
||||
}
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const state = { name: null, dir: 0, frame: 0, playing: true };
|
||||
const dirCanvases = [];
|
||||
|
||||
const KINDS = { p: 'Players', m: 'Monsters', t: 'Townspeople', o: 'Objects' };
|
||||
|
||||
function label(key) {
|
||||
const g = groups.get(key)[0], name = key.slice(2);
|
||||
if (g.label) return g.label;
|
||||
return `${CLASSES[name[0]]}, ${ARMOURS[name[1]]} armour · ${ANIMS[name.slice(3)] || name.slice(3)} · ${WEAPONS[name[2]] || name[2]}`;
|
||||
}
|
||||
|
||||
function current() { return groups.get(state.name)[state.dir]; }
|
||||
|
||||
function render() {
|
||||
const colors = themeColors();
|
||||
const g = current(), f = g.frames[state.frame % g.frames.length];
|
||||
paint($('cOrig'), g, f, 'orig', colors);
|
||||
paint($('cBody'), g, f, 'body', colors);
|
||||
paint($('cRegen'), g, f, 'regen', colors);
|
||||
paint($('cDiff'), g, f, 'diff', colors);
|
||||
const s = f.stats;
|
||||
$('diffNote').textContent = s.exact ? `Frame ${state.frame + 1}: identical to the original` : `Frame ${state.frame + 1}: ${s.orphan + s.stray} pixel${s.orphan + s.stray === 1 ? '' : 's'} differ`;
|
||||
$('frameLabel').textContent = `Frame ${state.frame + 1} of ${g.frames.length}`;
|
||||
$('frame').value = state.frame;
|
||||
const all = groups.get(state.name);
|
||||
if (all.length > 1) all.forEach((dg, d) => paint(dirCanvases[d], dg, dg.frames[state.frame % dg.frames.length], 'regen', colors));
|
||||
}
|
||||
|
||||
function selectDir(d) {
|
||||
state.dir = d;
|
||||
document.querySelectorAll('.dir').forEach((el, i) => el.setAttribute('aria-pressed', String(i === d)));
|
||||
const g = current(), t = g.total;
|
||||
$('frame').max = g.frames.length - 1;
|
||||
state.frame %= g.frames.length;
|
||||
const single = groups.get(state.name).length === 1;
|
||||
const row = g.own ? 'varies' : g.frames[0].B;
|
||||
$('readout').innerHTML = `<tr><td>${single ? 'only view' : DIRS[d]}</td><td>${t.frames}</td><td>${g.rule}</td><td>${row}</td><td>${t.black}</td><td>${t.shadow}</td><td>${t.bodyBlack}</td><td>${t.orphan}</td><td>${t.stray}</td><td>${t.exact} / ${t.frames}</td></tr>`;
|
||||
render();
|
||||
}
|
||||
|
||||
function selectSprite(name) {
|
||||
state.name = name;
|
||||
state.frame = 0;
|
||||
const all = groups.get(name);
|
||||
// Sprites with a single view (townspeople, objects) need no direction strip.
|
||||
$('dirs').hidden = all.length === 1;
|
||||
all.forEach((dg, d) => {
|
||||
const exact = dg.total.exact === dg.total.frames;
|
||||
document.querySelectorAll('.dir small')[d].innerHTML = `<b>${DIRS[d]}</b><span>${exact ? 'exact' : (dg.total.orphan + dg.total.stray) + ' px'}</span>`;
|
||||
});
|
||||
selectDir(state.dir < all.length ? state.dir : 0);
|
||||
}
|
||||
|
||||
function start() {
|
||||
const optgroups = {};
|
||||
for (const key of groups.keys()) {
|
||||
const kind = key[0];
|
||||
if (!optgroups[kind]) {
|
||||
optgroups[kind] = document.createElement('optgroup');
|
||||
optgroups[kind].label = KINDS[kind];
|
||||
$('sprite').appendChild(optgroups[kind]);
|
||||
}
|
||||
const o = document.createElement('option');
|
||||
o.value = key;
|
||||
o.textContent = label(key);
|
||||
optgroups[kind].appendChild(o);
|
||||
}
|
||||
for (let d = 0; d < 8; d++) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'dir';
|
||||
b.id = 'dir' + d;
|
||||
b.setAttribute('aria-label', 'Direction ' + DIRS[d]);
|
||||
const c = document.createElement('canvas');
|
||||
dirCanvases.push(c);
|
||||
b.append(c, document.createElement('small'));
|
||||
b.addEventListener('click', () => selectDir(d));
|
||||
$('dirs').appendChild(b);
|
||||
}
|
||||
$('sprite').addEventListener('change', (e) => selectSprite(e.target.value));
|
||||
$('play').addEventListener('click', () => { state.playing = !state.playing; $('play').textContent = state.playing ? 'Pause' : 'Play'; });
|
||||
$('frame').addEventListener('input', (e) => { state.frame = +e.target.value; state.playing = false; $('play').textContent = 'Play'; render(); });
|
||||
|
||||
const still = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (still) { state.playing = false; $('play').textContent = 'Play'; }
|
||||
selectSprite(groups.keys().next().value);
|
||||
|
||||
// Sprites shown next to the text: canvases with data-sprite, data-dir and data-mode. They ignore
|
||||
// the controls. Without animation each shows the frame that differs most from the original.
|
||||
const examples = [];
|
||||
for (const canvas of document.querySelectorAll('canvas[data-sprite]')) {
|
||||
const g = (groups.get(canvas.dataset.sprite) || [])[+canvas.dataset.dir || 0];
|
||||
if (!g) { canvas.closest('figure').hidden = true; continue; }
|
||||
const errors = g.frames.map((f) => f.stats.orphan + f.stats.stray);
|
||||
examples.push({ canvas, g, mode: canvas.dataset.mode, frame: still ? errors.indexOf(Math.max(...errors)) : 0 });
|
||||
}
|
||||
const drawExamples = () => {
|
||||
const colors = themeColors();
|
||||
for (const ex of examples) paint(ex.canvas, ex.g, ex.g.frames[ex.frame % ex.g.frames.length], ex.mode, colors);
|
||||
};
|
||||
drawExamples();
|
||||
|
||||
setInterval(() => {
|
||||
if (!still) {
|
||||
for (const ex of examples) ex.frame++;
|
||||
drawExamples();
|
||||
}
|
||||
if (!state.playing) return;
|
||||
state.frame = (state.frame + 1) % current().frames.length;
|
||||
render();
|
||||
}, 110);
|
||||
}
|
||||
|
||||
inflate(bytes(DATA.pixels)).then((pixels) => { decode(pixels); start(); }, () => { $('loadError').hidden = false; });
|
||||
</script>
|
||||
@@ -0,0 +1,675 @@
|
||||
/**
|
||||
* @file extract_check.cpp
|
||||
*
|
||||
* Runs the game's `ExtractClxShadows` on every player sprite of an MPQ and
|
||||
* checks the result against the original sprites:
|
||||
*
|
||||
* - only black pixels are removed from a sprite
|
||||
* - drawing the new sprite over its shadow mask filled with black gives back
|
||||
* the original
|
||||
*
|
||||
* Prints the number of pixels removed as shadow, to compare with the research
|
||||
* tool's `verify` command ("reproduced as shadow").
|
||||
*
|
||||
* Also reports the size of the shadow masks, which are stored in the CLX frame
|
||||
* headers.
|
||||
*
|
||||
* Usage: shadow_extract_check <mpq> <rule | @rules.tsv>
|
||||
* [--monsters|--towners|--objects][=<tsv>]
|
||||
*
|
||||
* The rule is either one rule for every file (e.g. `FirstFrame`) or `@`
|
||||
* followed by the output of `shadow_search ... filerules` (path and rule per
|
||||
* line). Files without a shadow are skipped.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "engine/render/clx_render.hpp"
|
||||
#include "engine/surface.hpp"
|
||||
#include "sprites.hpp"
|
||||
#include "utils/clx_shadow.hpp"
|
||||
#include "utils/palette_blending.hpp"
|
||||
#include "utils/sdl_wrap.h"
|
||||
|
||||
namespace devilution {
|
||||
|
||||
// The game library reports fatal errors through this function.
|
||||
[[noreturn]] void app_fatal(std::string_view str) {
|
||||
std::fprintf(stderr, "fatal: %.*s\n", static_cast<int>(str.size()),
|
||||
str.data());
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// Called by the SDL wrappers when SDL fails.
|
||||
[[noreturn]] void ErrDlg(const char *title, std::string_view error,
|
||||
std::string_view /*logFilePath*/, int /*logLineNr*/) {
|
||||
std::fprintf(stderr, "%s: %.*s\n", title, static_cast<int>(error.size()),
|
||||
error.data());
|
||||
std::abort();
|
||||
}
|
||||
|
||||
} // namespace devilution
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
using namespace shadow_search;
|
||||
if (argc < 3) {
|
||||
std::fprintf(stderr,
|
||||
"usage: %s <mpq> <rule | @rules.tsv> "
|
||||
"[--monsters|--towners|--objects][=<tsv>]\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
auto archive = devilution::MpqArchive::Open(argv[1]);
|
||||
if (!archive) {
|
||||
std::fprintf(stderr, "failed to open %s: %s\n", argv[1],
|
||||
archive.error().c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `trn0 <path>...`: prints what each colour translation table maps black
|
||||
// (index 0) to. A shadow drawn separately as black only matches the baked-in
|
||||
// one if that is 0.
|
||||
if (std::string(argv[2]) == "trn0") {
|
||||
int nonZero = 0, found = 0;
|
||||
for (int i = 3; i < argc; ++i) {
|
||||
size_t size = 0;
|
||||
const std::unique_ptr<uint8_t[]> data =
|
||||
archive->HasFile(argv[i]) ? ReadMpqFile(*archive, argv[i], size)
|
||||
: nullptr;
|
||||
if (data == nullptr || size == 0)
|
||||
continue;
|
||||
++found;
|
||||
if (data[0] != 0) {
|
||||
++nonZero;
|
||||
std::printf("%s maps black to %d\n", argv[i], data[0]);
|
||||
}
|
||||
}
|
||||
std::printf("%d tables found, %d map black to another colour\n", found,
|
||||
nonZero);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `overrun-cl2`: the same count as `overrun` but on the raw CL2 commands,
|
||||
// before conversion to CLX.
|
||||
if (std::string(argv[2]) == "overrun-cl2") {
|
||||
long long frames = 0, framesWithOverrun = 0, rowEndsOpaqueThenOpaque = 0;
|
||||
const SpriteSet players = LoadPlayerSprites(*archive);
|
||||
for (const SpriteFile &file : players.files) {
|
||||
size_t size = 0;
|
||||
const std::unique_ptr<uint8_t[]> data =
|
||||
ReadMpqFile(*archive, file.path, size);
|
||||
const auto le32 = [&](size_t pos) {
|
||||
return static_cast<uint32_t>(
|
||||
data[pos] | (data[pos + 1] << 8) | (data[pos + 2] << 16) |
|
||||
(static_cast<uint32_t>(data[pos + 3]) << 24));
|
||||
};
|
||||
const uint32_t first = le32(0);
|
||||
const uint32_t numGroups = le32(first * 4 + 4) != size ? first / 4 : 1;
|
||||
for (uint32_t g = 0; g < numGroups; ++g) {
|
||||
const size_t group = numGroups == 1 ? 0 : le32(g * 4);
|
||||
const uint32_t numFrames = le32(group);
|
||||
for (uint32_t f = 0; f < numFrames; ++f) {
|
||||
const uint8_t *begin = &data[group + le32(group + 4 * (f + 1))];
|
||||
const uint8_t *end = &data[group + le32(group + 4 * (f + 2))];
|
||||
const uint8_t *src = begin + (begin[0] | (begin[1] << 8));
|
||||
long long x = 0;
|
||||
bool any = false, prevOpaqueEndedRow = false;
|
||||
++frames;
|
||||
while (src < end) {
|
||||
const uint8_t control = *src++;
|
||||
long long len;
|
||||
bool opaque = true;
|
||||
if (!devilution::IsClxOpaque(control)) {
|
||||
len = control;
|
||||
opaque = false;
|
||||
} else if (devilution::IsClxOpaqueFill(control)) {
|
||||
len = devilution::GetClxOpaqueFillWidth(control);
|
||||
++src;
|
||||
} else {
|
||||
len = devilution::GetClxOpaquePixelsWidth(control);
|
||||
src += len;
|
||||
}
|
||||
if (opaque && x + len > file.width)
|
||||
any = true;
|
||||
// An opaque run that ends exactly at the row end, followed by an
|
||||
// opaque run: the converter joins the two.
|
||||
if (opaque && prevOpaqueEndedRow)
|
||||
++rowEndsOpaqueThenOpaque;
|
||||
prevOpaqueEndedRow = opaque && x + len == file.width;
|
||||
x = (x + len) % file.width;
|
||||
}
|
||||
if (any)
|
||||
++framesWithOverrun;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf("%lld raw CL2 frames, %lld with an opaque run past the end of "
|
||||
"a row, %lld places where an opaque run ends a row and another "
|
||||
"begins the next\n",
|
||||
frames, framesWithOverrun, rowEndsOpaqueThenOpaque);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `overrun`: counts opaque runs that continue past the end of a sprite row in
|
||||
// the player sprites. The renderer draws such a run on to the right, outside
|
||||
// of the sprite, instead of wrapping it.
|
||||
if (std::string(argv[2]) == "overrun") {
|
||||
long long sprites = 0, spritesWithOverrun = 0, overrunPixels = 0;
|
||||
std::string example;
|
||||
const SpriteSet players = LoadPlayerSprites(*archive);
|
||||
for (const SpriteFile &file : players.files) {
|
||||
const int width = file.width;
|
||||
size_t size = 0;
|
||||
std::unique_ptr<uint8_t[]> data = ReadMpqFile(*archive, file.path, size);
|
||||
const devilution::OwnedClxSpriteListOrSheet clx = devilution::Cl2ToClx(
|
||||
std::move(data), size,
|
||||
devilution::PointerOrValue<uint16_t>{static_cast<uint16_t>(width)});
|
||||
long long fileOverrun = 0;
|
||||
for (uint16_t l = 0; l < clx.numLists(); ++l) {
|
||||
const ClxSpriteList list = clx.sheet()[l];
|
||||
for (uint32_t i = 0; i < list.numSprites(); ++i) {
|
||||
const ClxSprite sprite = list[i];
|
||||
++sprites;
|
||||
const uint8_t *src = sprite.pixelData();
|
||||
const uint8_t *end = src + sprite.pixelDataSize();
|
||||
long long x = 0;
|
||||
bool any = false;
|
||||
while (src < end) {
|
||||
const uint8_t control = *src++;
|
||||
long long len;
|
||||
bool opaque = true;
|
||||
if (!devilution::IsClxOpaque(control)) {
|
||||
len = control;
|
||||
opaque = false;
|
||||
} else if (devilution::IsClxOpaqueFill(control)) {
|
||||
len = devilution::GetClxOpaqueFillWidth(control);
|
||||
++src;
|
||||
} else {
|
||||
len = devilution::GetClxOpaquePixelsWidth(control);
|
||||
src += len;
|
||||
}
|
||||
if (opaque && x + len > width) {
|
||||
any = true;
|
||||
fileOverrun += x + len - width;
|
||||
}
|
||||
x = (x + len) % width;
|
||||
}
|
||||
if (any)
|
||||
++spritesWithOverrun;
|
||||
}
|
||||
}
|
||||
overrunPixels += fileOverrun;
|
||||
if (fileOverrun != 0 && example.empty())
|
||||
example = file.path;
|
||||
}
|
||||
std::printf("%lld sprites, %lld with an opaque run past the end of a row, "
|
||||
"%lld such pixels, e.g. %s\n",
|
||||
sprites, spritesWithOverrun, overrunPixels, example.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `render <rule | @rules.tsv> [--monsters...]`: times drawing every sprite
|
||||
// frame with its shadow baked in (as before shadow extraction) against
|
||||
// drawing the shadow mask and then the sprite.
|
||||
if (std::string(argv[2]) == "render" && argc >= 4) {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
std::map<std::string, std::string> renderRules;
|
||||
std::string renderRuleForAll = argv[3];
|
||||
if (renderRuleForAll[0] == '@') {
|
||||
for (const auto &row : ReadTsv(renderRuleForAll.substr(1)))
|
||||
if (row.size() >= 2)
|
||||
renderRules[row[0]] = row[1];
|
||||
renderRuleForAll.clear();
|
||||
}
|
||||
SpriteSet renderSet;
|
||||
const std::string renderSelector = argc >= 5 ? argv[4] : "";
|
||||
if (renderSelector.empty()) {
|
||||
renderSet = LoadPlayerSprites(*archive);
|
||||
} else {
|
||||
const size_t eq = renderSelector.find('=');
|
||||
const std::string kind = renderSelector.substr(
|
||||
2, eq == std::string::npos ? std::string::npos : eq - 2);
|
||||
const std::string defaultTsv = kind == "monsters" ? "monstdat"
|
||||
: kind == "objects" ? "objdat"
|
||||
: "towners";
|
||||
const std::string tsv =
|
||||
eq == std::string::npos
|
||||
? "assets/txtdata/" + kind + "/" + defaultTsv + ".tsv"
|
||||
: renderSelector.substr(eq + 1);
|
||||
renderSet = kind == "monsters" ? LoadMonsterSprites(*archive, tsv)
|
||||
: kind == "objects" ? LoadObjectSprites(*archive, tsv)
|
||||
: LoadTownerSprites(*archive, tsv);
|
||||
}
|
||||
renderSet.frames.clear();
|
||||
|
||||
// Every frame of every file that has an extractable shadow, before and
|
||||
// after extraction.
|
||||
std::vector<devilution::OwnedClxSpriteListOrSheet> owned;
|
||||
std::vector<std::pair<ClxSprite, ClxSprite>> sprites;
|
||||
for (const SpriteFile &file : renderSet.files) {
|
||||
const auto rule = devilution::ParseShadowRule(
|
||||
renderRuleForAll.empty() ? renderRules[file.path] : renderRuleForAll);
|
||||
if (!rule)
|
||||
return 2;
|
||||
if (!rule->canExtractShadow())
|
||||
continue;
|
||||
size_t size = 0;
|
||||
std::unique_ptr<uint8_t[]> data = ReadMpqFile(*archive, file.path, size);
|
||||
const devilution::PointerOrValue<uint16_t> width{
|
||||
static_cast<uint16_t>(file.width)};
|
||||
const bool isCel =
|
||||
file.path.size() > 4 &&
|
||||
file.path.compare(file.path.size() - 4, 4, ".cel") == 0;
|
||||
owned.push_back(isCel
|
||||
? devilution::CelToClx(data.get(), size, width)
|
||||
: devilution::Cl2ToClx(std::move(data), size, width));
|
||||
const size_t originalIndex = owned.size() - 1;
|
||||
std::optional<devilution::OwnedClxSpriteListOrSheet> extracted =
|
||||
devilution::ExtractClxShadows(owned[originalIndex], *rule);
|
||||
if (!extracted.has_value())
|
||||
continue;
|
||||
owned.push_back(std::move(*extracted));
|
||||
const devilution::OwnedClxSpriteListOrSheet &before =
|
||||
owned[originalIndex];
|
||||
const devilution::OwnedClxSpriteListOrSheet &after = owned.back();
|
||||
const auto addList = [&](ClxSpriteList a, ClxSpriteList b) {
|
||||
for (uint32_t i = 0; i < a.numSprites(); ++i)
|
||||
sprites.emplace_back(a[i], b[i]);
|
||||
};
|
||||
if (before.isSheet()) {
|
||||
for (uint16_t l = 0; l < before.numLists(); ++l)
|
||||
addList(before.sheet()[l], after.sheet()[l]);
|
||||
} else {
|
||||
addList(before.list(), after.list());
|
||||
}
|
||||
}
|
||||
|
||||
const devilution::SDLSurfaceUniquePtr sdlSurface =
|
||||
devilution::SDLWrap::CreateRGBSurfaceWithFormat(
|
||||
/*flags=*/0, /*width=*/640, /*height=*/480, /*depth=*/8,
|
||||
SDL_PIXELFORMAT_INDEX8);
|
||||
const devilution::Surface out{sdlSurface.get()};
|
||||
// A light table, as used for most sprites in the dungeon. Black stays
|
||||
// black.
|
||||
uint8_t lightTable[256];
|
||||
for (unsigned i = 0; i < 256; ++i)
|
||||
lightTable[i] = static_cast<uint8_t>(i == 0 ? 0 : 1 + (i * 7) % 255);
|
||||
for (unsigned i = 0; i < 256; ++i)
|
||||
devilution::paletteTransparencyLookup[0][i] = static_cast<uint8_t>(i / 2);
|
||||
#if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT
|
||||
for (unsigned i = 0; i < 256; ++i)
|
||||
for (unsigned j = 0; j < 256; ++j)
|
||||
devilution::paletteTransparencyLookupBlack16[i | (j << 8U)] =
|
||||
static_cast<uint16_t>(
|
||||
devilution::paletteTransparencyLookup[0][i] |
|
||||
(devilution::paletteTransparencyLookup[0][j] << 8U));
|
||||
#endif
|
||||
|
||||
// Fully on screen, away from 4-byte alignment.
|
||||
const devilution::Point position{201, 400};
|
||||
constexpr int Passes = 7;
|
||||
const int repeats = static_cast<int>(
|
||||
std::max<size_t>(1, 2000000 / std::max<size_t>(1, sprites.size())));
|
||||
const auto measure = [&](auto &&draw) {
|
||||
double best = 1e30;
|
||||
for (int pass = 0; pass < Passes; ++pass) {
|
||||
const Clock::time_point t0 = Clock::now();
|
||||
for (int r = 0; r < repeats; ++r)
|
||||
for (const auto &[before, after] : sprites)
|
||||
draw(before, after);
|
||||
best = std::min(
|
||||
best, std::chrono::duration<double, std::nano>(Clock::now() - t0)
|
||||
.count());
|
||||
}
|
||||
return best / (static_cast<double>(repeats) * sprites.size());
|
||||
};
|
||||
struct Variant {
|
||||
const char *name;
|
||||
double direct, lit;
|
||||
};
|
||||
using devilution::ClxDraw;
|
||||
using devilution::ClxDrawShadow;
|
||||
using devilution::ClxDrawShadowBlended;
|
||||
using devilution::ClxDrawTRN;
|
||||
const Variant variants[] = {
|
||||
{"baked shadow (before)", measure([&](ClxSprite before, ClxSprite) {
|
||||
ClxDraw(out, position, before);
|
||||
}),
|
||||
measure([&](ClxSprite before, ClxSprite) {
|
||||
ClxDrawTRN(out, position, before, lightTable);
|
||||
})},
|
||||
{"separate shadow, opaque", measure([&](ClxSprite, ClxSprite after) {
|
||||
ClxDrawShadow(out, position, after);
|
||||
ClxDraw(out, position, after);
|
||||
}),
|
||||
measure([&](ClxSprite, ClxSprite after) {
|
||||
ClxDrawShadow(out, position, after);
|
||||
ClxDrawTRN(out, position, after, lightTable);
|
||||
})},
|
||||
{"separate shadow, transparent",
|
||||
measure([&](ClxSprite, ClxSprite after) {
|
||||
ClxDrawShadowBlended(out, position, after);
|
||||
ClxDraw(out, position, after);
|
||||
}),
|
||||
measure([&](ClxSprite, ClxSprite after) {
|
||||
ClxDrawShadowBlended(out, position, after);
|
||||
ClxDrawTRN(out, position, after, lightTable);
|
||||
})},
|
||||
{"sprite only, no shadow drawn",
|
||||
measure([&](ClxSprite, ClxSprite after) {
|
||||
ClxDraw(out, position, after);
|
||||
}),
|
||||
measure([&](ClxSprite, ClxSprite after) {
|
||||
ClxDrawTRN(out, position, after, lightTable);
|
||||
})},
|
||||
};
|
||||
std::printf("%zu sprite frames, %d repeats, best of %d passes; ns per "
|
||||
"sprite drawn\n",
|
||||
sprites.size(), repeats, Passes);
|
||||
std::printf("%-30s %12s %9s %14s %9s\n", "", "no lighting", "",
|
||||
"light table", "");
|
||||
for (const Variant &v : variants)
|
||||
std::printf("%-30s %9.1f ns %+8.1f%% %11.1f ns %+8.1f%%\n", v.name,
|
||||
v.direct, 100.0 * (v.direct / variants[0].direct - 1), v.lit,
|
||||
100.0 * (v.lit / variants[0].lit - 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `timing <rule | @rules.tsv> [--monsters...]`: times the CL2/CEL conversion
|
||||
// that the game already does when it loads sprites against the shadow
|
||||
// extraction, per sprite file, on one thread.
|
||||
if (std::string(argv[2]) == "timing" && argc >= 4) {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
std::map<std::string, std::string> timingRules;
|
||||
std::string timingRuleForAll = argv[3];
|
||||
if (timingRuleForAll[0] == '@') {
|
||||
for (const auto &row : ReadTsv(timingRuleForAll.substr(1)))
|
||||
if (row.size() >= 2)
|
||||
timingRules[row[0]] = row[1];
|
||||
timingRuleForAll.clear();
|
||||
}
|
||||
SpriteSet timingSet;
|
||||
const std::string timingSelector = argc >= 5 ? argv[4] : "";
|
||||
if (timingSelector.empty()) {
|
||||
timingSet = LoadPlayerSprites(*archive);
|
||||
} else {
|
||||
const size_t eq = timingSelector.find('=');
|
||||
const std::string kind = timingSelector.substr(
|
||||
2, eq == std::string::npos ? std::string::npos : eq - 2);
|
||||
const std::string defaultTsv = kind == "monsters" ? "monstdat"
|
||||
: kind == "objects" ? "objdat"
|
||||
: "towners";
|
||||
const std::string tsv =
|
||||
eq == std::string::npos
|
||||
? "assets/txtdata/" + kind + "/" + defaultTsv + ".tsv"
|
||||
: timingSelector.substr(eq + 1);
|
||||
timingSet = kind == "monsters" ? LoadMonsterSprites(*archive, tsv)
|
||||
: kind == "objects" ? LoadObjectSprites(*archive, tsv)
|
||||
: LoadTownerSprites(*archive, tsv);
|
||||
}
|
||||
std::vector<long long> frameCount(timingSet.files.size());
|
||||
for (const Frame &f : timingSet.frames)
|
||||
++frameCount[f.file];
|
||||
|
||||
struct Timing {
|
||||
double read = 0, convert = 0, extract = 0;
|
||||
long long frames = 0, files = 0, bytes = 0;
|
||||
};
|
||||
Timing total;
|
||||
// Players: one entry per class/armour/weapon (what one character has loaded
|
||||
// at most). Monsters: one entry per monster. Others: one entry per file.
|
||||
std::map<std::string, Timing> byGroup;
|
||||
constexpr int Repeats = 5;
|
||||
for (size_t fileIndex = 0; fileIndex < timingSet.files.size();
|
||||
++fileIndex) {
|
||||
const SpriteFile &file = timingSet.files[fileIndex];
|
||||
const auto rule = devilution::ParseShadowRule(
|
||||
timingRuleForAll.empty() ? timingRules[file.path] : timingRuleForAll);
|
||||
if (!rule)
|
||||
return 2;
|
||||
const bool isCel =
|
||||
file.path.size() > 4 &&
|
||||
file.path.compare(file.path.size() - 4, 4, ".cel") == 0;
|
||||
const devilution::PointerOrValue<uint16_t> width{
|
||||
static_cast<uint16_t>(file.width)};
|
||||
// Best of several runs, to leave out cache warm-up and scheduling noise.
|
||||
double bestRead = 1e30, bestConvert = 1e30, bestExtract = 1e30;
|
||||
size_t size = 0;
|
||||
for (int repeat = 0; repeat < Repeats; ++repeat) {
|
||||
const Clock::time_point t0 = Clock::now();
|
||||
std::unique_ptr<uint8_t[]> data =
|
||||
ReadMpqFile(*archive, file.path, size);
|
||||
const Clock::time_point t1 = Clock::now();
|
||||
const devilution::OwnedClxSpriteListOrSheet clx =
|
||||
isCel ? devilution::CelToClx(data.get(), size, width)
|
||||
: devilution::Cl2ToClx(std::move(data), size, width);
|
||||
const Clock::time_point t2 = Clock::now();
|
||||
const std::optional<devilution::OwnedClxSpriteListOrSheet> extracted =
|
||||
devilution::ExtractClxShadows(clx, *rule);
|
||||
const Clock::time_point t3 = Clock::now();
|
||||
bestRead = std::min(
|
||||
bestRead,
|
||||
std::chrono::duration<double, std::milli>(t1 - t0).count());
|
||||
bestConvert = std::min(
|
||||
bestConvert,
|
||||
std::chrono::duration<double, std::milli>(t2 - t1).count());
|
||||
bestExtract = std::min(
|
||||
bestExtract,
|
||||
std::chrono::duration<double, std::milli>(t3 - t2).count());
|
||||
}
|
||||
const std::string group = file.classChar == 'M'
|
||||
? file.name.substr(0, file.name.size() - 1)
|
||||
: file.classChar == 'T' || file.classChar == 'O'
|
||||
? file.name
|
||||
: file.name.substr(0, 3);
|
||||
for (Timing *t : {&total, &byGroup[group]}) {
|
||||
t->read += bestRead;
|
||||
t->convert += bestConvert;
|
||||
t->extract += bestExtract;
|
||||
t->frames += frameCount[fileIndex];
|
||||
t->bytes += static_cast<long long>(size);
|
||||
++t->files;
|
||||
}
|
||||
}
|
||||
std::printf("files %lld, frames %lld, %.1f MB\n", total.files, total.frames,
|
||||
total.bytes / 1e6);
|
||||
std::printf("all files: read %.0f ms, convert %.0f ms, extract shadows "
|
||||
"%.0f ms (%.2fx the conversion, %.1f us per frame)\n",
|
||||
total.read, total.convert, total.extract,
|
||||
total.extract / total.convert,
|
||||
1000.0 * total.extract / total.frames);
|
||||
double sumExtract = 0, maxExtract = 0, sumBase = 0;
|
||||
std::string maxGroup;
|
||||
for (const auto &[name, t] : byGroup) {
|
||||
sumExtract += t.extract;
|
||||
sumBase += t.read + t.convert;
|
||||
if (t.extract > maxExtract) {
|
||||
maxExtract = t.extract;
|
||||
maxGroup = name;
|
||||
}
|
||||
}
|
||||
const Timing &worst = byGroup[maxGroup];
|
||||
std::printf("per group (%zu groups): read+convert %.2f ms on average, "
|
||||
"extract %.2f ms on average\n",
|
||||
byGroup.size(), sumBase / byGroup.size(),
|
||||
sumExtract / byGroup.size());
|
||||
std::printf("slowest group %s: %lld files, %lld frames, read+convert %.2f "
|
||||
"ms, extract %.2f ms\n",
|
||||
maxGroup.c_str(), worst.files, worst.frames,
|
||||
worst.read + worst.convert, worst.extract);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> ruleByPath;
|
||||
std::string ruleForAll = argv[2];
|
||||
if (ruleForAll[0] == '@') {
|
||||
for (const auto &row : ReadTsv(ruleForAll.substr(1)))
|
||||
if (row.size() >= 2)
|
||||
ruleByPath[row[0]] = row[1];
|
||||
ruleForAll.clear();
|
||||
}
|
||||
|
||||
SpriteSet set;
|
||||
const std::string selector = argc >= 4 ? argv[3] : "";
|
||||
if (selector.empty()) {
|
||||
set = LoadPlayerSprites(*archive);
|
||||
} else {
|
||||
const size_t eq = selector.find('=');
|
||||
const std::string kind = selector.substr(
|
||||
2, eq == std::string::npos ? std::string::npos : eq - 2);
|
||||
const std::string defaultTsv = kind == "monsters" ? "monstdat"
|
||||
: kind == "objects" ? "objdat"
|
||||
: "towners";
|
||||
const std::string tsv =
|
||||
eq == std::string::npos
|
||||
? "assets/txtdata/" + kind + "/" + defaultTsv + ".tsv"
|
||||
: selector.substr(eq + 1);
|
||||
set = kind == "monsters" ? LoadMonsterSprites(*archive, tsv)
|
||||
: kind == "objects" ? LoadObjectSprites(*archive, tsv)
|
||||
: LoadTownerSprites(*archive, tsv);
|
||||
}
|
||||
|
||||
size_t files = 0, maxMaskBytes = 0, lists = 0;
|
||||
uint64_t outputHash = 1469598103934665603ULL;
|
||||
std::string maxMaskFile;
|
||||
long long frames = 0, blackPixels = 0, removed = 0, removedNotBlack = 0,
|
||||
added = 0, maskPixels = 0, maskOnTransparent = 0, mismatches = 0;
|
||||
long long originalBytes = 0, spriteBytes = 0, maskBytes = 0;
|
||||
size_t frameIndex = 0;
|
||||
for (size_t fileIndex = 0; fileIndex < set.files.size(); ++fileIndex) {
|
||||
const SpriteFile &file = set.files[fileIndex];
|
||||
const std::string ruleName =
|
||||
ruleForAll.empty() ? ruleByPath[file.path] : ruleForAll;
|
||||
const auto rule = devilution::ParseShadowRule(ruleName);
|
||||
if (!rule) {
|
||||
std::fprintf(stderr, "%s: %s\n", file.path.c_str(), rule.error().c_str());
|
||||
return 2;
|
||||
}
|
||||
if (!rule->canExtractShadow()) {
|
||||
while (frameIndex < set.frames.size() &&
|
||||
set.frames[frameIndex].file == static_cast<int>(fileIndex))
|
||||
++frameIndex;
|
||||
continue;
|
||||
}
|
||||
size_t size = 0;
|
||||
std::unique_ptr<uint8_t[]> data = ReadMpqFile(*archive, file.path, size);
|
||||
const devilution::PointerOrValue<uint16_t> width{
|
||||
static_cast<uint16_t>(file.width)};
|
||||
const bool isCel = file.path.size() > 4 &&
|
||||
file.path.compare(file.path.size() - 4, 4, ".cel") == 0;
|
||||
const devilution::OwnedClxSpriteListOrSheet original =
|
||||
isCel ? devilution::CelToClx(data.get(), size, width)
|
||||
: devilution::Cl2ToClx(std::move(data), size, width);
|
||||
const std::optional<devilution::OwnedClxSpriteListOrSheet> result =
|
||||
devilution::ExtractClxShadows(original, *rule);
|
||||
if (!result.has_value()) {
|
||||
std::fprintf(stderr, "no result for %s\n", file.path.c_str());
|
||||
return 1;
|
||||
}
|
||||
++files;
|
||||
originalBytes += static_cast<long long>(original.dataSize());
|
||||
spriteBytes += static_cast<long long>(result->dataSize());
|
||||
{
|
||||
// A fingerprint of the output bytes, to compare implementations of the
|
||||
// extraction.
|
||||
const devilution::ClxSpriteListOrSheet view{*result};
|
||||
const uint8_t *bytes =
|
||||
view.isSheet() ? view.sheet().data() : view.list().data();
|
||||
for (size_t b = 0, n = result->dataSize(); b < n; ++b)
|
||||
outputHash = (outputHash ^ bytes[b]) * 1099511628211ULL;
|
||||
}
|
||||
|
||||
const auto checkList = [&](ClxSpriteList sprites) {
|
||||
++lists;
|
||||
for (uint32_t i = 0; i < sprites.numSprites(); ++i, ++frameIndex) {
|
||||
const size_t maskSize =
|
||||
sprites[i].headerSize() - devilution::ClxSprite::BaseHeaderSize;
|
||||
maskBytes += static_cast<long long>(maskSize);
|
||||
if (maskSize > maxMaskBytes) {
|
||||
maxMaskBytes = maskSize;
|
||||
maxMaskFile = file.path;
|
||||
}
|
||||
const Frame &before = set.frames[frameIndex];
|
||||
const Frame after = DecodeFrame(sprites[i]);
|
||||
if (before.file != static_cast<int>(fileIndex) ||
|
||||
after.width != before.width || after.height != before.height) {
|
||||
std::fprintf(stderr, "frame mismatch in %s\n", file.path.c_str());
|
||||
std::exit(1);
|
||||
}
|
||||
std::vector<char> mask(before.pixels.size(), 0);
|
||||
if (devilution::HasShadowMask(sprites[i])) {
|
||||
devilution::GetShadowMask(sprites[i])
|
||||
.forEachSpan([&](uint16_t rowFromBottom,
|
||||
devilution::ShadowSpan span) {
|
||||
for (unsigned x = span.x;
|
||||
x < static_cast<unsigned>(span.x) + span.width; ++x)
|
||||
mask[static_cast<size_t>(before.height - 1 - rowFromBottom) *
|
||||
before.width +
|
||||
x] = 1;
|
||||
});
|
||||
}
|
||||
++frames;
|
||||
for (size_t p = 0; p < before.pixels.size(); ++p) {
|
||||
const int16_t b = before.pixels[p], a = after.pixels[p];
|
||||
if (b == 0)
|
||||
++blackPixels;
|
||||
if (mask[p] != 0) {
|
||||
++maskPixels;
|
||||
if (b == Transparent)
|
||||
++maskOnTransparent;
|
||||
}
|
||||
if (a != Transparent && a != b)
|
||||
++added;
|
||||
if (a == Transparent && b != Transparent) {
|
||||
++removed;
|
||||
if (b != 0)
|
||||
++removedNotBlack;
|
||||
}
|
||||
// Redraw: the sprite over its black shadow.
|
||||
const int16_t redrawn = a != Transparent ? a
|
||||
: mask[p] != 0 ? 0
|
||||
: Transparent;
|
||||
if (redrawn != b)
|
||||
++mismatches;
|
||||
}
|
||||
}
|
||||
};
|
||||
if (result->isSheet()) {
|
||||
for (uint16_t l = 0; l < result->numLists(); ++l)
|
||||
checkList(result->sheet()[l]);
|
||||
} else {
|
||||
checkList(result->list());
|
||||
}
|
||||
}
|
||||
|
||||
std::printf("files with a known shadow rule: %zu of %zu\n", files,
|
||||
set.files.size());
|
||||
std::printf("output fingerprint: %016llx\n",
|
||||
static_cast<unsigned long long>(outputHash));
|
||||
std::printf("sprite lists: %zu\n", lists);
|
||||
std::printf("largest shadow mask: %zu bytes (%s)\n",
|
||||
maxMaskBytes, maxMaskFile.c_str());
|
||||
std::printf("frames: %lld\n", frames);
|
||||
std::printf("black pixels in the originals: %lld\n", blackPixels);
|
||||
std::printf("pixels removed as shadow: %lld\n", removed);
|
||||
std::printf("black pixels kept in the sprites: %lld\n",
|
||||
blackPixels - removed);
|
||||
std::printf("removed pixels that were not black: %lld\n", removedNotBlack);
|
||||
std::printf("pixels added or changed: %lld\n", added);
|
||||
std::printf("mask pixels: %lld\n", maskPixels);
|
||||
std::printf("mask pixels on transparent pixels: %lld\n", maskOnTransparent);
|
||||
std::printf("pixels that differ when redrawn: %lld\n", mismatches);
|
||||
std::printf("CLX bytes, originals: %lld\n", originalBytes);
|
||||
std::printf("CLX bytes, sprites with masks: %lld\n", spriteBytes);
|
||||
std::printf(" of which shadow masks: %lld\n", maskBytes);
|
||||
return removedNotBlack == 0 && added == 0 && maskOnTransparent == 0 &&
|
||||
mismatches == 0
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,487 @@
|
||||
#pragma once
|
||||
/**
|
||||
* @file sprites.hpp
|
||||
*
|
||||
* Loads the original player sprites (CL2 files inside an MPQ) into plain
|
||||
* bitmaps and row bitmasks for the shadow transform search.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "engine/clx_sprite.hpp"
|
||||
#include "mpq/mpq_reader.hpp"
|
||||
#include "utils/cel_to_clx.hpp"
|
||||
#include "utils/cl2_to_clx.hpp"
|
||||
#include "utils/clx_decode.hpp"
|
||||
|
||||
namespace shadow_search {
|
||||
|
||||
using devilution::ClxSprite;
|
||||
using devilution::ClxSpriteList;
|
||||
using devilution::ClxSpriteSheet;
|
||||
|
||||
constexpr int MaxWidth = 256;
|
||||
|
||||
/** Bitmask of a single sprite row. Bit `x` is the pixel at column `x`. */
|
||||
struct RowMask {
|
||||
uint64_t w[4] = {0, 0, 0, 0};
|
||||
|
||||
constexpr RowMask() = default;
|
||||
constexpr RowMask(uint64_t low) // NOLINT(google-explicit-constructor): lets
|
||||
// `mask != 0` and `mask & 1` read naturally
|
||||
: w{low, 0, 0, 0} {}
|
||||
|
||||
friend constexpr bool operator==(const RowMask &a, const RowMask &b) {
|
||||
return a.w[0] == b.w[0] && a.w[1] == b.w[1] && a.w[2] == b.w[2] &&
|
||||
a.w[3] == b.w[3];
|
||||
}
|
||||
friend constexpr bool operator!=(const RowMask &a, const RowMask &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
friend constexpr RowMask operator&(const RowMask &a, const RowMask &b) {
|
||||
RowMask r;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
r.w[i] = a.w[i] & b.w[i];
|
||||
return r;
|
||||
}
|
||||
friend constexpr RowMask operator|(const RowMask &a, const RowMask &b) {
|
||||
RowMask r;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
r.w[i] = a.w[i] | b.w[i];
|
||||
return r;
|
||||
}
|
||||
constexpr RowMask operator~() const {
|
||||
RowMask r;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
r.w[i] = ~w[i];
|
||||
return r;
|
||||
}
|
||||
constexpr RowMask &operator&=(const RowMask &o) { return *this = *this & o; }
|
||||
constexpr RowMask &operator|=(const RowMask &o) { return *this = *this | o; }
|
||||
/** Shifts by `n` bits, `0 <= n`. Shifting by `MaxWidth` or more gives 0. */
|
||||
constexpr RowMask operator<<(int n) const {
|
||||
RowMask r;
|
||||
if (n >= MaxWidth)
|
||||
return r;
|
||||
const int words = n / 64, bits = n % 64;
|
||||
for (int i = 3; i >= words; --i) {
|
||||
r.w[i] = w[i - words] << bits;
|
||||
if (bits != 0 && i - words - 1 >= 0)
|
||||
r.w[i] |= w[i - words - 1] >> (64 - bits);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
constexpr RowMask operator>>(int n) const {
|
||||
RowMask r;
|
||||
if (n >= MaxWidth)
|
||||
return r;
|
||||
const int words = n / 64, bits = n % 64;
|
||||
for (int i = 0; i + words < 4; ++i) {
|
||||
r.w[i] = w[i + words] >> bits;
|
||||
if (bits != 0 && i + words + 1 < 4)
|
||||
r.w[i] |= w[i + words + 1] << (64 - bits);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
};
|
||||
constexpr int16_t Transparent = -1;
|
||||
|
||||
struct Frame {
|
||||
int file; // index into SpriteSet::files
|
||||
int direction; // 0..7, DevilutionX `Direction` order (South, SouthWest, West,
|
||||
// ...)
|
||||
int index; // frame index within the direction
|
||||
int width;
|
||||
int height;
|
||||
/** Palette indices, top-down, `Transparent` for transparent pixels. */
|
||||
std::vector<int16_t> pixels;
|
||||
/** Per-row masks, top-down. */
|
||||
std::vector<RowMask> opaque;
|
||||
std::vector<RowMask> black;
|
||||
|
||||
[[nodiscard]] int16_t at(int x, int y) const {
|
||||
if (x < 0 || y < 0 || x >= width || y >= height)
|
||||
return Transparent;
|
||||
return pixels[static_cast<size_t>(y) * width + x];
|
||||
}
|
||||
};
|
||||
|
||||
struct SpriteFile {
|
||||
std::string path; // e.g. plrgfx\warrior\wls\wlsas.cl2
|
||||
std::string name; // e.g. wlsas
|
||||
std::string anim; // e.g. as
|
||||
char classChar;
|
||||
char armorChar;
|
||||
char weaponChar;
|
||||
int width;
|
||||
};
|
||||
|
||||
struct SpriteSet {
|
||||
std::vector<SpriteFile> files;
|
||||
std::vector<Frame> frames;
|
||||
};
|
||||
|
||||
inline Frame DecodeFrame(ClxSprite sprite) {
|
||||
Frame frame;
|
||||
frame.width = sprite.width();
|
||||
frame.height = sprite.height();
|
||||
frame.pixels.assign(static_cast<size_t>(frame.width) * frame.height,
|
||||
Transparent);
|
||||
frame.opaque.assign(frame.height, 0);
|
||||
frame.black.assign(frame.height, 0);
|
||||
|
||||
// CL2/CLX pixel data is stored bottom-up; runs may wrap across lines.
|
||||
const uint8_t *src = sprite.pixelData();
|
||||
const uint8_t *end = src + sprite.pixelDataSize();
|
||||
size_t pos = 0;
|
||||
const size_t total = frame.pixels.size();
|
||||
const auto put = [&](uint8_t color) {
|
||||
if (pos >= total)
|
||||
return;
|
||||
const int x = static_cast<int>(pos % frame.width);
|
||||
const int y = frame.height - 1 - static_cast<int>(pos / frame.width);
|
||||
frame.pixels[static_cast<size_t>(y) * frame.width + x] = color;
|
||||
frame.opaque[y] |= static_cast<RowMask>(1) << x;
|
||||
if (color == 0)
|
||||
frame.black[y] |= static_cast<RowMask>(1) << x;
|
||||
++pos;
|
||||
};
|
||||
while (src < end) {
|
||||
const uint8_t control = *src++;
|
||||
if (!devilution::IsClxOpaque(control)) {
|
||||
pos += control;
|
||||
} else if (devilution::IsClxOpaqueFill(control)) {
|
||||
const uint8_t width = devilution::GetClxOpaqueFillWidth(control);
|
||||
const uint8_t color = *src++;
|
||||
for (int i = 0; i < width; ++i)
|
||||
put(color);
|
||||
} else {
|
||||
const uint8_t width = devilution::GetClxOpaquePixelsWidth(control);
|
||||
for (int i = 0; i < width; ++i)
|
||||
put(*src++);
|
||||
}
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
inline std::unique_ptr<uint8_t[]> ReadMpqFile(devilution::MpqArchive &archive,
|
||||
const std::string &path,
|
||||
size_t &size) {
|
||||
int32_t error = 0;
|
||||
std::unique_ptr<std::byte[]> data = archive.ReadFile(path, size, error);
|
||||
if (data == nullptr)
|
||||
return nullptr;
|
||||
return std::unique_ptr<uint8_t[]>{
|
||||
reinterpret_cast<uint8_t *>(data.release())};
|
||||
}
|
||||
|
||||
struct ClassSprites {
|
||||
const char *path;
|
||||
char classChar;
|
||||
// Frame widths, see assets/txtdata/classes/*/sprites.tsv
|
||||
int stand, walk, attack, bow, hit, block, lightning, fire, magic, death;
|
||||
};
|
||||
|
||||
inline int AnimWidth(const ClassSprites &cls, const std::string &anim,
|
||||
char weaponChar) {
|
||||
if (anim == "as" || anim == "st")
|
||||
return cls.stand;
|
||||
if (anim == "aw" || anim == "wl")
|
||||
return cls.walk;
|
||||
if (anim == "at")
|
||||
return weaponChar == 'b' ? cls.bow : cls.attack;
|
||||
if (anim == "ht")
|
||||
return cls.hit;
|
||||
if (anim == "bl")
|
||||
return cls.block;
|
||||
if (anim == "lm")
|
||||
return cls.lightning;
|
||||
if (anim == "fm")
|
||||
return cls.fire;
|
||||
if (anim == "qm")
|
||||
return cls.magic;
|
||||
return cls.death;
|
||||
}
|
||||
|
||||
/** Loads every player sprite present in the given MPQ. */
|
||||
inline SpriteSet LoadPlayerSprites(devilution::MpqArchive &archive) {
|
||||
static const ClassSprites Classes[] = {
|
||||
{"warrior", 'w', 96, 96, 128, 96, 96, 96, 96, 96, 96, 128},
|
||||
{"rogue", 'r', 96, 96, 128, 128, 96, 96, 96, 96, 96, 128},
|
||||
{"sorceror", 's', 96, 96, 128, 128, 96, 96, 128, 128, 128, 128},
|
||||
// Hellfire. Bard and barbarian sprites only exist in the optional
|
||||
// hfbard.mpq / hfbarb.mpq.
|
||||
{"monk", 'm', 112, 112, 130, 130, 98, 98, 114, 114, 114, 160},
|
||||
{"rogue", 'b', 96, 96, 128, 128, 96, 96, 96, 96, 96, 128},
|
||||
{"warrior", 'c', 96, 96, 128, 96, 96, 96, 96, 96, 96, 128},
|
||||
};
|
||||
static const char ArmorChars[] = {'l', 'm', 'h'};
|
||||
static const char WeaponChars[] = {'n', 'u', 's', 'm', 'b',
|
||||
'a', 'd', 't', 'h'};
|
||||
static const char *Anims[] = {"as", "aw", "at", "ht", "lm", "fm",
|
||||
"qm", "dt", "bl", "st", "wl"};
|
||||
|
||||
SpriteSet result;
|
||||
for (const ClassSprites &cls : Classes) {
|
||||
for (const char armor : ArmorChars) {
|
||||
for (const char weapon : WeaponChars) {
|
||||
for (const char *anim : Anims) {
|
||||
const std::string prefix{cls.classChar, armor, weapon};
|
||||
const std::string path = std::string("plrgfx\\") + cls.path + "\\" +
|
||||
prefix + "\\" + prefix + anim + ".cl2";
|
||||
if (!archive.HasFile(path))
|
||||
continue;
|
||||
size_t size = 0;
|
||||
std::unique_ptr<uint8_t[]> data = ReadMpqFile(archive, path, size);
|
||||
if (data == nullptr) {
|
||||
std::fprintf(stderr, "failed to read %s\n", path.c_str());
|
||||
continue;
|
||||
}
|
||||
const int width = AnimWidth(cls, anim, weapon);
|
||||
const devilution::OwnedClxSpriteListOrSheet clx =
|
||||
devilution::Cl2ToClx(std::move(data), size,
|
||||
devilution::PointerOrValue<uint16_t>{
|
||||
static_cast<uint16_t>(width)});
|
||||
const int fileIndex = static_cast<int>(result.files.size());
|
||||
result.files.push_back(SpriteFile{
|
||||
path, prefix + anim, anim, cls.classChar, armor, weapon, width});
|
||||
const auto addList = [&](ClxSpriteList list, int direction) {
|
||||
for (uint32_t i = 0; i < list.numSprites(); ++i) {
|
||||
Frame frame = DecodeFrame(list[i]);
|
||||
frame.file = fileIndex;
|
||||
frame.direction = direction;
|
||||
frame.index = static_cast<int>(i);
|
||||
result.frames.push_back(std::move(frame));
|
||||
}
|
||||
};
|
||||
if (clx.isSheet()) {
|
||||
const ClxSpriteSheet sheet = clx.sheet();
|
||||
for (uint16_t d = 0; d < sheet.numLists(); ++d)
|
||||
addList(sheet[d], d);
|
||||
} else {
|
||||
addList(clx.list(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Appends the frames of a converted sprite list or sheet as a new file of the
|
||||
* set. */
|
||||
inline void AddSprites(SpriteSet &result,
|
||||
const devilution::OwnedClxSpriteListOrSheet &clx,
|
||||
SpriteFile file) {
|
||||
const int fileIndex = static_cast<int>(result.files.size());
|
||||
result.files.push_back(std::move(file));
|
||||
const auto addList = [&](ClxSpriteList list, int direction) {
|
||||
for (uint32_t i = 0; i < list.numSprites(); ++i) {
|
||||
Frame frame = DecodeFrame(list[i]);
|
||||
frame.file = fileIndex;
|
||||
frame.direction = direction;
|
||||
frame.index = static_cast<int>(i);
|
||||
result.frames.push_back(std::move(frame));
|
||||
}
|
||||
};
|
||||
if (clx.isSheet()) {
|
||||
const ClxSpriteSheet sheet = clx.sheet();
|
||||
for (uint16_t d = 0; d < sheet.numLists(); ++d)
|
||||
addList(sheet[d], d);
|
||||
} else {
|
||||
addList(clx.list(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads a tab-separated file into rows of fields. The first row is the header.
|
||||
*/
|
||||
inline std::vector<std::vector<std::string>> ReadTsv(const std::string &path) {
|
||||
std::vector<std::vector<std::string>> rows;
|
||||
std::FILE *in = std::fopen(path.c_str(), "rb");
|
||||
if (in == nullptr)
|
||||
return rows;
|
||||
std::string text;
|
||||
char buffer[65536];
|
||||
for (size_t n; (n = std::fread(buffer, 1, sizeof(buffer), in)) > 0;)
|
||||
text.append(buffer, n);
|
||||
std::fclose(in);
|
||||
rows.emplace_back(1);
|
||||
for (const char c : text) {
|
||||
if (c == '\r')
|
||||
continue;
|
||||
if (c == '\n') {
|
||||
rows.emplace_back(1);
|
||||
} else if (c == '\t') {
|
||||
rows.back().emplace_back();
|
||||
} else {
|
||||
rows.back().back() += c;
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
inline int TsvColumn(const std::vector<std::vector<std::string>> &rows,
|
||||
const std::string &name) {
|
||||
if (rows.empty())
|
||||
return -1;
|
||||
for (size_t i = 0; i < rows[0].size(); ++i)
|
||||
if (rows[0][i] == name)
|
||||
return static_cast<int>(i);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the monster sprites listed in assets/txtdata/monsters/monstdat.tsv.
|
||||
* File names are the last path component plus the animation letter, e.g.
|
||||
* `zombien`.
|
||||
*/
|
||||
inline SpriteSet LoadMonsterSprites(devilution::MpqArchive &archive,
|
||||
const std::string &tsvPath) {
|
||||
SpriteSet result;
|
||||
const auto rows = ReadTsv(tsvPath);
|
||||
const int pathColumn = TsvColumn(rows, "assetsSuffix");
|
||||
const int widthColumn = TsvColumn(rows, "width");
|
||||
const int framesColumn = TsvColumn(rows, "frames[6]");
|
||||
if (pathColumn < 0 || widthColumn < 0 || framesColumn < 0) {
|
||||
std::fprintf(stderr, "failed to read %s\n", tsvPath.c_str());
|
||||
return result;
|
||||
}
|
||||
std::vector<std::string> seen;
|
||||
for (size_t r = 1; r < rows.size(); ++r) {
|
||||
if (static_cast<int>(rows[r].size()) <=
|
||||
std::max({pathColumn, widthColumn, framesColumn}))
|
||||
continue;
|
||||
const std::string &suffix = rows[r][pathColumn];
|
||||
bool duplicate = false;
|
||||
for (const std::string &s : seen)
|
||||
duplicate = duplicate || s == suffix;
|
||||
if (duplicate)
|
||||
continue;
|
||||
seen.push_back(suffix);
|
||||
const int width = std::atoi(rows[r][widthColumn].c_str());
|
||||
// The game only loads the animations that have a frame count.
|
||||
std::vector<int> frameCounts;
|
||||
for (size_t pos = 0; pos <= rows[r][framesColumn].size();) {
|
||||
frameCounts.push_back(std::atoi(rows[r][framesColumn].c_str() + pos));
|
||||
const size_t comma = rows[r][framesColumn].find(',', pos);
|
||||
if (comma == std::string::npos)
|
||||
break;
|
||||
pos = comma + 1;
|
||||
}
|
||||
const std::string letters = "nwahds";
|
||||
for (size_t anim = 0; anim < letters.size(); ++anim) {
|
||||
const char letter = letters[anim];
|
||||
if (anim >= frameCounts.size() || frameCounts[anim] == 0)
|
||||
continue;
|
||||
const std::string path = "monsters\\" + suffix + letter + ".cl2";
|
||||
if (!archive.HasFile(path))
|
||||
continue;
|
||||
size_t size = 0;
|
||||
std::unique_ptr<uint8_t[]> data = ReadMpqFile(archive, path, size);
|
||||
if (data == nullptr)
|
||||
continue;
|
||||
// Skip placeholder files such as darkmage\\dmagew.cl2 (96 bytes, no pixel
|
||||
// data).
|
||||
if (size < 256)
|
||||
continue;
|
||||
const devilution::OwnedClxSpriteListOrSheet clx = devilution::Cl2ToClx(
|
||||
std::move(data), size,
|
||||
devilution::PointerOrValue<uint16_t>{static_cast<uint16_t>(width)});
|
||||
const std::string name =
|
||||
suffix.substr(suffix.find_last_of('\\') + 1) + letter;
|
||||
AddSprites(
|
||||
result, clx,
|
||||
SpriteFile{path, name, std::string(1, letter), 'M', '-', '-', width});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Loads the towner sprites (CEL files) listed in
|
||||
* assets/txtdata/towners/towners.tsv. */
|
||||
inline SpriteSet LoadTownerSprites(devilution::MpqArchive &archive,
|
||||
const std::string &tsvPath) {
|
||||
SpriteSet result;
|
||||
const auto rows = ReadTsv(tsvPath);
|
||||
const int pathColumn = TsvColumn(rows, "animPath");
|
||||
const int widthColumn = TsvColumn(rows, "animWidth");
|
||||
if (pathColumn < 0 || widthColumn < 0) {
|
||||
std::fprintf(stderr, "failed to read %s\n", tsvPath.c_str());
|
||||
return result;
|
||||
}
|
||||
std::vector<std::string> seen;
|
||||
for (size_t r = 1; r < rows.size(); ++r) {
|
||||
if (static_cast<int>(rows[r].size()) <= std::max(pathColumn, widthColumn))
|
||||
continue;
|
||||
const std::string &base = rows[r][pathColumn];
|
||||
bool duplicate = base.empty();
|
||||
for (const std::string &s : seen)
|
||||
duplicate = duplicate || s == base;
|
||||
if (duplicate)
|
||||
continue;
|
||||
seen.push_back(base);
|
||||
const int width = std::atoi(rows[r][widthColumn].c_str());
|
||||
const std::string path = base + ".cel";
|
||||
if (!archive.HasFile(path))
|
||||
continue;
|
||||
size_t size = 0;
|
||||
const std::unique_ptr<uint8_t[]> data = ReadMpqFile(archive, path, size);
|
||||
if (data == nullptr)
|
||||
continue;
|
||||
const devilution::OwnedClxSpriteListOrSheet clx = devilution::CelToClx(
|
||||
data.get(), size,
|
||||
devilution::PointerOrValue<uint16_t>{static_cast<uint16_t>(width)});
|
||||
AddSprites(result, clx,
|
||||
SpriteFile{path, base.substr(base.find_last_of('\\') + 1), "",
|
||||
'T', '-', '-', width});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Loads the object sprites (CEL files) listed in
|
||||
* assets/txtdata/objects/objdat.tsv. */
|
||||
inline SpriteSet LoadObjectSprites(devilution::MpqArchive &archive,
|
||||
const std::string &tsvPath) {
|
||||
SpriteSet result;
|
||||
const auto rows = ReadTsv(tsvPath);
|
||||
const int fileColumn = TsvColumn(rows, "file");
|
||||
const int widthColumn = TsvColumn(rows, "animWidth");
|
||||
if (fileColumn < 0 || widthColumn < 0) {
|
||||
std::fprintf(stderr, "failed to read %s\n", tsvPath.c_str());
|
||||
return result;
|
||||
}
|
||||
std::vector<std::string> seen;
|
||||
for (size_t r = 1; r < rows.size(); ++r) {
|
||||
if (static_cast<int>(rows[r].size()) <= std::max(fileColumn, widthColumn))
|
||||
continue;
|
||||
const std::string &base = rows[r][fileColumn];
|
||||
bool duplicate = base.empty();
|
||||
for (const std::string &s : seen)
|
||||
duplicate = duplicate || s == base;
|
||||
if (duplicate)
|
||||
continue;
|
||||
seen.push_back(base);
|
||||
const int width = std::atoi(rows[r][widthColumn].c_str());
|
||||
const std::string path = "objects\\" + base + ".cel";
|
||||
if (width <= 0 || width > MaxWidth || !archive.HasFile(path))
|
||||
continue;
|
||||
size_t size = 0;
|
||||
const std::unique_ptr<uint8_t[]> data = ReadMpqFile(archive, path, size);
|
||||
if (data == nullptr || size < 256)
|
||||
continue;
|
||||
const devilution::OwnedClxSpriteListOrSheet clx = devilution::CelToClx(
|
||||
data.get(), size,
|
||||
devilution::PointerOrValue<uint16_t>{static_cast<uint16_t>(width)});
|
||||
AddSprites(result, clx, SpriteFile{path, base, "", 'O', '-', '-', width});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace shadow_search
|
||||
Reference in New Issue
Block a user